[resumes][feat] Add pagination on browse page (#388)

* [resumes][feat] Add pagination on browse page

* [resume][fix] Remove unused type
pull/396/head
Su Yin 2 years ago committed by GitHub
parent d8213639d3
commit a53c10483e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +1,22 @@
import clsx from 'clsx';
type Props = Readonly<{
isSelected: boolean;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
title: string;
}>;
export default function ResumeFilterPill({ title, onClick }: Props) {
export default function ResumeFilterPill({
title,
onClick,
isSelected,
}: Props) {
return (
<button
className="rounded-xl border border-indigo-500 border-transparent bg-white px-2 py-1 text-xs font-medium text-indigo-500 focus:bg-indigo-500 focus:text-white"
className={clsx(
'rounded-xl border border-indigo-500 border-transparent px-2 py-1 text-xs font-medium focus:bg-indigo-500 focus:text-white',
isSelected ? 'bg-indigo-500 text-white' : 'bg-white text-indigo-500',
)}
type="button"
onClick={onClick}>
{title}

@ -41,11 +41,6 @@ export type FilterState = Partial<CustomFilter> &
export type SortOrder = 'latest' | 'popular' | 'topComments';
type SortOption = {
name: string;
value: SortOrder;
};
export type Shortcut = {
customFilters?: CustomFilter;
filters: FilterState;
@ -59,11 +54,11 @@ export const BROWSE_TABS_VALUES = {
STARRED: 'starred',
};
export const SORT_OPTIONS: Array<SortOption> = [
{ name: 'Latest', value: 'latest' },
{ name: 'Popular', value: 'popular' },
{ name: 'Top Comments', value: 'topComments' },
];
export const SORT_OPTIONS: Record<string, string> = {
latest: 'Latest',
popular: 'Popular',
topComments: 'Top Comments',
};
export const ROLE: Array<FilterOption<RoleFilter>> = [
{

@ -1,8 +1,7 @@
import compareAsc from 'date-fns/compareAsc';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Disclosure } from '@headlessui/react';
import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
@ -10,6 +9,7 @@ import {
CheckboxInput,
CheckboxList,
DropdownMenu,
Pagination,
Tabs,
TextInput,
} from '@tih/ui';
@ -18,10 +18,7 @@ import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import type {
Filter,
FilterId,
FilterState,
FilterValue,
Shortcut,
SortOrder,
} from '~/components/resumes/browse/resumeFilters';
import {
BROWSE_TABS_VALUES,
@ -58,59 +55,64 @@ const filters: Array<Filter> = [
},
];
const filterResumes = (
resumes: Array<Resume>,
searchValue: string,
userFilters: FilterState,
) =>
resumes
.filter((resume) =>
resume.title.toLowerCase().includes(searchValue.toLocaleLowerCase()),
)
.filter(
({ experience, location, role }) =>
userFilters.role.includes(role as FilterValue) &&
userFilters.experience.includes(experience as FilterValue) &&
userFilters.location.includes(location as FilterValue),
)
.filter(
({ numComments }) =>
userFilters.numComments === undefined ||
numComments === userFilters.numComments,
);
const sortComparators: Record<
SortOrder,
(resume1: Resume, resume2: Resume) => number
> = {
latest: (resume1, resume2) =>
compareAsc(resume2.createdAt, resume1.createdAt),
popular: (resume1, resume2) => resume2.numStars - resume1.numStars,
topComments: (resume1, resume2) => resume2.numComments - resume1.numComments,
};
const sortResumes = (resumes: Array<Resume>, sortOrder: SortOrder) =>
resumes.sort(sortComparators[sortOrder]);
export default function ResumeHomePage() {
const { data: sessionData } = useSession();
const router = useRouter();
const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL);
const [sortOrder, setSortOrder] = useState(SORT_OPTIONS[0].value);
const [sortOrder, setSortOrder] = useState('latest');
const [searchValue, setSearchValue] = useState('');
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE);
const [shortcutSelected, setShortcutSelected] = useState('All');
const [resumes, setResumes] = useState<Array<Resume>>([]);
const [renderSignInButton, setRenderSignInButton] = useState(false);
const [signInButtonText, setSignInButtonText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const PAGE_LIMIT = 10;
const skip = (currentPage - 1) * PAGE_LIMIT;
const allResumesQuery = trpc.useQuery(['resumes.resume.findAll'], {
enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
onSuccess: (data) => {
setResumes(data);
setRenderSignInButton(false);
useEffect(() => {
setCurrentPage(1);
}, [userFilters, sortOrder]);
const allResumesQuery = trpc.useQuery(
[
'resumes.resume.findAll',
{
experienceFilters: userFilters.experience,
locationFilters: userFilters.location,
numComments: userFilters.numComments,
roleFilters: userFilters.role,
skip,
sortOrder,
},
],
{
enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
onSuccess: (data) => {
setTotalPages(
data.totalRecords % PAGE_LIMIT === 0
? data.totalRecords / PAGE_LIMIT
: Math.floor(data.totalRecords / PAGE_LIMIT) + 1,
);
setResumes(data.mappedResumeData);
setRenderSignInButton(false);
},
},
});
);
const starredResumesQuery = trpc.useQuery(
['resumes.resume.user.findUserStarred'],
[
'resumes.resume.user.findUserStarred',
{
experienceFilters: userFilters.experience,
locationFilters: userFilters.location,
numComments: userFilters.numComments,
roleFilters: userFilters.role,
skip,
sortOrder,
},
],
{
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
onError: () => {
@ -119,13 +121,28 @@ export default function ResumeHomePage() {
setSignInButtonText('to view starred resumes');
},
onSuccess: (data) => {
setResumes(data);
setTotalPages(
data.totalRecords % PAGE_LIMIT === 0
? data.totalRecords / PAGE_LIMIT
: Math.floor(data.totalRecords / PAGE_LIMIT) + 1,
);
setResumes(data.mappedResumeData);
},
retry: false,
},
);
const myResumesQuery = trpc.useQuery(
['resumes.resume.user.findUserCreated'],
[
'resumes.resume.user.findUserCreated',
{
experienceFilters: userFilters.experience,
locationFilters: userFilters.location,
numComments: userFilters.numComments,
roleFilters: userFilters.role,
skip,
sortOrder,
},
],
{
enabled: tabsValue === BROWSE_TABS_VALUES.MY,
onError: () => {
@ -134,7 +151,12 @@ export default function ResumeHomePage() {
setSignInButtonText('to view your submitted resumes');
},
onSuccess: (data) => {
setResumes(data);
setTotalPages(
data.totalRecords % PAGE_LIMIT === 0
? data.totalRecords / PAGE_LIMIT
: Math.floor(data.totalRecords / PAGE_LIMIT) + 1,
);
setResumes(data.mappedResumeData);
},
retry: false,
},
@ -171,11 +193,18 @@ export default function ResumeHomePage() {
const onShortcutChange = ({
sortOrder: shortcutSortOrder,
filters: shortcutFilters,
name: shortcutName,
}: Shortcut) => {
setShortcutSelected(shortcutName);
setSortOrder(shortcutSortOrder);
setUserFilters(shortcutFilters);
};
const onTabChange = (tab: string) => {
setTabsValue(tab);
setCurrentPage(1);
};
return (
<>
<Head>
@ -195,7 +224,7 @@ export default function ResumeHomePage() {
</div>
<div className="col-span-10">
<div className="border-grey-200 grid grid-cols-12 items-center gap-4 border-b pb-2">
<div className="col-span-7">
<div className="col-span-5">
<Tabs
label="Resume Browse Tabs"
tabs={[
@ -213,42 +242,44 @@ export default function ResumeHomePage() {
},
]}
value={tabsValue}
onChange={setTabsValue}
onChange={onTabChange}
/>
</div>
<div className="col-span-3 self-end">
<form>
<TextInput
label=""
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</form>
</div>
<div className="col-span-1 justify-self-center">
<DropdownMenu align="end" label="Sort">
{SORT_OPTIONS.map((option) => (
<DropdownMenu.Item
key={option.name}
isSelected={sortOrder === option.value}
label={option.name}
onClick={() =>
setSortOrder(option.value)
}></DropdownMenu.Item>
))}
</DropdownMenu>
</div>
<div className="col-span-1">
<button
className="rounded-md bg-indigo-500 py-1 px-3 text-sm font-medium text-white"
type="button"
onClick={onSubmitResume}>
Submit Resume
</button>
<div className="col-span-7 flex items-center justify-evenly">
<div className="w-64">
<form>
<TextInput
label=""
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</form>
</div>
<div>
<DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}>
{Object.entries(SORT_OPTIONS).map(([key, value]) => (
<DropdownMenu.Item
key={key}
isSelected={sortOrder === key}
label={value}
onClick={() =>
setSortOrder(key)
}></DropdownMenu.Item>
))}
</DropdownMenu>
</div>
<div>
<button
className="rounded-md bg-indigo-500 py-1 px-3 text-sm font-medium text-white"
type="button"
onClick={onSubmitResume}>
Submit Resume
</button>
</div>
</div>
</div>
</div>
@ -265,6 +296,7 @@ export default function ResumeHomePage() {
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
<ResumeFilterPill
isSelected={shortcutSelected === shortcut.name}
title={shortcut.name}
onClick={() => onShortcutChange(shortcut)}
/>
@ -339,17 +371,26 @@ export default function ResumeHomePage() {
{renderSignInButton && (
<ResumeSignInButton text={signInButtonText} />
)}
{totalPages === 0 && (
<div className="mt-4">Nothing to see here.</div>
)}
<ResumeListItems
isLoading={
allResumesQuery.isFetching ||
starredResumesQuery.isFetching ||
myResumesQuery.isFetching
}
resumes={sortResumes(
filterResumes(resumes, searchValue, userFilters),
sortOrder,
)}
resumes={resumes}
/>
<div className="my-4 flex justify-center">
<Pagination
current={currentPage}
end={totalPages}
label="pagination"
start={1}
onSelect={(page) => setCurrentPage(page)}
/>
</div>
</div>
</div>
</div>

@ -6,8 +6,36 @@ import type { Resume } from '~/types/resume';
export const resumesRouter = createRouter()
.query('findAll', {
async resolve({ ctx }) {
input: z.object({
experienceFilters: z.string().array(),
locationFilters: z.string().array(),
numComments: z.number().optional(),
roleFilters: z.string().array(),
skip: z.number(),
sortOrder: z.string(),
}),
async resolve({ ctx, input }) {
const {
roleFilters,
locationFilters,
experienceFilters,
sortOrder,
numComments,
skip,
} = input;
const userId = ctx.session?.user?.id;
const totalRecords = await ctx.prisma.resumesResume.count({
where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
},
});
const resumesData = await ctx.prisma.resumesResume.findMany({
include: {
_count: {
@ -16,6 +44,7 @@ export const resumesRouter = createRouter()
stars: true,
},
},
comments: true,
stars: {
where: {
OR: {
@ -29,11 +58,32 @@ export const resumesRouter = createRouter()
},
},
},
orderBy: {
createdAt: 'desc',
orderBy:
sortOrder === 'latest'
? {
createdAt: 'desc',
}
: sortOrder === 'popular'
? {
stars: {
_count: 'desc',
},
}
: { comments: { _count: 'desc' } },
skip,
take: 10,
where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
},
});
return resumesData.map((r) => {
const mappedResumeData = resumesData.map((r) => {
const resume: Resume = {
additionalInfo: r.additionalInfo,
createdAt: r.createdAt,
@ -50,6 +100,7 @@ export const resumesRouter = createRouter()
};
return resume;
});
return { mappedResumeData, totalRecords };
},
})
.query('findOne', {

@ -45,8 +45,39 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
})
.query('findUserStarred', {
async resolve({ ctx }) {
input: z.object({
experienceFilters: z.string().array(),
locationFilters: z.string().array(),
numComments: z.number().optional(),
roleFilters: z.string().array(),
skip: z.number(),
sortOrder: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
const {
roleFilters,
locationFilters,
experienceFilters,
sortOrder,
numComments,
skip,
} = input;
const totalRecords = await ctx.prisma.resumesStar.count({
where: {
resume: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
},
userId,
},
});
const resumeStarsData = await ctx.prisma.resumesStar.findMany({
include: {
resume: {
@ -65,14 +96,46 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
},
},
orderBy: {
createdAt: 'desc',
},
orderBy:
sortOrder === 'latest'
? {
resume: {
createdAt: 'desc',
},
}
: sortOrder === 'popular'
? {
resume: {
stars: {
_count: 'desc',
},
},
}
: {
resume: {
comments: {
_count: 'desc',
},
},
},
skip,
take: 10,
where: {
resume: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
},
userId,
},
});
return resumeStarsData.map((rs) => {
const mappedResumeData = resumeStarsData.map((rs) => {
const resume: Resume = {
additionalInfo: rs.resume.additionalInfo,
createdAt: rs.resume.createdAt,
@ -89,11 +152,41 @@ export const resumesResumeUserRouter = createProtectedRouter()
};
return resume;
});
return { mappedResumeData, totalRecords };
},
})
.query('findUserCreated', {
async resolve({ ctx }) {
input: z.object({
experienceFilters: z.string().array(),
locationFilters: z.string().array(),
numComments: z.number().optional(),
roleFilters: z.string().array(),
skip: z.number(),
sortOrder: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
const {
roleFilters,
locationFilters,
experienceFilters,
sortOrder,
numComments,
skip,
} = input;
const totalRecords = await ctx.prisma.resumesResume.count({
where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
userId,
},
});
const resumesData = await ctx.prisma.resumesResume.findMany({
include: {
_count: {
@ -113,14 +206,33 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
},
},
orderBy: {
createdAt: 'desc',
},
orderBy:
sortOrder === 'latest'
? {
createdAt: 'desc',
}
: sortOrder === 'popular'
? {
stars: {
_count: 'desc',
},
}
: { comments: { _count: 'desc' } },
skip,
take: 10,
where: {
...(numComments === 0 && {
comments: {
none: {},
},
}),
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
userId,
},
});
return resumesData.map((r) => {
const mappedResumeData = resumesData.map((r) => {
const resume: Resume = {
additionalInfo: r.additionalInfo,
createdAt: r.createdAt,
@ -137,5 +249,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
};
return resume;
});
return { mappedResumeData, totalRecords };
},
});

Loading…
Cancel
Save