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)}
- {getFilterLabel(
- EXPERIENCES,
- resumeInfo.experience as ExperienceFilter,
- )}
+ {getFilterLabel('experience', resumeInfo.experience)}
@@ -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)}
/>
-
-
+ (
+ onSelect('location', option)}
+ />
+ )}
+ rules={{ required: true }}
+ />
+ (
+ onSelect('role', option)}
+ />
+ )}
+ rules={{ required: true }}
+ />
onValueChange('location', val)}
+ onChange={(val) => onValueChange('experience', val)}
/>
{/* Upload resume form */}
{isNewForm && (
diff --git a/apps/portal/src/server/router/resumes/resumes-resume-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-router.ts
index ec4a36b4..81c5625b 100644
--- a/apps/portal/src/server/router/resumes/resumes-resume-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-resume-router.ts
@@ -1,8 +1,6 @@
import { z } from 'zod';
import { Vote } from '@prisma/client';
-import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
-
import { createRouter } from '../context';
import type { Resume } from '~/types/resume';
@@ -35,7 +33,7 @@ export const resumesRouter = createRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
@@ -49,6 +47,11 @@ export const resumesRouter = createRouter()
},
},
comments: true,
+ location: {
+ select: {
+ name: true,
+ },
+ },
stars: {
where: {
OR: {
@@ -79,7 +82,7 @@ export const resumesRouter = createRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
@@ -92,7 +95,8 @@ export const resumesRouter = createRouter()
id: r.id,
isResolved: r.isResolved,
isStarredByUser: r.stars.length > 0,
- location: r.location,
+ location: r.location.name,
+ locationId: r.locationId,
numComments: r._count.comments,
numStars: r._count.stars,
role: r.role,
@@ -103,7 +107,7 @@ export const resumesRouter = createRouter()
return resume;
});
- // Group by role and count, taking into account all role/experience/location/isUnreviewed filters and search value
+ // Group by role and count, taking into account all role/experience/locationId/isUnreviewed filters and search value
const roleCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
@@ -112,7 +116,7 @@ export const resumesRouter = createRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
});
@@ -122,20 +126,6 @@ export const resumesRouter = createRouter()
roleCounts.map((rc) => [rc.role, rc._count._all]),
);
- // Filter out roles with zero counts and map to object where key = role and value = 0
- const zeroRoleCounts = Object.fromEntries(
- ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [
- r.value,
- 0,
- ]),
- );
-
- // Combine to form singular role counts object
- const processedRoleCounts = {
- ...mappedRoleCounts,
- ...zeroRoleCounts,
- };
-
const experienceCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
@@ -143,7 +133,7 @@ export const resumesRouter = createRouter()
by: ['experience'],
where: {
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
@@ -151,21 +141,12 @@ export const resumesRouter = createRouter()
const mappedExperienceCounts = Object.fromEntries(
experienceCounts.map((ec) => [ec.experience, ec._count._all]),
);
- const zeroExperienceCounts = Object.fromEntries(
- EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map(
- (e) => [e.value, 0],
- ),
- );
- const processedExperienceCounts = {
- ...mappedExperienceCounts,
- ...zeroExperienceCounts,
- };
const locationCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
- by: ['location'],
+ by: ['locationId'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
@@ -174,23 +155,13 @@ export const resumesRouter = createRouter()
},
});
const mappedLocationCounts = Object.fromEntries(
- locationCounts.map((lc) => [lc.location, lc._count._all]),
+ locationCounts.map((lc) => [lc.locationId, lc._count._all]),
);
- const zeroLocationCounts = Object.fromEntries(
- LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [
- l.value,
- 0,
- ]),
- );
- const processedLocationCounts = {
- ...mappedLocationCounts,
- ...zeroLocationCounts,
- };
const filterCounts = {
- Experience: processedExperienceCounts,
- Location: processedLocationCounts,
- Role: processedRoleCounts,
+ experience: mappedExperienceCounts,
+ location: mappedLocationCounts,
+ role: mappedRoleCounts,
};
return {
@@ -217,6 +188,11 @@ export const resumesRouter = createRouter()
stars: true,
},
},
+ location: {
+ select: {
+ name: true,
+ },
+ },
stars: {
where: {
OR: {
diff --git a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
index 6f397241..aaeebeed 100644
--- a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
@@ -1,19 +1,16 @@
import { z } from 'zod';
-import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
-
import { createProtectedRouter } from '../context';
import type { Resume } from '~/types/resume';
export const resumesResumeUserRouter = createProtectedRouter()
.mutation('upsert', {
- // TODO: Use enums for experience, location, role
input: z.object({
additionalInfo: z.string().optional(),
experience: z.string(),
id: z.string().optional(),
- location: z.string(),
+ locationId: z.string(),
role: z.string(),
title: z.string(),
url: z.string(),
@@ -25,7 +22,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
create: {
additionalInfo: input.additionalInfo,
experience: input.experience,
- location: input.location,
+ locationId: input.locationId,
role: input.role,
title: input.title,
url: input.url,
@@ -34,7 +31,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
update: {
additionalInfo: input.additionalInfo,
experience: input.experience,
- location: input.location,
+ locationId: input.locationId,
role: input.role,
title: input.title,
url: input.url,
@@ -91,7 +88,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
resume: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
@@ -108,6 +105,11 @@ export const resumesResumeUserRouter = createProtectedRouter()
stars: true,
},
},
+ location: {
+ select: {
+ name: true,
+ },
+ },
user: {
select: {
name: true,
@@ -144,7 +146,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
resume: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
@@ -160,7 +162,8 @@ export const resumesResumeUserRouter = createProtectedRouter()
id: rs.resume.id,
isResolved: rs.resume.isResolved,
isStarredByUser: true,
- location: rs.resume.location,
+ location: rs.resume.location.name,
+ locationId: rs.resume.locationId,
numComments: rs.resume._count.comments,
numStars: rs.resume._count.stars,
role: rs.resume.role,
@@ -179,7 +182,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
stars: {
some: {
userId,
@@ -191,16 +194,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
const mappedRoleCounts = Object.fromEntries(
roleCounts.map((rc) => [rc.role, rc._count._all]),
);
- const zeroRoleCounts = Object.fromEntries(
- ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [
- r.value,
- 0,
- ]),
- );
- const processedRoleCounts = {
- ...mappedRoleCounts,
- ...zeroRoleCounts,
- };
const experienceCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
@@ -209,7 +202,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
by: ['experience'],
where: {
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
stars: {
some: {
@@ -222,21 +215,12 @@ export const resumesResumeUserRouter = createProtectedRouter()
const mappedExperienceCounts = Object.fromEntries(
experienceCounts.map((ec) => [ec.experience, ec._count._all]),
);
- const zeroExperienceCounts = Object.fromEntries(
- EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map(
- (e) => [e.value, 0],
- ),
- );
- const processedExperienceCounts = {
- ...mappedExperienceCounts,
- ...zeroExperienceCounts,
- };
const locationCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
- by: ['location'],
+ by: ['locationId'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
@@ -250,23 +234,13 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
});
const mappedLocationCounts = Object.fromEntries(
- locationCounts.map((lc) => [lc.location, lc._count._all]),
+ locationCounts.map((lc) => [lc.locationId, lc._count._all]),
);
- const zeroLocationCounts = Object.fromEntries(
- LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [
- l.value,
- 0,
- ]),
- );
- const processedLocationCounts = {
- ...mappedLocationCounts,
- ...zeroLocationCounts,
- };
const filterCounts = {
- Experience: processedExperienceCounts,
- Location: processedLocationCounts,
- Role: processedRoleCounts,
+ experience: mappedExperienceCounts,
+ location: mappedLocationCounts,
+ role: mappedRoleCounts,
};
return { filterCounts, mappedResumeData, totalRecords };
@@ -299,7 +273,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
@@ -313,6 +287,11 @@ export const resumesResumeUserRouter = createProtectedRouter()
stars: true,
},
},
+ location: {
+ select: {
+ name: true,
+ },
+ },
stars: {
where: {
userId,
@@ -341,7 +320,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
@@ -355,7 +334,8 @@ export const resumesResumeUserRouter = createProtectedRouter()
id: r.id,
isResolved: r.isResolved,
isStarredByUser: r.stars.length > 0,
- location: r.location,
+ location: r.location.name,
+ locationId: r.locationId,
numComments: r._count.comments,
numStars: r._count.stars,
role: r.role,
@@ -374,7 +354,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
},
@@ -382,16 +362,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
const mappedRoleCounts = Object.fromEntries(
roleCounts.map((rc) => [rc.role, rc._count._all]),
);
- const zeroRoleCounts = Object.fromEntries(
- ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [
- r.value,
- 0,
- ]),
- );
- const processedRoleCounts = {
- ...mappedRoleCounts,
- ...zeroRoleCounts,
- };
const experienceCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
@@ -400,7 +370,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
by: ['experience'],
where: {
isResolved: isUnreviewed ? false : {},
- location: { in: locationFilters },
+ locationId: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
@@ -409,21 +379,12 @@ export const resumesResumeUserRouter = createProtectedRouter()
const mappedExperienceCounts = Object.fromEntries(
experienceCounts.map((ec) => [ec.experience, ec._count._all]),
);
- const zeroExperienceCounts = Object.fromEntries(
- EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map(
- (e) => [e.value, 0],
- ),
- );
- const processedExperienceCounts = {
- ...mappedExperienceCounts,
- ...zeroExperienceCounts,
- };
const locationCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
- by: ['location'],
+ by: ['locationId'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
@@ -433,23 +394,13 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
});
const mappedLocationCounts = Object.fromEntries(
- locationCounts.map((lc) => [lc.location, lc._count._all]),
+ locationCounts.map((lc) => [lc.locationId, lc._count._all]),
);
- const zeroLocationCounts = Object.fromEntries(
- LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [
- l.value,
- 0,
- ]),
- );
- const processedLocationCounts = {
- ...mappedLocationCounts,
- ...zeroLocationCounts,
- };
const filterCounts = {
- Experience: processedExperienceCounts,
- Location: processedLocationCounts,
- Role: processedRoleCounts,
+ experience: mappedExperienceCounts,
+ location: mappedLocationCounts,
+ role: mappedRoleCounts,
};
return { filterCounts, mappedResumeData, totalRecords };
diff --git a/apps/portal/src/types/resume.d.ts b/apps/portal/src/types/resume.d.ts
index c9a3a567..072ae090 100644
--- a/apps/portal/src/types/resume.d.ts
+++ b/apps/portal/src/types/resume.d.ts
@@ -6,6 +6,7 @@ export type Resume = {
isResolved: boolean;
isStarredByUser: boolean;
location: string;
+ locationId: string;
numComments: number;
numStars: number;
role: string;
diff --git a/apps/portal/src/utils/resumes/resumeFilters.ts b/apps/portal/src/utils/resumes/resumeFilters.ts
index a6fc9734..3b324a1d 100644
--- a/apps/portal/src/utils/resumes/resumeFilters.ts
+++ b/apps/portal/src/utils/resumes/resumeFilters.ts
@@ -1,28 +1,14 @@
+import type { TypeaheadOption } from '@tih/ui';
+
+import type { JobTitleType } from '~/components/shared/JobTitles';
+import { JobTitleLabels } from '~/components/shared/JobTitles';
+
export type FilterId = 'experience' | 'location' | 'role';
-export type FilterLabel = 'Experience' | 'Location' | 'Role';
export type CustomFilter = {
isUnreviewed: boolean;
};
-export type RoleFilter =
- | 'Android Engineer'
- | 'Backend Engineer'
- | 'DevOps Engineer'
- | 'Frontend Engineer'
- | 'Full-Stack Engineer'
- | 'iOS Engineer';
-
-export type ExperienceFilter =
- | 'Entry Level (0 - 2 years)'
- | 'Internship'
- | 'Mid Level (3 - 5 years)'
- | 'Senior Level (5+ years)';
-
-export type LocationFilter = 'India' | 'Singapore' | 'United States';
-
-export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
-
export type FilterOption = {
label: string;
value: T;
@@ -30,11 +16,11 @@ export type FilterOption = {
export type Filter = {
id: FilterId;
- label: FilterLabel;
- options: Array>;
+ label: string;
};
-export type FilterState = CustomFilter & Record>;
+export type FilterState = CustomFilter &
+ Record>;
export type SortOrder = 'latest' | 'mostComments' | 'popular';
@@ -45,6 +31,31 @@ export type Shortcut = {
sortOrder: SortOrder;
};
+export const getTypeaheadOption = (
+ filterId: FilterId,
+ filterValue: string,
+ locationName?: string,
+) => {
+ switch (filterId) {
+ case 'experience':
+ return EXPERIENCES.find(({ value }) => value === filterValue);
+ case 'role':
+ return {
+ id: filterValue,
+ label: JobTitleLabels[filterValue as keyof typeof JobTitleLabels],
+ value: filterValue,
+ };
+ case 'location':
+ return {
+ id: filterValue,
+ label: locationName ?? '',
+ value: filterValue,
+ };
+ default:
+ break;
+ }
+};
+
export const BROWSE_TABS_VALUES = {
ALL: 'all',
MY: 'my',
@@ -57,45 +68,85 @@ export const SORT_OPTIONS: Array> = [
{ label: 'Most Comments', value: 'mostComments' },
];
-export const ROLES: Array> = [
- {
- label: 'Full-Stack Engineer',
- value: 'Full-Stack 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' },
+const INITIAL_ROLES_VALUES: Array = [
+ 'software-engineer',
+ 'back-end-engineer',
+ 'front-end-engineer',
+ 'full-stack-engineer',
+ 'ios-engineer',
+ 'android-engineer',
+ 'data-engineer',
];
+export const INITIAL_ROLES: Array = INITIAL_ROLES_VALUES.map(
+ (value) =>
+ getTypeaheadOption('role', value) ?? {
+ id: value,
+ label: value,
+ value,
+ },
+);
-export const EXPERIENCES: Array> = [
- { label: 'Internship', value: 'Internship' },
+export const EXPERIENCES: Array = [
+ {
+ id: 'internship',
+ label: 'Internship',
+ value: 'internship',
+ },
{
+ id: 'entry-level',
label: 'Entry Level (0 - 2 years)',
- value: 'Entry Level (0 - 2 years)',
+ value: 'entry-level',
},
{
+ id: 'mid-level',
label: 'Mid Level (3 - 5 years)',
- value: 'Mid Level (3 - 5 years)',
+ value: 'mid-level',
},
{
+ id: 'senior-level',
label: 'Senior Level (5+ years)',
- value: 'Senior Level (5+ years)',
+ value: 'senior-level',
},
];
-export const LOCATIONS: Array> = [
- { label: 'Singapore', value: 'Singapore' },
- { label: 'United States', value: 'United States' },
- { label: 'India', value: 'India' },
+export const INITIAL_LOCATIONS: Array = [
+ {
+ id: '196',
+ label: 'Singapore',
+ value: '196',
+ },
+ {
+ id: '101',
+ label: 'India',
+ value: '101',
+ },
+ {
+ id: '231',
+ label: 'United States',
+ value: '231',
+ },
+ {
+ id: '230',
+ label: 'United Kingdom',
+ value: '230',
+ },
+ {
+ id: '102',
+ label: 'Indonesia',
+ value: '102',
+ },
+ {
+ id: '44',
+ label: 'China',
+ value: '44',
+ },
];
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: INITIAL_LOCATIONS,
+ role: INITIAL_ROLES,
};
export const SHORTCUTS: Array = [
@@ -104,7 +155,7 @@ export const SHORTCUTS: Array = [
...INITIAL_FILTER_STATE,
isUnreviewed: false,
},
- name: 'All',
+ name: 'General',
sortOrder: 'latest',
},
{
@@ -118,7 +169,13 @@ export const SHORTCUTS: Array = [
{
filters: {
...INITIAL_FILTER_STATE,
- experience: ['Entry Level (0 - 2 years)'],
+ experience: [
+ {
+ id: 'entry-level',
+ label: 'Entry Level (0 - 2 years)',
+ value: 'entry-level',
+ },
+ ],
isUnreviewed: false,
},
name: 'Fresh Grad',
@@ -136,26 +193,46 @@ export const SHORTCUTS: Array = [
filters: {
...INITIAL_FILTER_STATE,
isUnreviewed: false,
- location: ['United States'],
+ location: [
+ {
+ id: '231',
+ label: 'United States',
+ value: '231',
+ },
+ ],
},
name: 'US Only',
sortOrder: 'latest',
},
];
-export const isInitialFilterState = (filters: FilterState) =>
- Object.keys(filters).every((filter) => {
- if (!['experience', 'location', 'role'].includes(filter)) {
- return true;
- }
- return INITIAL_FILTER_STATE[filter as FilterId].every((value) =>
- filters[filter as FilterId].includes(value),
- );
- });
-
+// We omit 'location' as its label should be fetched from the Country table.
export const getFilterLabel = (
- filters: Array<
- FilterOption
- >,
- filterValue: ExperienceFilter | LocationFilter | RoleFilter | SortOrder,
-) => filters.find(({ value }) => value === filterValue)?.label ?? filterValue;
+ filterId: Omit,
+ filterValue: SortOrder | string,
+): string | undefined => {
+ if (filterId === 'location') {
+ return filterValue;
+ }
+
+ let filters: Array = [];
+
+ switch (filterId) {
+ case 'experience':
+ filters = EXPERIENCES;
+ 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;
+};