mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +08:00
[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:
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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' },
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -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: {
|
||||||
|
Reference in New Issue
Block a user