diff --git a/apps/portal/package.json b/apps/portal/package.json
index d57cdf43..9948c01e 100644
--- a/apps/portal/package.json
+++ b/apps/portal/package.json
@@ -22,6 +22,7 @@
"@trpc/react": "^9.27.2",
"@trpc/server": "^9.27.2",
"clsx": "^1.2.1",
+ "date-fns": "^2.29.3",
"next": "12.3.1",
"next-auth": "~4.10.3",
"react": "18.2.0",
diff --git a/apps/portal/prisma/migrations/20221007135344_remove_resumes_profile_model/migration.sql b/apps/portal/prisma/migrations/20221007135344_remove_resumes_profile_model/migration.sql
new file mode 100644
index 00000000..5b9baead
--- /dev/null
+++ b/apps/portal/prisma/migrations/20221007135344_remove_resumes_profile_model/migration.sql
@@ -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;
diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma
index 086eb2bc..7ece9c5d 100644
--- a/apps/portal/prisma/schema.prisma
+++ b/apps/portal/prisma/schema.prisma
@@ -37,15 +37,18 @@ model Session {
}
model User {
- id String @id @default(cuid())
- name String?
- email String? @unique
- emailVerified DateTime?
- image String?
- accounts Account[]
- sessions Session[]
- todos Todo[]
- resumesProfile ResumesProfile?
+ id String @id @default(cuid())
+ name String?
+ email String? @unique
+ emailVerified DateTime?
+ image String?
+ accounts Account[]
+ sessions Session[]
+ todos Todo[]
+ resumesResumes ResumesResume[]
+ resumesStars ResumesStar[]
+ resumesComments ResumesComment[]
+ resumesCommentVotes ResumesCommentVote[]
}
model VerificationToken {
@@ -85,56 +88,45 @@ model Company {
// Add Resumes project models here, prefix all models with "Resumes",
// use camelCase for field names, and try to name them consistently
// 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 {
- id String @id @default(cuid())
- resumesProfileId String
- title String @db.Text
+ id String @id @default(cuid())
+ userId String
+ title String @db.Text
// TODO: Update role, experience, location to use Enums
- role String @db.Text
- experience String @db.Text
- location String @db.Text
- url String
- additionalInfo String? @db.Text
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
- stars ResumesStar[]
- comments ResumesComment[]
+ role String @db.Text
+ experience String @db.Text
+ location String @db.Text
+ url String
+ additionalInfo String? @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ stars ResumesStar[]
+ comments ResumesComment[]
}
model ResumesStar {
- id String @id @default(cuid())
- resumesProfileId String
- resumeId String
- createdAt DateTime @default(now())
- resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
- resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
+ id String @id @default(cuid())
+ userId String
+ resumeId String
+ createdAt DateTime @default(now())
+ 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 {
- id String @id @default(cuid())
- resumesProfileId String
- resumeId String
- description String @db.Text
- section ResumesSection
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
- resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
- votes ResumesCommentVote[]
+ id String @id @default(cuid())
+ userId String
+ resumeId String
+ description String @db.Text
+ section ResumesSection
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
+ votes ResumesCommentVote[]
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
enum ResumesSection {
@@ -146,16 +138,16 @@ enum ResumesSection {
}
model ResumesCommentVote {
- id String @id @default(cuid())
- resumesProfileId String
- commentId String
- value Int
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade)
- comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade)
+ id String @id @default(cuid())
+ userId String
+ commentId String
+ value Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ 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.
diff --git a/apps/portal/src/components/resumes/ResumePdf.tsx b/apps/portal/src/components/resumes/ResumePdf.tsx
index 12debea4..82e26395 100644
--- a/apps/portal/src/components/resumes/ResumePdf.tsx
+++ b/apps/portal/src/components/resumes/ResumePdf.tsx
@@ -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`;
-export default function ResumePdf() {
+type Props = Readonly<{
+ url: string;
+}>;
+
+export default function ResumePdf({ url }: Props) {
const [numPages, setNumPages] = useState(0);
const [pageNumber] = useState(1);
@@ -18,7 +22,7 @@ export default function ResumePdf() {
}
onLoadSuccess={onPdfLoadSuccess}>
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx
new file mode 100644
index 00000000..5a13a1c3
--- /dev/null
+++ b/apps/portal/src/pages/resumes/[resumeId].tsx
@@ -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 = (
+
+ );
+ 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 && }
+ {detailsQuery.isFetched && detailsQuery.data && (
+
+
+
+ {detailsQuery.data.title}
+
+
+
+
+
+
+ {detailsQuery.data.role}
+
+
+
+ {detailsQuery.data.location}
+
+
+
+ {detailsQuery.data.experience}
+
+
+
+ {`Uploaded ${formatDistanceToNow(
+ new Date(detailsQuery.data.createdAt),
+ { addSuffix: true },
+ )} by ${detailsQuery.data.user.name}`}
+