From 1ebd32ca2f38e7978d21ea97645a59f324f7f7a5 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Sat, 5 Nov 2022 21:56:46 +0800 Subject: [PATCH] [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 --- apps/portal/package.json | 1 - .../migration.sql | 13 ++ apps/portal/prisma/schema.prisma | 4 +- .../resumes/browse/ResumeListItem.tsx | 34 +-- .../shared/ResumeExperienceTypeahead.tsx | 51 +++++ .../shared/ResumeLocationTypeahead.tsx | 66 ++++++ .../resumes/shared/ResumeRoleTypeahead.tsx | 56 +++++ apps/portal/src/pages/resumes/[resumeId].tsx | 88 ++++---- apps/portal/src/pages/resumes/index.tsx | 204 ++++++++++-------- apps/portal/src/pages/resumes/submit.tsx | 92 +++++--- .../router/resumes/resumes-resume-router.ts | 68 ++---- .../resumes/resumes-resume-user-router.ts | 119 +++------- apps/portal/src/types/resume.d.ts | 1 + .../portal/src/utils/resumes/resumeFilters.ts | 201 +++++++++++------ 14 files changed, 605 insertions(+), 393 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221104095551_use_location_database_for_resumes/migration.sql create mode 100644 apps/portal/src/components/resumes/shared/ResumeExperienceTypeahead.tsx create mode 100644 apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx create mode 100644 apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx diff --git a/apps/portal/package.json b/apps/portal/package.json index cda1ec4e..fb8b60b9 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -43,7 +43,6 @@ "react-query": "^3.39.2", "read-excel-file": "^5.5.3", "superjson": "^1.10.0", - "xlsx": "^0.18.5", "unique-names-generator": "^4.7.1", "xlsx": "^0.18.5", "zod": "^3.18.0" diff --git a/apps/portal/prisma/migrations/20221104095551_use_location_database_for_resumes/migration.sql b/apps/portal/prisma/migrations/20221104095551_use_location_database_for_resumes/migration.sql new file mode 100644 index 00000000..e9bda081 --- /dev/null +++ b/apps/portal/prisma/migrations/20221104095551_use_location_database_for_resumes/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `location` on the `ResumesResume` table. All the data in the column will be lost. + - Added the required column `locationId` to the `ResumesResume` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable. Set default location to Singapore. +ALTER TABLE "ResumesResume" DROP COLUMN "location", +ADD COLUMN "locationId" TEXT NOT NULL DEFAULT '196'; + +-- AddForeignKey +ALTER TABLE "ResumesResume" ADD CONSTRAINT "ResumesResume_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Country"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 662a321c..77a8d44b 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -112,6 +112,7 @@ model Country { code String @unique states State[] questionsQuestionEncounters QuestionsQuestionEncounter[] + ResumesResume ResumesResume[] } model State { @@ -148,13 +149,14 @@ model ResumesResume { // TODO: Update role, experience, location to use Enums role String @db.Text experience String @db.Text - location String @db.Text + locationId String url String additionalInfo String? @db.Text isResolved Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) + location Country @relation(fields: [locationId], references: [id], onDelete: Cascade) stars ResumesStar[] comments ResumesComment[] } diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index a9d1731c..daa36dca 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'; @@ -47,15 +37,14 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
- {resumeInfo.title} +

{resumeInfo.title}

- - {resumeInfo.isResolved ? 'Reviewed' : 'Unreviewed'} - + {resumeInfo.isResolved ? 'Reviewed' : 'Unreviewed'}

@@ -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)}
@@ -102,9 +88,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) { addSuffix: true, })} by ${resumeInfo.user}`}
-
- {getFilterLabel(LOCATIONS, resumeInfo.location as LocationFilter)} -
+
{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..f343665c --- /dev/null +++ b/apps/portal/src/components/resumes/shared/ResumeExperienceTypeahead.tsx @@ -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, + | 'disabled' + | 'errorMessage' + | 'isLabelHidden' + | 'placeholder' + | 'required' + | 'textSize' +>; + +type Props = BaseProps & + Readonly<{ + onSelect: (option: TypeaheadOption | null) => void; + selectedValues?: Set; + 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 ( + + ); +} diff --git a/apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx b/apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx new file mode 100644 index 00000000..cbac0a18 --- /dev/null +++ b/apps/portal/src/components/resumes/shared/ResumeLocationTypeahead.tsx @@ -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, + | 'disabled' + | 'errorMessage' + | 'isLabelHidden' + | 'placeholder' + | 'required' + | 'textSize' +>; + +type Props = BaseProps & + Readonly<{ + onSelect: (option: TypeaheadOption | null) => void; + selectedValues?: Set; + 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 ( + + ); +} diff --git a/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx b/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx new file mode 100644 index 00000000..d619ccf2 --- /dev/null +++ b/apps/portal/src/components/resumes/shared/ResumeRoleTypeahead.tsx @@ -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, + | 'disabled' + | 'errorMessage' + | 'isLabelHidden' + | 'placeholder' + | 'required' + | 'textSize' +>; + +type Props = BaseProps & + Readonly<{ + onSelect: (option: TypeaheadOption | null) => void; + selectedValues?: Set; + 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 ( + + ); +} diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index ba7d9f42..fc547bb4 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -24,23 +24,17 @@ 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, getFilterLabel, + getTypeaheadOption, INITIAL_FILTER_STATE, - LOCATIONS, - ROLES, } from '~/utils/resumes/resumeFilters'; import { trpc } from '~/utils/trpc'; import SubmitResumeForm from './submit'; +import type { JobTitleType } from '../../components/shared/JobTitles'; +import { getLabelForJobTitleType } from '../../components/shared/JobTitles'; export default function ResumeReviewPage() { const ErrorPage = ( @@ -124,29 +118,24 @@ export default function ResumeReviewPage() { }; const onInfoTagClick = ({ - locationLabel, - experienceLabel, - roleLabel, + locationName, + locationValue, + experienceValue, + roleValue, }: { - experienceLabel?: string; - locationLabel?: string; - roleLabel?: string; + experienceValue?: string; + locationName?: string; + locationValue?: string; + roleValue?: string; }) => { - const getFilterValue = ( - label: string, - filterOptions: Array< - FilterOption - >, - ) => filterOptions.find((option) => option.label === label)?.value; - router.push({ pathname: '/resumes', query: { currentPage: JSON.stringify(1), isFiltersOpen: JSON.stringify({ - experience: experienceLabel !== undefined, - location: locationLabel !== undefined, - role: roleLabel !== undefined, + experience: experienceValue !== undefined, + location: locationValue !== undefined, + role: roleValue !== undefined, }), searchValue: JSON.stringify(''), shortcutSelected: JSON.stringify('all'), @@ -154,14 +143,16 @@ export default function ResumeReviewPage() { tabsValue: JSON.stringify(BROWSE_TABS_VALUES.ALL), userFilters: JSON.stringify({ ...INITIAL_FILTER_STATE, - ...(locationLabel && { - location: [getFilterValue(locationLabel, LOCATIONS)], + ...(locationValue && { + location: [ + getTypeaheadOption('location', locationValue, locationName), + ], }), - ...(roleLabel && { - role: [getFilterValue(roleLabel, ROLES)], + ...(roleValue && { + role: [getTypeaheadOption('role', roleValue)], }), - ...(experienceLabel && { - experience: [getFilterValue(experienceLabel, EXPERIENCES)], + ...(experienceValue && { + experience: [getTypeaheadOption('experience', experienceValue)], }), }), }, @@ -207,9 +198,19 @@ export default function ResumeReviewPage() { initFormDetails={{ additionalInfo: detailsQuery.data.additionalInfo ?? '', experience: detailsQuery.data.experience, - location: detailsQuery.data.location, + location: { + id: detailsQuery.data.locationId, + label: detailsQuery.data.location.name, + value: detailsQuery.data.locationId, + }, resumeId: resumeId as string, - role: detailsQuery.data.role, + role: { + id: detailsQuery.data.role, + label: getLabelForJobTitleType( + detailsQuery.data.role as JobTitleType, + ), + value: detailsQuery.data.role, + }, title: detailsQuery.data.title, url: detailsQuery.data.url, }} @@ -325,13 +326,10 @@ export default function ResumeReviewPage() { type="button" onClick={() => onInfoTagClick({ - roleLabel: detailsQuery.data?.role, + roleValue: detailsQuery.data?.role, }) }> - {getFilterLabel( - ROLES, - detailsQuery.data.role as RoleFilter, - )} + {getFilterLabel('role', detailsQuery.data.role)}
@@ -344,13 +342,11 @@ export default function ResumeReviewPage() { type="button" onClick={() => onInfoTagClick({ - locationLabel: detailsQuery.data?.location, + locationName: detailsQuery.data?.location.name, + locationValue: detailsQuery.data?.locationId, }) }> - {getFilterLabel( - LOCATIONS, - detailsQuery.data.location as LocationFilter, - )} + {detailsQuery.data?.location.name}
@@ -363,12 +359,12 @@ export default function ResumeReviewPage() { type="button" onClick={() => onInfoTagClick({ - experienceLabel: detailsQuery.data?.experience, + experienceValue: detailsQuery.data?.experience, }) }> {getFilterLabel( - EXPERIENCES, - detailsQuery.data.experience as ExperienceFilter, + 'experience', + detailsQuery.data.experience, )}
diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index 46bb9476..382425f5 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,23 +24,18 @@ 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'; -import type { - Filter, - FilterId, - FilterLabel, - Shortcut, -} from '~/utils/resumes/resumeFilters'; +import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters'; +import type { SortOrder } from '~/utils/resumes/resumeFilters'; import { BROWSE_TABS_VALUES, - EXPERIENCES, getFilterLabel, INITIAL_FILTER_STATE, - isInitialFilterState, - LOCATIONS, - ROLES, SHORTCUTS, SORT_OPTIONS, } from '~/utils/resumes/resumeFilters'; @@ -47,8 +43,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; @@ -56,17 +50,14 @@ const filters: Array = [ { id: 'role', label: 'Role', - options: ROLES, }, { id: 'experience', label: 'Experience', - options: EXPERIENCES, }, { id: 'location', label: 'Location', - options: LOCATIONS, }, ]; @@ -81,20 +72,14 @@ const getLoggedOutText = (tabsValue: string) => { } }; -const getEmptyDataText = ( - tabsValue: string, - searchValue: string, - userFilters: FilterState, -) => { +const getEmptyDataText = (tabsValue: string, searchValue: string) => { if (searchValue.length > 0) { return 'Try tweaking your search text to see more resumes.'; } - if (!isInitialFilterState(userFilters)) { - return 'Try tweaking your filters to see more resumes.'; - } + switch (tabsValue) { case BROWSE_TABS_VALUES.ALL: - return "There's nothing to see here..."; + return 'Oops, there is no resumes to see here. Maybe try tweaking your filters to see more.'; case BROWSE_TABS_VALUES.STARRED: return 'You have not starred any resumes. Star one to see it here!'; case BROWSE_TABS_VALUES.MY: @@ -200,10 +185,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 +204,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 +224,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 +249,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,12 +314,71 @@ export default function ResumeHomePage() { return getTabQueryData()?.filterCounts; }; - const getFilterCount = (filter: FilterLabel, value: string) => { + 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 ( + value)) + } + onSelect={onSelect} + /> + ); + case 'location': + return ( + value)) + } + onSelect={onSelect} + /> + ); + case 'role': + return ( + value)) + } + onSelect={onSelect} + /> + ); + default: + return null; + } + }; + + const getFilterCount = (filterId: FilterId, value: string) => { const filterCountsData = getTabFilterCounts(); - if (!filterCountsData) { + if ( + filterCountsData === undefined || + filterCountsData[filterId] === undefined || + filterCountsData[filterId][value] === undefined + ) { return 0; } - return filterCountsData[filter][value]; + return filterCountsData[filterId][value]; }; return ( @@ -461,29 +480,28 @@ export default function ResumeHomePage() {
- {filter.options.map((option) => ( + {getFilterTypeahead(filter.id)} + {userFilters[filter.id].map((option) => (
+ className="flex items-center px-1 text-sm"> - onFilterCheckboxChange( - isChecked, - filter.id, - option.value, - ) + value={true} + onChange={() => + setUserFilters({ + ...userFilters, + [filter.id]: userFilters[ + filter.id + ].filter( + ({ value }) => + value !== option.value, + ), + }) } /> - ( - {getFilterCount( - filter.label, - option.label, - )} + ({getFilterCount(filter.id, option.value)} )
@@ -570,32 +588,32 @@ export default function ResumeHomePage() { + {getFilterTypeahead(filter.id)} - {filter.options.map((option) => ( + {userFilters[filter.id].map((option) => (
+ className="flex items-center px-1 text-sm"> - onFilterCheckboxChange( - isChecked, - filter.id, - option.value, - ) + value={true} + onChange={() => + setUserFilters({ + ...userFilters, + [filter.id]: userFilters[ + filter.id + ].filter( + ({ value }) => value !== option.value, + ), + }) } /> - ( - {getFilterCount(filter.label, option.label)} - ) + ({getFilterCount(filter.id, option.value)})
))} @@ -660,7 +678,7 @@ export default function ResumeHomePage() {
+ label={getFilterLabel('sort', sortOrder)}> {SORT_OPTIONS.map(({ label, value }) => ( - {getEmptyDataText(tabsValue, searchValue, userFilters)} + {getEmptyDataText(tabsValue, searchValue)} ) : (
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index cb631866..d509334e 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -7,8 +7,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { FileRejection } from 'react-dropzone'; import { useDropzone } from 'react-dropzone'; import type { SubmitHandler } from 'react-hook-form'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { ArrowUpCircleIcon } from '@heroicons/react/24/outline'; +import type { TypeaheadOption } from '@tih/ui'; import { Button, CheckboxInput, @@ -20,12 +21,14 @@ 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 loginPageHref from '~/components/shared/loginPageHref'; import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; -import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters'; +import { EXPERIENCES } from '~/utils/resumes/resumeFilters'; import { trpc } from '~/utils/trpc'; const FILE_SIZE_LIMIT_MB = 3; @@ -41,19 +44,20 @@ type IFormInput = { experience: string; file: File; isChecked: boolean; - location: string; - role: string; + location: TypeaheadOption; + role: TypeaheadOption; title: string; }; type InputKeys = keyof IFormInput; +type TypeAheadKeys = keyof Pick; type InitFormDetails = { additionalInfo?: string; experience: string; - location: string; + location: TypeaheadOption; resumeId: string; - role: string; + role: TypeaheadOption; title: string; url: string; }; @@ -85,6 +89,7 @@ export default function SubmitResumeForm({ register, handleSubmit, setValue, + control, reset, watch, clearErrors, @@ -94,8 +99,6 @@ export default function SubmitResumeForm({ additionalInfo: '', experience: '', isChecked: false, - location: '', - role: '', title: '', ...initFormDetails, }, @@ -136,6 +139,11 @@ export default function SubmitResumeForm({ }, [router, status]); const onSubmit: SubmitHandler = async (data) => { + if (!isDirty) { + onClose(); + return; + } + setIsLoading(true); let fileUrl = initFormDetails?.url ?? ''; @@ -158,8 +166,8 @@ export default function SubmitResumeForm({ additionalInfo: data.additionalInfo, experience: data.experience, id: initFormDetails?.resumeId, - location: data.location, - role: data.role, + locationId: data.location.value, + role: data.role.value, title: data.title, url: fileUrl, }, @@ -235,6 +243,13 @@ export default function SubmitResumeForm({ setValue(section, value.trim(), { shouldDirty: true }); }; + const onSelect = (section: TypeAheadKeys, option: TypeaheadOption | null) => { + if (option == null) { + return; + } + setValue(section, option, { shouldDirty: true }); + }; + return ( <> @@ -299,35 +314,42 @@ export default function SubmitResumeForm({ required={true} onChange={(val) => onValueChange('title', val)} /> -
- onValueChange('experience', val)} - /> -
+ ( + onSelect('location', option)} + /> + )} + rules={{ required: true }} + /> + ( + onSelect('role', option)} + /> + )} + rules={{ required: true }} + />