ClauseGuard / web /app /api /pdf /report /route.tsx
gaurv007's picture
Fix build: rename PDF route .ts β†’ .tsx (JSX in API route needs .tsx extension for Turbopack)
e1126b7 verified
import { NextRequest, NextResponse } from "next/server";
import React from "react";
export const runtime = "nodejs";
export async function POST(req: NextRequest) {
try {
const data = await req.json();
// Dynamic import to avoid bundling issues
const { Document, Page, Text, View, StyleSheet, pdf } = await import("@react-pdf/renderer");
const styles = StyleSheet.create({
page: { padding: 40, fontFamily: "Helvetica", fontSize: 11, color: "#27272a" },
title: { fontSize: 22, fontWeight: "bold", marginBottom: 4 },
subtitle: { fontSize: 11, color: "#71717a" },
divider: { borderBottomWidth: 1, borderBottomColor: "#e4e4e7", marginVertical: 16 },
scoreRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 16 },
scoreLabel: { fontSize: 10, color: "#a1a1aa" },
scoreValue: { fontSize: 28, fontWeight: "bold" },
clauseCard: { marginBottom: 12, padding: 12, borderWidth: 1, borderColor: "#e4e4e7", borderRadius: 6 },
clauseText: { fontSize: 10, color: "#3f3f46", lineHeight: 1.5, marginBottom: 6 },
tag: { fontSize: 9, fontWeight: "bold", padding: "2 8", borderRadius: 3, marginRight: 4 },
tagHigh: { backgroundColor: "#fef2f2", color: "#b91c1c" },
tagMedium: { backgroundColor: "#fffbeb", color: "#a16207" },
tagLow: { backgroundColor: "#eff6ff", color: "#1d4ed8" },
footer: { position: "absolute", bottom: 30, left: 40, right: 40, fontSize: 8, color: "#a1a1aa", textAlign: "center" },
});
const flagged = (data.results || []).filter((r: any) => r.categories?.length > 0);
const doc = React.createElement(
Document,
null,
React.createElement(
Page,
{ size: "A4", style: styles.page },
// Header
React.createElement(View, null,
React.createElement(Text, { style: styles.title }, "ClauseGuard Report"),
React.createElement(Text, { style: styles.subtitle },
`${data.source_url || "Manual scan"} β€” ${new Date().toLocaleDateString()}`
),
),
// Score
React.createElement(View, { style: styles.scoreRow },
React.createElement(View, null,
React.createElement(Text, { style: styles.scoreLabel }, "RISK SCORE"),
React.createElement(Text, { style: styles.scoreValue }, `${data.risk_score}/100`),
),
React.createElement(Text, { style: { fontSize: 16, fontWeight: "bold" } }, `Grade ${data.grade}`),
),
React.createElement(Text, { style: styles.subtitle },
`${data.total_clauses} clauses scanned β€” ${data.flagged_count} flagged`
),
React.createElement(View, { style: styles.divider }),
// Clauses
...flagged.map((clause: any, i: number) =>
React.createElement(View, { key: i, style: styles.clauseCard },
React.createElement(Text, { style: styles.clauseText },
clause.text?.substring(0, 300) || ""
),
React.createElement(View, { style: { flexDirection: "row", flexWrap: "wrap" } },
...(clause.categories || []).map((cat: any, j: number) => {
const tagStyle = cat.severity === "HIGH" ? styles.tagHigh : cat.severity === "MEDIUM" ? styles.tagMedium : styles.tagLow;
return React.createElement(Text, { key: j, style: [styles.tag, tagStyle] }, cat.name);
})
),
)
),
// Footer
React.createElement(Text, { style: styles.footer },
"Generated by ClauseGuard β€” clauseguard.com β€” Not legal advice"
),
)
);
const instance = pdf(doc);
const buffer = await instance.toBuffer();
const chunks: Uint8Array[] = [];
for await (const chunk of buffer as any) {
chunks.push(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
}
return new Response(Buffer.concat(chunks), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="clauseguard-report.pdf"`,
},
});
} catch (error) {
console.error("PDF generation error:", error);
return NextResponse.json({ error: "PDF generation failed" }, { status: 500 });
}
}