[resumes][feat] implement filter shortcuts

pull/385/head
Wu Peirong 2 years ago
parent 4d22edabd0
commit 9f24e0bcca

@ -1,96 +0,0 @@
export const BROWSE_TABS_VALUES = {
ALL: 'all',
MY: 'my',
STARRED: 'starred',
};
export type SortOrder = 'latest' | 'popular' | 'topComments';
type SortOption = {
name: string;
value: SortOrder;
};
export const SORT_OPTIONS: Array<SortOption> = [
{ name: 'Latest', value: 'latest' },
{ name: 'Popular', value: 'popular' },
{ name: 'Top Comments', value: 'topComments' },
];
export const TOP_HITS = [
{ href: '#', name: 'Unreviewed' },
{ href: '#', name: 'Fresh Grad' },
{ href: '#', name: 'GOATs' },
{ href: '#', name: 'US Only' },
];
export type FilterOption = {
label: string;
value: string;
};
export const ROLE: Array<FilterOption> = [
{
label: 'Full-Stack Engineer',
value: 'Full-Stack Engineer',
},
{ label: 'Frontend Engineer', value: 'Frontend Engineer' },
{ label: 'Backend Engineer', value: 'Backend Engineer' },
{ label: 'DevOps Engineer', value: 'DevOps Engineer' },
{ label: 'iOS Engineer', value: 'iOS Engineer' },
{ label: 'Android Engineer', value: 'Android Engineer' },
];
export const EXPERIENCE: Array<FilterOption> = [
{ label: 'Freshman', value: 'Freshman' },
{ label: 'Sophomore', value: 'Sophomore' },
{ label: 'Junior', value: 'Junior' },
{ label: 'Senior', value: 'Senior' },
{
label: 'Entry Level (0 - 2 years)',
value: 'Entry Level (0 - 2 years)',
},
{
label: 'Mid Level (3 - 5 years)',
value: 'Mid Level (3 - 5 years)',
},
{
label: 'Senior Level (5+ years)',
value: 'Senior Level (5+ years)',
},
];
export const LOCATION: Array<FilterOption> = [
{ label: 'Singapore', value: 'Singapore' },
{ label: 'United States', value: 'United States' },
{ label: 'India', value: 'India' },
];
export const TEST_RESUMES = [
{
createdAt: new Date(),
experience: 'Fresh Grad (0-1 years)',
numComments: 9,
numStars: 1,
role: 'Backend Engineer',
title: 'Rejected from multiple companies, please help...:(',
user: 'Git Ji Ra',
},
{
createdAt: new Date(),
experience: 'Fresh Grad (0-1 years)',
numComments: 9,
numStars: 1,
role: 'Backend Engineer',
title: 'Rejected from multiple companies, please help...:(',
user: 'Git Ji Ra',
},
{
createdAt: new Date(),
experience: 'Fresh Grad (0-1 years)',
numComments: 9,
numStars: 1,
role: 'Backend Engineer',
title: 'Rejected from multiple companies, please help...:(',
user: 'Git Ji Ra',
},
];

