diff --git a/apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx b/apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx deleted file mode 100644 index a5f8efe4..00000000 --- a/apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { ComponentProps } from 'react'; -import { useMemo, useState } from 'react'; -import type { TypeaheadOption } from '@tih/ui'; -import { Typeahead } from '@tih/ui'; - -import { trpc } from '~/utils/trpc'; - -type BaseProps = Pick< - ComponentProps, - | 'disabled' - | 'errorMessage' - | 'isLabelHidden' - | 'placeholder' - | 'required' - | 'textSize' ->; - -type Props = BaseProps & - Readonly<{ - onSelect: (option: TypeaheadOption | null) => void; - selectedValues?: Set; - value?: TypeaheadOption | null; - }>; - -// TODO: Merge with CountriesTypeahead instead. -export default function ResumeLocationTypeahead({ - onSelect, - selectedValues = new Set(), - value, - ...props -}: Props) { - const [query, setQuery] = useState(''); - const countries = trpc.useQuery([ - 'locations.countries.list', - { - name: query, - }, - ]); - - const options = useMemo(() => { - const { data } = countries; - if (data == null) { - return []; - } - - return ( - data - // Client-side sorting by position of query string appearing - // in the country name since we can't do that in Prisma. - .sort((a, b) => a.name.indexOf(query) - b.name.indexOf(query)) - .map(({ id, name }) => ({ - id, - label: name, - value: id, - })) - .filter((option) => !selectedValues.has(option.value)) - ); - }, [countries, query, selectedValues]); - - return ( - - ); -} diff --git a/apps/portal/src/components/shared/CountriesTypeahead.tsx b/apps/portal/src/components/shared/CountriesTypeahead.tsx index 81cabd8c..af0a6465 100644 --- a/apps/portal/src/components/shared/CountriesTypeahead.tsx +++ b/apps/portal/src/components/shared/CountriesTypeahead.tsx @@ -17,11 +17,25 @@ type BaseProps = Pick< type Props = BaseProps & Readonly<{ + excludedValues?: Set; + label?: string; onSelect: (option: TypeaheadOption | null) => void; value?: TypeaheadOption | null; }>; +function stringPositionComparator(a: string, b: string, query: string): number { + const normalizedQueryString = query.trim().toLocaleLowerCase(); + const positionA = a.toLocaleLowerCase().indexOf(normalizedQueryString); + const positionB = b.toLocaleLowerCase().indexOf(normalizedQueryString); + return ( + (positionA === -1 ? 9999 : positionA) - + (positionB === -1 ? 9999 : positionB) + ); +} + export default function CountriesTypeahead({ + excludedValues, + label = 'Country', onSelect, value, ...props @@ -38,23 +52,37 @@ export default function CountriesTypeahead({ return ( - a.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) - - b.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()), - ) + .sort((a, b) => { + const normalizedQueryString = query.trim().toLocaleLowerCase(); + if ( + a.code.toLocaleLowerCase() === normalizedQueryString || + b.code.toLocaleLowerCase() === normalizedQueryString + ) { + return stringPositionComparator( + a.code, + b.code, + normalizedQueryString, + ); + } + + return stringPositionComparator( + a.name, + b.name, + normalizedQueryString, + ); + }) .map(({ id, name }) => ({ id, label: name, value: id, - }))} + })) + .filter((option) => !excludedValues?.has(option.value))} value={value} onQueryChange={setQuery} onSelect={onSelect} diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index 382425f5..ca4c1639 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -25,9 +25,9 @@ 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 CountriesTypeahead from '~/components/shared/CountriesTypeahead'; import loginPageHref from '~/components/shared/loginPageHref'; import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters'; @@ -344,12 +344,13 @@ export default function ResumeHomePage() { ); case 'location': return ( - value)) } + isLabelHidden={true} + label="Location" + placeholder="Select countries" onSelect={onSelect} /> ); diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index d509334e..c6721931 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -21,10 +21,10 @@ import { } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; -import ResumeLocationTypeahead from '~/components/resumes/shared/ResumeLocationTypeahead'; import ResumeRoleTypeahead from '~/components/resumes/shared/ResumeRoleTypeahead'; import ResumeSubmissionGuidelines from '~/components/resumes/submit-form/ResumeSubmissionGuidelines'; import Container from '~/components/shared/Container'; +import CountriesTypeahead from '~/components/shared/CountriesTypeahead'; import loginPageHref from '~/components/shared/loginPageHref'; import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; @@ -318,9 +318,10 @@ export default function SubmitResumeForm({ control={control} name="location" render={({ field: { value } }) => ( - onSelect('location', option)}