From 774acae664e40a52f4f54632ba0087bfe5e0b8fa Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Fri, 28 Oct 2022 20:18:04 +0800 Subject: [PATCH] [resumes][chore] move browse page to home (#453) --- .../components/resumes/ResumesNavigation.ts | 6 +- .../resumes/landing/CallToAction.tsx | 2 +- .../src/components/resumes/landing/Hero.tsx | 6 +- apps/portal/src/pages/resumes/[resumeId].tsx | 2 +- apps/portal/src/pages/resumes/browse.tsx | 677 ------------------ apps/portal/src/pages/resumes/features.tsx | 21 + apps/portal/src/pages/resumes/index.tsx | 674 ++++++++++++++++- apps/portal/src/pages/resumes/submit.tsx | 2 +- 8 files changed, 693 insertions(+), 697 deletions(-) delete mode 100644 apps/portal/src/pages/resumes/browse.tsx create mode 100644 apps/portal/src/pages/resumes/features.tsx diff --git a/apps/portal/src/components/resumes/ResumesNavigation.ts b/apps/portal/src/components/resumes/ResumesNavigation.ts index 22782307..94d6d4a1 100644 --- a/apps/portal/src/components/resumes/ResumesNavigation.ts +++ b/apps/portal/src/components/resumes/ResumesNavigation.ts @@ -1,12 +1,12 @@ import type { ProductNavigationItems } from '~/components/global/ProductNavigation'; const navigation: ProductNavigationItems = [ + { children: [], href: '/resumes/submit', name: 'Submit for review' }, { children: [], - href: '/resumes/browse', - name: 'Browse', + href: '/resumes/features', + name: 'Features', }, - { children: [], href: '/resumes/submit', name: 'Submit for review' }, { children: [], href: '/resumes/about', diff --git a/apps/portal/src/components/resumes/landing/CallToAction.tsx b/apps/portal/src/components/resumes/landing/CallToAction.tsx index c82ef8d7..f7dafe11 100644 --- a/apps/portal/src/components/resumes/landing/CallToAction.tsx +++ b/apps/portal/src/components/resumes/landing/CallToAction.tsx @@ -16,7 +16,7 @@ export function CallToAction() {

- - -
- - - {filters.map((filter) => ( - - {({ open }) => ( - <> -

- - - {filter.label} - - - {open ? ( - - -

- -
- {filter.options.map((option) => ( -
- - onFilterCheckboxChange( - isChecked, - filter.id, - option.value, - ) - } - /> -
- ))} -
-

onClearFilterClick(filter.id)}> - Clear -

-
- - )} -
- ))} -
- - - - - - - -
-
-
- {/* Quick Access Section */} -

- Quick access -

-
-
-
    - {SHORTCUTS.map((shortcut) => ( -
  • - onShortcutChange(shortcut)} - /> -
  • - ))} -
- {/* Filter Section */} -

- Explore these filters -