@ -0,0 +1,146 @@
export type FilterId = 'experience' | 'location' | 'role';
export type CustomFilter = {
numComments: number;
};
type RoleFilter =
| 'Android Engineer'
| 'Backend Engineer'
| 'DevOps Engineer'
| 'Frontend Engineer'
| 'Full-Stack Engineer'
| 'iOS Engineer';
type ExperienceFilter =
| 'Entry Level (0 - 2 years)'
| 'Freshman'
| 'Junior'
| 'Mid Level (3 - 5 years)'
| 'Senior Level (5+ years)'
| 'Senior'
| 'Sophomore';
type LocationFilter = 'India' | 'Singapore' | 'United States';
export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
export type FilterOption<T> = {
label: string;
value: T;
};
export type Filter = {
id: FilterId;
label: string;
options: Array<FilterOption<FilterValue>>;
};
export type FilterState = Partial<CustomFilter> &
Record<FilterId, Array<FilterValue>>;
export type SortOrder = 'latest' | 'popular' | 'topComments';
type SortOption = {
name: string;
value: SortOrder;
};
export type Shortcut = {
customFilters?: CustomFilter;
filters: FilterState;
name: string;
sortOrder: SortOrder;
};
export const BROWSE_TABS_VALUES = {
ALL: 'all',
MY: 'my',
STARRED: 'starred',
};
export const SORT_OPTIONS: Array<SortOption> = [
{ name: 'Latest', value: 'latest' },
{ name: 'Popular', value: 'popular' },
{ name: 'Top Comments', value: 'topComments' },
];
export const ROLE: Array<FilterOption<RoleFilter>> = [
{
label: 'Full-Stack Engineer',
value: 'Full-Stack Engineer',
},
{ label: 'Frontend Engineer', value: 'Frontend Engineer' },
{ label: 'Backend Engineer', value: 'Backend Engineer' },
{ label: 'DevOps Engineer', value: 'DevOps Engineer' },
{ label: 'iOS Engineer', value: 'iOS Engineer' },
{ label: 'Android Engineer', value: 'Android Engineer' },
];
export const EXPERIENCE: Array<FilterOption<ExperienceFilter>> = [
{ label: 'Freshman', value: 'Freshman' },
{ label: 'Sophomore', value: 'Sophomore' },
{ label: 'Junior', value: 'Junior' },
{ label: 'Senior', value: 'Senior' },
{
label: 'Entry Level (0 - 2 years)',
value: 'Entry Level (0 - 2 years)',
},
{
label: 'Mid Level (3 - 5 years)',
value: 'Mid Level (3 - 5 years)',
},
{
label: 'Senior Level (5+ years)',
value: 'Senior Level (5+ years)',
},
];
export const LOCATION: Array<FilterOption<LocationFilter>> = [
{ label: 'Singapore', value: 'Singapore' },
{ label: 'United States', value: 'United States' },
{ label: 'India', value: 'India' },
];
export const INITIAL_FILTER_STATE: FilterState = {
experience: Object.values(EXPERIENCE).map(({ value }) => value),
location: Object.values(LOCATION).map(({ value }) => value),
role: Object.values(ROLE).map(({ value }) => value),
};
export const SHORTCUTS: Array<Shortcut> = [
{
filters: INITIAL_FILTER_STATE,
name: 'All',
sortOrder: 'latest',
},
{
filters: {
...INITIAL_FILTER_STATE,
numComments: 0,
},
name: 'Unreviewed',
sortOrder: 'latest',
},
{
filters: {
...INITIAL_FILTER_STATE,
experience: ['Entry Level (0 - 2 years)'],
},
name: 'Fresh Grad',
sortOrder: 'latest',
},
{
filters: INITIAL_FILTER_STATE,
name: 'GOATs',
sortOrder: 'popular',
},
{
filters: {
...INITIAL_FILTER_STATE,
location: ['United States'],
},
name: 'US Only',
sortOrder: 'latest',
},
];

