diff --git a/web/src/app/api/resume/download/route.ts b/web/src/app/api/resume/download/route.ts index 422c440..ed91d25 100644 --- a/web/src/app/api/resume/download/route.ts +++ b/web/src/app/api/resume/download/route.ts @@ -166,19 +166,19 @@ export async function GET(req: Request) { doc.on("error", (err) => reject(err)); }); - // --- Header --- + // --- Modern ATS Header --- doc - .fontSize(22) - .fillColor("#111827") + .fontSize(24) + .fillColor("#0f172a") .font("Helvetica-Bold") - .text(name, { align: "center" }); + .text(name, 40, 40, { align: "left" }); if (headline) { doc - .fontSize(12) + .fontSize(11) .fillColor("#2563eb") .font("Helvetica-Bold") - .text(headline, { align: "center" }); + .text(headline.toUpperCase(), { align: "left" }); } const contactParts = []; @@ -188,14 +188,14 @@ export async function GET(req: Request) { if (contactParts.length > 0) { doc - .fontSize(9) - .fillColor("#4b5563") + .fontSize(8.5) + .fillColor("#64748b") .font("Helvetica") - .text(contactParts.join(" | "), { align: "center" }); + .text(contactParts.join(" | "), { align: "left" }); } - doc.moveDown(0.8); - doc.strokeColor("#e5e7eb").lineWidth(1).moveTo(40, doc.y).lineTo(572, doc.y).stroke(); + doc.moveDown(0.6); + doc.strokeColor("#0f172a").lineWidth(1.5).moveTo(40, doc.y).lineTo(572, doc.y).stroke(); doc.moveDown(0.8); // --- Professional Summary --- diff --git a/web/src/components/ResumePreview.tsx b/web/src/components/ResumePreview.tsx index 711d418..4d579d5 100644 --- a/web/src/components/ResumePreview.tsx +++ b/web/src/components/ResumePreview.tsx @@ -3,140 +3,339 @@ import React, { useEffect, useState } from "react"; import { Document, Page, Text, View, StyleSheet, PDFDownloadLink } from "@react-pdf/renderer"; -const pdfStyles = StyleSheet.create({ - page: { - padding: 35, - fontFamily: "Helvetica", - fontSize: 9.5, - color: "#18181b", - lineHeight: 1.45, - }, - header: { - marginBottom: 14, - borderBottomWidth: 1.5, - borderBottomColor: "#18181b", - paddingBottom: 8, - }, - name: { - fontSize: 18, - fontFamily: "Helvetica-Bold", - color: "#09090b", - marginBottom: 2, - textTransform: "uppercase", - letterSpacing: 0.5, - }, - title: { - fontSize: 10.5, - color: "#3f3f46", - marginBottom: 4, - fontFamily: "Helvetica-Bold", - }, - contact: { - fontSize: 8.5, - color: "#52525b", - }, - section: { - marginBottom: 12, - }, - sectionTitle: { - fontSize: 9.5, - fontFamily: "Helvetica-Bold", - color: "#09090b", - borderBottomWidth: 1, - borderBottomColor: "#e4e4e7", - paddingBottom: 2, - marginBottom: 6, - textTransform: "uppercase", - letterSpacing: 0.5, - }, - jobHeader: { - flexDirection: "row", - justifyContent: "space-between", - marginBottom: 2, - }, - jobTitle: { - fontFamily: "Helvetica-Bold", - fontSize: 9.5, - }, - company: { - color: "#52525b", - fontFamily: "Helvetica", - }, - bullet: { - marginLeft: 8, - marginBottom: 2, - fontSize: 8.5, - color: "#27272a", - }, -}); +// Styles for React-PDF export by theme +const themesPDF = { + modern: StyleSheet.create({ + page: { + padding: 38, + fontFamily: "Helvetica", + fontSize: 9.5, + color: "#1e293b", + lineHeight: 1.45, + }, + header: { + marginBottom: 14, + borderBottomWidth: 2, + borderBottomColor: "#0f172a", + paddingBottom: 10, + }, + name: { + fontSize: 22, + fontFamily: "Helvetica-Bold", + color: "#0f172a", + marginBottom: 3, + letterSpacing: -0.2, + }, + title: { + fontSize: 11, + color: "#2563eb", + marginBottom: 6, + fontFamily: "Helvetica-Bold", + textTransform: "uppercase", + letterSpacing: 0.8, + }, + contact: { + fontSize: 8.5, + color: "#64748b", + }, + section: { + marginBottom: 13, + }, + sectionTitle: { + fontSize: 10, + fontFamily: "Helvetica-Bold", + color: "#0f172a", + borderBottomWidth: 1, + borderBottomColor: "#cbd5e1", + paddingBottom: 3, + marginBottom: 7, + textTransform: "uppercase", + letterSpacing: 0.8, + }, + jobHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "baseline", + marginBottom: 2, + }, + jobTitle: { + fontFamily: "Helvetica-Bold", + fontSize: 10, + color: "#0f172a", + }, + company: { + color: "#2563eb", + fontFamily: "Helvetica-Bold", + fontSize: 9.5, + }, + bullet: { + marginLeft: 10, + marginBottom: 2.5, + fontSize: 9, + color: "#334155", + lineHeight: 1.4, + }, + skillsRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: 4, + }, + skillBadge: { + backgroundColor: "#f1f5f9", + borderWidth: 0.5, + borderColor: "#cbd5e1", + borderRadius: 3, + paddingVertical: 2, + paddingHorizontal: 6, + fontSize: 8, + color: "#334155", + fontFamily: "Helvetica-Bold", + marginBottom: 3, + marginRight: 4, + }, + }), -const ResumePDFDocument = ({ data }: { data: any }) => ( - - - - {data.profile?.name || "Applicant Name"} - {data.profile?.title && {data.profile.title}} - - {[data.profile?.email, data.profile?.phone, data.profile?.location].filter(Boolean).join(" | ")} - - + editorial: StyleSheet.create({ + page: { + padding: 40, + fontFamily: "Times-Roman", + fontSize: 10, + color: "#1c1917", + lineHeight: 1.5, + }, + header: { + marginBottom: 16, + borderBottomWidth: 1, + borderBottomColor: "#1c1917", + paddingBottom: 10, + textAlign: "center", + }, + name: { + fontSize: 24, + fontFamily: "Times-Bold", + color: "#1c1917", + marginBottom: 4, + letterSpacing: 0.5, + }, + title: { + fontSize: 11, + color: "#78350f", + marginBottom: 5, + fontFamily: "Times-Italic", + }, + contact: { + fontSize: 9, + color: "#57534e", + }, + section: { + marginBottom: 14, + }, + sectionTitle: { + fontSize: 10.5, + fontFamily: "Times-Bold", + color: "#1c1917", + borderBottomWidth: 0.5, + borderBottomColor: "#a8a29e", + paddingBottom: 2, + marginBottom: 7, + textTransform: "uppercase", + letterSpacing: 1, + }, + jobHeader: { + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 2, + }, + jobTitle: { + fontFamily: "Times-Bold", + fontSize: 10.5, + color: "#1c1917", + }, + company: { + color: "#44403c", + fontFamily: "Times-Italic", + fontSize: 10, + }, + bullet: { + marginLeft: 10, + marginBottom: 2.5, + fontSize: 9.5, + color: "#292524", + }, + skillsRow: { + flexDirection: "row", + flexWrap: "wrap", + }, + skillBadge: { + fontSize: 9, + color: "#292524", + fontFamily: "Times-Roman", + marginRight: 6, + }, + }), - {data.profile?.summary && ( - - Professional Summary - {data.profile.summary} + minimal: StyleSheet.create({ + page: { + padding: 36, + fontFamily: "Helvetica", + fontSize: 9, + color: "#18181b", + lineHeight: 1.4, + }, + header: { + marginBottom: 14, + paddingBottom: 8, + borderBottomWidth: 1, + borderBottomColor: "#e4e4e7", + }, + name: { + fontSize: 20, + fontFamily: "Helvetica-Bold", + color: "#09090b", + marginBottom: 2, + }, + title: { + fontSize: 10, + color: "#52525b", + marginBottom: 4, + fontFamily: "Helvetica", + }, + contact: { + fontSize: 8.5, + color: "#71717a", + }, + section: { + marginBottom: 12, + }, + sectionTitle: { + fontSize: 9, + fontFamily: "Helvetica-Bold", + color: "#09090b", + borderBottomWidth: 0.5, + borderBottomColor: "#e4e4e7", + paddingBottom: 2, + marginBottom: 6, + textTransform: "uppercase", + letterSpacing: 0.5, + }, + jobHeader: { + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 2, + }, + jobTitle: { + fontFamily: "Helvetica-Bold", + fontSize: 9.5, + }, + company: { + color: "#52525b", + fontFamily: "Helvetica", + }, + bullet: { + marginLeft: 8, + marginBottom: 2, + fontSize: 8.5, + color: "#27272a", + }, + skillsRow: { + flexDirection: "row", + flexWrap: "wrap", + }, + skillBadge: { + fontSize: 8.5, + color: "#27272a", + marginRight: 8, + }, + }), +}; + +const ResumePDFDocument = ({ data, theme = "modern" }: { data: any; theme?: "modern" | "editorial" | "minimal" }) => { + const styles = themesPDF[theme] || themesPDF.modern; + + return ( + + + {/* Header */} + + {data.profile?.name || "Applicant Name"} + {data.profile?.title && {data.profile.title}} + + {[data.profile?.email, data.profile?.phone, data.profile?.location].filter(Boolean).join(" | ")} + - )} - {Array.isArray(data.work) && data.work.length > 0 && ( - - Professional Experience - {data.work.map((item: any, i: number) => ( - - - - {item.position || "Position"}{" "} - {item.company ? — {item.company} : null} + {/* Summary */} + {data.profile?.summary && ( + + Professional Summary + + {data.profile.summary} + + + )} + + {/* Experience */} + {Array.isArray(data.work) && data.work.length > 0 && ( + + Professional Experience + {data.work.map((item: any, i: number) => ( + + + {item.position || "Position"} + {item.company && {item.company}} + + {item.summary && ( + + {item.summary} + + )} + {Array.isArray(item.highlights) && + item.highlights.map((h: string, hIdx: number) => ( + • {h} + ))} + + ))} + + )} + + {/* Skills */} + {Array.isArray(data.skills) && data.skills.length > 0 && ( + + Skills & Technical Proficiencies + + {theme === "modern" ? ( + data.skills.map((s: string, idx: number) => ( + {s} + )) + ) : ( + {data.skills.join(" • ")} + )} + + + )} + + {/* Education */} + {Array.isArray(data.education) && data.education.length > 0 && ( + + Education & Certifications + {data.education.map((edu: any, i: number) => ( + + + {edu.degree || "Degree / Program"} + {edu.institution ? ` — ${edu.institution}` : ""} - {item.summary && {item.summary}} - {Array.isArray(item.highlights) && - item.highlights.map((h: string, hIdx: number) => ( - • {h} - ))} - - ))} - - )} - - {Array.isArray(data.skills) && data.skills.length > 0 && ( - - Skills & Competencies - {data.skills.join(" • ")} - - )} - - {Array.isArray(data.education) && data.education.length > 0 && ( - - Education & Certifications - {data.education.map((edu: any, i: number) => ( - - - {edu.degree && edu.institution - ? `${edu.degree} — ${edu.institution}` - : edu.degree || edu.institution || "High School / Technical Education"} - - - ))} - - )} - - -); + ))} + + )} + + + ); +}; export function ResumePreview({ data }: { data: any }) { const [isClient, setIsClient] = useState(false); - const [theme, setTheme] = useState<"classic" | "modern" | "compact">("classic"); + const [theme, setTheme] = useState<"modern" | "editorial" | "minimal">("modern"); useEffect(() => { setIsClient(true); @@ -148,78 +347,147 @@ export function ResumePreview({ data }: { data: any }) { const education = data.education || []; return ( -
-
+
+
- - Live ATS Preview + + ATS Visual Template
{isClient && ( } - fileName={`${(profile.name || "Resume").replace(/\s+/g, "_")}_ATS.pdf`} - className="text-xs font-mono font-semibold bg-zinc-900 hover:bg-zinc-800 text-white px-3 py-1 rounded transition-colors inline-block" + document={} + fileName={`${(profile.name || "Resume").replace(/\s+/g, "_")}_${theme.toUpperCase()}_ATS.pdf`} + className="text-xs font-mono font-semibold bg-stone-900 hover:bg-stone-800 text-stone-100 px-3.5 py-1.5 border border-stone-950 shadow-[1px_1px_0px_#000] transition-colors inline-block" > - Download ATS PDF + Export Styled PDF )}
+ {/* Styled Responsive Visual Document View */}
-
-

