diff --git a/src/app/api/reports/upload-pdf/route.ts b/src/app/api/reports/upload-pdf/route.ts new file mode 100644 index 0000000..1c13143 --- /dev/null +++ b/src/app/api/reports/upload-pdf/route.ts @@ -0,0 +1,29 @@ +import { auth } from "@/lib/auth"; +import { uploadFile } from "@/lib/upload-file"; + +export async function POST(request: Request) { + const session = await auth.api.getSession({ headers: request.headers }); + + if (!session) { + return Response.json({ error: "unauthorized" }, { status: 401 }); + } + + const body = await request.blob(); + const pathname = `reports/${session.user.id}/preview-${Date.now()}.pdf`; + + try { + await uploadFile(pathname, body, { + access: "private", + contentType: "application/pdf", + addRandomSuffix: true, + }); + } catch (error) { + console.error("[reports/upload-pdf] uploadFile failed", error); + return Response.json( + { error: "upload failed" }, + { status: 500 } + ); + } + + return Response.json({ success: true }); +} diff --git a/src/app/api/reports/upload/route.ts b/src/app/api/reports/upload/route.ts new file mode 100644 index 0000000..5711294 --- /dev/null +++ b/src/app/api/reports/upload/route.ts @@ -0,0 +1,26 @@ +import { auth } from "@/lib/auth"; +import { uploadFile } from "@/lib/upload-file"; + +export async function POST(request: Request) { + const session = await auth.api.getSession({ headers: request.headers }); + + if (!session) { + return Response.json({ error: "unauthorized" }, { status: 401 }); + } + + const pathname = `reports/${session.user.id}/placeholder-${Date.now()}.txt`; + const body = "Report upload placeholder"; + + try { + await uploadFile(pathname, body, { + access: "private", + contentType: "text/plain", + addRandomSuffix: true, + }); + } catch (error) { + console.error("[reports/upload] uploadFile failed (ignored for now)", error); + } + + return Response.json({ success: true }); +} + diff --git a/src/app/preview/report-doc.tsx b/src/app/preview/report-doc.tsx index 506f4dc..19beb38 100644 --- a/src/app/preview/report-doc.tsx +++ b/src/app/preview/report-doc.tsx @@ -1,15 +1,6 @@ "use client"; -import { - Page, - Text, - View, - Document, - Image, - StyleSheet, - PDFViewer, - Font, -} from "@react-pdf/renderer"; +import { Page, Text, View, Document, Image, StyleSheet, PDFViewer, Font, pdf } from "@react-pdf/renderer"; import { ReactElement, useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { LoaderCircle } from "lucide-react"; @@ -118,6 +109,45 @@ function ReportDoc({ setIsLoading(false); }, [charts]); + // generate pdf blob and uplaod via API + const hasUploadedRef = useRef(false); + useEffect(() => { + if (isLoading || hasUploadedRef.current) return; + + let cancelled = false; + (async () => { + const doc = ( + + + + + {reportTitle} + + + {chartImages.length > 0 + ? chartImages.map((src, i) => ( + + )) + : null} + + + + ); + const blob = await pdf(doc).toBlob(); + if (cancelled) return; + hasUploadedRef.current = true; + await fetch("/api/reports/upload-pdf", { + method: "POST", + body: blob, + headers: { "Content-Type": "application/pdf" }, + }); + })(); + + return () => { + cancelled = true; + }; + }, [reportTitle, chartImages, isLoading]); + return ( <> {!isLoading && ( diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx index 9556623..560ab52 100644 --- a/src/app/reports/page.tsx +++ b/src/app/reports/page.tsx @@ -1,4 +1,4 @@ -import { Download, SquarePen, Calendar, Trash2, FileText, ArchiveIcon, X} from "lucide-react"; +import { Download, SquarePen, Calendar, Trash2, FileText, Info, ArchiveIcon, X} from "lucide-react"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import ReportChart from "@/components/ReportChart"; @@ -166,9 +166,25 @@ export default function Archive() { {/* */}
-

- Archived Reports -

+
+

+ Archived Reports +

+ + + + The system can save a maximum of 20 archived reports. + When the limit is reached, you'll need to remove old + reports before saving new ones. + + +
void; }) { return (
{count === 0 ? ( - ) : ( - @@ -106,6 +115,7 @@ export default function ReportBuilder({ onClose, onCountChange, }: ReportBuilderProps) { + const router = useRouter(); const [charts, setCharts] = useState([]); const scrollYRef = useRef(0); @@ -282,7 +292,19 @@ export default function ReportBuilder({
- + { + fetch("/api/reports/upload", { + method: "POST", + }).catch(() => { + // ignore upload errors for now + }); + router.push("/preview"); + onClose(); + }} + />
diff --git a/src/lib/upload-file.ts b/src/lib/upload-file.ts index d540bde..5179fd0 100644 --- a/src/lib/upload-file.ts +++ b/src/lib/upload-file.ts @@ -1,5 +1,27 @@ -import { put } from '@vercel/blob' +import { put, type PutBlobResult } from '@vercel/blob' -export async function uploadFile() { - // TODO: complete functionality to save a file to vercel blob +type UploadAccess = 'public' | 'private' + +export type UploadFileOptions = { + access?: UploadAccess + contentType?: string + addRandomSuffix?: boolean + token?: string +} + +export async function uploadFile( + pathname: string, + body: Blob | File | ArrayBuffer | ReadableStream | string, + options: UploadFileOptions = {} +): Promise { + const { access = 'public', contentType, addRandomSuffix, token } = options + + const result = await put(pathname, body, { + access, + contentType, + addRandomSuffix, + token, + }) + + return result }