- {filters.map((filter) => ( - - {({ open }) => ( - <> -

- - - {filter.label} - - - {open ? ( - - -

- - - {filter.options.map((option) => ( -
- - onFilterCheckboxChange( - isChecked, - filter.id, - option.value, - ) - } - /> -
- ))} -
-

onClearFilterClick(filter.id)}> - Clear -

-
- - )} -
- ))} -
-
-
-
-
-
-
- -
-
-
-
- -
- - {SORT_OPTIONS.map(({ label, value }) => ( - setSortOrder(value)}> - ))} - -
-
- {isFetchingResumes ? ( -
- {' '} - {' '} -
- ) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? ( - - ) : getTabResumes().length === 0 ? ( -
- - {getEmptyDataText(tabsValue, searchValue, userFilters)} -
- ) : ( -
-
-
- -
-
-
- {getTabTotalPages() > 1 && ( -
- setCurrentPage(page)} - /> -
- )} -
-
- )} -
-
-
- - ); -} diff --git a/apps/portal/src/pages/resumes/features.tsx b/apps/portal/src/pages/resumes/features.tsx new file mode 100644 index 00000000..9ad3afbf --- /dev/null +++ b/apps/portal/src/pages/resumes/features.tsx @@ -0,0 +1,21 @@ +import Head from 'next/head'; + +import { CallToAction } from '~/components/resumes/landing/CallToAction'; +import { Hero } from '~/components/resumes/landing/Hero'; +import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures'; + +export default function Home() { + return ( + <> + + Resume Review Portal + + +
+ + + +
+ + ); +} diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index a7af72aa..de2b13c6 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -1,20 +1,676 @@ import Head from 'next/head'; +import Router, { useRouter } from 'next/router'; +import { useSession } from 'next-auth/react'; +import { Fragment, useEffect, useMemo, useState } from 'react'; +import { Dialog, Disclosure, Transition } from '@headlessui/react'; +import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; +import { + MagnifyingGlassIcon, + NewspaperIcon, + XMarkIcon, +} from '@heroicons/react/24/outline'; +import { + Button, + CheckboxInput, + CheckboxList, + DropdownMenu, + Pagination, + Spinner, + Tabs, + TextInput, +} from '@tih/ui'; -import { CallToAction } from '~/components/resumes/landing/CallToAction'; -import { Hero } from '~/components/resumes/landing/Hero'; -import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures'; +import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill'; +import ResumeListItems from '~/components/resumes/browse/ResumeListItems'; +import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton'; + +import type { + Filter, + FilterId, + FilterLabel, + Shortcut, +} from '~/utils/resumes/resumeFilters'; +import { + BROWSE_TABS_VALUES, + EXPERIENCES, + getFilterLabel, + INITIAL_FILTER_STATE, + isInitialFilterState, + LOCATIONS, + ROLES, + SHORTCUTS, + SORT_OPTIONS, +} from '~/utils/resumes/resumeFilters'; +import useDebounceValue from '~/utils/resumes/useDebounceValue'; +import useSearchParams from '~/utils/resumes/useSearchParams'; +import { trpc } from '~/utils/trpc'; + +import type { FilterState, SortOrder } from '../../utils/resumes/resumeFilters'; + +const STALE_TIME = 5 * 60 * 1000; +const DEBOUNCE_DELAY = 800; +const PAGE_LIMIT = 10; +const filters: Array = [ + { + id: 'role', + label: 'Role', + options: ROLES, + }, + { + id: 'experience', + label: 'Experience', + options: EXPERIENCES, + }, + { + id: 'location', + label: 'Location', + options: LOCATIONS, + }, +]; + +const getLoggedOutText = (tabsValue: string) => { + switch (tabsValue) { + case BROWSE_TABS_VALUES.STARRED: + return 'to view starred resumes!'; + case BROWSE_TABS_VALUES.MY: + return 'to view your submitted resumes!'; + default: + return ''; + } +}; + +const getEmptyDataText = ( + tabsValue: string, + searchValue: string, + userFilters: FilterState, +) => { + if (searchValue.length > 0) { + return 'Try tweaking your search text to see more resumes.'; + } + if (!isInitialFilterState(userFilters)) { + return 'Try tweaking your filters to see more resumes.'; + } + switch (tabsValue) { + case BROWSE_TABS_VALUES.ALL: + return 'Looks like SWEs are feeling lucky!'; + case BROWSE_TABS_VALUES.STARRED: + return 'You have not starred any resumes. Star one to see it here!'; + case BROWSE_TABS_VALUES.MY: + return 'Upload a resume to see it here!'; + default: + return ''; + } +}; + +export default function ResumeHomePage() { + const { data: sessionData } = useSession(); + const router = useRouter(); + const [tabsValue, setTabsValue, isTabsValueInit] = useSearchParams( + 'tabsValue', + BROWSE_TABS_VALUES.ALL, + ); + const [sortOrder, setSortOrder, isSortOrderInit] = useSearchParams( + 'sortOrder', + 'latest', + ); + const [searchValue, setSearchValue, isSearchValueInit] = useSearchParams( + 'searchValue', + '', + ); + const [shortcutSelected, setShortcutSelected, isShortcutInit] = + useSearchParams('shortcutSelected', 'All'); + const [currentPage, setCurrentPage, isCurrentPageInit] = useSearchParams( + 'currentPage', + 1, + ); + const [userFilters, setUserFilters, isUserFiltersInit] = useSearchParams( + 'userFilters', + INITIAL_FILTER_STATE, + ); + const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); + + const skip = (currentPage - 1) * PAGE_LIMIT; + const isSearchOptionsInit = useMemo(() => { + return ( + isTabsValueInit && + isSortOrderInit && + isSearchValueInit && + isShortcutInit && + isCurrentPageInit && + isUserFiltersInit + ); + }, [ + isTabsValueInit, + isSortOrderInit, + isSearchValueInit, + isShortcutInit, + isCurrentPageInit, + isUserFiltersInit, + ]); + + useEffect(() => { + setCurrentPage(1); + }, [userFilters, sortOrder, setCurrentPage, searchValue]); + + useEffect(() => { + // Router.replace used instead of router.replace to avoid + // the page reloading itself since the router.replace + // callback changes on every page load + if (!isSearchOptionsInit) { + return; + } + + Router.replace({ + pathname: router.pathname, + query: { + currentPage: JSON.stringify(currentPage), + searchValue: JSON.stringify(searchValue), + shortcutSelected: JSON.stringify(shortcutSelected), + sortOrder: JSON.stringify(sortOrder), + tabsValue: JSON.stringify(tabsValue), + userFilters: JSON.stringify(userFilters), + }, + }); + }, [ + tabsValue, + sortOrder, + searchValue, + userFilters, + shortcutSelected, + currentPage, + router.pathname, + isSearchOptionsInit, + ]); + + const filterCountsQuery = trpc.useQuery( + ['resumes.resume.getTotalFilterCounts'], + { + staleTime: STALE_TIME, + }, + ); + + const allResumesQuery = trpc.useQuery( + [ + 'resumes.resume.findAll', + { + experienceFilters: userFilters.experience, + locationFilters: userFilters.location, + numComments: userFilters.numComments, + roleFilters: userFilters.role, + searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), + skip, + sortOrder, + take: PAGE_LIMIT, + }, + ], + { + enabled: tabsValue === BROWSE_TABS_VALUES.ALL, + staleTime: STALE_TIME, + }, + ); + const starredResumesQuery = trpc.useQuery( + [ + 'resumes.resume.user.findUserStarred', + { + experienceFilters: userFilters.experience, + locationFilters: userFilters.location, + numComments: userFilters.numComments, + roleFilters: userFilters.role, + searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), + skip, + sortOrder, + take: PAGE_LIMIT, + }, + ], + { + enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, + retry: false, + staleTime: STALE_TIME, + }, + ); + const myResumesQuery = trpc.useQuery( + [ + 'resumes.resume.user.findUserCreated', + { + experienceFilters: userFilters.experience, + locationFilters: userFilters.location, + numComments: userFilters.numComments, + roleFilters: userFilters.role, + searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), + skip, + sortOrder, + take: PAGE_LIMIT, + }, + ], + { + enabled: tabsValue === BROWSE_TABS_VALUES.MY, + retry: false, + staleTime: STALE_TIME, + }, + ); + + const getFilterCount = (filter: FilterLabel, value: string) => { + if (filterCountsQuery.isLoading) { + return 0; + } + const filterCountsData = filterCountsQuery.data!; + return filterCountsData[filter][value]; + }; + + const onSubmitResume = () => { + if (sessionData === null) { + router.push('/api/auth/signin'); + } else { + router.push('/resumes/submit'); + } + }; + + const onFilterCheckboxChange = ( + isChecked: boolean, + filterSection: FilterId, + filterValue: string, + ) => { + if (isChecked) { + setUserFilters({ + ...userFilters, + [filterSection]: [...userFilters[filterSection], filterValue], + }); + } else { + setUserFilters({ + ...userFilters, + [filterSection]: userFilters[filterSection].filter( + (value) => value !== filterValue, + ), + }); + } + }; + + const onClearFilterClick = (filterSection: FilterId) => { + setUserFilters({ + ...userFilters, + [filterSection]: [], + }); + }; + + const onShortcutChange = ({ + sortOrder: shortcutSortOrder, + filters: shortcutFilters, + name: shortcutName, + }: Shortcut) => { + setShortcutSelected(shortcutName); + setSortOrder(shortcutSortOrder); + setUserFilters(shortcutFilters); + }; + + const onTabChange = (tab: string) => { + setTabsValue(tab); + setCurrentPage(1); + }; + + const getTabQueryData = () => { + switch (tabsValue) { + case BROWSE_TABS_VALUES.ALL: + return allResumesQuery.data; + case BROWSE_TABS_VALUES.STARRED: + return starredResumesQuery.data; + case BROWSE_TABS_VALUES.MY: + return myResumesQuery.data; + default: + return null; + } + }; + + const getTabResumes = () => { + return getTabQueryData()?.mappedResumeData ?? []; + }; + + const getTabTotalPages = () => { + const numRecords = getTabQueryData()?.totalRecords ?? 0; + return numRecords % PAGE_LIMIT === 0 + ? numRecords / PAGE_LIMIT + : Math.floor(numRecords / PAGE_LIMIT) + 1; + }; + + const isFetchingResumes = + allResumesQuery.isFetching || + starredResumesQuery.isFetching || + myResumesQuery.isFetching; -export default function Home() { return ( <> - Resume Review + Resume Review Portal -
- - - + {/* Mobile Filters */} +
+ + + +
+ + +
+ + +
+