@ -14,19 +14,24 @@ import {
TextInput, TextInput,
} from '@tih/ui'; } from '@tih/ui';
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import type { import type {
FilterOption, Filter,
FilterId,
FilterState,
FilterValue,
Shortcut,
SortOrder, SortOrder,
} from '~/components/resumes/browse/resumeConstants'; } from '~/components/resumes/browse/resumeFilters';
import { import {
BROWSE_TABS_VALUES, BROWSE_TABS_VALUES,
EXPERIENCE, EXPERIENCE,
INITIAL_FILTER_STATE,
LOCATION, LOCATION,
ROLE, ROLE,
SHORTCUTS,
SORT_OPTIONS, SORT_OPTIONS,
TOP_HITS, } from '~/components/resumes/browse/resumeFilters';
} from '~/components/resumes/browse/resumeConstants';
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems'; import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton'; import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
@ -35,38 +40,24 @@ import { trpc } from '~/utils/trpc';
import type { Resume } from '~/types/resume'; import type { Resume } from '~/types/resume';
type FilterId = 'experience' | 'location' | 'role';
type Filter = {
id: FilterId;
name: string;
options: Array<FilterOption>;
};
type FilterState = Record<FilterId, Array<string>>;
const filters: Array<Filter> = [ const filters: Array<Filter> = [
{ {
id: 'role', id: 'role',
name: 'Role', label: 'Role',
options: ROLE, options: ROLE,
}, },
{ {
id: 'experience', id: 'experience',
name: 'Experience', label: 'Experience',
options: EXPERIENCE, options: EXPERIENCE,
}, },
{ {
id: 'location', id: 'location',
name: 'Location', label: 'Location',
options: LOCATION, options: LOCATION,
}, },
]; ];
const INITIAL_FILTER_STATE: FilterState = {
experience: Object.values(EXPERIENCE).map(({ value }) => value),
location: Object.values(LOCATION).map(({ value }) => value),
role: Object.values(ROLE).map(({ value }) => value),
};
const filterResumes = ( const filterResumes = (
resumes: Array<Resume>, resumes: Array<Resume>,
searchValue: string, searchValue: string,
@ -78,9 +69,14 @@ const filterResumes = (
) )
.filter( .filter(
({ experience, location, role }) => ({ experience, location, role }) =>
userFilters.role.includes(role) && userFilters.role.includes(role as FilterValue) &&
userFilters.experience.includes(experience) && userFilters.experience.includes(experience as FilterValue) &&
userFilters.location.includes(location), userFilters.location.includes(location as FilterValue),
)
.filter(
({ numComments }) =>
userFilters.numComments === undefined ||
numComments === userFilters.numComments,
); );
const sortComparators: Record< const sortComparators: Record<
@ -172,6 +168,14 @@ export default function ResumeHomePage() {
} }
}; };
const onShortcutChange = ({
sortOrder: shortcutSortOrder,
filters: shortcutFilters,
}: Shortcut) => {
setSortOrder(shortcutSortOrder);
setUserFilters(shortcutFilters);
};
return ( return (
<> <>
<Head> <Head>
@ -258,12 +262,11 @@ export default function ResumeHomePage() {
<ul <ul
className="flex flex-wrap justify-start gap-4 pb-6 text-sm font-medium text-gray-900" className="flex flex-wrap justify-start gap-4 pb-6 text-sm font-medium text-gray-900"
role="list"> role="list">
{TOP_HITS.map((category) => ( {SHORTCUTS.map((shortcut) => (
<li key={category.name}> <li key={shortcut.name}>
{/* TODO: Replace onClick with filtering function */}
<ResumeFilterPill <ResumeFilterPill
title={category.name} title={shortcut.name}
onClick={() => true} onClick={() => onShortcutChange(shortcut)}
/> />
</li> </li>
))} ))}
@ -271,9 +274,9 @@ export default function ResumeHomePage() {
<h3 className="text-md my-4 font-medium tracking-tight text-gray-900"> <h3 className="text-md my-4 font-medium tracking-tight text-gray-900">
Explore these filters: Explore these filters:
</h3> </h3>
{filters.map((section) => ( {filters.map((filter) => (
<Disclosure <Disclosure
key={section.id} key={filter.id}
as="div" as="div"
className="border-b border-gray-200 py-6"> className="border-b border-gray-200 py-6">
{({ open }) => ( {({ open }) => (
@ -281,7 +284,7 @@ export default function ResumeHomePage() {
<h3 className="-my-3 flow-root"> <h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-gray-400 hover:text-gray-500"> <Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900"> <span className="font-medium text-gray-900">
{section.name} {filter.label}
</span> </span>
<span className="ml-6 flex items-center"> <span className="ml-6 flex items-center">
{open ? ( {open ? (
@ -304,19 +307,19 @@ export default function ResumeHomePage() {
isLabelHidden={true} isLabelHidden={true}
label="" label=""
orientation="vertical"> orientation="vertical">
{section.options.map((option) => ( {filter.options.map((option) => (
<div <div
key={option.value} key={option.value}
className="[&>div>div:nth-child(2)>label]:font-normal [&>div>div:nth-child(1)>input]:text-indigo-600 [&>div>div:nth-child(1)>input]:ring-indigo-500"> className="[&>div>div:nth-child(2)>label]:font-normal [&>div>div:nth-child(1)>input]:text-indigo-600 [&>div>div:nth-child(1)>input]:ring-indigo-500">
<CheckboxInput <CheckboxInput
label={option.label} label={option.label}
value={userFilters[section.id].includes( value={userFilters[filter.id].includes(
option.value, option.value,
)} )}
onChange={(isChecked) => onChange={(isChecked) =>
onFilterCheckboxChange( onFilterCheckboxChange(
isChecked, isChecked,
section.id, filter.id,
option.value, option.value,
) )
} }

@ -18,12 +18,12 @@ import {
TextInput, TextInput,
} from '@tih/ui'; } from '@tih/ui';
import type { FilterOption } from '~/components/resumes/browse/resumeConstants'; import type { Filter } from '~/components/resumes/browse/resumeFilters';
import { import {
EXPERIENCE, EXPERIENCE,
LOCATION, LOCATION,
ROLE, ROLE,
} from '~/components/resumes/browse/resumeConstants'; } from '~/components/resumes/browse/resumeFilters';
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines'; import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
@ -47,17 +47,10 @@ type IFormInput = {
title: string; title: string;
}; };
type SelectorType = 'experience' | 'location' | 'role'; const selectors: Array<Filter> = [
type SelectorOptions = { { id: 'role', label: 'Role', options: ROLE },
key: SelectorType; { id: 'experience', label: 'Experience Level', options: EXPERIENCE },
label: string; { id: 'location', label: 'Location', options: LOCATION },
options: Array<FilterOption>;
};
const selectors: Array<SelectorOptions> = [
{ key: 'role', label: 'Role', options: ROLE },
{ key: 'experience', label: 'Experience Level', options: EXPERIENCE },
{ key: 'location', label: 'Location', options: LOCATION },
]; ];
type InitFormDetails = { type InitFormDetails = {
@ -309,14 +302,14 @@ export default function SubmitResumeForm({
</div> </div>
{/* Selectors */} {/* Selectors */}
{selectors.map((item) => ( {selectors.map((item) => (
<div key={item.key} className="mb-4"> <div key={item.id} className="mb-4">
<Select <Select
{...register(item.key, { required: true })} {...register(item.id, { required: true })}
disabled={isLoading} disabled={isLoading}
label={item.label} label={item.label}
options={item.options} options={item.options}
required={true} required={true}
onChange={(val) => setValue(item.key, val)} onChange={(val) => setValue(item.id, val)}
/> />
</div> </div>
))} ))}

Loading…
Cancel
Save