[resumes][feat] Add dynamic filters and unreviewed shortcut ()

pull/456/head
Su Yin 2 years ago committed by GitHub
parent b1f16a06e9
commit 8dfe6b0bdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -118,7 +118,7 @@ export default function ResumeHomePage() {
'', '',
); );
const [shortcutSelected, setShortcutSelected, isShortcutInit] = const [shortcutSelected, setShortcutSelected, isShortcutInit] =
useSearchParams('shortcutSelected', 'All'); useSearchParams('shortcutSelected', 'Unreviewed');
const [currentPage, setCurrentPage, isCurrentPageInit] = useSearchParams( const [currentPage, setCurrentPage, isCurrentPageInit] = useSearchParams(
'currentPage', 'currentPage',
1, 1,
@ -182,20 +182,13 @@ export default function ResumeHomePage() {
isSearchOptionsInit, isSearchOptionsInit,
]); ]);
const filterCountsQuery = trpc.useQuery(
['resumes.resume.getTotalFilterCounts'],
{
staleTime: STALE_TIME,
},
);
const allResumesQuery = trpc.useQuery( const allResumesQuery = trpc.useQuery(
[ [
'resumes.resume.findAll', 'resumes.resume.findAll',
{ {
experienceFilters: userFilters.experience, experienceFilters: userFilters.experience,
isUnreviewed: userFilters.isUnreviewed,
locationFilters: userFilters.location, locationFilters: userFilters.location,
numComments: userFilters.numComments,
roleFilters: userFilters.role, roleFilters: userFilters.role,
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
skip, skip,
@ -213,8 +206,8 @@ export default function ResumeHomePage() {
'resumes.resume.user.findUserStarred', 'resumes.resume.user.findUserStarred',
{ {
experienceFilters: userFilters.experience, experienceFilters: userFilters.experience,
isUnreviewed: userFilters.isUnreviewed,
locationFilters: userFilters.location, locationFilters: userFilters.location,
numComments: userFilters.numComments,
roleFilters: userFilters.role, roleFilters: userFilters.role,
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
skip, skip,
@ -233,8 +226,8 @@ export default function ResumeHomePage() {
'resumes.resume.user.findUserCreated', 'resumes.resume.user.findUserCreated',
{ {
experienceFilters: userFilters.experience, experienceFilters: userFilters.experience,
isUnreviewed: userFilters.isUnreviewed,
locationFilters: userFilters.location, locationFilters: userFilters.location,
numComments: userFilters.numComments,
roleFilters: userFilters.role, roleFilters: userFilters.role,
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY), searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
skip, skip,
@ -249,14 +242,6 @@ export default function ResumeHomePage() {
}, },
); );
const getFilterCount = (filter: FilterLabel, value: string) => {
if (filterCountsQuery.isLoading) {
return 0;
}
const filterCountsData = filterCountsQuery.data!;
return filterCountsData[filter][value];
};
const onSubmitResume = () => { const onSubmitResume = () => {
if (sessionData === null) { if (sessionData === null) {
router.push('/api/auth/signin'); router.push('/api/auth/signin');
@ -336,6 +321,18 @@ export default function ResumeHomePage() {
starredResumesQuery.isFetching || starredResumesQuery.isFetching ||
myResumesQuery.isFetching; myResumesQuery.isFetching;
const getTabFilterCounts = () => {
return getTabQueryData()?.filterCounts;
};
const getFilterCount = (filter: FilterLabel, value: string) => {
const filterCountsData = getTabFilterCounts();
if (!filterCountsData) {
return 0;
}
return filterCountsData[filter][value];
};
return ( return (
<> <>
<Head> <Head>

@ -11,8 +11,8 @@ export const resumesRouter = createRouter()
.query('findAll', { .query('findAll', {
input: z.object({ input: z.object({
experienceFilters: z.string().array(), experienceFilters: z.string().array(),
isUnreviewed: z.boolean(),
locationFilters: z.string().array(), locationFilters: z.string().array(),
numComments: z.number().optional(),
roleFilters: z.string().array(), roleFilters: z.string().array(),
searchValue: z.string(), searchValue: z.string(),
skip: z.number(), skip: z.number(),
@ -25,7 +25,7 @@ export const resumesRouter = createRouter()
locationFilters, locationFilters,
experienceFilters, experienceFilters,
sortOrder, sortOrder,
numComments, isUnreviewed,
skip, skip,
searchValue, searchValue,
take, take,
@ -33,12 +33,8 @@ export const resumesRouter = createRouter()
const userId = ctx.session?.user?.id; const userId = ctx.session?.user?.id;
const totalRecords = await ctx.prisma.resumesResume.count({ const totalRecords = await ctx.prisma.resumesResume.count({
where: { where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
@ -81,12 +77,8 @@ export const resumesRouter = createRouter()
skip, skip,
take, take,
where: { where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
@ -110,7 +102,105 @@ export const resumesRouter = createRouter()
}; };
return resume; return resume;
}); });
return { mappedResumeData, totalRecords };
// Group by role and count, taking into account all role/experience/location/isUnreviewed filters and search value
const roleCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
by: ['role'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
});
// Map all nonzero counts from array to object where key = role and value = count
const mappedRoleCounts = Object.fromEntries(
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,
},
by: ['experience'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
});
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'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
});
const mappedLocationCounts = Object.fromEntries(
locationCounts.map((lc) => [lc.location, 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,
};
return {
filterCounts,
mappedResumeData,
totalRecords,
};
}, },
}) })
.query('findOne', { .query('findOne', {

@ -1,5 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
import { createProtectedRouter } from '../context'; import { createProtectedRouter } from '../context';
import type { Resume } from '~/types/resume'; import type { Resume } from '~/types/resume';
@ -64,8 +66,8 @@ export const resumesResumeUserRouter = createProtectedRouter()
.query('findUserStarred', { .query('findUserStarred', {
input: z.object({ input: z.object({
experienceFilters: z.string().array(), experienceFilters: z.string().array(),
isUnreviewed: z.boolean(),
locationFilters: z.string().array(), locationFilters: z.string().array(),
numComments: z.number().optional(),
roleFilters: z.string().array(), roleFilters: z.string().array(),
searchValue: z.string(), searchValue: z.string(),
skip: z.number(), skip: z.number(),
@ -80,19 +82,15 @@ export const resumesResumeUserRouter = createProtectedRouter()
experienceFilters, experienceFilters,
searchValue, searchValue,
sortOrder, sortOrder,
numComments, isUnreviewed,
skip, skip,
take, take,
} = input; } = input;
const totalRecords = await ctx.prisma.resumesStar.count({ const totalRecords = await ctx.prisma.resumesStar.count({
where: { where: {
resume: { resume: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
@ -144,12 +142,8 @@ export const resumesResumeUserRouter = createProtectedRouter()
take, take,
where: { where: {
resume: { resume: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
@ -176,14 +170,116 @@ export const resumesResumeUserRouter = createProtectedRouter()
}; };
return resume; return resume;
}); });
return { mappedResumeData, totalRecords };
const roleCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
by: ['role'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
stars: {
some: {
userId,
},
},
title: { contains: searchValue, mode: 'insensitive' },
},
});
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: {
_all: true,
},
by: ['experience'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
stars: {
some: {
userId,
},
},
title: { contains: searchValue, mode: 'insensitive' },
},
});
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'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
stars: {
some: {
userId,
},
},
title: { contains: searchValue, mode: 'insensitive' },
},
});
const mappedLocationCounts = Object.fromEntries(
locationCounts.map((lc) => [lc.location, 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,
};
return { filterCounts, mappedResumeData, totalRecords };
}, },
}) })
.query('findUserCreated', { .query('findUserCreated', {
input: z.object({ input: z.object({
experienceFilters: z.string().array(), experienceFilters: z.string().array(),
isUnreviewed: z.boolean(),
locationFilters: z.string().array(), locationFilters: z.string().array(),
numComments: z.number().optional(),
roleFilters: z.string().array(), roleFilters: z.string().array(),
searchValue: z.string(), searchValue: z.string(),
skip: z.number(), skip: z.number(),
@ -198,18 +294,14 @@ export const resumesResumeUserRouter = createProtectedRouter()
experienceFilters, experienceFilters,
sortOrder, sortOrder,
searchValue, searchValue,
numComments, isUnreviewed,
take, take,
skip, skip,
} = input; } = input;
const totalRecords = await ctx.prisma.resumesResume.count({ const totalRecords = await ctx.prisma.resumesResume.count({
where: { where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
@ -250,12 +342,8 @@ export const resumesResumeUserRouter = createProtectedRouter()
skip, skip,
take, take,
where: { where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
@ -280,6 +368,96 @@ export const resumesResumeUserRouter = createProtectedRouter()
}; };
return resume; return resume;
}); });
return { mappedResumeData, totalRecords };
const roleCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
by: ['role'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
},
});
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: {
_all: true,
},
by: ['experience'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
},
});
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'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
},
});
const mappedLocationCounts = Object.fromEntries(
locationCounts.map((lc) => [lc.location, 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,
};
return { filterCounts, mappedResumeData, totalRecords };
}, },
}); });

@ -2,7 +2,7 @@ export type FilterId = 'experience' | 'location' | 'role';
export type FilterLabel = 'Experience' | 'Location' | 'Role'; export type FilterLabel = 'Experience' | 'Location' | 'Role';
export type CustomFilter = { export type CustomFilter = {
numComments: number; isUnreviewed: boolean;
}; };
export type RoleFilter = export type RoleFilter =
@ -34,8 +34,7 @@ export type Filter = {
options: Array<FilterOption<FilterValue>>; options: Array<FilterOption<FilterValue>>;
}; };
export type FilterState = Partial<CustomFilter> & export type FilterState = CustomFilter & Record<FilterId, Array<FilterValue>>;
Record<FilterId, Array<FilterValue>>;
export type SortOrder = 'latest' | 'mostComments' | 'popular'; export type SortOrder = 'latest' | 'mostComments' | 'popular';
@ -94,20 +93,24 @@ export const LOCATIONS: Array<FilterOption<LocationFilter>> = [
export const INITIAL_FILTER_STATE: FilterState = { export const INITIAL_FILTER_STATE: FilterState = {
experience: Object.values(EXPERIENCES).map(({ value }) => value), experience: Object.values(EXPERIENCES).map(({ value }) => value),
isUnreviewed: true,
location: Object.values(LOCATIONS).map(({ value }) => value), location: Object.values(LOCATIONS).map(({ value }) => value),
role: Object.values(ROLES).map(({ value }) => value), role: Object.values(ROLES).map(({ value }) => value),
}; };
export const SHORTCUTS: Array<Shortcut> = [ export const SHORTCUTS: Array<Shortcut> = [
{ {
filters: INITIAL_FILTER_STATE, filters: {
...INITIAL_FILTER_STATE,
isUnreviewed: false,
},
name: 'All', name: 'All',
sortOrder: 'latest', sortOrder: 'latest',
}, },
{ {
filters: { filters: {
...INITIAL_FILTER_STATE, ...INITIAL_FILTER_STATE,
numComments: 0, isUnreviewed: true,
}, },
name: 'Unreviewed', name: 'Unreviewed',
sortOrder: 'latest', sortOrder: 'latest',
@ -116,18 +119,23 @@ export const SHORTCUTS: Array<Shortcut> = [
filters: { filters: {
...INITIAL_FILTER_STATE, ...INITIAL_FILTER_STATE,
experience: ['Entry Level (0 - 2 years)'], experience: ['Entry Level (0 - 2 years)'],
isUnreviewed: false,
}, },
name: 'Fresh Grad', name: 'Fresh Grad',
sortOrder: 'latest', sortOrder: 'latest',
}, },
{ {
filters: INITIAL_FILTER_STATE, filters: {
...INITIAL_FILTER_STATE,
isUnreviewed: false,
},
name: 'Top 10', name: 'Top 10',
sortOrder: 'popular', sortOrder: 'popular',
}, },
{ {
filters: { filters: {
...INITIAL_FILTER_STATE, ...INITIAL_FILTER_STATE,
isUnreviewed: false,
location: ['United States'], location: ['United States'],
}, },
name: 'US Only', name: 'US Only',

Loading…
Cancel
Save