[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

@ -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>
);
}