+ Quick access +

+ +
+ +
+
    + {SHORTCUTS.map((shortcut) => ( +
  • + onShortcutChange(shortcut)} + /> +
  • + ))} +
+ + {filters.map((filter) => ( + + {({ open }) => ( + <> +

+ + + {filter.label} + + + {open ? ( + + +

+ +
+ {filter.options.map((option) => ( +
+ + onFilterCheckboxChange( + isChecked, + filter.id, + option.value, + ) + } + /> +
+ ))} +
+

onClearFilterClick(filter.id)}> + Clear +

+
+ + )} +
+ ))} +
+
+
+
+
+
+
+ +
+
+
+ {/* Quick Access Section */} +

+ Quick access +

+
+
+
    + {SHORTCUTS.map((shortcut) => ( +
  • + onShortcutChange(shortcut)} + /> +
  • + ))} +
+ {/* Filter Section */} +

+ Explore these filters +

+ {filters.map((filter) => ( + + {({ open }) => ( + <> +

+ + + {filter.label} + + + {open ? ( + + +

+ + + {filter.options.map((option) => ( +
+ + onFilterCheckboxChange( + isChecked, + filter.id, + option.value, + ) + } + /> +
+ ))} +
+

onClearFilterClick(filter.id)}> + Clear +

+
+ + )} +
+ ))} +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + {SORT_OPTIONS.map(({ label, value }) => ( + setSortOrder(value)}> + ))} + +
+
+ {isFetchingResumes ? ( +
+ {' '} + {' '} +
+ ) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? ( + + ) : getTabResumes().length === 0 ? ( +
+ + {getEmptyDataText(tabsValue, searchValue, userFilters)} +
+ ) : ( +
+
+
+ +
+
+
+ {getTabTotalPages() > 1 && ( +
+ setCurrentPage(page)} + /> +
+ )} +
+
+ )} +
+
); diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index 61bb7cc5..2efa9112 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -171,7 +171,7 @@ export default function SubmitResumeForm({ trpcContext.invalidateQueries( 'resumes.resume.getTotalFilterCounts', ); - router.push('/resumes/browse'); + router.push('/resumes'); } else { onClose(); }