[resumes][chore] move browse page to home (#453)

pull/454/head
Keane Chan 2 years ago committed by GitHub
parent 766bee0e0c
commit 774acae664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +1,12 @@
import type { ProductNavigationItems } from '~/components/global/ProductNavigation'; import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
const navigation: ProductNavigationItems = [ const navigation: ProductNavigationItems = [
{ children: [], href: '/resumes/submit', name: 'Submit for review' },
{ {
children: [], children: [],
href: '/resumes/browse', href: '/resumes/features',
name: 'Browse', name: 'Features',
}, },
{ children: [], href: '/resumes/submit', name: 'Submit for review' },
{ {
children: [], children: [],
href: '/resumes/about', href: '/resumes/about',

@ -16,7 +16,7 @@ export function CallToAction() {
</p> </p>
<Button <Button
className="mt-4" className="mt-4"
href="/resumes/browse" href="/resumes"
label="Start browsing now" label="Start browsing now"
variant="primary" variant="primary"
/> />

@ -25,11 +25,7 @@ export function Hero() {
your fellow engineers your fellow engineers
</p> </p>
<div className="mt-10 flex justify-center gap-x-4"> <div className="mt-10 flex justify-center gap-x-4">
<Button <Button href="/resumes" label="Start browsing now" variant="primary" />
href="/resumes/browse"
label="Start browsing now"
variant="primary"
/>
{/* TODO: Update video */} {/* TODO: Update video */}
<Link href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"> <Link href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
<button <button

@ -121,7 +121,7 @@ export default function ResumeReviewPage() {
) => filterOptions.find((option) => option.label === label)?.value; ) => filterOptions.find((option) => option.label === label)?.value;
router.push({ router.push({
pathname: '/resumes/browse', pathname: '/resumes',
query: { query: {
currentPage: JSON.stringify(1), currentPage: JSON.stringify(1),
searchValue: JSON.stringify(''), searchValue: JSON.stringify(''),

@ -1,677 +0,0 @@
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 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<Filter> = [
{
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>(
'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;
return (
<>
<Head>
<title>Resume Review Portal</title>
</Head>
{/* Mobile Filters */}
<div>
<Transition.Root as={Fragment} show={mobileFiltersOpen}>
<Dialog
as="div"
className="relative z-40 lg:hidden"
onClose={setMobileFiltersOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 z-40 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="translate-x-full">
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-scroll bg-white py-4 pb-12 shadow-xl">
<div className="flex items-center justify-between px-4">
<h2 className="text-lg font-medium text-slate-900">
Quick access
</h2>
<button
className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-slate-400"
type="button"
onClick={() => setMobileFiltersOpen(false)}>
<span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="h-6 w-6" />
</button>
</div>
<form className="mt-4 border-t border-slate-200">
<ul
className="flex w-11/12 flex-wrap justify-start gap-2 px-4 py-4 font-medium text-slate-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
<ResumeFilterPill
isSelected={shortcutSelected === shortcut.name}
title={shortcut.name}
onClick={() => onShortcutChange(shortcut)}
/>
</li>
))}
</ul>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-t border-slate-200 px-4 pt-6 pb-4">
{({ open }) => (
<>
<h3 className="-mx-2 -my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-slate-400 hover:text-slate-500">
<span className="font-medium text-slate-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusIcon
aria-hidden="true"
className="h-5 w-5"
/>
) : (
<PlusIcon
aria-hidden="true"
className="h-5 w-5"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="space-y-4 pt-6">
<div className="space-y-3">
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={`${option.label} (${getFilterCount(
filter.label,
option.label,
)})`}
value={userFilters[filter.id].includes(
option.value,
)}
onChange={(isChecked) =>
onFilterCheckboxChange(
isChecked,
filter.id,
option.value,
)
}
/>
</div>
))}
</div>
<p
className="cursor-pointer text-sm text-slate-500 underline"
onClick={() => onClearFilterClick(filter.id)}>
Clear
</p>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</div>
<main className="h-full flex-auto px-8 pb-4">
<div className="flex justify-start">
<div className="fixed top-0 bottom-0 mt-24 hidden w-64 overflow-auto lg:block">
{/* Quick Access Section */}
<h3 className="text-md font-medium tracking-tight text-gray-900">
Quick access
</h3>
<div className="w-100 pt-4 sm:pr-0 md:pr-4">
<form>
<ul
className="flex w-11/12 flex-wrap justify-start gap-2 pb-6 text-sm font-medium text-slate-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
<ResumeFilterPill
isSelected={shortcutSelected === shortcut.name}
title={shortcut.name}
onClick={() => onShortcutChange(shortcut)}
/>
</li>
))}
</ul>
{/* Filter Section */}
<h3 className="text-md font-medium tracking-tight text-slate-900">
Explore these filters
</h3>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-b border-slate-200 pt-6 pb-4">
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500">
<span className="font-medium text-slate-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusIcon
aria-hidden="true"
className="h-5 w-5"
/>
) : (
<PlusIcon
aria-hidden="true"
className="h-5 w-5"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="space-y-4 pt-4">
<CheckboxList
description=""
isLabelHidden={true}
label=""
orientation="vertical">
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 px-1 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={`${option.label} (${getFilterCount(
filter.label,
option.label,
)})`}
value={userFilters[filter.id].includes(
option.value,
)}
onChange={(isChecked) =>
onFilterCheckboxChange(
isChecked,
filter.id,
option.value,
)
}
/>
</div>
))}
</CheckboxList>
<p
className="cursor-pointer text-sm text-slate-500 underline"
onClick={() => onClearFilterClick(filter.id)}>
Clear
</p>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</div>
</div>
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]">
<div className="lg:border-grey-200 sticky top-0 z-10 flex flex-wrap items-center justify-between pt-6 pb-2 lg:border-b">
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none xl:pb-0">
<div>
<Tabs
label="Resume Browse Tabs"
tabs={[
{
label: 'All Resumes',
value: BROWSE_TABS_VALUES.ALL,
},
{
label: 'Starred Resumes',
value: BROWSE_TABS_VALUES.STARRED,
},
{
label: 'My Resumes',
value: BROWSE_TABS_VALUES.MY,
},
]}
value={tabsValue}
onChange={onTabChange}
/>
</div>
</div>
<div className="flex flex-wrap items-center justify-start gap-4 lg:gap-6">
<div className="w-64">
<TextInput
isLabelHidden={true}
label="search"
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</div>
<DropdownMenu
align="end"
label={getFilterLabel(SORT_OPTIONS, sortOrder)}>
{SORT_OPTIONS.map(({ label, value }) => (
<DropdownMenu.Item
key={value}
isSelected={sortOrder === value}
label={label}
onClick={() => setSortOrder(value)}></DropdownMenu.Item>
))}
</DropdownMenu>
<Button
className="lg:hidden"
icon={FunnelIcon}
isLabelHidden={true}
label="Filters"
variant="tertiary"
onClick={() => setMobileFiltersOpen(true)}
/>
<Button
className="whitespace-pre-wrap px-2 lg:block"
label="Submit Resume"
variant="primary"
onClick={onSubmitResume}
/>
</div>
</div>
{isFetchingResumes ? (
<div className="w-full pt-4">
{' '}
<Spinner display="block" size="lg" />{' '}
</div>
) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? (
<ResumeSignInButton
className="mt-8"
text={getLoggedOutText(tabsValue)}
/>
) : getTabResumes().length === 0 ? (
<div className="mt-24 flex flex-wrap justify-center">
<NewspaperIcon
className="mb-12 basis-full"
height={196}
width={196}
/>
{getEmptyDataText(tabsValue, searchValue, userFilters)}
</div>
) : (
<div className="h-[calc(100vh-9rem)] pb-10 lg:h-[calc(100vh-6rem)]">
<div className="h-[85%] overflow-y-auto">
<div>
<ResumeListItems resumes={getTabResumes()} />
</div>
</div>
<div className="flex h-[15%] items-center justify-center">
{getTabTotalPages() > 1 && (
<div>
<Pagination
current={currentPage}
end={getTabTotalPages()}
label="pagination"
start={1}
onSelect={(page) => setCurrentPage(page)}
/>
</div>
)}
</div>
</div>
)}
</div>
</div>
</main>
</>
);
}

@ -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 (
<>
<Head>
<title>Resume Review Portal</title>
</Head>
<main className="h-full w-full overflow-y-auto">
<Hero />
<PrimaryFeatures />
<CallToAction />
</main>
</>
);
}

@ -1,20 +1,676 @@
import Head from 'next/head'; 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 ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import { Hero } from '~/components/resumes/landing/Hero'; import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures'; 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<Filter> = [
{
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>(
'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 ( return (
<> <>
<Head> <Head>
<title>Resume Review</title> <title>Resume Review Portal</title>
</Head> </Head>
<main className="h-full w-full overflow-y-auto"> {/* Mobile Filters */}
<Hero /> <div>
<PrimaryFeatures /> <Transition.Root as={Fragment} show={mobileFiltersOpen}>
<CallToAction /> <Dialog
as="div"
className="relative z-40 lg:hidden"
onClose={setMobileFiltersOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 z-40 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="translate-x-full">
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-scroll bg-white py-4 pb-12 shadow-xl">
<div className="flex items-center justify-between px-4">
<h2 className="text-lg font-medium text-slate-900">
Quick access
</h2>
<button
className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-slate-400"
type="button"
onClick={() => setMobileFiltersOpen(false)}>
<span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="h-6 w-6" />
</button>
</div>
<form className="mt-4 border-t border-slate-200">
<ul
className="flex w-11/12 flex-wrap justify-start gap-2 px-4 py-4 font-medium text-slate-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
<ResumeFilterPill
isSelected={shortcutSelected === shortcut.name}
title={shortcut.name}
onClick={() => onShortcutChange(shortcut)}
/>
</li>
))}
</ul>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-t border-slate-200 px-4 pt-6 pb-4">
{({ open }) => (
<>
<h3 className="-mx-2 -my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-slate-400 hover:text-slate-500">
<span className="font-medium text-slate-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusIcon
aria-hidden="true"
className="h-5 w-5"
/>
) : (
<PlusIcon
aria-hidden="true"
className="h-5 w-5"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="space-y-4 pt-6">
<div className="space-y-3">
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={`${option.label} (${getFilterCount(
filter.label,
option.label,
)})`}
value={userFilters[filter.id].includes(
option.value,
)}
onChange={(isChecked) =>
onFilterCheckboxChange(
isChecked,
filter.id,
option.value,
)
}
/>
</div>
))}
</div>
<p
className="cursor-pointer text-sm text-slate-500 underline"
onClick={() => onClearFilterClick(filter.id)}>
Clear
</p>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</div>
<main className="h-full flex-auto px-8 pb-4">
<div className="flex justify-start">
<div className="fixed top-0 bottom-0 mt-24 hidden w-64 overflow-auto lg:block">
{/* Quick Access Section */}
<h3 className="text-md font-medium tracking-tight text-gray-900">
Quick access
</h3>
<div className="w-100 pt-4 sm:pr-0 md:pr-4">
<form>
<ul
className="flex w-11/12 flex-wrap justify-start gap-2 pb-6 text-sm font-medium text-slate-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
<ResumeFilterPill
isSelected={shortcutSelected === shortcut.name}
title={shortcut.name}
onClick={() => onShortcutChange(shortcut)}
/>
</li>
))}
</ul>
{/* Filter Section */}
<h3 className="text-md font-medium tracking-tight text-slate-900">
Explore these filters
</h3>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-b border-slate-200 pt-6 pb-4">
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500">
<span className="font-medium text-slate-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusIcon
aria-hidden="true"
className="h-5 w-5"
/>
) : (
<PlusIcon
aria-hidden="true"
className="h-5 w-5"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="space-y-4 pt-4">
<CheckboxList
description=""
isLabelHidden={true}
label=""
orientation="vertical">
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 px-1 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={`${option.label} (${getFilterCount(
filter.label,
option.label,
)})`}
value={userFilters[filter.id].includes(
option.value,
)}
onChange={(isChecked) =>
onFilterCheckboxChange(
isChecked,
filter.id,
option.value,
)
}
/>
</div>
))}
</CheckboxList>
<p
className="cursor-pointer text-sm text-slate-500 underline"
onClick={() => onClearFilterClick(filter.id)}>
Clear
</p>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</div>
</div>
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]">
<div className="lg:border-grey-200 sticky top-0 z-10 flex flex-wrap items-center justify-between pt-6 pb-2 lg:border-b">
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none xl:pb-0">
<div>
<Tabs
label="Resume Browse Tabs"
tabs={[
{
label: 'All Resumes',
value: BROWSE_TABS_VALUES.ALL,
},
{
label: 'Starred Resumes',
value: BROWSE_TABS_VALUES.STARRED,
},
{
label: 'My Resumes',
value: BROWSE_TABS_VALUES.MY,
},
]}
value={tabsValue}
onChange={onTabChange}
/>
</div>
</div>
<div className="flex flex-wrap items-center justify-start gap-4 lg:gap-6">
<div className="w-64">
<TextInput
isLabelHidden={true}
label="search"
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</div>
<DropdownMenu
align="end"
label={getFilterLabel(SORT_OPTIONS, sortOrder)}>
{SORT_OPTIONS.map(({ label, value }) => (
<DropdownMenu.Item
key={value}
isSelected={sortOrder === value}
label={label}
onClick={() => setSortOrder(value)}></DropdownMenu.Item>
))}
</DropdownMenu>
<Button
className="lg:hidden"
icon={FunnelIcon}
isLabelHidden={true}
label="Filters"
variant="tertiary"
onClick={() => setMobileFiltersOpen(true)}
/>
<Button
className="whitespace-pre-wrap px-2 lg:block"
label="Submit Resume"
variant="primary"
onClick={onSubmitResume}
/>
</div>
</div>
{isFetchingResumes ? (
<div className="w-full pt-4">
{' '}
<Spinner display="block" size="lg" />{' '}
</div>
) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? (
<ResumeSignInButton
className="mt-8"
text={getLoggedOutText(tabsValue)}
/>
) : getTabResumes().length === 0 ? (
<div className="mt-24 flex flex-wrap justify-center">
<NewspaperIcon
className="mb-12 basis-full"
height={196}
width={196}
/>
{getEmptyDataText(tabsValue, searchValue, userFilters)}
</div>
) : (
<div className="h-[calc(100vh-9rem)] pb-10 lg:h-[calc(100vh-6rem)]">
<div className="h-[85%] overflow-y-auto">
<div>
<ResumeListItems resumes={getTabResumes()} />
</div>
</div>
<div className="flex h-[15%] items-center justify-center">
{getTabTotalPages() > 1 && (
<div>
<Pagination
current={currentPage}
end={getTabTotalPages()}
label="pagination"
start={1}
onSelect={(page) => setCurrentPage(page)}
/>
</div>
)}
</div>
</div>
)}
</div>
</div>
</main> </main>
</> </>
); );

@ -171,7 +171,7 @@ export default function SubmitResumeForm({
trpcContext.invalidateQueries( trpcContext.invalidateQueries(
'resumes.resume.getTotalFilterCounts', 'resumes.resume.getTotalFilterCounts',
); );
router.push('/resumes/browse'); router.push('/resumes');
} else { } else {
onClose(); onClose();
} }

Loading…
Cancel
Save