mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-30 05:34:33 +08:00
[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:
48
apps/portal/src/components/resumes/ResumePdf.tsx
Normal file
48
apps/portal/src/components/resumes/ResumePdf.tsx
Normal 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>
|
||||
);
|
||||
}
|
149
apps/portal/src/components/resumes/comments/CommentsForm.tsx
Normal file
149
apps/portal/src/components/resumes/comments/CommentsForm.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
32
apps/portal/src/components/resumes/comments/CommentsList.tsx
Normal file
32
apps/portal/src/components/resumes/comments/CommentsList.tsx
Normal 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 */}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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} />
|
||||
);
|
||||
}
|
23
apps/portal/src/components/resumes/comments/constants.ts
Normal file
23
apps/portal/src/components/resumes/comments/constants.ts
Normal 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',
|
||||
},
|
||||
];
|
Reference in New Issue
Block a user