From 0fc5c23248c93ef7e397bd35e37f0dca554fb3c0 Mon Sep 17 00:00:00 2001 From: Vansh Chavda Date: Sun, 1 Mar 2026 18:14:23 -0500 Subject: [PATCH 1/3] removed comment --- src/lib/upload-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/upload-file.ts b/src/lib/upload-file.ts index d540bde..4a90f03 100644 --- a/src/lib/upload-file.ts +++ b/src/lib/upload-file.ts @@ -1,5 +1,5 @@ import { put } from '@vercel/blob' export async function uploadFile() { - // TODO: complete functionality to save a file to vercel blob + } From 59d26ac5cc50c1fc2e0544ad9be86e0c0cf95b31 Mon Sep 17 00:00:00 2001 From: Vansh Chavda Date: Sun, 1 Mar 2026 20:34:12 -0500 Subject: [PATCH 2/3] completed ticket --- src/app/api/reports/upload-pdf/route.ts | 29 +++++++++++++++++ src/app/api/reports/upload/route.ts | 26 ++++++++++++++++ src/app/preview/report-doc.tsx | 41 ++++++++++++++++++++++++- src/components/report_builder.tsx | 28 +++++++++++++++-- src/lib/upload-file.ts | 28 +++++++++++++++-- src/types/resend.d.ts | 1 + 6 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/app/api/reports/upload-pdf/route.ts create mode 100644 src/app/api/reports/upload/route.ts create mode 100644 src/types/resend.d.ts 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 2d33231..5710e6e 100644 --- a/src/app/preview/report-doc.tsx +++ b/src/app/preview/report-doc.tsx @@ -1,6 +1,6 @@ "use client"; -import { Page, Text, View, Document, Image, StyleSheet, PDFViewer } from "@react-pdf/renderer"; +import { Page, Text, View, Document, Image, StyleSheet, PDFViewer, pdf } from "@react-pdf/renderer"; import { ReactElement, useEffect, useRef, useState } from "react"; import dynamic from "next/dynamic"; import Logo from "../../../public/Logo.png" @@ -55,6 +55,45 @@ function ReportDoc({ generateImages(); }, [charts]); + // Generate PDF blob and upload via API (uploadFile runs server-side) + 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 ( <> {/* cover div with charts */} diff --git a/src/components/report_builder.tsx b/src/components/report_builder.tsx index 891f610..f6c22da 100644 --- a/src/components/report_builder.tsx +++ b/src/components/report_builder.tsx @@ -2,6 +2,7 @@ "use client"; import { useEffect, useState, useRef } from "react"; +import { useRouter } from "next/navigation"; import { X, GripVertical, @@ -71,19 +72,27 @@ function ChartEntry({ function DownloadButton({ doctype, count, + onClick, }: { doctype: string; count: number; + onClick?: () => 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 4a90f03..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() { - +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 } diff --git a/src/types/resend.d.ts b/src/types/resend.d.ts new file mode 100644 index 0000000..8c57d55 --- /dev/null +++ b/src/types/resend.d.ts @@ -0,0 +1 @@ +declare module "resend"; From 6d3d1ea439f21e6eb2594e293823400c1db112c2 Mon Sep 17 00:00:00 2001 From: Vansh Chavda Date: Sun, 1 Mar 2026 22:20:57 -0500 Subject: [PATCH 3/3] added tooltip --- src/app/preview/report-doc.tsx | 2 +- src/app/reports/page.tsx | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/app/preview/report-doc.tsx b/src/app/preview/report-doc.tsx index 5710e6e..5c65078 100644 --- a/src/app/preview/report-doc.tsx +++ b/src/app/preview/report-doc.tsx @@ -55,7 +55,7 @@ function ReportDoc({ generateImages(); }, [charts]); - // Generate PDF blob and upload via API (uploadFile runs server-side) + // generate pdf blob and uplaod via API const hasUploadedRef = useRef(false); useEffect(() => { if (isLoading || hasUploadedRef.current) return; diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx index dc2d62f..b4378a1 100644 --- a/src/app/reports/page.tsx +++ b/src/app/reports/page.tsx @@ -1,4 +1,4 @@ -import { Download, SquarePen, Calendar, Trash2, FileText } from "lucide-react"; +import { Download, SquarePen, Calendar, Trash2, FileText, Info } from "lucide-react"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import ReportChart from "@/components/ReportChart"; @@ -190,9 +190,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. + + +