mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-04-13 16:29:43 +08:00
[resumes][refactor] use CountriesTypeahead instead
This commit is contained in:
@@ -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<typeof Typeahead>,
|
||||
| 'disabled'
|
||||
| 'errorMessage'
|
||||
| 'isLabelHidden'
|
||||
| 'placeholder'
|
||||
| 'required'
|
||||
| 'textSize'
|
||||
>;
|
||||
|
||||
type Props = BaseProps &
|
||||
Readonly<{
|
||||
onSelect: (option: TypeaheadOption | null) => void;
|
||||
selectedValues?: Set<string>;
|
||||
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 (
|
||||
<Typeahead
|
||||
label="Location"
|
||||
minQueryLength={2}
|
||||
noResultsMessage="No location found"
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -17,11 +17,25 @@ type BaseProps = Pick<
|
||||
|
||||
type Props = BaseProps &
|
||||
Readonly<{
|
||||
excludedValues?: Set<string>;
|
||||
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 (
|
||||
<Typeahead
|
||||
label="Country"
|
||||
minQueryLength={2}
|
||||
label={label}
|
||||
noResultsMessage="No countries found"
|
||||
nullable={true}
|
||||
options={(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.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}
|
||||
|
||||
@@ -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 (
|
||||
<ResumeLocationTypeahead
|
||||
isLabelHidden={true}
|
||||
placeholder="Select locations"
|
||||
selectedValues={
|
||||
<CountriesTypeahead
|
||||
excludedValues={
|
||||
new Set(userFilters[filterId].map(({ value }) => value))
|
||||
}
|
||||
isLabelHidden={true}
|
||||
label="Location"
|
||||
placeholder="Select countries"
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 } }) => (
|
||||
<ResumeLocationTypeahead
|
||||
<CountriesTypeahead
|
||||
disabled={isLoading}
|
||||
placeholder="Select a location"
|
||||
label="Location"
|
||||
placeholder="Enter a country"
|
||||
required={true}
|
||||
value={value}
|
||||
onSelect={(option) => onSelect('location', option)}
|
||||
|
||||
Reference in New Issue
Block a user