{profile.name || "Your Full Name"}

-

{profile.title || "Target Job Title"}

-
+ {/* Header Block */} +
+

+ {profile.name || "Your Full Name"} +

+ +

+ {profile.title || "Target Professional Title"} +

+ +
{profile.email && {profile.email}} - {profile.phone && {profile.phone}} - {profile.location && {profile.location}} + {profile.phone && • {profile.phone}} + {profile.location && • {profile.location}}
+ {/* Summary Block */} {profile.summary && ( -
-

- Summary +
+

+ Professional Summary

-

{profile.summary}

+

{profile.summary}

)} + {/* Experience Block */} {work.length > 0 && ( -
-

+
+

Professional Experience

-
+ +
{work.map((w: any, idx: number) => ( -
-

- {w.position || "Position"}{" "} - {w.company ? — {w.company} : null} -

- {w.summary &&

{w.summary}

} +
+
+ {w.position || "Position"} + {w.company && ( + + {w.company} + + )} +
+ + {w.summary &&

{w.summary}

} + {Array.isArray(w.highlights) && w.highlights.length > 0 && ( -
    +
      {w.highlights.map((h: string, hIdx: number) => (
    • {h}
    • ))} @@ -231,26 +499,61 @@ export function ResumePreview({ data }: { data: any }) {
)} + {/* Skills Block */} {skills.length > 0 && ( -
-

- Skills & Competencies +
+

+ Skills & Technical Proficiencies

-

{skills.join(" • ")}

+ + {theme === "modern" ? ( +
+ {skills.map((s: string, idx: number) => ( + + {s} + + ))} +
+ ) : ( +

{skills.join(" • ")}

+ )}
)} + {/* Education Block */} {education.length > 0 && ( -
-

+
+

Education & Certifications

+
{education.map((edu: any, idx: number) => ( -

- {edu.degree || edu.institution || "High School Diploma"} - {edu.institution && edu.degree ? — {edu.institution} : null} -

+
+ {edu.degree || "Degree / Program"} + {edu.institution && ( + — {edu.institution} + )} +
))}