From 5dbf2ae6aa59447ac6839c006e0c29870f2f6559 Mon Sep 17 00:00:00 2001 From: Wu Peirong Date: Sat, 5 Nov 2022 10:50:08 +0800 Subject: [PATCH] [resumes][refactor] use typeahead for browse filters --- .../resumes/browse/ResumeListItem.tsx | 21 +-- .../shared/ResumeExperienceTypeahead.tsx | 47 +++++ .../resumes/shared/ResumeRoleTypeahead.tsx | 2 +- apps/portal/src/pages/resumes/[resumeId].tsx | 167 +++++++++--------- apps/portal/src/pages/resumes/index.tsx | 143 +++++++++------ .../portal/src/utils/resumes/resumeFilters.ts | 107 ++++++----- 6 files changed, 288 insertions(+), 199 deletions(-) create mode 100644 apps/portal/src/components/resumes/shared/ResumeExperienceTypeahead.tsx diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index a9d1731c..00dbcc4f 100644 --- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -12,17 +12,7 @@ import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; -import type { - ExperienceFilter, - LocationFilter, - RoleFilter, -} from '~/utils/resumes/resumeFilters'; -import { - EXPERIENCES, - getFilterLabel, - LOCATIONS, - ROLES, -} from '~/utils/resumes/resumeFilters'; +import { getFilterLabel } from '~/utils/resumes/resumeFilters'; import type { Resume } from '~/types/resume'; @@ -64,17 +54,14 @@ export default function ResumeListItem({ href, resumeInfo }: Props) { aria-hidden="true" className="mr-1.5 h-4 w-4 flex-shrink-0" /> - {getFilterLabel(ROLES, resumeInfo.role as RoleFilter)} + {getFilterLabel('role', resumeInfo.role)}
@@ -103,7 +90,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) { })} by ${resumeInfo.user}`}
- {getFilterLabel(LOCATIONS, resumeInfo.location as LocationFilter)} + {getFilterLabel('location', resumeInfo.location)}
diff --git a/apps/portal/src/components/resumes/shared/ResumeExperienceTypeahead.tsx b/apps/portal/src/components/resumes/shared/ResumeExperienceTypeahead.tsx new file mode 100644 index 00000000..5a53103a --- /dev/null +++ b/apps/portal/src/components/resumes/shared/ResumeExperienceTypeahead.tsx @@ -0,0 +1,47 @@ +import type { ComponentProps } from 'react'; +import { useState } from 'react'; +import type { TypeaheadOption } from '@tih/ui'; +import { Typeahead } from '@tih/ui'; + +import { EXPERIENCES } from '~/utils/resumes/resumeFilters'; + +type BaseProps = Pick< + ComponentProps, + | 'disabled' + | 'errorMessage' + | 'isLabelHidden' + | 'placeholder' + | 'required' + | 'textSize' +>; + +type Props = BaseProps & + Readonly<{ + onSelect: (option: TypeaheadOption | null) => void; + value?: TypeaheadOption | null; + }>; + +export default function ResumeExperienceTypeahead({ + onSelect, + value, + ...props +}: Props) { + const [query, setQuery] = useState(''); + const options = EXPERIENCES.filter( + ({ label }) => + label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1, + ); + + return ( + + ); +} diff --git a/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx b/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx index 47ebb8a9..a42bbf37 100644 --- a/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx +++ b/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import type { TypeaheadOption } from '@tih/ui'; import { Typeahead } from '@tih/ui'; -import { JobTitleLabels } from '../../shared/JobTitles'; +import { JobTitleLabels } from '~/components/shared/JobTitles'; type BaseProps = Pick< ComponentProps, diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index ba7d9f42..8fc6b696 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -15,6 +15,7 @@ import { PencilSquareIcon, StarIcon, } from '@heroicons/react/20/solid'; +import type { TypeaheadOption } from '@tih/ui'; import { Button, Spinner } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; @@ -24,12 +25,6 @@ import ResumePdf from '~/components/resumes/ResumePdf'; import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText'; import loginPageHref from '~/components/shared/loginPageHref'; -import type { - ExperienceFilter, - FilterOption, - LocationFilter, - RoleFilter, -} from '~/utils/resumes/resumeFilters'; import { BROWSE_TABS_VALUES, EXPERIENCES, @@ -134,9 +129,7 @@ export default function ResumeReviewPage() { }) => { const getFilterValue = ( label: string, - filterOptions: Array< - FilterOption - >, + filterOptions: Array, ) => filterOptions.find((option) => option.label === label)?.value; router.push({ @@ -313,76 +306,92 @@ export default function ResumeReviewPage() {
{renderReviewButton()}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {detailsQuery.data.additionalInfo && ( +
+
+ )} + +
+
+ +
+
+
+ {renderReviewButton()} +
+
+ + Reviews + +
{detailsQuery.data.additionalInfo && ( diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index 46bb9476..28e69114 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -9,6 +9,7 @@ import { NewspaperIcon, XMarkIcon, } from '@heroicons/react/24/outline'; +import type { TypeaheadOption } from '@tih/ui'; import { Button, CheckboxInput, @@ -23,6 +24,9 @@ import { import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill'; import ResumeListItems from '~/components/resumes/browse/ResumeListItems'; +import ResumeExperienceTypeahead from '~/components/resumes/shared/ResumeExperienceTypeahead'; +import ResumeLocationTypeahead from '~/components/resumes/shared/ResumeLocationTypeahead'; +import ResumeRoleTypeahead from '~/components/resumes/shared/ResumeRoleTypeahead'; import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton'; import loginPageHref from '~/components/shared/loginPageHref'; @@ -32,6 +36,7 @@ import type { FilterLabel, Shortcut, } from '~/utils/resumes/resumeFilters'; +import type { FilterState, SortOrder } from '~/utils/resumes/resumeFilters'; import { BROWSE_TABS_VALUES, EXPERIENCES, @@ -47,8 +52,6 @@ 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; @@ -200,10 +203,10 @@ export default function ResumeHomePage() { [ 'resumes.resume.findAll', { - experienceFilters: userFilters.experience, + experienceFilters: userFilters.experience.map(({ value }) => value), isUnreviewed: userFilters.isUnreviewed, - locationFilters: userFilters.location, - roleFilters: userFilters.role, + locationFilters: userFilters.location.map(({ value }) => value), + roleFilters: userFilters.role.map(({ value }) => value), searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), skip, sortOrder, @@ -219,10 +222,10 @@ export default function ResumeHomePage() { [ 'resumes.resume.user.findUserStarred', { - experienceFilters: userFilters.experience, + experienceFilters: userFilters.experience.map(({ value }) => value), isUnreviewed: userFilters.isUnreviewed, - locationFilters: userFilters.location, - roleFilters: userFilters.role, + locationFilters: userFilters.location.map(({ value }) => value), + roleFilters: userFilters.role.map(({ value }) => value), searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), skip, sortOrder, @@ -239,10 +242,10 @@ export default function ResumeHomePage() { [ 'resumes.resume.user.findUserCreated', { - experienceFilters: userFilters.experience, + experienceFilters: userFilters.experience.map(({ value }) => value), isUnreviewed: userFilters.isUnreviewed, - locationFilters: userFilters.location, - roleFilters: userFilters.role, + locationFilters: userFilters.location.map(({ value }) => value), + roleFilters: userFilters.role.map(({ value }) => value), searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), skip, sortOrder, @@ -264,31 +267,6 @@ export default function ResumeHomePage() { } }; - 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, - ), - }); - } - gaEvent({ - action: 'resumes.filter_checkbox_click', - category: 'engagement', - label: 'Select Filter', - }); - }; - const onClearFilterClick = (filterSection: FilterId) => { setUserFilters({ ...userFilters, @@ -354,6 +332,52 @@ export default function ResumeHomePage() { return getTabQueryData()?.filterCounts; }; + const getFilterTypeahead = (filterId: FilterId) => { + const onSelect = (option: TypeaheadOption | null) => { + if (option === null) { + return; + } + setUserFilters({ + ...userFilters, + [filterId]: [...userFilters[filterId], option], + }); + gaEvent({ + action: 'resumes.filter_typeahead_click', + category: 'engagement', + label: 'Select Filter', + }); + }; + + switch (filterId) { + case 'experience': + return ( + + ); + case 'location': + return ( + + ); + case 'role': + return ( + + ); + default: + return null; + } + }; + const getFilterCount = (filter: FilterLabel, value: string) => { const filterCountsData = getTabFilterCounts(); if (!filterCountsData) { @@ -461,21 +485,24 @@ export default function ResumeHomePage() {
- {filter.options.map((option) => ( + {getFilterTypeahead(filter.id)} + {userFilters[filter.id].map((option) => (
- onFilterCheckboxChange( - isChecked, - filter.id, - option.value, - ) + value={true} + onChange={() => + setUserFilters({ + ...userFilters, + [filter.id]: userFilters[ + filter.id + ].filter( + ({ value }) => + value !== option.value, + ), + }) } /> @@ -570,26 +597,28 @@ export default function ResumeHomePage() { + {getFilterTypeahead(filter.id)} - {filter.options.map((option) => ( + {userFilters[filter.id].map((option) => (
- onFilterCheckboxChange( - isChecked, - filter.id, - option.value, - ) + value={true} + onChange={() => + setUserFilters({ + ...userFilters, + [filter.id]: userFilters[ + filter.id + ].filter( + ({ value }) => value !== option.value, + ), + }) } /> @@ -660,7 +689,7 @@ export default function ResumeHomePage() {
+ label={getFilterLabel('sort', sortOrder)}> {SORT_OPTIONS.map(({ label, value }) => ( = { label: string; value: T; @@ -31,10 +17,11 @@ export type FilterOption = { export type Filter = { id: FilterId; label: FilterLabel; - options: Array>; + options: Array; }; -export type FilterState = CustomFilter & Record>; +export type FilterState = CustomFilter & + Record>; export type SortOrder = 'latest' | 'mostComments' | 'popular'; @@ -57,45 +44,54 @@ export const SORT_OPTIONS: Array> = [ { label: 'Most Comments', value: 'mostComments' }, ]; -export const ROLES: Array> = [ +export const ROLES: Array = [ + { + id: 'software-engineer', + label: 'Software Engineer', + value: 'software-engineer', + }, + { + id: 'back-end-engineer', + label: 'Back End Engineer', + value: 'back-end-engineer', + }, { - label: 'Full-Stack Engineer', - value: 'Full-Stack Engineer', + id: 'front-end-engineer', + label: 'Front End Engineer', + value: 'front-end-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 EXPERIENCES: Array> = [ - { label: 'Internship', value: 'Internship' }, +export const EXPERIENCES: Array = [ { + id: 'Entry Level (0 - 2 years)', label: 'Entry Level (0 - 2 years)', value: 'Entry Level (0 - 2 years)', }, { + id: 'Internship', + label: 'Internship', + value: 'Internship', + }, + { + id: 'Mid Level (3 - 5 years)', label: 'Mid Level (3 - 5 years)', value: 'Mid Level (3 - 5 years)', }, { + id: 'Senior Level (5+ years)', label: 'Senior Level (5+ years)', value: 'Senior Level (5+ years)', }, ]; -export const LOCATIONS: Array> = [ - { label: 'Singapore', value: 'Singapore' }, - { label: 'United States', value: 'United States' }, - { label: 'India', value: 'India' }, -]; +export const LOCATIONS: Array = []; export const INITIAL_FILTER_STATE: FilterState = { - experience: Object.values(EXPERIENCES).map(({ value }) => value), + experience: EXPERIENCES, isUnreviewed: true, - location: Object.values(LOCATIONS).map(({ value }) => value), - role: Object.values(ROLES).map(({ value }) => value), + location: [], + role: ROLES, }; export const SHORTCUTS: Array = [ @@ -118,7 +114,7 @@ export const SHORTCUTS: Array = [ { filters: { ...INITIAL_FILTER_STATE, - experience: ['Entry Level (0 - 2 years)'], + experience: [], isUnreviewed: false, }, name: 'Fresh Grad', @@ -136,7 +132,7 @@ export const SHORTCUTS: Array = [ filters: { ...INITIAL_FILTER_STATE, isUnreviewed: false, - location: ['United States'], + location: [], }, name: 'US Only', sortOrder: 'latest', @@ -154,8 +150,29 @@ export const isInitialFilterState = (filters: FilterState) => }); export const getFilterLabel = ( - filters: Array< - FilterOption - >, - filterValue: ExperienceFilter | LocationFilter | RoleFilter | SortOrder, -) => filters.find(({ value }) => value === filterValue)?.label ?? filterValue; + filterId: FilterId | 'sort', + filterValue: SortOrder | string, +) => { + let filters: Array = []; + + switch (filterId) { + case 'experience': + filters = EXPERIENCES; + break; + case 'location': + break; + case 'role': + filters = Object.entries(JobTitleLabels).map(([slug, label]) => ({ + id: slug, + label, + value: slug, + })); + break; + case 'sort': + return SORT_OPTIONS.find(({ value }) => value === filterValue)?.label; + default: + break; + } + + return filters.find(({ value }) => value === filterValue)?.label; +};