mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-19 21:33:30 +08:00
[resumes][feat] Fetch resume details from database (#322)
* [resumes][feat] Add resume details router * [resumes][feat] Change review page to dynamic routing * [resumes][feat] Toggle resume star button * [resumes][refactor] Revert routers to User model
This commit is contained in:
@ -22,6 +22,7 @@
|
|||||||
"@trpc/react": "^9.27.2",
|
"@trpc/react": "^9.27.2",
|
||||||
"@trpc/server": "^9.27.2",
|
"@trpc/server": "^9.27.2",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"next": "12.3.1",
|
"next": "12.3.1",
|
||||||
"next-auth": "~4.10.3",
|
"next-auth": "~4.10.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `resumesProfileId` on the `ResumesComment` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `resumesProfileId` on the `ResumesCommentVote` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `resumesProfileId` on the `ResumesResume` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `resumesProfileId` on the `ResumesStar` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `ResumesProfile` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- A unique constraint covering the columns `[userId,commentId]` on the table `ResumesCommentVote` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- A unique constraint covering the columns `[userId,resumeId]` on the table `ResumesStar` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `userId` to the `ResumesComment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `userId` to the `ResumesCommentVote` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `userId` to the `ResumesResume` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `userId` to the `ResumesStar` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ResumesComment" DROP CONSTRAINT "ResumesComment_resumesProfileId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ResumesCommentVote" DROP CONSTRAINT "ResumesCommentVote_resumesProfileId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ResumesProfile" DROP CONSTRAINT "ResumesProfile_userId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ResumesResume" DROP CONSTRAINT "ResumesResume_resumesProfileId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ResumesStar" DROP CONSTRAINT "ResumesStar_resumesProfileId_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "ResumesCommentVote_commentId_resumesProfileId_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "ResumesStar_resumeId_resumesProfileId_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ResumesComment" DROP COLUMN "resumesProfileId",
|
||||||
|
ADD COLUMN "userId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ResumesCommentVote" DROP COLUMN "resumesProfileId",
|
||||||
|
ADD COLUMN "userId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ResumesResume" DROP COLUMN "resumesProfileId",
|
||||||
|
ADD COLUMN "userId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ResumesStar" DROP COLUMN "resumesProfileId",
|
||||||
|
ADD COLUMN "userId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "ResumesProfile";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ResumesCommentVote_userId_commentId_key" ON "ResumesCommentVote"("userId", "commentId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ResumesStar_userId_resumeId_key" ON "ResumesStar"("userId", "resumeId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ResumesResume" ADD CONSTRAINT "ResumesResume_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ResumesStar" ADD CONSTRAINT "ResumesStar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ResumesCommentVote" ADD CONSTRAINT "ResumesCommentVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -45,7 +45,10 @@ model User {
|
|||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
todos Todo[]
|
todos Todo[]
|
||||||
resumesProfile ResumesProfile?
|
resumesResumes ResumesResume[]
|
||||||
|
resumesStars ResumesStar[]
|
||||||
|
resumesComments ResumesComment[]
|
||||||
|
resumesCommentVotes ResumesCommentVote[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
@ -85,20 +88,9 @@ model Company {
|
|||||||
// Add Resumes project models here, prefix all models with "Resumes",
|
// Add Resumes project models here, prefix all models with "Resumes",
|
||||||
// use camelCase for field names, and try to name them consistently
|
// use camelCase for field names, and try to name them consistently
|
||||||
// across all models in this file.
|
// across all models in this file.
|
||||||
|
|
||||||
model ResumesProfile {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
userId String @unique
|
|
||||||
resumesResumes ResumesResume[]
|
|
||||||
resumesStars ResumesStar[]
|
|
||||||
resumesComments ResumesComment[]
|
|
||||||
resumesCommentVotes ResumesCommentVote[]
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
}
|
|
||||||
|
|
||||||
model ResumesResume {
|
model ResumesResume {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
resumesProfileId String
|
userId String
|
||||||
title String @db.Text
|
title String @db.Text
|
||||||
// TODO: Update role, experience, location to use Enums
|
// TODO: Update role, experience, location to use Enums
|
||||||
role String @db.Text
|
role String @db.Text
|
||||||
@ -108,33 +100,33 @@ model ResumesResume {
|
|||||||
additionalInfo String? @db.Text
|
additionalInfo String? @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
stars ResumesStar[]
|
stars ResumesStar[]
|
||||||
comments ResumesComment[]
|
comments ResumesComment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ResumesStar {
|
model ResumesStar {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
resumesProfileId String
|
userId String
|
||||||
resumeId String
|
resumeId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
|
|
||||||
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
|
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([resumeId, resumesProfileId])
|
@@unique([userId, resumeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ResumesComment {
|
model ResumesComment {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
resumesProfileId String
|
userId String
|
||||||
resumeId String
|
resumeId String
|
||||||
description String @db.Text
|
description String @db.Text
|
||||||
section ResumesSection
|
section ResumesSection
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
|
|
||||||
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
|
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
|
||||||
votes ResumesCommentVote[]
|
votes ResumesCommentVote[]
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResumesSection {
|
enum ResumesSection {
|
||||||
@ -147,15 +139,15 @@ enum ResumesSection {
|
|||||||
|
|
||||||
model ResumesCommentVote {
|
model ResumesCommentVote {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
resumesProfileId String
|
userId String
|
||||||
commentId String
|
commentId String
|
||||||
value Int
|
value Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
|
|
||||||
comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([commentId, resumesProfileId])
|
@@unique([userId, commentId])
|
||||||
}
|
}
|
||||||
|
|
||||||
// End of Resumes project models.
|
// End of Resumes project models.
|
||||||
|
@ -6,7 +6,11 @@ import { Button, Spinner } from '@tih/ui';
|
|||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
|
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
|
||||||
|
|
||||||
export default function ResumePdf() {
|
type Props = Readonly<{
|
||||||
|
url: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function ResumePdf({ url }: Props) {
|
||||||
const [numPages, setNumPages] = useState(0);
|
const [numPages, setNumPages] = useState(0);
|
||||||
const [pageNumber] = useState(1);
|
const [pageNumber] = useState(1);
|
||||||
|
|
||||||
@ -18,7 +22,7 @@ export default function ResumePdf() {
|
|||||||
<div>
|
<div>
|
||||||
<Document
|
<Document
|
||||||
className="h-[calc(100vh-17rem)] overflow-scroll"
|
className="h-[calc(100vh-17rem)] overflow-scroll"
|
||||||
file="/test_resume.pdf"
|
file={url}
|
||||||
loading={<Spinner display="block" label="" size="lg" />}
|
loading={<Spinner display="block" label="" size="lg" />}
|
||||||
onLoadSuccess={onPdfLoadSuccess}>
|
onLoadSuccess={onPdfLoadSuccess}>
|
||||||
<Page pageNumber={pageNumber} />
|
<Page pageNumber={pageNumber} />
|
||||||
|
148
apps/portal/src/pages/resumes/[resumeId].tsx
Normal file
148
apps/portal/src/pages/resumes/[resumeId].tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||||
|
import Error from 'next/error';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
AcademicCapIcon,
|
||||||
|
BriefcaseIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
StarIcon,
|
||||||
|
} from '@heroicons/react/20/solid';
|
||||||
|
import { Spinner } from '@tih/ui';
|
||||||
|
|
||||||
|
import CommentsSection from '~/components/resumes/comments/CommentsSection';
|
||||||
|
import ResumePdf from '~/components/resumes/ResumePdf';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
export default function ResumeReviewPage() {
|
||||||
|
const ErrorPage = (
|
||||||
|
<Error statusCode={404} title="Requested resume does not exist." />
|
||||||
|
);
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const { resumeId } = router.query;
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
// Safe to assert resumeId type as string because query is only sent if so
|
||||||
|
const detailsQuery = trpc.useQuery(
|
||||||
|
['resumes.details.find', { resumeId: resumeId as string }],
|
||||||
|
{
|
||||||
|
enabled: typeof resumeId === 'string' && session?.user?.id !== undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const starMutation = trpc.useMutation('resumes.details.update_star', {
|
||||||
|
onSuccess() {
|
||||||
|
utils.invalidateQueries();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (detailsQuery.data?.stars.length) {
|
||||||
|
document.getElementById('star-button')?.focus();
|
||||||
|
} else {
|
||||||
|
document.getElementById('star-button')?.blur();
|
||||||
|
}
|
||||||
|
}, [detailsQuery.data?.stars]);
|
||||||
|
|
||||||
|
const onStarButtonClick = () => {
|
||||||
|
// Star button only rendered if resume exists
|
||||||
|
// Star button only clickable if user exists
|
||||||
|
starMutation.mutate({
|
||||||
|
resumeId: resumeId as string,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{detailsQuery.isError && ErrorPage}
|
||||||
|
{detailsQuery.isLoading && <Spinner display="block" label="" size="lg" />}
|
||||||
|
{detailsQuery.isFetched && detailsQuery.data && (
|
||||||
|
<main className="flex-1 p-4">
|
||||||
|
<div className="flex flex-row md:space-x-8">
|
||||||
|
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||||
|
{detailsQuery.data.title}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
className="isolate inline-flex max-h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
disabled={session?.user === null}
|
||||||
|
id="star-button"
|
||||||
|
type="button"
|
||||||
|
onClick={onStarButtonClick}>
|
||||||
|
<span className="relative inline-flex">
|
||||||
|
<StarIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={clsx(
|
||||||
|
detailsQuery.data?.stars.length
|
||||||
|
? 'text-orange-400'
|
||||||
|
: 'text-gray-400',
|
||||||
|
'-ml-1 mr-2 h-5 w-5',
|
||||||
|
)}
|
||||||
|
id="star-icon"
|
||||||
|
/>
|
||||||
|
Star
|
||||||
|
</span>
|
||||||
|
<span className="relative -ml-px inline-flex">
|
||||||
|
{detailsQuery.data._count.stars}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col pt-1 sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-8">
|
||||||
|
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||||
|
<BriefcaseIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{detailsQuery.data.role}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center pt-2 text-sm text-gray-500">
|
||||||
|
<MapPinIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{detailsQuery.data.location}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center pt-2 text-sm text-gray-500">
|
||||||
|
<AcademicCapIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{detailsQuery.data.experience}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center pt-2 text-sm text-gray-500">
|
||||||
|
<CalendarIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{`Uploaded ${formatDistanceToNow(
|
||||||
|
new Date(detailsQuery.data.createdAt),
|
||||||
|
{ addSuffix: true },
|
||||||
|
)} by ${detailsQuery.data.user.name}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{detailsQuery.data.additionalInfo && (
|
||||||
|
<div className="flex items-center pt-2 text-sm text-gray-500">
|
||||||
|
<InformationCircleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{detailsQuery.data.additionalInfo}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex h-full w-full flex-row py-4">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<ResumePdf url={detailsQuery.data.url} />
|
||||||
|
</div>
|
||||||
|
<div className="mx-8 w-1/2">
|
||||||
|
{/* TODO: Update resumeId */}
|
||||||
|
<CommentsSection resumeId="cl8y6gtez0009yedbne9qp5zi" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
import {
|
|
||||||
AcademicCapIcon,
|
|
||||||
BriefcaseIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
StarIcon,
|
|
||||||
} from '@heroicons/react/20/solid';
|
|
||||||
|
|
||||||
import CommentsSection from '~/components/resumes/comments/CommentsSection';
|
|
||||||
import ResumePdf from '~/components/resumes/ResumePdf';
|
|
||||||
|
|
||||||
export default function ResumeReviewPage() {
|
|
||||||
return (
|
|
||||||
<main className="flex-1 p-4">
|
|
||||||
<div className="flex flex-row md:space-x-8">
|
|
||||||
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
|
||||||
Please help moi, applying for medtech startups in Singapore
|
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
className="isolate inline-flex max-h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
||||||
type="button">
|
|
||||||
<span className="relative inline-flex">
|
|
||||||
<StarIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="-ml-1 mr-2 h-5 w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
Star
|
|
||||||
</span>
|
|
||||||
<span className="relative -ml-px inline-flex">12k</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col pt-1 sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-8">
|
|
||||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
|
||||||
<BriefcaseIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
/>
|
|
||||||
Software Engineer (Backend)
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center pt-2 text-sm text-gray-500">
|
|
||||||
<MapPinIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
/>
|
|
||||||
Singapore
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center pt-2 text-sm text-gray-500">
|
|
||||||
<AcademicCapIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
/>
|
|
||||||
Fresh Grad
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center pt-2 text-sm text-gray-500">
|
|
||||||
<CalendarIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
/>
|
|
||||||
Uploaded 2 days ago by Git Ji Ra
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center pt-2 text-sm text-gray-500">
|
|
||||||
<InformationCircleIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
/>
|
|
||||||
Looking to break into SWE roles after doing engineering for the past 2
|
|
||||||
years
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full w-full flex-row py-4">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<ResumePdf />
|
|
||||||
</div>
|
|
||||||
<div className="mx-8 w-1/2">
|
|
||||||
{/* TODO: Update resumeId */}
|
|
||||||
<CommentsSection resumeId="cl8y6gtez0009yedbne9qp5zi" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ import superjson from 'superjson';
|
|||||||
|
|
||||||
import { createRouter } from './context';
|
import { createRouter } from './context';
|
||||||
import { protectedExampleRouter } from './protected-example-router';
|
import { protectedExampleRouter } from './protected-example-router';
|
||||||
|
import { resumesDetailsRouter } from './resumes-details-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';
|
||||||
@ -16,6 +17,7 @@ export const appRouter = createRouter()
|
|||||||
.merge('auth.', protectedExampleRouter)
|
.merge('auth.', protectedExampleRouter)
|
||||||
.merge('todos.', todosRouter)
|
.merge('todos.', todosRouter)
|
||||||
.merge('todos.user.', todosUserRouter)
|
.merge('todos.user.', todosUserRouter)
|
||||||
|
.merge('resumes.details.', resumesDetailsRouter)
|
||||||
.merge('resumes.resume.user.', resumesResumeUserRouter)
|
.merge('resumes.resume.user.', resumesResumeUserRouter)
|
||||||
.merge('resumes.reviews.', resumeReviewsRouter)
|
.merge('resumes.reviews.', resumeReviewsRouter)
|
||||||
.merge('resumes.reviews.user.', resumesReviewsUserRouter);
|
.merge('resumes.reviews.user.', resumesReviewsUserRouter);
|
||||||
|
79
apps/portal/src/server/router/resumes-details-router.ts
Normal file
79
apps/portal/src/server/router/resumes-details-router.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createRouter } from './context';
|
||||||
|
|
||||||
|
export const resumesDetailsRouter = createRouter()
|
||||||
|
.query('find', {
|
||||||
|
input: z.object({
|
||||||
|
resumeId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { resumeId } = input;
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
|
||||||
|
// Use the resumeId to query all related information of a single resume
|
||||||
|
// from Resumesresume:
|
||||||
|
return await ctx.prisma.resumesResume.findUnique({
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
stars: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stars: {
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: resumeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('update_star', {
|
||||||
|
input: z.object({
|
||||||
|
resumeId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { resumeId } = input;
|
||||||
|
// Update_star will only be called if user is logged in
|
||||||
|
const userId = ctx.session!.user!.id;
|
||||||
|
|
||||||
|
// Use the resumeId and resumeProfileId to check if star exists
|
||||||
|
const resumesStar = await ctx.prisma.resumesStar.findUnique({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
userId_resumeId: {
|
||||||
|
resumeId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resumesStar === null) {
|
||||||
|
return await ctx.prisma.resumesStar.create({
|
||||||
|
data: {
|
||||||
|
resumeId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await ctx.prisma.resumesStar.delete({
|
||||||
|
where: {
|
||||||
|
userId_resumeId: {
|
||||||
|
resumeId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -15,23 +15,12 @@ export const resumesResumeUserRouter = createProtectedRouter().mutation(
|
|||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.session?.user.id;
|
||||||
const resumeProfile = await ctx.prisma.resumesProfile.upsert({
|
|
||||||
create: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Store file in file storage and retrieve URL
|
// TODO: Store file in file storage and retrieve URL
|
||||||
|
|
||||||
return await ctx.prisma.resumesResume.create({
|
return await ctx.prisma.resumesResume.create({
|
||||||
data: {
|
data: {
|
||||||
...input,
|
...input,
|
||||||
resumesProfileId: resumeProfile.id,
|
|
||||||
url: '',
|
url: '',
|
||||||
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -7,18 +7,9 @@ export const resumeReviewsRouter = createRouter().query('list', {
|
|||||||
resumeId: z.string(),
|
resumeId: z.string(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
const { resumeId } = input;
|
const { resumeId } = input;
|
||||||
|
|
||||||
const { resumesProfileId } =
|
|
||||||
await ctx.prisma.resumesResume.findUniqueOrThrow({
|
|
||||||
select: {
|
|
||||||
resumesProfileId: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: resumeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// For this resume, we retrieve every comment's information, along with:
|
// For this resume, we retrieve every comment's information, along with:
|
||||||
// The user's name and image to render
|
// The user's name and image to render
|
||||||
// Number of votes, and whether the user (if-any) has voted
|
// Number of votes, and whether the user (if-any) has voted
|
||||||
@ -29,20 +20,16 @@ export const resumeReviewsRouter = createRouter().query('list', {
|
|||||||
votes: true,
|
votes: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resumesProfile: {
|
|
||||||
include: {
|
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
image: true,
|
image: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
votes: {
|
||||||
take: 1,
|
take: 1,
|
||||||
where: {
|
where: {
|
||||||
resumesProfileId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -6,8 +6,8 @@ import { createProtectedRouter } from './context';
|
|||||||
type IResumeCommentInput = Readonly<{
|
type IResumeCommentInput = Readonly<{
|
||||||
description: string;
|
description: string;
|
||||||
resumeId: string;
|
resumeId: string;
|
||||||
resumesProfileId: string;
|
|
||||||
section: ResumesSection;
|
section: ResumesSection;
|
||||||
|
userId: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const resumesReviewsUserRouter = createProtectedRouter().mutation(
|
export const resumesReviewsUserRouter = createProtectedRouter().mutation(
|
||||||
@ -22,19 +22,10 @@ export const resumesReviewsUserRouter = createProtectedRouter().mutation(
|
|||||||
skills: z.string(),
|
skills: z.string(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
const { resumeId, education, experience, general, projects, skills } =
|
const { resumeId, education, experience, general, projects, skills } =
|
||||||
input;
|
input;
|
||||||
|
|
||||||
const { resumesProfileId } =
|
|
||||||
await ctx.prisma.resumesResume.findUniqueOrThrow({
|
|
||||||
select: {
|
|
||||||
resumesProfileId: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: resumeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// For each section, convert them into ResumesComment model if provided
|
// For each section, convert them into ResumesComment model if provided
|
||||||
const comments: Array<IResumeCommentInput> = [
|
const comments: Array<IResumeCommentInput> = [
|
||||||
{ description: education, section: ResumesSection.EDUCATION },
|
{ description: education, section: ResumesSection.EDUCATION },
|
||||||
@ -50,8 +41,8 @@ export const resumesReviewsUserRouter = createProtectedRouter().mutation(
|
|||||||
return {
|
return {
|
||||||
description,
|
description,
|
||||||
resumeId,
|
resumeId,
|
||||||
resumesProfileId,
|
|
||||||
section,
|
section,
|
||||||
|
userId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6049,7 +6049,7 @@ damerau-levenshtein@^1.0.8:
|
|||||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||||
|
|
||||||
date-fns@^2.29.1:
|
date-fns@^2.29.1, date-fns@^2.29.3:
|
||||||
version "2.29.3"
|
version "2.29.3"
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
|
||||||
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
||||||
|
Reference in New Issue
Block a user