CV-Engine/Cv-engine.md
geulah 1bede93cd1 feat: initialize CV Engine project with core functionality
- Set up React + TypeScript + Vite project with TailwindCSS
- Implement Zustand store for CV data management
- Create personal details editor with validation
- Add ATS-friendly template for CV preview
- Build stepper navigation component
- Include schema validation with Zod
- Configure ESLint and Prettier for code quality
2025-10-05 22:53:40 +01:00

14 KiB

CV Editor React Component — README

A reusable, plug-and-play React component that provides a CV/resume builder with:

  • a form-based editor (stepper UI),
  • a rich-text Tiptap summary editor,
  • live preview (inline) and iframe-isolated printable preview (A4),
  • structured JSON CV model (single source of truth),
  • easy integration points for saving, exporting (server-side Puppeteer example), theming and templates.

This README explains how to install, integrate, customize, test and deploy the component in an existing React app or run it standalone.


Table of contents

  1. What this component does
  2. Key features & design decisions
  3. Quick start (install + run)
  4. Component API (props & events)
  5. File / folder structure (suggested)
  6. How to plug into an existing React app (examples)
  7. Templates: how they work & how to add more
  8. Exporting PDFs (server-side Puppeteer example)
  9. Security & sanitization (DOMPurify)
  10. State management recommendations (Zustand + TanStack)
  11. Styling and theming (Tailwind / CSS isolation)
  12. Accessibility checklist
  13. Testing & CI suggestions
  14. Troubleshooting / common errors
  15. Next steps & extension ideas

1. What this component does

This component provides a full CV editor UI you can drop into your React app. It stores the CV as a structured JSON model and renders templates from that model. The summary field is a rich editor (Tiptap) that stores content as HTML/JSON; previews are rendered inline and inside an iframe (print-ready HTML). For production PDF export you can reuse the same printable HTML on the server and render it with Puppeteer.


2. Key features & design decisions

  • Single source of truth: the CV is a JSON object (easy to save, modify, export).
  • Templates as renderers: templates are modular React components / HTML + CSS that accept the CV JSON and return DOM or printable HTML.
  • Rich summary: Tiptap (ProseMirror) used for structured, extensible rich text with predictable HTML output.
  • Preview parity: inline preview uses the same React template rendering to reduce export surprises; iframe preview isolates CSS for print fidelity.
  • Server-side parity: printable HTML builder can be used on server in Puppeteer to generate pixel-perfect PDFs.

3. Quick start (install + run)

Minimal dependencies

Add these packages to your project:

# Core
npm install react react-dom

# UI + styling (suggested)
npm install tailwindcss

# Rich text
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder

# Optional & recommended
npm install dompurify
npm install axios
npm install zustand @tanstack/react-query

Example package.json dev/start scripts

{
  "scripts": {
    "start": "vite",            // or react-scripts start / next dev
    "build": "vite build",
    "test": "vitest"
  }
}

Run

Add the component file (e.g. CVEditorWireframe.jsx) to your app and import it into a page. Start your app as usual (npm start / npm run dev).


4. Component API (props & events)

Use this component as <CVEditor />. Example usage:

import CVEditor from './components/CVEditorWireframe'

function App() {
  const initialCV = { /* optional initial JSON model */ }

  async function handleSave(cvJson) {
    // e.g. POST /api/cv
    await axios.post('/api/cv', cvJson)
  }

  return (
    <CVEditor
      initialCV={initialCV}
      templates={myTemplateList}
      onSave={handleSave}
      exportEndpoint="/api/export/pdf" // optional: server endpoint for export jobs
      allowDownload={true}
      theme="light"
    />
  )
}

Props

  • initialCV{} CV JSON model to pre-populate the editor (optional).
  • templates[{ id, name, renderer }] list of templates. renderer can be a React component that accepts {cv} or a function that returns printable HTML.
  • onSave(cv) — async callback called when user saves (or autosave triggers).
  • onExportRequest(cv) — callback to call when user requests PDF export. If provided, component can call it (you can send to server for Puppeteer).
  • exportEndpoint — optional server endpoint URL to POST CV JSON for export.
  • allowDownloadboolean, whether to show Export/Download controls.
  • theme'light' | 'dark' (optional, used to add top-level class).
  • className — additional CSS class for outer container.
  • onChange(cv) — optional callback fired on any CV change (useful for autosave).

5. File / folder structure (suggested)

