[resumes][feat] Add Resume Review Page (#306)

* [resumes][feat] WIP: Add scaffold

* [resumes][refactor] Shift comments section to its own component

* [resumes][feat] Add resume pdf view

* [resumes][feat] Add CommentsForm

* [resumes][refactor] Refactor comments form

* [resumes][fix] Fix viewport height not set

* [resumes][feat] Add form validation

* [resumes][refactor] Remove unused CommentsSection

* [resumes][fix] Manually calculate height for pdf view instead

* [resumes][refactor] Remove @tih/ui styles.scss import

Co-authored-by: Wu Peirong <wupeirong294@gmail.com>
Co-authored-by: Terence Ho <>
This commit is contained in:
Terence
2022-10-06 20:09:40 +08:00
committed by GitHub
parent 2906dbdc75
commit 1441fc90af
10 changed files with 420 additions and 4 deletions

View File

@ -27,6 +27,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.36.1",
"react-pdf": "^5.7.2",
"react-query": "^3.39.2",
"superjson": "^1.10.0",
"zod": "^3.18.0"
@ -37,6 +38,7 @@
"@types/node": "^18.0.0",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/react-pdf": "^5.7.2",
"autoprefixer": "^10.4.12",
"postcss": "^8.4.16",
"prettier-plugin-tailwindcss": "^0.1.13",

Binary file not shown.

View File

@ -0,0 +1,48 @@
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 { 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() {
const [numPages, setNumPages] = useState(0);
const [pageNumber] = useState(1);
const onPdfLoadSuccess = (pdf: PDFDocumentProxy) => {
setNumPages(pdf.numPages);
};
return (
<div>
<Document
className="h-[calc(100vh-17rem)] overflow-scroll"
file="/test_resume.pdf"
loading={<Spinner display="block" label="" size="lg" />}
onLoadSuccess={onPdfLoadSuccess}>
<Page pageNumber={pageNumber} />
</Document>
<div className="flex flex-row items-center justify-between p-4">
<Button
disabled={pageNumber === 1}
icon={ArrowLeftIcon}
isLabelHidden={true}
label="Previous"
variant="tertiary"
/>
<p className="text-md text-gray-600">
Page {pageNumber} of {numPages}
</p>
<Button
disabled={pageNumber === numPages}
icon={ArrowRightIcon}
isLabelHidden={true}
label="Next"
variant="tertiary"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,149 @@
import { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { Button, Dialog, TextInput } from '@tih/ui';
type CommentsFormProps = Readonly<{
setShowCommentsForm: (show: boolean) => void;
}>;
type IFormInput = {
education: string;
experience: string;
general: string;
projects: string;
skills: string;
};
type InputKeys = keyof IFormInput;
export default function CommentsForm({
setShowCommentsForm,
}: CommentsFormProps) {
const [showDialog, setShowDialog] = useState(false);
const {
register,
handleSubmit,
setValue,
formState: { isDirty },
} = useForm<IFormInput>({
defaultValues: {
education: '',
experience: '',
general: '',
projects: '',
skills: '',
},
});
// TODO: Implement mutation to database
const onSubmit: SubmitHandler<IFormInput> = (data) => {
alert(JSON.stringify(data));
};
const onCancel = () => {
if (isDirty) {
setShowDialog(true);
} else {
setShowCommentsForm(false);
}
};
const onValueChange = (section: InputKeys, value: string) => {
setValue(section, value.trim(), { shouldDirty: true });
};
return (
<>
<h2 className="text-xl font-semibold text-gray-800">Add your review</h2>
<form
className="w-full space-y-8 divide-y divide-gray-200"
onSubmit={handleSubmit(onSubmit)}>
{/* TODO: Convert TextInput to TextArea */}
<div className="mt-4 space-y-4">
<TextInput
{...(register('general'), {})}
label="General"
placeholder="General comments about the resume"
type="text"
onChange={(value) => onValueChange('general', value)}
/>
<TextInput
{...(register('education'), {})}
label="Education"
placeholder="Comments about the Education section"
type="text"
onChange={(value) => onValueChange('education', value)}
/>
<TextInput
{...(register('experience'), {})}
label="Experience"
placeholder="Comments about the Experience section"
type="text"
onChange={(value) => onValueChange('experience', value)}
/>
<TextInput
{...(register('projects'), {})}
label="Projects"
placeholder="Comments about the Projects section"
type="text"
onChange={(value) => onValueChange('projects', value)}
/>
<TextInput
{...(register('skills'), {})}
label="Skills"
placeholder="Comments about the Skills section"
type="text"
onChange={(value) => onValueChange('skills', value)}
/>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
label="Cancel"
type="button"
variant="tertiary"
onClick={onCancel}
/>
<Button
disabled={!isDirty}
label="Submit"
type="submit"
variant="primary"
/>
</div>
</form>
<Dialog
isShown={showDialog}
primaryButton={
<Button
display="block"
label="OK"
variant="primary"
onClick={() => setShowCommentsForm(false)}
/>
}
secondaryButton={
<Button
display="block"
label="Cancel"
variant="tertiary"
onClick={() => setShowDialog(false)}
/>
}
title="Are you sure you want to leave?"
onClose={() => {
setShowDialog(false);
}}>
<div>Note that your review will not be saved!</div>
</Dialog>
</>
);
}

View File

@ -0,0 +1,32 @@
import { useState } from 'react';
import { Button, Tabs } from '@tih/ui';
import { COMMENTS_SECTIONS } from './constants';
type CommentsListProps = Readonly<{
setShowCommentsForm: (show: boolean) => void;
}>;
export default function CommentsList({
setShowCommentsForm,
}: CommentsListProps) {
const [tab, setTab] = useState(COMMENTS_SECTIONS[0].value);
return (
<>
<Button
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
<Tabs
label="comments"
tabs={COMMENTS_SECTIONS}
value={tab}
onChange={(value) => setTab(value)}
/>
{/* TODO: Add comments lists */}
</>
);
}

View File

@ -0,0 +1,14 @@
import { useState } from 'react';
import CommentsForm from './CommentsForm';
import CommentsList from './CommentsList';
export default function CommentsSection() {
const [showCommentsForm, setShowCommentsForm] = useState(false);
return showCommentsForm ? (
<CommentsForm setShowCommentsForm={setShowCommentsForm} />
) : (
<CommentsList setShowCommentsForm={setShowCommentsForm} />
);
}

View File

@ -0,0 +1,23 @@
// TODO: Move to a general enums/constants file? For resumes
export const COMMENTS_SECTIONS = [
{
label: 'General',
value: 'general',
},
{
label: 'Education',
value: 'education',
},
{
label: 'Experience',
value: 'experience',
},
{
label: 'Projects',
value: 'projects',
},
{
label: 'Skills',
value: 'skills',
},
];

View File

@ -9,8 +9,8 @@ export default function ProfilePage() {
}
return (
<main className="flex-1 overflow-y-auto p-6 space-y-6">
<h1 className="font-bold text-4xl">Profile</h1>
<main className="flex-1 space-y-6 overflow-y-auto p-6">
<h1 className="text-4xl font-bold">Profile</h1>
{session?.user?.image && (
<img
alt={session?.user?.email ?? session?.user?.name ?? ''}

View File

@ -0,0 +1,81 @@
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">
<CommentsSection />
</div>
</div>
</main>
);
}

View File

@ -3532,6 +3532,14 @@
dependencies:
"@types/react" "*"
"@types/react-pdf@^5.7.2":
version "5.7.2"
resolved "https://registry.yarnpkg.com/@types/react-pdf/-/react-pdf-5.7.2.tgz#8e0ec89efeb4e574ec62b2370495bd3ee11d8ed8"
integrity sha512-6cUselXlQSNd9pMswJGvHqki3Lq0cnls/3hNwrFizdDeHBAfTFXTScEBObfGPznEmtO2LvmZMeced43BV9Wbog==
dependencies:
"@types/react" "*"
pdfjs-dist "^2.10.377"
"@types/react-router-config@*", "@types/react-router-config@^5.0.6":
version "5.0.6"
resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.6.tgz#87c5c57e72d241db900d9734512c50ccec062451"
@ -6381,6 +6389,11 @@ domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dommatrix@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dommatrix/-/dommatrix-1.0.3.tgz#e7c18e8d6f3abdd1fef3dd4aa74c4d2e620a0525"
integrity sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==
domutils@^2.0.0, domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
@ -7588,7 +7601,7 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-loader@^6.2.0:
file-loader@^6.0.0, file-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
@ -9772,6 +9785,11 @@ magic-string@^0.26.1:
dependencies:
sourcemap-codec "^1.4.8"
make-cancellable-promise@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/make-cancellable-promise/-/make-cancellable-promise-1.1.0.tgz#b4e9fcb31db3a27417e44f80cffa598ec9ac9f4e"
integrity sha512-X5Opjm2xcZsOLuJ+Bnhb4t5yfu4ehlA3OKEYLtqUchgVzL/QaqW373ZUVxVHKwvJ38cmYuR4rAHD2yUvAIkTPA==
make-dir@^2.0.0, make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -9792,6 +9810,11 @@ make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
make-event-props@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-event-props/-/make-event-props-1.3.0.tgz#2434cb390d58bcf40898d009ef5b1f936de9671b"
integrity sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q==
makeerror@1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"
@ -9942,11 +9965,21 @@ meow@^3.1.0:
redent "^1.0.0"
trim-newlines "^1.0.0"
merge-class-names@^1.1.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/merge-class-names/-/merge-class-names-1.4.2.tgz#78d6d95ab259e7e647252a7988fd25a27d5a8835"
integrity sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
merge-refs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/merge-refs/-/merge-refs-1.0.0.tgz#388348bce22e623782c6df9d3c4fc55888276120"
integrity sha512-WZ4S5wqD9FCR9hxkLgvcHJCBxzXzy3VVE6p8W2OzxRzB+hLRlcadGE2bW9xp2KSzk10rvp4y+pwwKO6JQVguMg==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -11042,6 +11075,19 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
pdfjs-dist@2.12.313:
version "2.12.313"
resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.12.313.tgz#62f2273737bb956267ae2e02cdfaddcb1099819c"
integrity sha512-1x6iXO4Qnv6Eb+YFdN5JdUzt4pAkxSp3aLAYPX93eQCyg/m7QFzXVWJHJVtoW48CI8HCXju4dSkhQZwoheL5mA==
pdfjs-dist@^2.10.377:
version "2.16.105"
resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz#937b9c4a918f03f3979c88209d84c1ce90122c2a"
integrity sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==
dependencies:
dommatrix "^1.0.3"
web-streams-polyfill "^3.2.1"
picocolors@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
@ -12024,6 +12070,22 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
react-pdf@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/react-pdf/-/react-pdf-5.7.2.tgz#c458dedf7983822668b40dcac1eae052c1f6e056"
integrity sha512-hdDwvf007V0i2rPCqQVS1fa70CXut17SN3laJYlRHzuqcu8sLLjEoeXihty6c0Ev5g1mw31b8OT8EwRw1s8C4g==
dependencies:
"@babel/runtime" "^7.0.0"
file-loader "^6.0.0"
make-cancellable-promise "^1.0.0"
make-event-props "^1.1.0"
merge-class-names "^1.1.1"
merge-refs "^1.0.0"
pdfjs-dist "2.12.313"
prop-types "^15.6.2"
tiny-invariant "^1.0.0"
tiny-warning "^1.0.0"
react-query@^3.39.2:
version "3.39.2"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.2.tgz#9224140f0296f01e9664b78ed6e4f69a0cc9216f"
@ -13712,7 +13774,7 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tiny-invariant@^1.0.2:
tiny-invariant@^1.0.0, tiny-invariant@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
@ -14509,6 +14571,11 @@ web-namespaces@^1.0.0:
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
web-streams-polyfill@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"