mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +08:00
[resumes][feat] implement filter shortcuts
This commit is contained in:
@ -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',
|
|
||||||
},
|
|
||||||
];
|
|
146
apps/portal/src/components/resumes/browse/resumeFilters.ts
Normal file
146
apps/portal/src/components/resumes/browse/resumeFilters.ts
Normal file
@ -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>
|
||||||
))}
|
))}
|
||||||
|
Reference in New Issue
Block a user