src/
  components/
    CVEditorWireframe.jsx        // main component (editor + preview)
    templates/
      TemplateClassic.jsx
      TemplateATS.jsx
    editors/
      RichSummaryEditor.jsx      // Tiptap wrapper
  utils/
    printableHtml.js             // buildPrintableHtml(cv)
    escapeHtml.js
  hooks/
    useCvAutosave.js
  pages/
    EditorPage.jsx               // where you mount CVEditor
server/
  export/
    exportPdf.js                 // Express + Puppeteer server code (optional)

6. How to plug into an existing React app

  1. Copy CVEditorWireframe.jsx and its dependencies into your project.
  2. Import and render it inside an existing route or component.
  3. Provide initialCV, onSave and templates props as needed.
// routes/ProfileEditor.jsx
import CVEditor from '../components/CVEditorWireframe'
import TemplateClassic from '../components/templates/TemplateClassic'

const templates = [
  { id: 'classic', name: 'Classic', renderer: TemplateClassic },
  // add ATS or other templates
]

export default function ProfileEditor() {
  const initialCV = {...}
  async function handleSave(cv) { await api.saveCv(cv) }

  return <CVEditor initialCV={initialCV} templates={templates} onSave={handleSave} exportEndpoint="/api/export/pdf" />
}

B. As an isolated micro-frontend

If you prefer it to be self-contained: bundle the component into its own package or iframe it from a separate deployment. But prefer simple import for easier code sharing and debugging.


7. Templates: how they work & how to add more

Two options for templates:

  1. React component template — a React component that accepts { cv } and renders markup. Use for inline preview and server rendering (with renderToString).
  2. HTML template function — a function that receives cv and returns printable HTML string. Use for server-side export or when you want designers to provide standalone HTML/CSS files.

Example React template

export default function TemplateClassic({ cv }) {
  return (
    <div className="template-root">
      <h1>{cv.personal.firstName} {cv.personal.lastName}</h1>
      <div dangerouslySetInnerHTML={{ __html: cv.summary }} />
      {cv.work.map(w => (
        <div key={w.id}>
          <h3>{w.title}</h3>
          <div>{w.company}  {w.period}</div>
          <ul>{w.bullets.map((b,i) => <li key={i}>{b}</li>)}</ul>
        </div>
      ))}
    </div>
  );
}

Guidelines

  • Provide both visual templates (fancier CSS) and ATS templates (semantic, simple markup).
  • Keep template CSS isolated (CSS modules, scoped styles or render in iframe).
  • Use the same rendering logic on the server for PDFs to ensure parity.

8. Exporting PDFs (server-side Puppeteer example)

Why server-side?

Puppeteer produces consistent, pixel-perfect PDFs (server or worker) and you can offload CPU work off the client.

Minimal Express endpoint (example)

// server/export/exportPdf.js
import express from 'express';
import puppeteer from 'puppeteer';

const router = express.Router();

