mirror of
https://github.com/srbhr/Resume-Matcher.git
synced 2026-01-18 23:16:50 +00:00
- Introduced functionality to generate and update cover letters and outreach messages for resumes. - Enhanced the resume schema to include fields for cover letter and outreach message content. - Implemented new API endpoints for updating cover letters and outreach messages. - Added UI components for editing and previewing cover letters and outreach messages in the Resume Builder. - Updated feature configuration settings to enable or disable cover letter and outreach message generation. - Enhanced documentation to reflect new features and usage instructions for cover letter and outreach message functionalities.
32 KiB
32 KiB
Template System Documentation
This document provides comprehensive guidance for understanding, extending, and creating new resume templates in the Resume Matcher application.
Table of Contents
- Current Template Architecture
- Template File Structure
- CSS Custom Properties System
- Creating a New Template
- Template Settings Integration
- Print & PDF Considerations
- Template Examples
1. Current Template Architecture
Available Templates
| Template ID | Name | Layout | Status |
|---|---|---|---|
swiss-single |
Swiss Single Column | Traditional single-column | Production |
swiss-two-column |
Swiss Two Column | 65%/35% split layout | Production |
Template Selection Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ TEMPLATE SELECTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
User selects template in Builder
│
▼
┌─────────────────┐
│ FormattingCtrls │
│ updates │
│ templateSettings│
└────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ ResumeBuilder │────>│ Resume │
│ passes settings │ │ component │
└─────────────────┘ └────────┬────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ swiss-single │ │ swiss-two-column│
│ ResumeSingle │ │ ResumeTwoColumn │
│ Column.tsx │ │ .tsx │
└─────────────────┘ └─────────────────┘
Component Hierarchy
Resume (components/dashboard/resume-component.tsx)
├── Delegates to template based on settings.template
│
├── ResumeSingleColumn (components/resume/resume-single-column.tsx)
│ ├── PersonalInfoSection
│ ├── ExperienceSection
│ ├── EducationSection
│ ├── ProjectsSection
│ └── SkillsSection
│
└── ResumeTwoColumn (components/resume/resume-two-column.tsx)
├── LeftColumn (65%)
│ ├── PersonalInfoSection
│ ├── ExperienceSection (sidebar items)
│ └── ProjectsSection
│
└── RightColumn (35%)
├── EducationSection
└── SkillsSection
2. Template File Structure
Directory Layout
apps/frontend/
├── components/
│ ├── dashboard/
│ │ └── resume-component.tsx # Main router component
│ │
│ └── resume/
│ ├── index.ts # Barrel export
│ ├── resume-single-column.tsx # Swiss single column
│ ├── resume-two-column.tsx # Swiss two column
│ └── sections/ # Shared section components
│ ├── personal-info.tsx
│ ├── experience.tsx
│ ├── education.tsx
│ ├── projects.tsx
│ └── skills.tsx
│
├── lib/
│ ├── types/
│ │ └── template-settings.ts # Settings types & defaults
│ │
│ └── constants/
│ └── page-dimensions.ts # A4/Letter dimensions
│
└── app/
└── (default)/
└── css/
└── globals.css # CSS custom properties
Key Files
Resume Router (components/dashboard/resume-component.tsx)
// Lines 1-50 - Main resume component that routes to templates
interface ResumeProps {
data: ResumeData;
settings?: TemplateSettings;
className?: string;
}
export function Resume({ data, settings, className }: ResumeProps) {
const templateSettings = settings ?? DEFAULT_TEMPLATE_SETTINGS;
// Apply CSS custom properties
const style = getTemplateStyles(templateSettings);
switch (templateSettings.template) {
case "swiss-single":
return <ResumeSingleColumn data={data} style={style} className={className} />;
case "swiss-two-column":
return <ResumeTwoColumn data={data} style={style} className={className} />;
default:
return <ResumeSingleColumn data={data} style={style} className={className} />;
}
}
Template Settings Types (lib/types/template-settings.ts)
// Lines 1-80 - Complete type definitions
export type TemplateType = "swiss-single" | "swiss-two-column";
export type PageSize = "A4" | "LETTER";
export interface TemplateSettings {
template: TemplateType;
pageSize: PageSize;
margins: {
top: number; // mm
bottom: number;
left: number;
right: number;
};
sectionSpacing: number; // 1-5 scale
itemSpacing: number; // 1-5 scale
lineHeight: number; // 1-5 scale
fontSize: number; // 1-5 scale
headerScale: number; // 1-5 scale
}
export const DEFAULT_TEMPLATE_SETTINGS: TemplateSettings = {
template: "swiss-single",
pageSize: "A4",
margins: { top: 15, bottom: 15, left: 15, right: 15 },
sectionSpacing: 3,
itemSpacing: 3,
lineHeight: 3,
fontSize: 3,
headerScale: 3,
};
// CSS custom property mapping
export const SPACING_VALUES = {
sectionSpacing: ["0.75rem", "1rem", "1.25rem", "1.5rem", "2rem"],
itemSpacing: ["0.25rem", "0.5rem", "0.75rem", "1rem", "1.25rem"],
lineHeight: ["1.2", "1.3", "1.4", "1.5", "1.6"],
fontSize: ["0.8rem", "0.875rem", "1rem", "1.125rem", "1.25rem"],
headerScale: ["1.5", "1.75", "2", "2.25", "2.5"],
};
export function getTemplateStyles(settings: TemplateSettings): CSSProperties {
return {
"--section-spacing": SPACING_VALUES.sectionSpacing[settings.sectionSpacing - 1],
"--item-spacing": SPACING_VALUES.itemSpacing[settings.itemSpacing - 1],
"--line-height": SPACING_VALUES.lineHeight[settings.lineHeight - 1],
"--font-size": SPACING_VALUES.fontSize[settings.fontSize - 1],
"--header-scale": SPACING_VALUES.headerScale[settings.headerScale - 1],
} as CSSProperties;
}
3. CSS Custom Properties System
Global CSS Variables (globals.css)
/* Resume Template Variables */
:root {
/* Spacing */
--section-spacing: 1.25rem;
--item-spacing: 0.75rem;
/* Typography */
--line-height: 1.4;
--font-size: 1rem;
--header-scale: 2;
/* Colors (Swiss Design) */
--resume-bg: #FFFFFF;
--resume-text: #1a1a1a;
--resume-accent: #000000;
--resume-muted: #666666;
--resume-border: #E5E5E0;
}
/* Resume Base Styles */
.resume-container {
font-size: var(--font-size);
line-height: var(--line-height);
color: var(--resume-text);
background: var(--resume-bg);
}
.resume-section {
margin-bottom: var(--section-spacing);
}
.resume-item {
margin-bottom: var(--item-spacing);
}
.resume-header {
font-size: calc(var(--font-size) * var(--header-scale));
}
/* Page Break Control */
.resume-section {
break-inside: avoid;
}
.resume-item {
break-inside: avoid;
}
.resume-section-title {
break-after: avoid;
}
How Settings Map to CSS
┌─────────────────────────────────────────────────────────────────────────────┐
│ SETTINGS → CSS MAPPING │
└─────────────────────────────────────────────────────────────────────────────┘
TemplateSettings CSS Custom Properties
─────────────────────────────────────────────────────────────────────────────
sectionSpacing: 3 ──────────────────> --section-spacing: 1.25rem
(index 2 of SPACING_VALUES array)
itemSpacing: 3 ──────────────────> --item-spacing: 0.75rem
lineHeight: 3 ──────────────────> --line-height: 1.4
fontSize: 3 ──────────────────> --font-size: 1rem
headerScale: 3 ──────────────────> --header-scale: 2
(h1 becomes 2rem with --font-size: 1rem)
Value Scales
| Setting | Level 1 | Level 2 | Level 3 | Level 4 | Level 5 |
|---|---|---|---|---|---|
| Section Spacing | 0.75rem | 1rem | 1.25rem | 1.5rem | 2rem |
| Item Spacing | 0.25rem | 0.5rem | 0.75rem | 1rem | 1.25rem |
| Line Height | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 |
| Font Size | 0.8rem | 0.875rem | 1rem | 1.125rem | 1.25rem |
| Header Scale | 1.5x | 1.75x | 2x | 2.25x | 2.5x |
Bold = Default (Level 3)
4. Creating a New Template
Step 1: Define Template ID
Add the new template type to lib/types/template-settings.ts:
// Before
export type TemplateType = "swiss-single" | "swiss-two-column";
// After
export type TemplateType = "swiss-single" | "swiss-two-column" | "modern-minimal";
Step 2: Create Template Component
Create a new file components/resume/resume-modern-minimal.tsx:
import { CSSProperties } from "react";
import { ResumeData } from "@/lib/types/resume";
interface Props {
data: ResumeData;
style?: CSSProperties;
className?: string;
}
export function ResumeModernMinimal({ data, style, className }: Props) {
return (
<div
className={`resume-container ${className ?? ""}`}
style={style}
>
{/* Header Section */}
<header className="resume-section border-b-2 border-black pb-4 mb-6">
<h1 className="resume-header font-bold tracking-tight">
{data.personal_info.name}
</h1>
<div className="flex gap-4 text-sm mt-2">
{data.personal_info.email && (
<span>{data.personal_info.email}</span>
)}
{data.personal_info.phone && (
<span>{data.personal_info.phone}</span>
)}
{data.personal_info.location && (
<span>{data.personal_info.location}</span>
)}
</div>
</header>
{/* Summary */}
{data.personal_info.summary && (
<section className="resume-section">
<p className="text-gray-700 leading-relaxed">
{data.personal_info.summary}
</p>
</section>
)}
{/* Experience */}
{data.experience && data.experience.length > 0 && (
<section className="resume-section">
<h2 className="text-lg font-bold uppercase tracking-widest mb-3">
Experience
</h2>
{data.experience.map((exp, i) => (
<article key={i} className="resume-item">
<div className="flex justify-between items-baseline">
<h3 className="font-semibold">{exp.title}</h3>
<span className="text-sm text-gray-500">
{exp.start_date} - {exp.end_date || "Present"}
</span>
</div>
<p className="text-gray-600">{exp.company}</p>
{exp.bullets && (
<ul className="mt-2 space-y-1">
{exp.bullets.map((bullet, j) => (
<li key={j} className="text-sm pl-4 relative before:content-['–'] before:absolute before:left-0">
{bullet}
</li>
))}
</ul>
)}
</article>
))}
</section>
)}
{/* Education */}
{data.education && data.education.length > 0 && (
<section className="resume-section">
<h2 className="text-lg font-bold uppercase tracking-widest mb-3">
Education
</h2>
{data.education.map((edu, i) => (
<article key={i} className="resume-item">
<div className="flex justify-between items-baseline">
<h3 className="font-semibold">{edu.degree}</h3>
<span className="text-sm text-gray-500">
{edu.graduation_date}
</span>
</div>
<p className="text-gray-600">{edu.institution}</p>
</article>
))}
</section>
)}
{/* Skills */}
{data.skills && (
<section className="resume-section">
<h2 className="text-lg font-bold uppercase tracking-widest mb-3">
Skills
</h2>
<p className="text-sm">
{Array.isArray(data.skills)
? data.skills.join(" • ")
: data.skills}
</p>
</section>
)}
</div>
);
}
Step 3: Register in Router
Update components/dashboard/resume-component.tsx:
import { ResumeModernMinimal } from "@/components/resume/resume-modern-minimal";
// In the switch statement:
switch (templateSettings.template) {
case "swiss-single":
return <ResumeSingleColumn data={data} style={style} className={className} />;
case "swiss-two-column":
return <ResumeTwoColumn data={data} style={style} className={className} />;
case "modern-minimal":
return <ResumeModernMinimal data={data} style={style} className={className} />;
default:
return <ResumeSingleColumn data={data} style={style} className={className} />;
}
Step 4: Add to Template Selector UI
Update components/builder/formatting-controls.tsx:
const TEMPLATES = [
{
id: "swiss-single",
name: "Single Column",
preview: "/templates/swiss-single.png",
},
{
id: "swiss-two-column",
name: "Two Column",
preview: "/templates/swiss-two-column.png",
},
{
id: "modern-minimal",
name: "Modern Minimal",
preview: "/templates/modern-minimal.png",
},
];
Step 5: Export from Index
Update components/resume/index.ts:
export { ResumeSingleColumn } from "./resume-single-column";
export { ResumeTwoColumn } from "./resume-two-column";
export { ResumeModernMinimal } from "./resume-modern-minimal";
5. Template Settings Integration
Builder Integration
The ResumeBuilder component manages template settings through state:
// components/builder/resume-builder.tsx
const [templateSettings, setTemplateSettings] = useState<TemplateSettings>(
DEFAULT_TEMPLATE_SETTINGS
);
// Load from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem("resume_builder_settings");
if (saved) {
setTemplateSettings(JSON.parse(saved));
}
}, []);
// Save to localStorage on change
useEffect(() => {
localStorage.setItem(
"resume_builder_settings",
JSON.stringify(templateSettings)
);
}, [templateSettings]);
FormattingControls Component
// components/builder/formatting-controls.tsx
interface FormattingControlsProps {
settings: TemplateSettings;
onChange: (settings: TemplateSettings) => void;
}
export function FormattingControls({ settings, onChange }: FormattingControlsProps) {
return (
<div className="space-y-4 p-4 border rounded-lg">
{/* Template Selection */}
<div>
<label className="block text-sm font-medium mb-2">Template</label>
<div className="grid grid-cols-2 gap-2">
{TEMPLATES.map((template) => (
<button
key={template.id}
onClick={() => onChange({ ...settings, template: template.id })}
className={`p-2 border rounded ${
settings.template === template.id
? "border-blue-500 bg-blue-50"
: "border-gray-200"
}`}
>
<img src={template.preview} alt={template.name} />
<span className="text-xs">{template.name}</span>
</button>
))}
</div>
</div>
{/* Page Size */}
<div>
<label className="block text-sm font-medium mb-2">Page Size</label>
<div className="flex gap-2">
<button
onClick={() => onChange({ ...settings, pageSize: "A4" })}
className={`px-3 py-1 rounded ${
settings.pageSize === "A4" ? "bg-black text-white" : "bg-gray-100"
}`}
>
A4
</button>
<button
onClick={() => onChange({ ...settings, pageSize: "LETTER" })}
className={`px-3 py-1 rounded ${
settings.pageSize === "LETTER" ? "bg-black text-white" : "bg-gray-100"
}`}
>
US Letter
</button>
</div>
</div>
{/* Spacing Sliders */}
<div>
<label className="block text-sm font-medium mb-2">
Section Spacing: {settings.sectionSpacing}
</label>
<input
type="range"
min="1"
max="5"
value={settings.sectionSpacing}
onChange={(e) =>
onChange({ ...settings, sectionSpacing: parseInt(e.target.value) })
}
className="w-full"
/>
</div>
{/* ... More controls */}
</div>
);
}
PDF Download with Settings
Settings are passed to the PDF endpoint:
// lib/api/resume.ts
export async function downloadResumePdf(
resumeId: string,
settings: TemplateSettings
): Promise<Blob> {
const params = new URLSearchParams({
template: settings.template,
pageSize: settings.pageSize,
marginTop: settings.margins.top.toString(),
marginBottom: settings.margins.bottom.toString(),
marginLeft: settings.margins.left.toString(),
marginRight: settings.margins.right.toString(),
sectionSpacing: settings.sectionSpacing.toString(),
itemSpacing: settings.itemSpacing.toString(),
lineHeight: settings.lineHeight.toString(),
fontSize: settings.fontSize.toString(),
headerScale: settings.headerScale.toString(),
});
const response = await fetch(
`${API_BASE}/resumes/${resumeId}/pdf?${params}`
);
if (!response.ok) {
throw new Error("Failed to download PDF");
}
return response.blob();
}
6. Print & PDF Considerations
Print Route
The frontend has a dedicated print route for PDF rendering:
apps/frontend/app/print/resumes/[id]/page.tsx
This page:
- Receives resume ID and settings via URL params
- Fetches resume data from backend
- Renders the Resume component with settings
- Playwright visits this page and generates PDF
Print Styles
/* globals.css - Print-specific styles */
@media print {
.resume-container {
width: 100%;
max-width: none;
padding: 0;
margin: 0;
}
.resume-section {
break-inside: avoid;
}
.resume-item {
break-inside: avoid;
}
/* Hide non-print elements */
.no-print {
display: none !important;
}
}
Playwright PDF Configuration
# apps/backend/app/pdf.py
async def render_resume_pdf(
resume_id: str,
template: str,
page_size: str,
margins: dict,
**kwargs
) -> bytes:
# Build URL with all settings
params = urlencode({
"template": template,
"pageSize": page_size,
**{f"margin{k.title()}": v for k, v in margins.items()},
**kwargs,
})
url = f"{FRONTEND_URL}/print/resumes/{resume_id}?{params}"
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(url, wait_until="networkidle")
# Wait for fonts to load
await page.wait_for_function("document.fonts.ready")
pdf_bytes = await page.pdf(
format=page_size,
margin={"top": "0mm", "right": "0mm", "bottom": "0mm", "left": "0mm"},
print_background=True,
prefer_css_page_size=False,
)
await browser.close()
return pdf_bytes
Margin Application
Margins are applied in the HTML, not Playwright:
// app/print/resumes/[id]/page.tsx
export default function PrintResumePage({ searchParams }) {
const { marginTop = 15, marginBottom = 15, marginLeft = 15, marginRight = 15 } = searchParams;
return (
<div
style={{
padding: `${marginTop}mm ${marginRight}mm ${marginBottom}mm ${marginLeft}mm`,
width: searchParams.pageSize === "A4" ? "210mm" : "215.9mm",
minHeight: searchParams.pageSize === "A4" ? "297mm" : "279.4mm",
}}
>
<Resume data={resumeData} settings={settings} />
</div>
);
}
7. Template Examples
Swiss Single Column
┌─────────────────────────────────────────────────────────────────┐
│ │
│ JOHN DOE │
│ john@email.com | (555) 123-4567 | New York, NY │
│ linkedin.com/in/johndoe | github.com/johndoe │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ PROFESSIONAL SUMMARY │
│ Experienced software engineer with 8+ years building │
│ scalable web applications... │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ EXPERIENCE │
│ │
│ Senior Software Engineer │
│ Tech Company Inc. | Jan 2020 - Present │
│ • Led team of 5 engineers... │
│ • Improved performance by 40%... │
│ │
│ Software Engineer │
│ Startup Co. | Jun 2016 - Dec 2019 │
│ • Built microservices architecture... │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ EDUCATION │
│ │
│ B.S. Computer Science │
│ University of Technology | 2016 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ SKILLS │
│ Python, JavaScript, React, Node.js, PostgreSQL, AWS │
│ │
└─────────────────────────────────────────────────────────────────┘
Swiss Two Column
┌─────────────────────────────────────────────────────────────────┐
│ │
│ JOHN DOE │
│ john@email.com | (555) 123-4567 | New York, NY │
│ │
├─────────────────────────────────────────┬───────────────────────┤
│ │ │
│ EXPERIENCE │ EDUCATION │
│ │ │
│ Senior Software Engineer │ B.S. Computer Sci. │
│ Tech Company Inc. │ Univ. of Technology │
│ Jan 2020 - Present │ 2016 │
│ • Led team of 5 engineers... │ │
│ • Improved performance by 40%... │ ─────────────────────│
│ │ │
│ Software Engineer │ SKILLS │
│ Startup Co. │ │
│ Jun 2016 - Dec 2019 │ Languages: │
│ • Built microservices... │ Python, JavaScript │
│ │ │
│ ───────────────────────────────────── │ Frameworks: │
│ │ React, Node.js │
│ PROJECTS │ │
│ │ Databases: │
│ Open Source CLI Tool │ PostgreSQL, MongoDB │
│ github.com/johndoe/cli-tool │ │
│ • Built popular CLI with 1k+ stars │ Cloud: │
│ │ AWS, GCP │
│ │ │
└─────────────────────────────────────────┴───────────────────────┘
Modern Minimal (Example New Template)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ john doe │
│ ═══════════════════════════════════════════════════════════ │
│ john@email.com • (555) 123-4567 • New York, NY │
│ │
│ │
│ Experienced software engineer with 8+ years building │
│ scalable web applications using modern technologies. │
│ │
│ │
│ E X P E R I E N C E │
│ │
│ Senior Software Engineer 2020 – Present │
│ Tech Company Inc. │
│ – Led team of 5 engineers on customer platform │
│ – Improved API response time by 40% │
│ │
│ Software Engineer 2016 – 2019 │
│ Startup Co. │
│ – Built microservices architecture from scratch │
│ │
│ │
│ E D U C A T I O N │
│ │
│ B.S. Computer Science 2016 │
│ University of Technology │
│ │
│ │
│ S K I L L S │
│ │
│ Python • JavaScript • React • Node.js • PostgreSQL • AWS │
│ │
└─────────────────────────────────────────────────────────────────┘
Template Checklist
When creating a new template, ensure:
- Template ID added to
TemplateTypeunion - Component file created in
components/resume/ - Component exported from
components/resume/index.ts - Switch case added in
resume-component.tsx - Preview thumbnail created (300x400px recommended)
- Template added to
TEMPLATESarray in formatting controls - CSS custom properties used for spacing/typography
.resume-sectionand.resume-itemclasses applied- Page break rules respected
- Print styles tested
- PDF generation tested with all page sizes
- All ResumeData fields handled (including optional ones)
Files Changed Summary
For Adding a New Template
| File | Change |
|---|---|
lib/types/template-settings.ts |
Add to TemplateType union |
components/resume/[new-template].tsx |
Create new component |
components/resume/index.ts |
Export new component |
components/dashboard/resume-component.tsx |
Add switch case |
components/builder/formatting-controls.tsx |
Add to TEMPLATES array |
public/templates/[new-template].png |
Add preview thumbnail |
For Modifying Template System
| File | Purpose |
|---|---|
lib/types/template-settings.ts |
Settings types, defaults, CSS mapping |
app/(default)/css/globals.css |
CSS custom properties, print styles |
components/builder/resume-builder.tsx |
Settings state management |
components/builder/formatting-controls.tsx |
Settings UI |
app/print/resumes/[id]/page.tsx |
Print route with margins |
apps/backend/app/pdf.py |
Playwright PDF generation |