mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-05-04 15:12:51 +08:00
[resumes][feat] migrate to use location db and role enum (#506)
* [resumes][feat] use role and countries typeaheads * [resumes][feat] add location and role typeaheads * [resumes][chore] locationId migration * [resumes][fix] update upsert to take in locationId * [resumes][refactor] use typeahead for browse filters * [resumes][feat] use role and countries typeaheads * [resumes][chore] locationId migration * [resumes][feat] fetch location on resumes page * [resumes][feat] enable edit resume form * [resumes][misc] update namings and oredrings * [resumes][feat] add default locations * [resumes][fix] truncate title text in resume card * [resumes][fix] filter out selected options * [resumes][feat] add more countries to default search * [resumes][feat] update default roles * [resumes][chore] revert removal of value * [resumes]feat] add default location for migration file * [resumes][fix] fix merge conflicts Co-authored-by: Wu Peirong <wupeirong294@gmail.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -47,15 +37,14 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
|
||||
<div className="col-span-7 grid gap-4 border-b border-slate-200 p-4 hover:bg-slate-100 sm:grid-cols-7">
|
||||
<div className="sm:col-span-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{resumeInfo.title}
|
||||
<p className="truncate">{resumeInfo.title}</p>
|
||||
<p
|
||||
className={clsx(
|
||||
'w-auto items-center space-x-4 rounded-xl border border-slate-300 px-2 py-1 text-xs font-medium text-white opacity-60',
|
||||
resumeInfo.isResolved ? 'bg-slate-400' : 'bg-success-500',
|
||||
'w-auto items-center space-x-4 rounded-xl border px-2 py-1 text-xs font-medium',
|
||||
resumeInfo.isResolved ? 'bg-slate-300' : 'bg-success-100',
|
||||
resumeInfo.isResolved ? 'text-slate-600' : 'text-success-700',
|
||||
)}>
|
||||
<span className="opacity-100">
|
||||
{resumeInfo.isResolved ? 'Reviewed' : 'Unreviewed'}
|
||||
</span>
|
||||
{resumeInfo.isResolved ? 'Reviewed' : 'Unreviewed'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-primary-500 mt-2 flex items-center justify-start text-xs">
|
||||
@@ -64,17 +53,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)}
|
||||
</div>
|
||||
<div className="ml-4 flex">
|
||||
<AcademicCapIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
{getFilterLabel(
|
||||
EXPERIENCES,
|
||||
resumeInfo.experience as ExperienceFilter,
|
||||
)}
|
||||
{getFilterLabel('experience', resumeInfo.experience)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-start text-xs text-slate-500">
|
||||
@@ -102,9 +88,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
|
||||
addSuffix: true,
|
||||
})} by ${resumeInfo.user}`}
|
||||
</div>
|
||||
<div className="mt-2 text-slate-400">
|
||||
{getFilterLabel(LOCATIONS, resumeInfo.location as LocationFilter)}
|
||||
</div>
|
||||
<div className="mt-2 text-slate-400">{resumeInfo.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="col-span-1 w-8 self-center justify-self-center text-slate-400" />
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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<typeof Typeahead>,
|
||||
| 'disabled'
|
||||
| 'errorMessage'
|
||||
| 'isLabelHidden'
|
||||
| 'placeholder'
|
||||
| 'required'
|
||||
| 'textSize'
|
||||
>;
|
||||
|
||||
type Props = BaseProps &
|
||||
Readonly<{
|
||||
onSelect: (option: TypeaheadOption | null) => void;
|
||||
selectedValues?: Set<string>;
|
||||
value?: TypeaheadOption | null;
|
||||
}>;
|
||||
|
||||
export default function ResumeExperienceTypeahead({
|
||||
onSelect,
|
||||
selectedValues = new Set(),
|
||||
value,
|
||||
...props
|
||||
}: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const options = EXPERIENCES.filter(
|
||||
(option) => !selectedValues.has(option.value),
|
||||
).filter(
|
||||
({ label }) =>
|
||||
label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1,
|
||||
);
|
||||
|
||||
return (
|
||||
<Typeahead
|
||||
label="Experiences"
|
||||
noResultsMessage="No available experiences."
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
}>;
|
||||
|
||||
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
|
||||
.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name,
|
||||
value: id,
|
||||
}))
|
||||
.filter((option) => !selectedValues.has(option.value));
|
||||
}, [countries, selectedValues]);
|
||||
|
||||
return (
|
||||
<Typeahead
|
||||
label="Location"
|
||||
noResultsMessage="No location found"
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { TypeaheadOption } from '@tih/ui';
|
||||
import { Typeahead } from '@tih/ui';
|
||||
|
||||
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||
|
||||
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;
|
||||
}>;
|
||||
|
||||
export default function ResumeRoleTypeahead({
|
||||
onSelect,
|
||||
selectedValues = new Set(),
|
||||
value,
|
||||
...props
|
||||
}: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const options = Object.entries(JobTitleLabels)
|
||||
.map(([slug, label]) => ({
|
||||
id: slug,
|
||||
label,
|
||||
value: slug,
|
||||
}))
|
||||
.filter((option) => !selectedValues.has(option.value))
|
||||
.filter(
|
||||
({ label }) =>
|
||||
label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1,
|
||||
);
|
||||
|
||||
return (
|
||||
<Typeahead
|
||||
label="Role"
|
||||
noResultsMessage="No available roles."
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user