mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +08:00
[resumes][feat] submit resume mutation (#310)
This commit is contained in:
@ -6,8 +6,11 @@ CREATE TABLE "ResumesResume" (
|
|||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" TEXT NOT NULL,
|
||||||
"title" TEXT NOT NULL,
|
"title" TEXT NOT NULL,
|
||||||
"additionalInfo" TEXT NOT NULL,
|
"role" TEXT NOT NULL,
|
||||||
|
"experience" TEXT NOT NULL,
|
||||||
|
"location" TEXT NOT NULL,
|
||||||
"url" TEXT NOT NULL,
|
"url" TEXT NOT NULL,
|
||||||
|
"additionalInfo" TEXT,
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
@ -94,9 +94,12 @@ model ResumesResume {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
title String @db.Text
|
title String @db.Text
|
||||||
additionalInfo String @db.Text
|
// TODO: Update role, experience, location to use Enums
|
||||||
// TODO: Add role, experience, location from Enums
|
role String @db.Text
|
||||||
|
experience String @db.Text
|
||||||
|
location String @db.Text
|
||||||
url String
|
url String
|
||||||
|
additionalInfo String? @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { PaperClipIcon } from '@heroicons/react/24/outline';
|
import { PaperClipIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button, Select, TextInput } from '@tih/ui';
|
import { Button, Select, TextInput } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
const TITLE_PLACEHOLDER =
|
const TITLE_PLACEHOLDER =
|
||||||
'e.g. Applying for Company XYZ, please help me to review!';
|
'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 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!`;
|
||||||
@ -13,7 +16,7 @@ const FILE_UPLOAD_ERROR = 'Please upload a PDF file that is less than 10MB.';
|
|||||||
const MAX_FILE_SIZE_LIMIT = 10485760;
|
const MAX_FILE_SIZE_LIMIT = 10485760;
|
||||||
|
|
||||||
type IFormInput = {
|
type IFormInput = {
|
||||||
additionalInformation?: string;
|
additionalInfo?: string;
|
||||||
experience: string;
|
experience: string;
|
||||||
file: File;
|
file: File;
|
||||||
location: string;
|
location: string;
|
||||||
@ -68,6 +71,9 @@ export default function SubmitResumeForm() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [resumeFile, setResumeFile] = useState<File | null>();
|
const [resumeFile, setResumeFile] = useState<File | null>();
|
||||||
const [invalidFileUploadError, setInvalidFileUploadError] = useState<
|
const [invalidFileUploadError, setInvalidFileUploadError] = useState<
|
||||||
string | null
|
string | null
|
||||||
@ -81,10 +87,11 @@ export default function SubmitResumeForm() {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<IFormInput>();
|
} = useForm<IFormInput>();
|
||||||
|
|
||||||
// TODO: Add Create resume mutation
|
const onSubmit: SubmitHandler<IFormInput> = async (data) => {
|
||||||
const onSubmit: SubmitHandler<IFormInput> = (data) => {
|
await resumeCreateMutation.mutate({
|
||||||
alert(JSON.stringify(data));
|
...data,
|
||||||
onClickReset();
|
});
|
||||||
|
router.push('/resumes');
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const onUploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -196,10 +203,10 @@ export default function SubmitResumeForm() {
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{/* TODO: Use TextInputArea instead */}
|
{/* TODO: Use TextInputArea instead */}
|
||||||
<TextInput
|
<TextInput
|
||||||
{...register('additionalInformation')}
|
{...register('additionalInfo')}
|
||||||
label="Additional Information"
|
label="Additional Information"
|
||||||
placeholder={ADDITIONAL_INFO_PLACEHOLDER}
|
placeholder={ADDITIONAL_INFO_PLACEHOLDER}
|
||||||
onChange={(val) => setValue('additionalInformation', val)}
|
onChange={(val) => setValue('additionalInfo', val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex justify-end gap-4">
|
<div className="mt-4 flex justify-end gap-4">
|
||||||
|
@ -2,6 +2,7 @@ import superjson from 'superjson';
|
|||||||
|
|
||||||
import { createRouter } from './context';
|
import { createRouter } from './context';
|
||||||
import { protectedExampleRouter } from './protected-example-router';
|
import { protectedExampleRouter } from './protected-example-router';
|
||||||
|
import { resumesResumeUserRouter } from './resumes-resume-user-router';
|
||||||
import { todosRouter } from './todos';
|
import { todosRouter } from './todos';
|
||||||
import { todosUserRouter } from './todos-user-router';
|
import { todosUserRouter } from './todos-user-router';
|
||||||
|
|
||||||
@ -12,7 +13,8 @@ export const appRouter = createRouter()
|
|||||||
// Example routers. Learn more about tRPC routers: https://trpc.io/docs/v9/router
|
// Example routers. Learn more about tRPC routers: https://trpc.io/docs/v9/router
|
||||||
.merge('auth.', protectedExampleRouter)
|
.merge('auth.', protectedExampleRouter)
|
||||||
.merge('todos.', todosRouter)
|
.merge('todos.', todosRouter)
|
||||||
.merge('todos.user.', todosUserRouter);
|
.merge('todos.user.', todosUserRouter)
|
||||||
|
.merge('resumes.resume.user.', resumesResumeUserRouter);
|
||||||
|
|
||||||
// Export type definition of API
|
// Export type definition of API
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
30
apps/portal/src/server/router/resumes-resume-user-router.ts
Normal file
30
apps/portal/src/server/router/resumes-resume-user-router.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createProtectedRouter } from './context';
|
||||||
|
|
||||||
|
export const resumesResumeUserRouter = createProtectedRouter().mutation(
|
||||||
|
'create',
|
||||||
|
{
|
||||||
|
// TODO: Use enums for experience, location, role
|
||||||
|
input: z.object({
|
||||||
|
additionalInfo: z.string().optional(),
|
||||||
|
experience: z.string(),
|
||||||
|
location: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user.id;
|
||||||
|
|
||||||
|
// TODO: Store file in file storage and retrieve URL
|
||||||
|
|
||||||
|
return await ctx.prisma.resumesResume.create({
|
||||||
|
data: {
|
||||||
|
...input,
|
||||||
|
url: '',
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
Reference in New Issue
Block a user