[resumes][fix] fix merge conflicts

pull/506/head
Keane Chan 3 years ago
parent 80dbfe39a1
commit 87f85c1ff8
No known key found for this signature in database
GPG Key ID: 32718398E1E9F87C

@ -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"

@ -15,7 +15,6 @@ import {
PencilSquareIcon,
StarIcon,
} from '@heroicons/react/20/solid';
import type { TypeaheadOption } from '@tih/ui';
import { Button, Spinner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
@ -27,11 +26,9 @@ import loginPageHref from '~/components/shared/loginPageHref';
import {
BROWSE_TABS_VALUES,
EXPERIENCES,
getFilterLabel,
getTypeaheadOption,
INITIAL_FILTER_STATE,
LOCATIONS,
ROLES,
} from '~/utils/resumes/resumeFilters';
import { trpc } from '~/utils/trpc';
@ -121,27 +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<TypeaheadOption>,
) => 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'),
@ -149,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)],
}),
}),
},
@ -318,92 +314,71 @@ export default function ResumeReviewPage() {
<div className="hidden xl:block">{renderReviewButton()}</div>
</div>
</div>
</div>
<div className="flex flex-col lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8">
<div className="mt-2 flex items-center text-sm text-slate-600 xl:mt-1">
<BriefcaseIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
roleLabel: detailsQuery.data?.role,
})
}>
{getFilterLabel('role', detailsQuery.data.role)}
</button>
</div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
locationLabel: detailsQuery.data?.locationId,
})
}>
{detailsQuery.data?.location.name}
</button>
</div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<AcademicCapIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
experienceLabel: detailsQuery.data?.experience,
})
}>
{getFilterLabel('experience', detailsQuery.data.experience)}
</button>
</div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<CalendarIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{`Uploaded ${formatDistanceToNow(detailsQuery.data.createdAt, {
addSuffix: true,
})} by ${detailsQuery.data.user.name}`}
</div>
</div>
{detailsQuery.data.additionalInfo && (
<div className="flex items-start whitespace-pre-wrap pt-2 text-sm text-slate-600 xl:pt-1">
<InformationCircleIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<ResumeExpandableText
key={detailsQuery.data.additionalInfo}
text={detailsQuery.data.additionalInfo}
/>
</div>
)}
<div className="flex w-full flex-col gap-6 py-4 xl:flex-row xl:py-0">
<div className="w-full xl:w-1/2">
<ResumePdf url={detailsQuery.data.url} />
</div>
<div className="grow">
<div className="mb-6 space-y-4 xl:hidden">
{renderReviewButton()}
<div className="flex items-center space-x-2">
<hr className="flex-grow border-slate-300" />
<span className="bg-slate-50 px-3 text-lg font-medium text-slate-900">
Reviews
</span>
<hr className="flex-grow border-slate-300" />
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2 lg:flex lg:flex-wrap lg:space-x-8">
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
<BriefcaseIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
roleValue: detailsQuery.data?.role,
})
}>
{getFilterLabel('role', detailsQuery.data.role)}
</button>
</div>
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
locationName: detailsQuery.data?.location.name,
locationValue: detailsQuery.data?.locationId,
})
}>
{detailsQuery.data?.location.name}
</button>
</div>
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
<AcademicCapIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
experienceValue: detailsQuery.data?.experience,
})
}>
{getFilterLabel(
'experience',
detailsQuery.data.experience,
)}
</button>
</div>
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
<CalendarIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{`Uploaded ${formatDistanceToNow(
detailsQuery.data.createdAt,
{
addSuffix: true,
},
)} by ${detailsQuery.data.user.name}`}
</div>
</div>
{detailsQuery.data.additionalInfo && (

@ -30,21 +30,12 @@ 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 { FilterState, SortOrder } 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';
@ -59,17 +50,14 @@ const filters: Array<Filter> = [
{
id: 'role',
label: 'Role',
options: ROLES,
},
{
id: 'experience',
label: 'Experience',
options: EXPERIENCES,
},
{
id: 'location',
label: 'Location',
options: LOCATIONS,
},
];
@ -84,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:
@ -387,12 +369,16 @@ export default function ResumeHomePage() {
}
};
const getFilterCount = (filter: FilterLabel, value: string) => {
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 (
@ -498,7 +484,7 @@ export default function ResumeHomePage() {
{userFilters[filter.id].map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 flex items-center px-1 text-sm [&>div>div:nth-child(2)>label]:font-normal">
className="flex items-center px-1 text-sm">
<CheckboxInput
label={option.label}
value={true}
@ -515,11 +501,7 @@ export default function ResumeHomePage() {
}
/>
<span className="ml-1 text-slate-500">
(
{getFilterCount(
filter.label,
option.label,
)}
({getFilterCount(filter.id, option.value)}
)
</span>
</div>
@ -615,7 +597,7 @@ export default function ResumeHomePage() {
{userFilters[filter.id].map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 flex items-center px-1 text-sm [&>div>div:nth-child(2)>label]:font-normal">
className="flex items-center px-1 text-sm">
<CheckboxInput
label={option.label}
value={true}
@ -631,9 +613,7 @@ export default function ResumeHomePage() {
}
/>
<span className="ml-1 text-slate-500">
(
{getFilterCount(filter.label, option.label)}
)
({getFilterCount(filter.id, option.value)})
</span>
</div>
))}
@ -740,7 +720,7 @@ export default function ResumeHomePage() {
height={196}
width={196}
/>
{getEmptyDataText(tabsValue, searchValue, userFilters)}
{getEmptyDataText(tabsValue, searchValue)}
</div>
) : (
<div>

@ -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';
@ -98,6 +96,7 @@ export const resumesRouter = createRouter()
isResolved: r.isResolved,
isStarredByUser: r.stars.length > 0,
location: r.location.name,
locationId: r.locationId,
numComments: r._count.comments,
numStars: r._count.stars,
role: r.role,
@ -127,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,
@ -156,15 +141,6 @@ 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: {
@ -181,21 +157,11 @@ export const resumesRouter = createRouter()
const mappedLocationCounts = Object.fromEntries(
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 {

@ -1,7 +1,5 @@
import { z } from 'zod';
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
import { createProtectedRouter } from '../context';
import type { Resume } from '~/types/resume';
@ -165,6 +163,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
isResolved: rs.resume.isResolved,
isStarredByUser: true,
location: rs.resume.location.name,
locationId: rs.resume.locationId,
numComments: rs.resume._count.comments,
numStars: rs.resume._count.stars,
role: rs.resume.role,
@ -195,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: {
@ -226,15 +215,6 @@ 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: {
@ -256,21 +236,11 @@ export const resumesResumeUserRouter = createProtectedRouter()
const mappedLocationCounts = Object.fromEntries(
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 };
@ -365,6 +335,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
isResolved: r.isResolved,
isStarredByUser: r.stars.length > 0,
location: r.location.name,
locationId: r.locationId,
numComments: r._count.comments,
numStars: r._count.stars,
role: r.role,
@ -391,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: {
@ -418,15 +379,6 @@ 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: {
@ -444,21 +396,11 @@ export const resumesResumeUserRouter = createProtectedRouter()
const mappedLocationCounts = Object.fromEntries(
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 };

@ -6,6 +6,7 @@ export type Resume = {
isResolved: boolean;
isStarredByUser: boolean;
location: string;
locationId: string;
numComments: number;
numStars: number;
role: string;

@ -4,7 +4,6 @@ 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;
@ -17,8 +16,7 @@ export type FilterOption<T> = {
export type Filter = {
id: FilterId;
label: FilterLabel;
options: Array<TypeaheadOption>;
label: string;
};
export type FilterState = CustomFilter &
@ -33,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',
@ -54,12 +77,20 @@ const INITIAL_ROLES_VALUES: Array<JobTitleType> = [
'android-engineer',
'data-engineer',
];
export const INITIAL_ROLES: Array<TypeaheadOption> = INITIAL_ROLES_VALUES.map(
(value) =>
getTypeaheadOption('role', value) ?? {
id: value,
label: value,
value,
},
);
export const EXPERIENCES: Array<TypeaheadOption> = [
{
id: 'internship',
label: 'Internship',
value: 'Internship',
value: 'internship',
},
{
id: 'entry-level',
@ -69,21 +100,26 @@ export const EXPERIENCES: Array<TypeaheadOption> = [
{
id: 'mid-level',
label: 'Mid Level (3 - 5 years)',
value: 'Mid Level (3 - 5 years)',
value: 'mid-level',
},
{
id: 'Senior Level (5+ years)',
id: 'senior-level',
label: 'Senior Level (5+ years)',
value: 'Senior Level (5+ years)',
value: 'senior-level',
},
];
export const LOCATIONS: Array<TypeaheadOption> = [
export const INITIAL_LOCATIONS: Array<TypeaheadOption> = [
{
id: '196',
label: 'Singapore',
value: '196',
},
{
id: '101',
label: 'India',
value: '101',
},
{
id: '231',
label: 'United States',
@ -109,8 +145,8 @@ export const LOCATIONS: Array<TypeaheadOption> = [
export const INITIAL_FILTER_STATE: FilterState = {
experience: EXPERIENCES,
isUnreviewed: true,
location: LOCATIONS,
role: ROLES,
location: INITIAL_LOCATIONS,
role: INITIAL_ROLES,
};
export const SHORTCUTS: Array<Shortcut> = [
@ -119,7 +155,7 @@ export const SHORTCUTS: Array<Shortcut> = [
...INITIAL_FILTER_STATE,
isUnreviewed: false,
},
name: 'All',
name: 'General',
sortOrder: 'latest',
},
{
@ -133,7 +169,13 @@ export const SHORTCUTS: Array<Shortcut> = [
{
filters: {
...INITIAL_FILTER_STATE,
experience: [],
experience: [
{
id: 'entry-level',
label: 'Entry Level (0 - 2 years)',
value: 'entry-level',
},
],
isUnreviewed: false,
},
name: 'Fresh Grad',
@ -151,25 +193,22 @@ export const SHORTCUTS: Array<Shortcut> = [
filters: {
...INITIAL_FILTER_STATE,
isUnreviewed: false,
location: [],
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 = (
filterId: FilterId | 'sort',
filterId: Omit<FilterId | 'sort', 'location'>,
filterValue: SortOrder | string,
): string | undefined => {
if (filterId === 'location') {

Loading…
Cancel
Save