[resumes][feat] add edit form functionality ()

* [resumes][feat] add edit form functionality

* [resumes][chore] remove comment
pull/380/head
Keane Chan 2 years ago committed by GitHub
parent b7e0d8ff90
commit dccc68b710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,12 +4,14 @@ import Error from 'next/error';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { import {
AcademicCapIcon, AcademicCapIcon,
BriefcaseIcon, BriefcaseIcon,
CalendarIcon, CalendarIcon,
InformationCircleIcon, InformationCircleIcon,
MapPinIcon, MapPinIcon,
PencilSquareIcon,
StarIcon, StarIcon,
} from '@heroicons/react/20/solid'; } from '@heroicons/react/20/solid';
import { Spinner } from '@tih/ui'; import { Spinner } from '@tih/ui';
@ -20,6 +22,8 @@ import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableTe
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import SubmitResumeForm from './submit';
export default function ResumeReviewPage() { export default function ResumeReviewPage() {
const ErrorPage = ( const ErrorPage = (
<Error statusCode={404} title="Requested resume does not exist" /> <Error statusCode={404} title="Requested resume does not exist" />
@ -46,6 +50,11 @@ export default function ResumeReviewPage() {
}, },
}); });
const userIsOwner =
session?.user?.id != null && session.user.id === detailsQuery.data?.userId;
const [isEditMode, setIsEditMode] = useState(false);
const onStarButtonClick = () => { const onStarButtonClick = () => {
if (session?.user?.id == null) { if (session?.user?.id == null) {
router.push('/api/auth/signin'); router.push('/api/auth/signin');
@ -65,6 +74,30 @@ export default function ResumeReviewPage() {
} }
}; };
const onEditButtonClick = () => {
setIsEditMode(true);
};
if (isEditMode && detailsQuery.data != null) {
return (
<SubmitResumeForm
initFormDetails={{
additionalInfo: detailsQuery.data.additionalInfo ?? '',
experience: detailsQuery.data.experience,
location: detailsQuery.data.location,
resumeId: resumeId as string,
role: detailsQuery.data.role,
title: detailsQuery.data.title,
url: detailsQuery.data.url,
}}
onClose={() => {
utils.invalidateQueries(['resumes.resume.findOne']);
setIsEditMode(false);
}}
/>
);
}
return ( return (
<> <>
{(detailsQuery.isError || detailsQuery.data === null) && ErrorPage} {(detailsQuery.isError || detailsQuery.data === null) && ErrorPage}
@ -80,45 +113,46 @@ export default function ResumeReviewPage() {
<title>{detailsQuery.data.title}</title> <title>{detailsQuery.data.title}</title>
</Head> </Head>
<main className="h-[calc(100vh-2rem)] flex-1 overflow-y-auto p-4"> <main className="h-[calc(100vh-2rem)] flex-1 overflow-y-auto p-4">
<div className="flex flex-row space-x-8"> <div className="flex space-x-8">
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight"> <h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{detailsQuery.data.title} {detailsQuery.data.title}
</h1> </h1>
<button <div className="flex gap-4">
className={clsx( <button
detailsQuery.data?.stars.length className="isolate inline-flex 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"
? 'z-10 border-indigo-500 outline-none ring-1 ring-indigo-500' disabled={starMutation.isLoading || unstarMutation.isLoading}
: '', type="button"
'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', onClick={onStarButtonClick}>
<span className="relative inline-flex">
<div className="-ml-1 mr-2 h-5 w-5">
{starMutation.isLoading || unstarMutation.isLoading ? (
<Spinner className="mt-0.5" size="xs" />
) : (
<StarIcon
aria-hidden="true"
className={clsx(
detailsQuery.data?.stars.length
? 'text-orange-400'
: 'text-gray-400',
)}
/>
)}
</div>
Star
</span>
<span className="relative -ml-px inline-flex">
{detailsQuery.data?._count.stars}
</span>
</button>
{userIsOwner && (
<button
className="p h-10 rounded-md border border-gray-300 bg-white py-1 px-2 text-center"
type="button"
onClick={onEditButtonClick}>
<PencilSquareIcon className="h-6 w-6 text-indigo-500 hover:text-indigo-300" />
</button>
)} )}
disabled={ </div>
session?.user === undefined ||
starMutation.isLoading ||
unstarMutation.isLoading
}
type="button"
onClick={onStarButtonClick}>
<span className="relative inline-flex">
<div className="-ml-1 mr-2 h-5 w-5">
{starMutation.isLoading || unstarMutation.isLoading ? (
<Spinner className="mt-0.5" size="xs" />
) : (
<StarIcon
aria-hidden="true"
className={clsx(
detailsQuery.data?.stars.length
? 'text-orange-400'
: 'text-gray-400',
)}
/>
)}
</div>
Star
</span>
<span className="relative -ml-px inline-flex">
{detailsQuery.data?._count.stars}
</span>
</button>
</div> </div>
<div className="flex flex-col pt-1 lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8"> <div className="flex flex-col pt-1 lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8">
<div className="mt-2 flex items-center text-sm text-gray-500"> <div className="mt-2 flex items-center text-sm text-gray-500">

@ -60,8 +60,26 @@ const selectors: Array<SelectorOptions> = [
{ key: 'location', label: 'Location', options: LOCATION }, { key: 'location', label: 'Location', options: LOCATION },
]; ];
export default function SubmitResumeForm() { type InitFormDetails = {
const [resumeFile, setResumeFile] = useState<File | null>(); additionalInfo?: string;
experience: string;
location: string;
resumeId: string;
role: string;
title: string;
url: string;
};
type Props = Readonly<{
initFormDetails?: InitFormDetails | null;
onClose: () => void;
}>;
export default function SubmitResumeForm({
initFormDetails,
onClose = () => undefined,
}: Props) {
const [resumeFile, setResumeFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [invalidFileUploadError, setInvalidFileUploadError] = useState< const [invalidFileUploadError, setInvalidFileUploadError] = useState<
string | null string | null
@ -70,7 +88,8 @@ export default function SubmitResumeForm() {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const router = useRouter(); const router = useRouter();
const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create'); const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert');
const isNewForm = initFormDetails == null;
const { const {
register, register,
@ -81,6 +100,7 @@ export default function SubmitResumeForm() {
} = useForm<IFormInput>({ } = useForm<IFormInput>({
defaultValues: { defaultValues: {
isChecked: false, isChecked: false,
...initFormDetails,
}, },
}); });
@ -89,7 +109,9 @@ export default function SubmitResumeForm() {
if (fileRejections.length === 0) { if (fileRejections.length === 0) {
setInvalidFileUploadError(''); setInvalidFileUploadError('');
setResumeFile(acceptedFiles[0]); setResumeFile(acceptedFiles[0]);
setValue('file', acceptedFiles[0]); setValue('file', acceptedFiles[0], {
shouldDirty: true,
});
} else { } else {
setInvalidFileUploadError(FILE_UPLOAD_ERROR); setInvalidFileUploadError(FILE_UPLOAD_ERROR);
} }
@ -106,6 +128,30 @@ export default function SubmitResumeForm() {
onDrop: onFileDrop, onDrop: onFileDrop,
}); });
const fetchFilePdf = useCallback(async () => {
const fileUrl = initFormDetails?.url;
if (fileUrl == null) {
return;
}
const data = await axios
.get(fileUrl, {
responseType: 'blob',
})
.then((res) => res.data);
const keyAndFileName = fileUrl.substring(fileUrl.indexOf('resumes'));
const fileName = keyAndFileName.substring(keyAndFileName.indexOf('-') + 1);
const file = new File([data], fileName);
setResumeFile(file);
setValue('file', file, {
shouldDirty: false,
});
}, [initFormDetails?.url, setValue]);
// Route user to sign in if not logged in
useEffect(() => { useEffect(() => {
if (status !== 'loading') { if (status !== 'loading') {
if (session?.user?.id == null) { if (session?.user?.id == null) {
@ -114,6 +160,11 @@ export default function SubmitResumeForm() {
} }
}, [router, session, status]); }, [router, session, status]);
// Fetch initial file PDF for edit form
useEffect(() => {
fetchFilePdf();
}, [fetchFilePdf]);
const onSubmit: SubmitHandler<IFormInput> = async (data) => { const onSubmit: SubmitHandler<IFormInput> = async (data) => {
if (resumeFile == null) { if (resumeFile == null) {
return; return;
@ -131,10 +182,11 @@ export default function SubmitResumeForm() {
}); });
const { url } = res.data; const { url } = res.data;
resumeCreateMutation.mutate( resumeUpsertMutation.mutate(
{ {
additionalInfo: data.additionalInfo, additionalInfo: data.additionalInfo,
experience: data.experience, experience: data.experience,
id: initFormDetails?.resumeId,
location: data.location, location: data.location,
role: data.role, role: data.role,
title: data.title, title: data.title,
@ -148,19 +200,26 @@ export default function SubmitResumeForm() {
setIsLoading(false); setIsLoading(false);
}, },
onSuccess() { onSuccess() {
router.push('/resumes/browse'); if (isNewForm) {
router.push('/resumes/browse');
} else {
onClose();
}
}, },
}, },
); );
}; };
const onClickClear = () => { const onClickClear = () => {
if (isDirty || resumeFile != null) { if (isDirty) {
setIsDialogShown(true); setIsDialogShown(true);
} else {
onClose();
} }
}; };
const onClickResetDialog = () => { const onClickResetDialog = () => {
onClose();
setIsDialogShown(false); setIsDialogShown(false);
reset(); reset();
setResumeFile(null); setResumeFile(null);
@ -227,7 +286,11 @@ export default function SubmitResumeForm() {
onClick={() => setIsDialogShown(false)} onClick={() => setIsDialogShown(false)}
/> />
} }
title="Are you sure you want to clear?" title={
isNewForm
? 'Are you sure you want to clear?'
: 'Are you sure you want to leave?'
}
onClose={() => setIsDialogShown(false)}> onClose={() => setIsDialogShown(false)}>
Note that your current input will not be saved! Note that your current input will not be saved!
</Dialog> </Dialog>
@ -346,7 +409,7 @@ export default function SubmitResumeForm() {
<Button <Button
addonPosition="start" addonPosition="start"
disabled={isLoading} disabled={isLoading}
label="Clear" label={isNewForm ? 'Clear' : 'Cancel'}
variant="tertiary" variant="tertiary"
onClick={onClickClear} onClick={onClickClear}
/> />

@ -5,11 +5,12 @@ import { createProtectedRouter } from '../context';
import type { Resume } from '~/types/resume'; import type { Resume } from '~/types/resume';
export const resumesResumeUserRouter = createProtectedRouter() export const resumesResumeUserRouter = createProtectedRouter()
.mutation('create', { .mutation('upsert', {
// TODO: Use enums for experience, location, role // TODO: Use enums for experience, location, role
input: z.object({ input: z.object({
additionalInfo: z.string().optional(), additionalInfo: z.string().optional(),
experience: z.string(), experience: z.string(),
id: z.string().optional(),
location: z.string(), location: z.string(),
role: z.string(), role: z.string(),
title: z.string(), title: z.string(),
@ -17,11 +18,29 @@ export const resumesResumeUserRouter = createProtectedRouter()
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user.id; const userId = ctx.session?.user.id;
return await ctx.prisma.resumesResume.create({
data: { return await ctx.prisma.resumesResume.upsert({
...input, create: {
additionalInfo: input.additionalInfo,
experience: input.experience,
location: input.location,
role: input.role,
title: input.title,
url: input.url,
userId,
},
update: {
additionalInfo: input.additionalInfo,
experience: input.experience,
location: input.location,
role: input.role,
title: input.title,
url: input.url,
userId, userId,
}, },
where: {
id: input.id ?? '',
},
}); });
}, },
}) })

Loading…
Cancel
Save