router.post('/export/pdf', async (req, res) => {
  try {
    const cv = req.body;
    // reuse printable-html builder (same logic as client)
    const html = buildPrintableHtml(cv);
    const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
    const page = await browser.newPage();
    await page.setContent(html, { waitUntil: 'networkidle0' });
    const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true });
    await browser.close();

    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="${cv.personal.firstName}_${cv.personal.lastName}_CV.pdf"`
    });
    res.send(pdfBuffer);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to create PDF' });
  }
});

export default router;

Notes

  • Use a worker or job queue (BullMQ) for high volume.
  • Reuse the same HTML builder (buildPrintableHtml) on the server (move the builder to a shared package or duplicate carefully).
  • If using serverless (Vercel / Netlify), be aware Puppeteer needs special setup (use chrome-aws-lambda).

9. Security & sanitization (DOMPurify)

Because summary HTML comes from users, always sanitize before rendering into srcDoc or sending to server.

npm install dompurify
import DOMPurify from 'dompurify'

const safeHtml = DOMPurify.sanitize(cv.summary)
<div dangerouslySetInnerHTML={{ __html: safeHtml }} />

Sanitize on both client and server if the server stores the HTML or returns it to other users.


10. State management recommendations

  • Zustand — simple local/global client state (editor draft, UI toggles).
  • TanStack Query — server-state (fetching/saving CVs, export job status).
  • Use both: keep in-memory editing state in Zustand, and use React Query for network operations (cache, optimistic updates, retries).

Example:

// useCvAutosave.js
import { useMutation } from '@tanstack/react-query';
export function useAutosave() {
  return useMutation(cv => api.saveCv(cv), { /* ... */ })
}

11. Styling & theming

  • Use Tailwind CSS for fast iteration: the wireframe uses tailwind utility classes.
  • For templates, prefer scoped CSS (CSS modules) or provide a template.css imported only by the template to avoid leakage.
  • For iframe preview, srcDoc includes template CSS directly—this isolates styles perfectly.

12. Accessibility checklist

  • All form fields have accessible labels (use label with htmlFor).
  • Provide keyboard navigation for stepper and drag/reorder (if implemented).
  • Ensure color contrast meets WCAG AA for text.
  • Use semantic elements (<header>, <main>, <section>, <ul>) in templates.
  • Add aria-live regions for autosave status and export progress.

13. Testing & CI suggestions

  • Unit tests: Vitest/Jest for utils (escapeHtml, buildPrintableHtml).
  • Visual regression: Percy / Chromatic for templates and export output.
  • E2E: Cypress or Playwright for full editor flows (create CV → preview → export).
  • CI: run tests + lint; optionally run Puppeteer to generate sample PDFs in a job (be mindful of resource usage).

Example unit test for escapeHtml:

import { escapeHtml } from '../utils/escapeHtml'
test('escapes special chars', () => {
  expect(escapeHtml(`Tom & Jerry <script>`)).toContain('&amp;')
})

14. Troubleshooting / common errors

  • Unterminated string constant: often caused by unescaped newline characters inside template literals or improperly closed quotes. Ensure:

    • Use backticks ` for multi-line template strings.
    • Escape \n inside single/double quoted strings (e.g. join('\\n')).
    • Ensure escapeHtml function returns properly and ends with }.
  • Tiptap SSR: Tiptap relies on DOM; rendering server-side requires guarding the editor initialisation (only run in browser). Use if (typeof window !== 'undefined') or dynamic import.

  • Puppeteer memory/CPU: scale PDF generation using a worker pool (e.g. puppeteer-cluster) or separate service.

  • Iframe srcDoc not rendering: ensure the printable HTML is a full HTML document (doctype, head, meta charset). Some browsers block certain inline scripts—best to avoid inline scripts in srcDoc.


15. Next steps & extension ideas

  • Add template gallery with thumbnails and filters (ATS vs Visual).
  • Add resume parser (import old CVs) — use Rchilli/Sovren or build simple NER workflow.
  • Add AI suggestions (OpenAI) for summary and bullet generation — integrate as optional helper.
  • Add collaboration (CRDT or ProseMirror-based real-time editing).
  • Add version history and downloads per version.
  • Add internationalisation for multi-language templates.

Example: Minimal buildPrintableHtml and escapeHtml utils

// utils/escapeHtml.js
export function escapeHtml(str) {
  if (!str) return ''
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/\"/g, '&quot;')
    .replace(/'/g, '&#39;');
}
// utils/printableHtml.js
import { escapeHtml } from './escapeHtml'
export function buildPrintableHtml(cv) {
  const css = `@page{size:A4;margin:20mm}body{font-family:Inter,Arial...}`;
  const summary = cv.summary || '';
  const workHtml = (cv.work || []).map(w => `
    <article>
      <div style="display:flex;justify-content:space-between">
        <div><strong>${escapeHtml(w.title)}</strong><div>${escapeHtml(w.company)}</div></div>
        <div>${escapeHtml(w.period)}</div>
      </div>
      <ul>${(w.bullets || []).map(b => `<li>${escapeHtml(b)}</li>`).join('')}</ul>
    </article>
  `).join('');
  return `<!doctype html><html><head><meta charset="utf-8"><style>${css}</style></head><body>
    <header><h1>${escapeHtml(cv.personal.firstName)} ${escapeHtml(cv.personal.lastName)}</h1></header>
    <section>${summary}</section>
    <section>${workHtml}</section>
  </body></html>`;
}

Final notes

  • This component is intentionally modular: swap the templates, storage, and export backend independently.
  • For production, focus on sanitization (DOMPurify), server-side rendering parity (use same templates or same printable-html builder), and performance (worker pool for PDF generation).

If you'd like, I can:

  • generate a ready-to-drop CVEditorWireframe.jsx file (complete component) and TemplateClassic.jsx for you to copy, or
  • provide the Express + Puppeteer server file and a docker-compose example to run the PDF service, or
  • scaffold a small repo with the full stack (client + server export).

Which would you like me to produce next?