From 21e5e0672ad39b2d29effe9f4029161b93010a49 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sun, 9 Oct 2022 08:54:57 +0800 Subject: [PATCH 01/22] [ui][radio list] remove disabled prop on radio list level --- apps/storybook/stories/radio-list.stories.tsx | 24 +++++++------------ packages/ui/src/RadioList/RadioList.tsx | 6 ++--- packages/ui/src/RadioList/RadioListContext.ts | 1 - packages/ui/src/RadioList/RadioListItem.tsx | 8 ++++--- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/apps/storybook/stories/radio-list.stories.tsx b/apps/storybook/stories/radio-list.stories.tsx index 4217bd13..91ca7b72 100644 --- a/apps/storybook/stories/radio-list.stories.tsx +++ b/apps/storybook/stories/radio-list.stories.tsx @@ -29,7 +29,11 @@ export default { export function Basic({ description, label, -}: Pick, 'description' | 'label'>) { + orientation, +}: Pick< + React.ComponentProps, + 'description' | 'label' | 'orientation' +>) { const items = [ { label: 'Apple', @@ -50,7 +54,8 @@ export function Basic({ defaultValue="apple" description={description} label={label} - name="fruit"> + name="fruit" + orientation={orientation}> {items.map(({ label: itemLabel, value }) => ( ))} @@ -61,6 +66,7 @@ export function Basic({ Basic.args = { description: 'Your favorite fruit', label: 'Choose a fruit', + orientation: 'vertical', }; export function Controlled() { @@ -148,22 +154,10 @@ export function Disabled() { }, ]; - const [value, setValue] = useState('apple'); - return (
setValue(newValue)}> - {items.map(({ label: itemLabel, value: itemValue }) => ( - - ))} - - - {items.map( diff --git a/packages/ui/src/RadioList/RadioList.tsx b/packages/ui/src/RadioList/RadioList.tsx index 974491f1..6919c819 100644 --- a/packages/ui/src/RadioList/RadioList.tsx +++ b/packages/ui/src/RadioList/RadioList.tsx @@ -11,7 +11,6 @@ type Props = Readonly<{ children: ReadonlyArray>; defaultValue?: T; description?: string; - disabled?: boolean; isLabelHidden?: boolean; label: string; name?: string; @@ -27,10 +26,9 @@ export default function RadioList({ children, defaultValue, description, - disabled, - orientation = 'vertical', isLabelHidden, name, + orientation = 'vertical', label, required, value, @@ -41,7 +39,7 @@ export default function RadioList({ + value={{ defaultValue, name, onChange, value }}>
{fileUploadError && ( From 8481ab1044601252d26f41dfa19ba6bc8a65810a Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sun, 9 Oct 2022 11:08:57 +0800 Subject: [PATCH 07/22] [portal][ui] change app shell UI --- apps/portal/public/favicon.ico | Bin 9662 -> 9797 bytes .../portal/src/components/global/AppShell.tsx | 90 +++++++++++++----- .../src/components/global/HomeNavigation.ts | 21 ++++ .../components/global/ProductNavigation.tsx | 72 ++++++++++++++ .../src/components/offers/OffersNavigation.ts | 22 +++++ .../src/components/questions/NavBar.tsx | 56 ----------- .../questions/QuestionsNavigation.ts | 15 +++ .../components/resumes/ResumesNavigation.ts | 22 +++++ apps/portal/src/pages/index.tsx | 4 - apps/portal/src/pages/questions/index.tsx | 6 +- 10 files changed, 218 insertions(+), 90 deletions(-) create mode 100644 apps/portal/src/components/global/HomeNavigation.ts create mode 100644 apps/portal/src/components/global/ProductNavigation.tsx create mode 100644 apps/portal/src/components/offers/OffersNavigation.ts delete mode 100644 apps/portal/src/components/questions/NavBar.tsx create mode 100644 apps/portal/src/components/questions/QuestionsNavigation.ts create mode 100644 apps/portal/src/components/resumes/ResumesNavigation.ts diff --git a/apps/portal/public/favicon.ico b/apps/portal/public/favicon.ico index d3b456c0702274473e7d64cca7f8815d089677c2..2b50a78284f6df84d92268e4a3948a2a7acdf04c 100644 GIT binary patch literal 9797 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 HrvkqLky{gH diff --git a/apps/portal/src/components/global/AppShell.tsx b/apps/portal/src/components/global/AppShell.tsx index fde9d3b6..c94c1f58 100644 --- a/apps/portal/src/components/global/AppShell.tsx +++ b/apps/portal/src/components/global/AppShell.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx'; import Link from 'next/link'; +import { useRouter } from 'next/router'; import { signIn, signOut, useSession } from 'next-auth/react'; import type { ReactNode } from 'react'; import { Fragment, useState } from 'react'; @@ -13,6 +14,14 @@ import { XMarkIcon, } from '@heroicons/react/24/outline'; +import HomeNavigation from '~/components/global/HomeNavigation'; +import OffersNavigation from '~/components/offers/OffersNavigation'; +import QuestionsNavigation from '~/components/questions/QuestionsNavigation'; +import ResumesNavigation from '~/components/resumes/ResumesNavigation'; + +import type { ProductNavigationItems } from './ProductNavigation'; +import ProductNavigation from './ProductNavigation'; + const sidebarNavigation = [ { current: false, href: '/', icon: HomeIcon, name: 'Home' }, { current: false, href: '/resumes', icon: DocumentTextIcon, name: 'Resumes' }, @@ -39,14 +48,15 @@ function ProfileJewel() { if (session == null) { return ( - { event.preventDefault(); signIn(); }}> Sign in - + ); } @@ -65,7 +75,7 @@ function ProfileJewel() { return (
- + Open user menu {session?.user?.image == null ? ( Render some icon @@ -110,18 +120,41 @@ function ProfileJewel() { export default function AppShell({ children }: Props) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const router = useRouter(); + + const currentProductNavigation: Readonly<{ + navigation: ProductNavigationItems; + title: string; + }> = (() => { + const path = router.pathname; + if (path.startsWith('/resumes')) { + return ResumesNavigation; + } + + if (path.startsWith('/offers')) { + return OffersNavigation; + } + + if (path.startsWith('/questions')) { + return QuestionsNavigation; + } + + return HomeNavigation; + })(); return (
{/* Narrow sidebar */} -
+
- Your Company + + Tech Interview Handbook +
{sidebarNavigation.map((item) => ( @@ -130,8 +163,8 @@ export default function AppShell({ children }: Props) { aria-current={item.current ? 'page' : undefined} className={clsx( item.current - ? 'bg-indigo-800 text-white' - : 'text-indigo-100 hover:bg-indigo-800 hover:text-white', + ? 'bg-primary-50 text-slate-700' + : 'text-slate-700 hover:bg-slate-200', 'group flex w-full flex-col items-center rounded-md p-3 text-xs font-medium', )} href={item.href}> @@ -139,8 +172,8 @@ export default function AppShell({ children }: Props) { aria-hidden="true" className={clsx( item.current - ? 'text-white' - : 'text-indigo-300 group-hover:text-white', + ? 'text-primary-500' + : 'text-slate-500 group-hover:text-slate-700', 'h-6 w-6', )} /> @@ -177,7 +210,7 @@ export default function AppShell({ children }: Props) { leave="transition ease-in-out duration-300 transform" leaveFrom="translate-x-0" leaveTo="-translate-x-full"> - +
- Your Company + + Tech Interview Handbook +
+
+
+ +
- -
-
-
-
-

Categories

-
    - {TOP_HITS.map((category) => ( -
  • - {/* TODO: Replace onClick with filtering function */} - true} +
    +
    +
    + +

    Categories

    +
      + {TOP_HITS.map((category) => ( +
    • + {/* TODO: Replace onClick with filtering function */} + true} + /> +
    • + ))} +
    + + {filters.map((section) => ( + + {({ open }) => ( + <> +

    + + + {section.name} + + + {open ? ( + + +

    + +
    + {section.options.map((option, optionIdx) => ( +
    + + +
    + ))} +
    +
    + + )} +
    + ))} + +
    +
    + {allResumesQuery.isLoading || + starredResumesQuery.isLoading || + myResumesQuery.isLoading ? ( +
    + +
    + ) : ( +
    +
      + {resumes.map((resumeObj) => ( +
    • +
    • ))}
    - - {filters.map((section) => ( - - {({ open }) => ( - <> -

    - - - {section.name} - - - {open ? ( - - -

    - -
    - {section.options.map((option, optionIdx) => ( -
    - - -
    - ))} -
    -
    - - )} -
    - ))} - -
    +
    + )}
- {allResumesQuery.isLoading || - starredResumesQuery.isLoading || - myResumesQuery.isLoading ? ( -
Loading...
- ) : ( -
-
    - {resumes.map((resumeObj) => ( -
  • - -
  • - ))} -
-
- )}
- - + + ); } diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index 819f6e6f..fc573f14 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -103,7 +103,7 @@ export default function SubmitResumeForm() { return ( <> - Upload a resume + Upload a Resume
Date: Sun, 9 Oct 2022 15:30:58 +0800 Subject: [PATCH 10/22] [portal][nav] hide global nav for resumes --- .../portal/src/components/global/AppShell.tsx | 61 ++++++++++--------- .../src/components/global/HomeNavigation.ts | 1 + .../src/components/offers/OffersNavigation.ts | 1 + .../questions/QuestionsNavigation.ts | 1 + .../components/resumes/ResumesNavigation.ts | 1 + 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/apps/portal/src/components/global/AppShell.tsx b/apps/portal/src/components/global/AppShell.tsx index c0fb33a1..eeb2dfc9 100644 --- a/apps/portal/src/components/global/AppShell.tsx +++ b/apps/portal/src/components/global/AppShell.tsx @@ -107,6 +107,7 @@ export default function AppShell({ children }: Props) { const currentProductNavigation: Readonly<{ navigation: ProductNavigationItems; + showGlobalNav: boolean; title: string; }> = (() => { const path = router.pathname; @@ -128,39 +129,41 @@ export default function AppShell({ children }: Props) { return (
{/* Narrow sidebar */} -
-
-
- - Tech Interview Handbook - -
-
- {GlobalNavigation.map((item) => ( - -
-
+ )} {/* Mobile menu */} Date: Sun, 9 Oct 2022 16:34:17 +0800 Subject: [PATCH 11/22] [questions][feat] add questions crud (#327) --- apps/portal/src/server/router/index.ts | 4 +- .../router/questions-question-router.ts | 264 ++++++++++++++++++ apps/portal/src/types/questions.d.ts | 13 + 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 apps/portal/src/server/router/questions-question-router.ts create mode 100644 apps/portal/src/types/questions.d.ts diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts index cae45dde..e6d5a960 100644 --- a/apps/portal/src/server/router/index.ts +++ b/apps/portal/src/server/router/index.ts @@ -2,6 +2,7 @@ import superjson from 'superjson'; import { createRouter } from './context'; import { protectedExampleRouter } from './protected-example-router'; +import { questionsQuestionRouter} from './questions-question-router'; import { resumesRouter } from './resumes'; import { resumesDetailsRouter } from './resumes-details-router'; import { resumesResumeProtectedTabsRouter } from './resumes-resume-protected-tabs-router'; @@ -24,7 +25,8 @@ export const appRouter = createRouter() .merge('resumes.resume.user.', resumesResumeUserRouter) .merge('resumes.resume.browse.', resumesResumeProtectedTabsRouter) .merge('resumes.reviews.', resumeReviewsRouter) - .merge('resumes.reviews.user.', resumesReviewsUserRouter); + .merge('resumes.reviews.user.', resumesReviewsUserRouter) + .merge('questions.questions.', questionsQuestionRouter); // Export type definition of API export type AppRouter = typeof appRouter; diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts new file mode 100644 index 00000000..8918f036 --- /dev/null +++ b/apps/portal/src/server/router/questions-question-router.ts @@ -0,0 +1,264 @@ +import { z } from 'zod'; +import {QuestionsQuestionType, Vote } from '@prisma/client'; +import { TRPCError } from '@trpc/server'; + +import { createProtectedRouter } from './context'; + +import type { Question } from '~/types/questions'; + +export const questionsQuestionRouter = createProtectedRouter() + .query('getQuestionsByFilter', { + input: z.object({ + company: z.string().optional(), + location: z.string().optional(), + questionType: z.nativeEnum(QuestionsQuestionType), + role: z.string().optional(), + }), + async resolve({ ctx, input }) { + const questionsData = await ctx.prisma.questionsQuestion.findMany({ + include: { + _count: { + select: { + answers: true, + comments: true, + }, + }, + encounters: { + select: { + company: true, + location: true, + role: true, + }, + }, + user: { + select: { + name: true, + }, + }, + votes: true, + }, + orderBy: { + createdAt: 'desc', + }, + where: { + questionType: input.questionType, + }, + }); + return questionsData + .filter((data) => { + for (let i = 0; i < data.encounters.length; i++) { + const encounter = data.encounters[i] + const matchCompany = (!input.company || (encounter.company === input.company)); + const matchLocation = (!input.location || (encounter.location === input.location)); + const matchRole = (!input.company || (encounter.role === input.role)); + if (matchCompany && matchLocation && matchRole) {return true}; + } + return false; + }) + .map((data) => { + const votes:number = data.votes.reduce( + (previousValue:number, currentValue) => { + let result:number = previousValue; + + switch(currentValue.vote) { + case Vote.UPVOTE: + result += 1 + break; + case Vote.DOWNVOTE: + result -= 1 + break; + } + return result; + }, + 0 + ); + + let userName = ""; + + if (data.user) { + userName = data.user.name!; + } + + const question: Question = { + company: "", + content: data.content, + id: data.id, + location: "", + numAnswers: data._count.answers, + numComments: data._count.comments, + numVotes: votes, + role: "", + updatedAt: data.updatedAt, + user: userName, + }; + return question; + }); + } + }) + .mutation('create', { + input: z.object({ + content: z.string(), + questionType: z.nativeEnum(QuestionsQuestionType), + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + + return await ctx.prisma.questionsQuestion.create({ + data: { + ...input, + userId, + }, + }); + }, + }) + .mutation('update', { + input: z.object({ + content: z.string().optional(), + id: z.string(), + questionType: z.nativeEnum(QuestionsQuestionType).optional(), + + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + + const questionToUpdate = await ctx.prisma.questionsQuestion.findUnique({ + where: { + id: input.id, + }, + }); + + if (questionToUpdate?.id !== userId) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'User have no authorization to record.', + // Optional: pass the original error to retain stack trace + }); + } + + return await ctx.prisma.questionsQuestion.update({ + data: { + ...input, + }, + where: { + id: input.id, + }, + }); + }, + }) + .mutation('delete', { + input: z.object({ + id: z.string(), + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + + const questionToDelete = await ctx.prisma.questionsQuestion.findUnique({ + where: { + id: input.id, + }, + }); + + if (questionToDelete?.id !== userId) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'User have no authorization to record.', + // Optional: pass the original error to retain stack trace + }); + } + + return await ctx.prisma.questionsQuestion.delete({ + where: { + id: input.id, + }, + }); + }, + }) + .query('getVote', { + input: z.object({ + questionId: z.string(), + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + const {questionId} = input + + return await ctx.prisma.questionsQuestionVote.findUnique({ + where: { + questionId_userId : {questionId,userId } + }, + }); + }, + }) + .mutation('createVote', { + input: z.object({ + questionId: z.string(), + vote: z.nativeEnum(Vote), + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + + return await ctx.prisma.questionsQuestionVote.create({ + data: { + ...input, + userId, + }, + }); + }, + }) + .mutation('updateVote', { + input: z.object({ + id: z.string(), + vote: z.nativeEnum(Vote), + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + const {id, vote} = input + + const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({ + where: { + id: input.id, + }, + }); + + if (voteToUpdate?.id !== userId) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'User have no authorization to record.', + }); + } + + return await ctx.prisma.questionsQuestionVote.update({ + data: { + vote, + }, + where: { + id, + }, + }); + }, + }) + .mutation('deleteVote', { + input: z.object({ + id: z.string(), + }), + async resolve({ ctx, input }) { + const userId = ctx.session?.user?.id; + + const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({ + where: { + id: input.id, + },}); + + if (voteToDelete?.id !== userId) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'User have no authorization to record.', + }); + } + + return await ctx.prisma.questionsQuestionVote.delete({ + where: { + id: input.id, + }, + }); + }, + }); \ No newline at end of file diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts new file mode 100644 index 00000000..fa72f638 --- /dev/null +++ b/apps/portal/src/types/questions.d.ts @@ -0,0 +1,13 @@ +export type Question = { + // TODO: company, location, role maps + company: string; + content: string; + id: string; + location: string; + numAnswers: number; + numComments: number; + numVotes: number; + role: string; + updatedAt: Date; + user: string; +}; \ No newline at end of file From 632439dad486b14f206bcb91dc119eaabe246b48 Mon Sep 17 00:00:00 2001 From: Su Yin <53945359+tnsyn@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:45:16 +0800 Subject: [PATCH 12/22] [resumes][refactor] Filter comments on FE (#336) * [resumes][fix] Fix fetch id * [resumes][refactor] Change to filtering on FE for comments * [resumes][fix] Fix lint errors --- .../resumes/comments/CommentsList.tsx | 25 +++++++++---------- .../resumes-resume-protected-tabs-router.ts | 2 +- .../server/router/resumes-reviews-router.ts | 5 +--- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/apps/portal/src/components/resumes/comments/CommentsList.tsx b/apps/portal/src/components/resumes/comments/CommentsList.tsx index 54843bca..7e15e4d8 100644 --- a/apps/portal/src/components/resumes/comments/CommentsList.tsx +++ b/apps/portal/src/components/resumes/comments/CommentsList.tsx @@ -21,10 +21,7 @@ export default function CommentsList({ const { data: session } = useSession(); // Fetch the most updated comments to render - const commentsQuery = trpc.useQuery([ - 'resumes.reviews.list', - { resumeId, section: tab }, - ]); + const commentsQuery = trpc.useQuery(['resumes.reviews.list', { resumeId }]); // TODO: Add loading prompt @@ -39,15 +36,17 @@ export default function CommentsList({ />
- {commentsQuery.data?.map((comment) => { - return ( - - ); - })} + {commentsQuery.data + ?.filter((c) => c.section === tab) + .map((comment) => { + return ( + + ); + })}
); diff --git a/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts b/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts index 0f5ebaa9..663c0f80 100644 --- a/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts +++ b/apps/portal/src/server/router/resumes-resume-protected-tabs-router.ts @@ -36,7 +36,7 @@ export const resumesResumeProtectedTabsRouter = createProtectedRouter() additionalInfo: rs.resume.additionalInfo, createdAt: rs.resume.createdAt, experience: rs.resume.experience, - id: rs.id, + id: rs.resume.id, location: rs.resume.location, numComments: rs.resume._count.comments, numStars: rs.resume._count.stars, diff --git a/apps/portal/src/server/router/resumes-reviews-router.ts b/apps/portal/src/server/router/resumes-reviews-router.ts index 8219edce..f908717e 100644 --- a/apps/portal/src/server/router/resumes-reviews-router.ts +++ b/apps/portal/src/server/router/resumes-reviews-router.ts @@ -1,5 +1,4 @@ import { z } from 'zod'; -import { ResumesSection } from '@prisma/client'; import { createRouter } from './context'; @@ -8,11 +7,10 @@ import type { ResumeComment } from '~/types/resume-comments'; export const resumeReviewsRouter = createRouter().query('list', { input: z.object({ resumeId: z.string(), - section: z.nativeEnum(ResumesSection), }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const { resumeId, section } = input; + const { resumeId } = input; // For this resume, we retrieve every comment's information, along with: // The user's name and image to render @@ -42,7 +40,6 @@ export const resumeReviewsRouter = createRouter().query('list', { }, where: { resumeId, - section, }, }); From a1cd0f4e9b60edb00203d8a5c29ecc88b8efef59 Mon Sep 17 00:00:00 2001 From: Keane Chan Date: Sun, 9 Oct 2022 17:09:24 +0800 Subject: [PATCH 13/22] [resumes][feat] add submission guidelines box (#335) --- apps/portal/src/pages/resumes/submit.tsx | 58 ++++++++++++++++--- .../ui/src/CheckboxInput/CheckboxInput.tsx | 27 +++++---- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index fc573f14..e6adec0f 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -6,7 +6,7 @@ import { useMemo, useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { PaperClipIcon } from '@heroicons/react/24/outline'; -import { Button, Select, TextArea, TextInput } from '@tih/ui'; +import { Button, CheckboxInput, Select, TextArea, TextInput } from '@tih/ui'; import { EXPERIENCE, @@ -17,17 +17,19 @@ import { import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; import { trpc } from '~/utils/trpc'; +const FILE_SIZE_LIMIT_MB = 3; +const FILE_SIZE_LIMIT_BYTES = FILE_SIZE_LIMIT_MB * 1000000; + const TITLE_PLACEHOLDER = 'e.g. Applying for Company XYZ, please help me to review!'; const ADDITIONAL_INFO_PLACEHOLDER = `e.g. I’m applying for company XYZ. I have been resume-rejected by N companies that I have applied for. Please help me to review so company XYZ gives me an interview!`; -const FILE_UPLOAD_ERROR = 'Please upload a PDF file that is less than 3MB.'; - -const MAX_FILE_SIZE_LIMIT = 3000000; +const FILE_UPLOAD_ERROR = `Please upload a PDF file that is less than ${FILE_SIZE_LIMIT_MB}MB.`; type IFormInput = { additionalInfo?: string; experience: string; file: File; + isChecked: boolean; location: string; role: string; title: string; @@ -67,7 +69,11 @@ export default function SubmitResumeForm() { const { url } = res.data; await resumeCreateMutation.mutate({ - ...data, + additionalInfo: data.additionalInfo, + experience: data.experience, + location: data.location, + role: data.role, + title: data.title, url, }); router.push('/resumes'); @@ -78,7 +84,7 @@ export default function SubmitResumeForm() { if (file == null) { return; } - if (file.type !== 'application/pdf' || file.size > MAX_FILE_SIZE_LIMIT) { + if (file.type !== 'application/pdf' || file.size > FILE_SIZE_LIMIT_BYTES) { setInvalidFileUploadError(FILE_UPLOAD_ERROR); return; } @@ -186,14 +192,16 @@ export default function SubmitResumeForm() { />
-

PDF up to 3MB

+

+ PDF up to {FILE_SIZE_LIMIT_MB}MB +

{fileUploadError && (

{fileUploadError}

)} -
+