Skip to content

Commit

Permalink
Add upload PDF file resume
Browse files Browse the repository at this point in the history
  • Loading branch information
salimi-my committed Jan 31, 2024
1 parent 614c4a4 commit 6a108e1
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 0 deletions.
42 changes: 42 additions & 0 deletions app/admin/resume/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { redirect } from 'next/navigation';

import prismadb from '@/lib/prismadb';
import { currentUser } from '@/lib/authentication';
import ViewResume from '@/components/admin/view-resume';
import UploadPdfButton from '@/components/admin/upload-pdf-button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@/components/ui/card';

export default async function AboutPage() {
const user = await currentUser();

if (!user || !user.id) {
redirect('/auth/sign-in');
}

const resume = await prismadb.resume.findFirst({
where: {
userId: user.id
}
});

return (
<Card className='rounded-lg border-none'>
<CardHeader className='mx-[1px] pb-9'>
<CardTitle className='text-xl font-bold items-center flex justify-between'>
Resume
<UploadPdfButton resume={resume} />
</CardTitle>
<CardDescription>Manage your resume pdf file.</CardDescription>
</CardHeader>
<CardContent>
<ViewResume url={resume?.pdf ?? null} />
</CardContent>
</Card>
);
}
8 changes: 8 additions & 0 deletions app/api/edgestore/[...edgestore]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const edgeStoreRouter = es.router({
.imageBucket({
maxSize: 1024 * 1024 * 2 // 2MB
})
.beforeDelete(() => {
return true;
}),
publicFiles: es
.fileBucket({
maxSize: 1024 * 1024 * 2, // 2MB
accept: ['application/pdf'] // accept only pdf file
})
.beforeDelete(() => {
return true;
})
Expand Down
66 changes: 66 additions & 0 deletions app/api/resume/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache';

import prismadb from '@/lib/prismadb';
import { currentUser } from '@/lib/authentication';

export async function POST(req: Request) {
try {
const body = await req.json();
const { pdf } = body;

const user = await currentUser();

if (!pdf) {
return NextResponse.json(
{ success: false, error: 'File is required.' },
{ status: 400 }
);
}

if (!user || !user.id) {
return NextResponse.json(
{ success: false, error: 'Unauthenticated.' },
{ status: 401 }
);
}

const currentResume = await prismadb.resume.findFirst({
where: {
userId: user.id
}
});

if (currentResume) {
const resume = await prismadb.resume.update({
where: {
id: currentResume.id
},
data: {
pdf
}
});

revalidatePath('/');

return NextResponse.json({ success: true, resume });
} else {
const resume = await prismadb.resume.create({
data: {
pdf,
userId: user.id
}
});

revalidatePath('/');

return NextResponse.json({ success: true, resume });
}
} catch (error: any) {
console.log('[RESUME_POST]', error);
return NextResponse.json(
{ success: false, error: error.message },
{ status: 500 }
);
}
}
7 changes: 7 additions & 0 deletions components/admin/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation';
import {
Book,
ClipboardCheck,
FileText,
FolderGit2,
GraduationCap,
LayoutGrid,
Expand Down Expand Up @@ -81,6 +82,12 @@ export default function Menu({ isOpen }: MenuProps) {
active: pathname.includes('/admin/tool'),
icon: <TerminalSquare size={18} />
},
{
href: '/admin/resume',
label: 'Resume',
active: pathname.includes('/admin/resume'),
icon: <FileText size={18} />
},
{
href: '/admin/account',
label: 'Account',
Expand Down
2 changes: 2 additions & 0 deletions components/admin/page-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default function PageTitle() {
pageTitle = 'Miscellaneous';
} else if (pathname.includes('/admin/tool')) {
pageTitle = 'Tool & Apps';
} else if (pathname.includes('/admin/resume')) {
pageTitle = 'Resume';
} else if (pathname.includes('/admin/account')) {
pageTitle = 'Account';
}
Expand Down
30 changes: 30 additions & 0 deletions components/admin/upload-pdf-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import { useState } from 'react';
import { Upload } from 'lucide-react';
import { Resume } from '@prisma/client';

import { Button } from '@/components/ui/button';
import UploadPdfModal from '@/components/modals/upload-pdf-modal';

interface UploadPdfButtonProps {
resume: Resume | null;
}

export default function UploadPdfButton({ resume }: UploadPdfButtonProps) {
const [open, setOpen] = useState(false);

return (
<>
<Button size='sm' onClick={() => setOpen(true)}>
<Upload className='mr-2 h-4 w-4' />
Upload PDF
</Button>
<UploadPdfModal
isOpen={open}
onClose={() => setOpen(false)}
resume={resume}
/>
</>
);
}
161 changes: 161 additions & 0 deletions components/admin/upload-pdf-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client';

import * as z from 'zod';
import axios from 'axios';
import { useState } from 'react';
import { Loader2 } from 'lucide-react';
import { Resume } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';

import { Input } from '@/components/ui/input';
import { useEdgeStore } from '@/lib/edgestore';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
import {
Form,
FormControl,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form';

interface UploadPdfFormProps {
onClose: () => void;
resume: Resume | null;
}

const formSchema = z.object({
pdf: z.string()
});

export default function UploadPdfForm({ onClose, resume }: UploadPdfFormProps) {
const router = useRouter();
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [fileError, setFileError] = useState('');
const [file, setFile] = useState<File | string | undefined>(
resume?.pdf ?? undefined
);

const { edgestore } = useEdgeStore();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
pdf: resume?.pdf ?? ''
}
});

const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
setLoading(true);

let pdfURL = resume?.pdf ?? '';
if (file && file instanceof File) {
const res = await edgestore.publicFiles.upload({
file,
options: {
replaceTargetUrl: resume?.pdf ?? undefined
}
});

if (res.url) {
pdfURL = res.url;
}
}

if (pdfURL === '') {
setFileError('Please select file.');
return;
}

setFileError('');
const newValues = { ...values, pdf: pdfURL };

const response = await axios.post('/api/resume', newValues);

if (response.data.success) {
onClose();
router.refresh();

toast({
variant: 'default',
title: 'Success!',
description: 'Data has been successfully saved.'
});
}
} catch (error) {
console.log(error);

toast({
variant: 'destructive',
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.'
});
} finally {
setLoading(false);
}
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='flex flex-col gap-4'>
<FormItem>
<FormLabel htmlFor='pdfFile'>PDF File</FormLabel>
<FormControl>
<Input
id='pdfFile'
type='file'
disabled={loading}
defaultValue={file instanceof File ? file.name : ''}
onChange={(event) => {
const selectedFile = event.target.files?.[0];
if (selectedFile) {
if (
selectedFile.type !== 'application/pdf' &&
selectedFile.type !== 'application/x-pdf'
) {
setFileError('Invalid file type.');
return;
}

if (selectedFile.size > 1024 * 1024 * 2) {
setFileError('File size is too large. Max size is 2MB.');
return;
}

setFileError('');
setFile(selectedFile);
}
}}
/>
</FormControl>
<FormMessage>{fileError}</FormMessage>
</FormItem>
<div className='pt-3 space-x-2 flex items-center justify-end w-full'>
<Button
type='button'
disabled={loading}
variant='outline'
onClick={onClose}
>
Cancel
</Button>
<Button disabled={loading} type='submit' variant='default'>
{loading && (
<>
<Loader2 className='animate-spin mr-2' size={18} />
Saving...
</>
)}
{!loading && <p className='px-4'>Save</p>}
</Button>
</div>
</div>
</form>
</Form>
);
}
37 changes: 37 additions & 0 deletions components/admin/view-resume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

interface ViewResumeProps {
url: string | null;
}

export default function ViewResume({ url }: ViewResumeProps) {
return (
<>
{!url && (
<div className='w-full h-[500px] flex justify-center items-center p-9 border rounded-md'>
<p className='text-center font-medium'>No resume found.</p>
</div>
)}
{url && (
<>
<object
data={url}
type='application/pdf'
width='100%'
height='100%'
className='w-full h-[1000px] rounded-md'
>
<iframe
src={url}
width='100%'
height='100%'
className='w-full h-[1000px]'
>
Your browser does not support PDF.
</iframe>
</object>
</>
)}
</>
);
}
Loading

0 comments on commit 6a108e1

Please sign in to comment.