diff --git a/app/admin/resume/page.tsx b/app/admin/resume/page.tsx
new file mode 100644
index 0000000..9fa3f06
--- /dev/null
+++ b/app/admin/resume/page.tsx
@@ -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 (
+
+
+
+ Resume
+
+
+ Manage your resume pdf file.
+
+
+
+
+
+ );
+}
diff --git a/app/api/edgestore/[...edgestore]/route.ts b/app/api/edgestore/[...edgestore]/route.ts
index 56ed0c6..bdc7f95 100644
--- a/app/api/edgestore/[...edgestore]/route.ts
+++ b/app/api/edgestore/[...edgestore]/route.ts
@@ -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;
})
diff --git a/app/api/resume/route.ts b/app/api/resume/route.ts
new file mode 100644
index 0000000..3a55bb7
--- /dev/null
+++ b/app/api/resume/route.ts
@@ -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 }
+ );
+ }
+}
diff --git a/components/admin/menu.tsx b/components/admin/menu.tsx
index 21aeefa..0175c06 100644
--- a/components/admin/menu.tsx
+++ b/components/admin/menu.tsx
@@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation';
import {
Book,
ClipboardCheck,
+ FileText,
FolderGit2,
GraduationCap,
LayoutGrid,
@@ -81,6 +82,12 @@ export default function Menu({ isOpen }: MenuProps) {
active: pathname.includes('/admin/tool'),
icon:
},
+ {
+ href: '/admin/resume',
+ label: 'Resume',
+ active: pathname.includes('/admin/resume'),
+ icon:
+ },
{
href: '/admin/account',
label: 'Account',
diff --git a/components/admin/page-title.tsx b/components/admin/page-title.tsx
index aefc5b8..a6734a4 100644
--- a/components/admin/page-title.tsx
+++ b/components/admin/page-title.tsx
@@ -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';
}
diff --git a/components/admin/upload-pdf-button.tsx b/components/admin/upload-pdf-button.tsx
new file mode 100644
index 0000000..5ed4b6a
--- /dev/null
+++ b/components/admin/upload-pdf-button.tsx
@@ -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 (
+ <>
+
+ setOpen(false)}
+ resume={resume}
+ />
+ >
+ );
+}
diff --git a/components/admin/upload-pdf-form.tsx b/components/admin/upload-pdf-form.tsx
new file mode 100644
index 0000000..810775f
--- /dev/null
+++ b/components/admin/upload-pdf-form.tsx
@@ -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(
+ resume?.pdf ?? undefined
+ );
+
+ const { edgestore } = useEdgeStore();
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ pdf: resume?.pdf ?? ''
+ }
+ });
+
+ const onSubmit = async (values: z.infer) => {
+ 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 (
+
+
+ );
+}
diff --git a/components/admin/view-resume.tsx b/components/admin/view-resume.tsx
new file mode 100644
index 0000000..86d3138
--- /dev/null
+++ b/components/admin/view-resume.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+interface ViewResumeProps {
+ url: string | null;
+}
+
+export default function ViewResume({ url }: ViewResumeProps) {
+ return (
+ <>
+ {!url && (
+
+ )}
+ {url && (
+ <>
+
+ >
+ )}
+ >
+ );
+}
diff --git a/components/modals/upload-pdf-modal.tsx b/components/modals/upload-pdf-modal.tsx
new file mode 100644
index 0000000..82250c7
--- /dev/null
+++ b/components/modals/upload-pdf-modal.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { Resume } from '@prisma/client';
+import { useEffect, useState } from 'react';
+
+import { Modal } from '@/components/ui/modal';
+import UploadPdfForm from '@/components/admin/upload-pdf-form';
+
+interface UploadPdfModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ resume: Resume | null;
+}
+
+export default function UploadPdfModal({
+ isOpen,
+ onClose,
+ resume
+}: UploadPdfModalProps) {
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 714a436..f9eb49c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -27,6 +27,7 @@ model User {
portfolios Portfolio[]
miscellaneous Miscellaneous?
tools Tool[]
+ resume Resume?
}
model Account {
@@ -238,5 +239,18 @@ model Tool {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ @@index([userId])
+}
+
+model Resume {
+ id String @id @default(cuid())
+ pdf String?
+
+ userId String @unique
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
@@index([userId])
}
\ No newline at end of file