Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/app/api/reports/upload-pdf/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
26 changes: 26 additions & 0 deletions src/app/api/reports/upload/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}

50 changes: 40 additions & 10 deletions src/app/preview/report-doc.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 = (
<Document title={reportTitle}>
<Page size="LETTER">
<View style={styles.headerSection}>
<Image src={Logo.src} style={{ width: 168 }} />
<Text style={styles.header}>{reportTitle}</Text>
</View>
<View style={styles.chartSection}>
{chartImages.length > 0
? chartImages.map((src, i) => (
<Image key={i} src={src} style={{ width: "50%" }} />
))
: null}
</View>
</Page>
</Document>
);
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 && (
Expand Down
24 changes: 20 additions & 4 deletions src/app/reports/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -166,9 +166,25 @@ export default function Archive() {
{/* <DraftReport /> */}
<DraftReportPopulated />
<div className="flex flex-col gap-y-4">
<h2 className="text-xl font-extrabold text-[#555555] gap-8">
Archived Reports
</h2>
<div className="flex items-center gap-2">
<h2 className="text-xl font-extrabold text-[#555555]">
Archived Reports
</h2>
<span className="relative inline-flex group">
<Info
className="w-4 h-4 text-[#555555] group-hover:text-[#E76C82] transition-colors"
aria-label="Archived reports info"
/>
<span
role="tooltip"
className="pointer-events-none absolute left-1/2 bottom-full z-10 mb-2 w-[320px] rounded-xl bg-[rgba(239,246,255,1)] px-2 py-2 text-left text-sm text-[rgba(28,57,142,1)] shadow-lg ring-1 ring-black/10 opacity-0 group-hover:opacity-100 transition-opacity"
>
The system can save a maximum of 20 archived reports.
When the limit is reached, you&apos;ll need to remove old
reports before saving new ones.
</span>
</span>
</div>
<ReportEntry
title="Q4 Report 2025"
date={new Date(2025, 0, 4)}
Expand Down
28 changes: 25 additions & 3 deletions src/components/report_builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"use client";

import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/navigation";
import {
X,
GripVertical,
Expand Down Expand Up @@ -71,19 +72,27 @@ function ChartEntry({
function DownloadButton({
doctype,
count,
onClick,
}: {
doctype: string;
count: number;
onClick?: () => void;
}) {
return (
<div className="flex">
{count === 0 ? (
<button className="flex text-gray-200 border-gray-200 border-2 py-2 px-8 rounded-full">
<button
className="flex text-gray-200 border-gray-200 border-2 py-2 px-8 rounded-full"
disabled
>
<FileDown className="mr-2 mt-0.5 w-5 h-5" />
{doctype}
</button>
) : (
<button className="flex text-gray-700 border-gray-200 border-2 py-2 px-8 rounded-full">
<button
className="flex text-gray-700 border-gray-200 border-2 py-2 px-8 rounded-full"
onClick={onClick}
>
<FileDown className="mr-2 mt-0.5 w-5 h-5" />
{doctype}
</button>
Expand All @@ -106,6 +115,7 @@ export default function ReportBuilder({
onClose,
onCountChange,
}: ReportBuilderProps) {
const router = useRouter();
const [charts, setCharts] = useState<ReportChartEntry[]>([]);
const scrollYRef = useRef(0);

Expand Down Expand Up @@ -282,7 +292,19 @@ export default function ReportBuilder({
<hr className="border-gray-200" />

<div className="mt-5 flex justify-evenly w-full py-2">
<DownloadButton doctype="PDF" count={count} />
<DownloadButton
doctype="PDF"
count={count}
onClick={() => {
fetch("/api/reports/upload", {
method: "POST",
}).catch(() => {
// ignore upload errors for now
});
router.push("/preview");
onClose();
}}
/>
<DownloadButton doctype="CSV" count={count} />
<DownloadButton doctype="PNG" count={count} />
</div>
Expand Down
28 changes: 25 additions & 3 deletions src/lib/upload-file.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array> | string,
options: UploadFileOptions = {}
): Promise<PutBlobResult> {
const { access = 'public', contentType, addRandomSuffix, token } = options

const result = await put(pathname, body, {
access,
contentType,
addRandomSuffix,
token,
})

return result
}