[resumes][feat] Fetch resumes for browse tabs (#326)

* [resumes][fix] Change browse list item styling

* [resumes][feat] Add protected tabs router for browse page

* [resumes][feat] Fetch all, starred and my resumes in browse page

* [resumes][fix] Fix overflow y scrolling

* [resumes][fix] Use date-fns to format upload time text
This commit is contained in:
Su Yin
2022-10-08 20:57:04 +08:00
committed by GitHub
parent 827550a5fd
commit 2f50694016
6 changed files with 158 additions and 16 deletions

View File

@ -1,3 +1,4 @@
import { formatDistanceToNow } from 'date-fns';
import Link from 'next/link'; import Link from 'next/link';
import type { UrlObject } from 'url'; import type { UrlObject } from 'url';
import { ChevronRightIcon } from '@heroicons/react/20/solid'; import { ChevronRightIcon } from '@heroicons/react/20/solid';
@ -13,8 +14,8 @@ type Props = Readonly<{
export default function BrowseListItem({ href, resumeInfo }: Props) { export default function BrowseListItem({ href, resumeInfo }: Props) {
return ( return (
<Link href={href}> <Link href={href}>
<div className="flex justify-between border-b border-slate-200 p-4"> <div className="grid grid-cols-8 border-b border-slate-200 p-4">
<div> <div className="col-span-4">
{resumeInfo.title} {resumeInfo.title}
<div className="mt-2 flex items-center justify-start text-xs text-indigo-500"> <div className="mt-2 flex items-center justify-start text-xs text-indigo-500">
{resumeInfo.role} {resumeInfo.role}
@ -33,11 +34,11 @@ export default function BrowseListItem({ href, resumeInfo }: Props) {
</div> </div>
</div> </div>
</div> </div>
<div className="self-center text-sm text-slate-500"> <div className="col-span-3 self-center text-sm text-slate-500">
{/* TODO: Replace hardcoded days ago with calculated days ago*/} Uploaded {formatDistanceToNow(resumeInfo.createdAt)} ago by{' '}
Uploaded 2 days ago by {resumeInfo.user} {resumeInfo.user}
</div> </div>
<ChevronRightIcon className="w-8" /> <ChevronRightIcon className="col-span-1 w-8 self-center justify-self-center" />
</div> </div>
</Link> </Link>
); );

View File

@ -1,3 +1,9 @@
export const BROWSE_TABS_VALUES = {
ALL: 'all',
MY: 'my',
STARRED: 'starred',
};
export const SORT_OPTIONS = [ export const SORT_OPTIONS = [
{ current: true, href: '#', name: 'Latest' }, { current: true, href: '#', name: 'Latest' },
{ current: false, href: '#', name: 'Popular' }, { current: false, href: '#', name: 'Popular' },

View File

@ -1,5 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { Fragment, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { Disclosure, Menu, Transition } from '@headlessui/react'; import { Disclosure, Menu, Transition } from '@headlessui/react';
import { import {
ChevronDownIcon, ChevronDownIcon,
@ -11,6 +11,7 @@ import { Tabs, TextInput } from '@tih/ui';
import BrowseListItem from '~/components/resumes/browse/BrowseListItem'; import BrowseListItem from '~/components/resumes/browse/BrowseListItem';
import { import {
BROWSE_TABS_VALUES,
EXPERIENCE, EXPERIENCE,
LOCATION, LOCATION,
ROLES, ROLES,
@ -19,6 +20,8 @@ import {
} from '~/components/resumes/browse/constants'; } from '~/components/resumes/browse/constants';
import FilterPill from '~/components/resumes/browse/FilterPill'; import FilterPill from '~/components/resumes/browse/FilterPill';
import type { Resume } from '~/types/resume';
const filters = [ const filters = [
{ {
id: 'roles', id: 'roles',
@ -41,12 +44,47 @@ import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
export default function ResumeHomePage() { export default function ResumeHomePage() {
const [tabsValue, setTabsValue] = useState('all'); const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const resumesQuery = trpc.useQuery(['resumes.resume.list']); const [resumes, setResumes] = useState(Array<Resume>());
const allResumesQuery = trpc.useQuery(['resumes.resume.all'], {
enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
});
const starredResumesQuery = trpc.useQuery(['resumes.resume.browse.stars'], {
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
});
const myResumesQuery = trpc.useQuery(['resumes.resume.browse.my'], {
enabled: tabsValue === BROWSE_TABS_VALUES.MY,
});
useEffect(() => {
switch (tabsValue) {
case BROWSE_TABS_VALUES.ALL: {
setResumes(allResumesQuery.data ?? Array<Resume>());
break;
}
case BROWSE_TABS_VALUES.STARRED: {
setResumes(starredResumesQuery.data ?? Array<Resume>());
break;
}
case BROWSE_TABS_VALUES.MY: {
setResumes(myResumesQuery.data ?? Array<Resume>());
break;
}
default: {
setResumes(Array<Resume>());
}
}
}, [
allResumesQuery.data,
starredResumesQuery.data,
myResumesQuery.data,
tabsValue,
]);
return ( return (
<main className="h-full flex-1 overflow-y-auto"> <main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll">
<div className="ml-4 py-4"> <div className="ml-4 py-4">
<ResumeReviewsTitle /> <ResumeReviewsTitle />
</div> </div>
@ -64,15 +102,15 @@ export default function ResumeHomePage() {
tabs={[ tabs={[
{ {
label: 'All Resumes', label: 'All Resumes',
value: 'all', value: BROWSE_TABS_VALUES.ALL,
}, },
{ {
label: 'Starred Resumes', label: 'Starred Resumes',
value: 'starred', value: BROWSE_TABS_VALUES.STARRED,
}, },
{ {
label: 'My Resumes', label: 'My Resumes',
value: 'my', value: BROWSE_TABS_VALUES.MY,
}, },
]} ]}
value={tabsValue} value={tabsValue}
@ -223,12 +261,14 @@ export default function ResumeHomePage() {
</form> </form>
</div> </div>
</div> </div>
{resumesQuery.isLoading ? ( {allResumesQuery.isLoading ||
starredResumesQuery.isLoading ||
myResumesQuery.isLoading ? (
<div>Loading...</div> <div>Loading...</div>
) : ( ) : (
<div className="col-span-10 pr-8"> <div className="col-span-10 pr-8">
<ul role="list"> <ul role="list">
{resumesQuery.data?.map((resumeObj) => ( {resumes.map((resumeObj) => (
<li key={resumeObj.id}> <li key={resumeObj.id}>
<BrowseListItem href="#" resumeInfo={resumeObj} /> <BrowseListItem href="#" resumeInfo={resumeObj} />
</li> </li>

View File

@ -4,6 +4,7 @@ import { createRouter } from './context';
import { protectedExampleRouter } from './protected-example-router'; import { protectedExampleRouter } from './protected-example-router';
import { resumesRouter } from './resumes'; import { resumesRouter } from './resumes';
import { resumesDetailsRouter } from './resumes-details-router'; import { resumesDetailsRouter } from './resumes-details-router';
import { resumesResumeProtectedTabsRouter } from './resumes-resume-protected-tabs-router';
import { resumesResumeUserRouter } from './resumes-resume-user-router'; import { resumesResumeUserRouter } from './resumes-resume-user-router';
import { resumeReviewsRouter } from './resumes-reviews-router'; import { resumeReviewsRouter } from './resumes-reviews-router';
import { resumesReviewsUserRouter } from './resumes-reviews-user-router'; import { resumesReviewsUserRouter } from './resumes-reviews-user-router';
@ -21,6 +22,7 @@ export const appRouter = createRouter()
.merge('resumes.resume.', resumesRouter) .merge('resumes.resume.', resumesRouter)
.merge('resumes.details.', resumesDetailsRouter) .merge('resumes.details.', resumesDetailsRouter)
.merge('resumes.resume.user.', resumesResumeUserRouter) .merge('resumes.resume.user.', resumesResumeUserRouter)
.merge('resumes.resume.browse.', resumesResumeProtectedTabsRouter)
.merge('resumes.reviews.', resumeReviewsRouter) .merge('resumes.reviews.', resumeReviewsRouter)
.merge('resumes.reviews.user.', resumesReviewsUserRouter); .merge('resumes.reviews.user.', resumesReviewsUserRouter);

View File

@ -0,0 +1,93 @@
import { createProtectedRouter } from './context';
import type { Resume } from '~/types/resume';
export const resumesResumeProtectedTabsRouter = createProtectedRouter()
.query('stars', {
async resolve({ ctx }) {
const userId = ctx.session?.user?.id;
const resumeStarsData = await ctx.prisma.resumesStar.findMany({
include: {
resume: {
include: {
_count: {
select: {
comments: true,
stars: true,
},
},
},
},
user: {
select: {
name: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
where: {
userId,
},
});
return resumeStarsData.map((rs) => {
const resume: Resume = {
additionalInfo: rs.resume.additionalInfo,
createdAt: rs.resume.createdAt,
experience: rs.resume.experience,
id: rs.id,
location: rs.resume.location,
numComments: rs.resume._count.comments,
numStars: rs.resume._count.stars,
role: rs.resume.role,
title: rs.resume.title,
url: rs.resume.url,
user: rs.user.name!,
};
return resume;
});
},
})
.query('my', {
async resolve({ ctx }) {
const userId = ctx.session?.user?.id;
const resumesData = await ctx.prisma.resumesResume.findMany({
include: {
_count: {
select: {
comments: true,
stars: true,
},
},
user: {
select: {
name: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
where: {
userId,
},
});
return resumesData.map((r) => {
const resume: Resume = {
additionalInfo: r.additionalInfo,
createdAt: r.createdAt,
experience: r.experience,
id: r.id,
location: r.location,
numComments: r._count.comments,
numStars: r._count.stars,
role: r.role,
title: r.title,
url: r.url,
user: r.user.name!,
};
return resume;
});
},
});

View File

@ -2,7 +2,7 @@ import { createRouter } from './context';
import type { Resume } from '~/types/resume'; import type { Resume } from '~/types/resume';
export const resumesRouter = createRouter().query('list', { export const resumesRouter = createRouter().query('all', {
async resolve({ ctx }) { async resolve({ ctx }) {
const resumesData = await ctx.prisma.resumesResume.findMany({ const resumesData = await ctx.prisma.resumesResume.findMany({
include: { include: {