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

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

Loading…
Cancel
Save