mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-29 21:23:14 +08:00
[resumes][fix] fix resumes starring lag + add zoom controls (#359)
* [resumes][fix] Fix star button delay * [resumes][feat] add zoom controls for pdf
This commit is contained in:
@ -1,7 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import type { PDFDocumentProxy } from 'react-pdf/node_modules/pdfjs-dist';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import { Button, Spinner } from '@tih/ui';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
|
||||
@ -13,6 +18,7 @@ type Props = Readonly<{
|
||||
export default function ResumePdf({ url }: Props) {
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [scale, setScale] = useState(1);
|
||||
|
||||
const onPdfLoadSuccess = (pdf: PDFDocumentProxy) => {
|
||||
setNumPages(pdf.numPages);
|
||||
@ -20,14 +26,36 @@ export default function ResumePdf({ url }: Props) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="group relative">
|
||||
<Document
|
||||
className="flex h-[calc(100vh-17rem)] flex-row justify-center overflow-scroll"
|
||||
className="flex h-[calc(100vh-17rem)] flex-row justify-center overflow-auto"
|
||||
file={url}
|
||||
loading={<Spinner display="block" label="" size="lg" />}
|
||||
loading={<Spinner display="block" size="lg" />}
|
||||
noData=""
|
||||
onLoadSuccess={onPdfLoadSuccess}>
|
||||
<Page pageNumber={pageNumber} />
|
||||
<Page pageNumber={pageNumber} scale={scale} width={750} />
|
||||
<div className="absolute top-2 right-5 hidden hover:block group-hover:block">
|
||||
<Button
|
||||
className="rounded-r-none focus:ring-0 focus:ring-offset-0"
|
||||
disabled={scale === 0.5}
|
||||
icon={MagnifyingGlassMinusIcon}
|
||||
isLabelHidden={true}
|
||||
label="Zoom Out"
|
||||
variant="tertiary"
|
||||
onClick={() => setScale(scale - 0.25)}
|
||||
/>
|
||||
<Button
|
||||
className="rounded-l-none focus:ring-0 focus:ring-offset-0"
|
||||
disabled={scale === 1.5}
|
||||
icon={MagnifyingGlassPlusIcon}
|
||||
isLabelHidden={true}
|
||||
label="Zoom In"
|
||||
variant="tertiary"
|
||||
onClick={() => setScale(scale + 0.25)}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between p-4">
|
||||
<Button
|
||||
|
@ -22,7 +22,7 @@ export default function CommentListItems({ comments, isLoading }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-3 overflow-y-scroll">
|
||||
<div className="m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-3 overflow-y-auto">
|
||||
{comments.map((comment) => (
|
||||
<Comment
|
||||
key={comment.id}
|
||||
|
@ -77,7 +77,7 @@ export default function CommentsForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-13rem)] overflow-y-scroll">
|
||||
<div className="h-[calc(100vh-13rem)] overflow-y-auto">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Add your review</h2>
|
||||
<p className="text-gray-800">
|
||||
Please fill in at least one section to submit your review
|
||||
|
@ -4,7 +4,7 @@ import Error from 'next/error';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
AcademicCapIcon,
|
||||
BriefcaseIcon,
|
||||
@ -22,12 +22,11 @@ import { trpc } from '~/utils/trpc';
|
||||
|
||||
export default function ResumeReviewPage() {
|
||||
const ErrorPage = (
|
||||
<Error statusCode={404} title="Requested resume does not exist." />
|
||||
<Error statusCode={404} title="Requested resume does not exist" />
|
||||
);
|
||||
const { data: session } = useSession();
|
||||
const router = useRouter();
|
||||
const { resumeId } = router.query;
|
||||
const trpcContext = trpc.useContext();
|
||||
// Safe to assert resumeId type as string because query is only sent if so
|
||||
const detailsQuery = trpc.useQuery(
|
||||
['resumes.resume.findOne', { resumeId: resumeId as string }],
|
||||
@ -35,31 +34,59 @@ export default function ResumeReviewPage() {
|
||||
enabled: typeof resumeId === 'string',
|
||||
},
|
||||
);
|
||||
const starMutation = trpc.useMutation('resumes.star.user.create_or_delete', {
|
||||
onSuccess() {
|
||||
trpcContext.invalidateQueries(['resumes.resume.findOne']);
|
||||
const starMutation = trpc.useMutation('resumes.resume.star', {
|
||||
onError() {
|
||||
setStarDetails({
|
||||
isStarred: false,
|
||||
numStars: starDetails.numStars - 1,
|
||||
});
|
||||
},
|
||||
});
|
||||
const unstarMutation = trpc.useMutation('resumes.resume.unstar', {
|
||||
onError() {
|
||||
setStarDetails({
|
||||
isStarred: true,
|
||||
numStars: starDetails.numStars + 1,
|
||||
});
|
||||
},
|
||||
});
|
||||
const [starDetails, setStarDetails] = useState({
|
||||
isStarred: false,
|
||||
numStars: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (detailsQuery.data?.stars.length) {
|
||||
document.getElementById('star-button')?.focus();
|
||||
} else {
|
||||
document.getElementById('star-button')?.blur();
|
||||
if (detailsQuery?.data !== undefined) {
|
||||
setStarDetails({
|
||||
isStarred: !!detailsQuery.data?.stars.length,
|
||||
numStars: detailsQuery.data?._count.stars ?? 0,
|
||||
});
|
||||
}
|
||||
}, [detailsQuery.data?.stars]);
|
||||
}, [detailsQuery.data]);
|
||||
|
||||
const onStarButtonClick = () => {
|
||||
setStarDetails({
|
||||
isStarred: !starDetails.isStarred,
|
||||
numStars: starDetails.isStarred
|
||||
? starDetails.numStars - 1
|
||||
: starDetails.numStars + 1,
|
||||
});
|
||||
// Star button only rendered if resume exists
|
||||
// Star button only clickable if user exists
|
||||
if (starDetails.isStarred) {
|
||||
unstarMutation.mutate({
|
||||
resumeId: resumeId as string,
|
||||
});
|
||||
} else {
|
||||
starMutation.mutate({
|
||||
resumeId: resumeId as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{detailsQuery.isError && ErrorPage}
|
||||
{(detailsQuery.isError || detailsQuery.data === null) && ErrorPage}
|
||||
{detailsQuery.isLoading && (
|
||||
<div className="w-full pt-4">
|
||||
{' '}
|
||||
@ -71,14 +98,19 @@ export default function ResumeReviewPage() {
|
||||
<Head>
|
||||
<title>{detailsQuery.data.title}</title>
|
||||
</Head>
|
||||
<main className="h-[calc(100vh-2rem)] flex-1 overflow-y-scroll p-4">
|
||||
<main className="h-[calc(100vh-2rem)] flex-1 overflow-y-auto p-4">
|
||||
<div className="flex flex-row 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}
|
||||
className={clsx(
|
||||
starDetails.isStarred
|
||||
? 'z-10 border-indigo-500 outline-none ring-1 ring-indigo-500'
|
||||
: '',
|
||||
'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 disabled:hover:bg-white',
|
||||
)}
|
||||
disabled={session?.user === undefined}
|
||||
id="star-button"
|
||||
type="button"
|
||||
onClick={onStarButtonClick}>
|
||||
@ -86,7 +118,7 @@ export default function ResumeReviewPage() {
|
||||
<StarIcon
|
||||
aria-hidden="true"
|
||||
className={clsx(
|
||||
detailsQuery.data?.stars.length
|
||||
starDetails.isStarred
|
||||
? 'text-orange-400'
|
||||
: 'text-gray-400',
|
||||
'-ml-1 mr-2 h-5 w-5',
|
||||
@ -96,7 +128,7 @@ export default function ResumeReviewPage() {
|
||||
Star
|
||||
</span>
|
||||
<span className="relative -ml-px inline-flex">
|
||||
{detailsQuery.data._count.stars}
|
||||
{starDetails.numStars}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@ export const appRouter = createRouter()
|
||||
.merge('companies.', companiesRouter)
|
||||
.merge('resumes.resume.', resumesRouter)
|
||||
.merge('resumes.resume.user.', resumesResumeUserRouter)
|
||||
.merge('resumes.star.user.', resumesStarUserRouter)
|
||||
.merge('resumes.resume.', resumesStarUserRouter)
|
||||
.merge('resumes.reviews.', resumeReviewsRouter)
|
||||
.merge('resumes.reviews.user.', resumesReviewsUserRouter)
|
||||
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
|
||||
|
@ -62,9 +62,11 @@ export const resumesRouter = createRouter()
|
||||
},
|
||||
stars: {
|
||||
where: {
|
||||
OR: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
|
@ -2,38 +2,14 @@ import { z } from 'zod';
|
||||
|
||||
import { createProtectedRouter } from '../context';
|
||||
|
||||
export const resumesStarUserRouter = createProtectedRouter().mutation(
|
||||
'create_or_delete',
|
||||
{
|
||||
export const resumesStarUserRouter = createProtectedRouter()
|
||||
.mutation('unstar', {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
const userId = ctx.session.user.id;
|
||||
return await ctx.prisma.resumesStar.delete({
|
||||
where: {
|
||||
userId_resumeId: {
|
||||
@ -43,5 +19,19 @@ export const resumesStarUserRouter = createProtectedRouter().mutation(
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('star', {
|
||||
input: z.object({
|
||||
resumeId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { resumeId } = input;
|
||||
const userId = ctx.session.user.id;
|
||||
return await ctx.prisma.resumesStar.create({
|
||||
data: {
|
||||
resumeId,
|
||||
userId,
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user