Skip to content

Commit 51409ab

Browse files
authored
feat: add PDF export and compact CV view (Issue #29) (#31)
* ci: enable PR builds and previews with static export defaults - Add pull_request trigger for main branch to run builds on PRs - Set NEXT_PUBLIC_EXPORT default to 'export' when variable is not defined - Remove check_var job that blocked workflow when variable was missing - Upgrade Node.js from 16 to 20 for Vercel compatibility - Add deploy_preview job for GitHub Pages preview on internal PRs - Add .nvmrc file and engines field in package.json for Node 20 Closes #29 * feat: add PDF export button for CV download - Add LogoPdf component with download icon - Integrate PDF export button alongside LinkedIn, GitHub, and Malt icons - Use html2pdf.js library for client-side PDF generation - Configure high-quality A4 PDF output - Add print-specific CSS styles for better PDF rendering - Add TypeScript type definitions for html2pdf.js Closes #29 * feat: improve PDF export with professional 2-column layout - Create dedicated PdfCvLayout component for optimized PDF rendering - Add pdfExportService for extracting CV data from DOM - Implement compact two-column design (sidebar + main content) - Limit content to fit on 1-2 A4 pages maximum - Show only 5 most recent jobs for concise presentation - Modern professional design with clean typography - French/English language support The PDF now generates a clean, professional-looking CV that fits on 1-2 pages with proper formatting for printing. * refactor: optimize PDF CV layout for single page - Switch to pixel-based layout for better control - Show only 3 most recent experiences with short descriptions - Filter technologies to show only main ones per experience - Simplify education section to 3 key items - Truncate about section to 120 characters - Add compact clients section - Add experience summary block mentioning +20 years This should fit the CV on 1-2 A4 pages maximum. * fix: improve PDF data extraction and revert debug mode - Fix pdfExportService to correctly extract job data from DOM - Extract client name, role, dates, description from job.tsx structure - Properly parse contact info from #contact section - Revert debug mode (hidden container, shorter delay) The PDF now shows correct experience data with proper formatting. * feat: replace PDF export with toggle CV mode (full/compact) - Add toggle button to switch between full CV and compact 1-page version - Compact mode uses same design system (colors, fonts) as main site - Hide main header in compact mode (compact CV has its own header) - Hide toggle on mobile (only print button available) - Remove html2pdf.js dependency and related files - Use native browser print for PDF export - Reduce skill/framework tag sizes on mobile with no text wrapping New components: - CvModeContext: React context for CV display mode - CvModeToggle: Toggle button component - CompactCvLayout: Professional 1-page CV layout - CvContent: Wrapper for full/compact mode switching - HeaderContent: Conditionally hidden header - logoPrint: Print button component Removed: - html2pdf.js dependency - PdfCvLayout, pdfExportService, logoPdf components - types/html2pdf.d.ts * chore: shorten toggle button label * feat: add URL routing for compact CV and fix print behavior - Add /fr/short and /en/short routes for direct access to compact CV - Replace React state toggle with URL-based navigation - Fix mobile print: shows compact version instead of full version - Fix desktop print: shows full version with header - Add PrintCompactVersion component for mobile print rendering - Add ShortPageWrapper for short page navigation Routes: - /fr, /en → Full CV version - /fr/short, /en/short → Compact CV version Print behavior: - Mobile: prints compact version (1 page) - Desktop: prints full version with header Removed: - CvModeContext (replaced by URL routing) - CvContent (no longer needed) * feat: improve print logic and compact CV layout Print behavior: - Print what you see: full page prints full, short page prints short - Remove screen-size-based print logic that was confusing - Remove PrintCompactVersion component (no longer needed) Toggle visibility: - Show toggle button on all screen sizes (not just desktop) - Users can now switch between full/short on mobile too Compact CV layout improvements: - Larger header with more vertical padding (py-8/py-12) - Bigger header text (text-6xl/text-7xl for name) - Increased section spacing (gap-8/gap-10) - Larger section titles (text-2xl) - Better visual hierarchy with border opacity Simplified main page: - Removed compact data query and PrintCompactVersion - Cleaner code structure * fix: make toggle button icon-only on mobile * fix: make ShortPageWrapper toggle button consistent with CvModeToggle on mobile * fix: unify header structure and button sizes between full and short versions - Make toggle button icon same size as other buttons (h-5 w-5) - Move header (name + role) to ShortPageWrapper using HeaderContent - Remove duplicate header from CompactCvLayout - Both versions now have identical header structure: - Nav bar with locale switcher and icons - HeaderContent with name and role - Content area This ensures consistent UX between full and short CV views. * feat: move Profile section to full-width above two columns * feat: optimize compact CV layout for space efficiency - Domains: show only titles inline (Agile · Dev · Ops) without tags - Move Domains above Skills in left column - Experience: remove role line, keep client/location/dates/description/tech - Education: move to right column, inline format showing all studies - Reduced spacing throughout for better print fit This makes the compact CV much more condensed while keeping all key info. * feat: move Education back to left column for better balance The left column was too short after removing tags from Domains. Moving Education back to the left column creates better visual balance between the two columns. * feat: redesign compact CV with full domains, complete education - Domains displayed like full CV (title + description + tags) - Skills with simpler border style (no gradient) - Complete education with all studies and dates (sorted descending) - Better print optimization for A4 * feat: simplify compact CV - remove domain tags, education end year only - Remove competency tags under domain descriptions - Education shows only title and end year (not full date range) * feat: increase section titles and spacing in compact CV - Bigger section titles (text-xl/text-2xl instead of text-lg) - More horizontal spacing between main sections (Profil, Domains, columns) - Better visual hierarchy * feat: match skills badge style between full and compact CV Skills badges now use border style (no gradient) like in compact CV. Added hover effect for better interactivity. Same text size preserved. * feat: skills badges with auto-width, left-aligned, inline layout - Remove md:flex-col to keep flex-wrap on all screen sizes - Remove text-center from skill badges - Badges now adapt to text content width - Left-aligned layout with natural wrapping * feat: use DatoCMS titles in compact CV, show 5 jobs - Fetch section titles from DatoCMS (about, skills, contact, studies, jobs) - Use contact field labels from DatoCMS (phoneTitle, emailTitle, locationTitle) - Display 5 jobs instead of 4 for better page fill - Keep fallback labels if DatoCMS titles are empty * fix: harmonize colors between full and short CV - Job dates: text-sky-300 (was text-sky-300/70) - Job frameworks: bg-fuchsia-200 (was bg-fuchsia-300/80) - Remove text-gray-300 from descriptions (use default like full CV) - Add skill link field to short CV query * fix: align toggle button height with other header buttons Remove md:py-1.5 to keep consistent p-2 padding like LinkedIn/Print buttons * feat: harmonize Profile and Domains sections between full/short views - Use same spacing (mt-10) between sections - Use same title styles (text-2xl, solid border-b) - Use same text spacing (mt-4 for descriptions) - Add mt-10 before two-column layout for consistency * fix: unify all section title sizes to text-2xl All section titles in CompactCvLayout now use text-2xl to match full CV: - Contact, Skills, Education, Experience titles updated - Removed semi-transparent borders for consistency with full CV * refactor: reuse Skill and Domain components in compact view Hybrid approach for component reuse: - Skill component: add compact prop for print-optimized styling - Domain component: add showTags and compact props - CompactCvLayout now imports and uses these shared components - Update data structure to pass raw domain data with competencies - Maintain visual consistency between full and short views * refactor: extract Display components and add dynamic PDF titles - Create ContactDisplay, JobDisplay, StudyDisplay presentational components - Update contact.tsx, job.tsx, study.tsx to use new Display components - Refactor CompactCvLayout to reuse Display components with compact prop - Add dynamic document titles for PDF export (developer style: lowercase with underscores) - Increase spacing between domains and two-column layout in short view - Remove unused frameworks.tsx.backup file * chore: add .playwright-mcp to gitignore * chore: remove tracked Playwright screenshots * fix: resolve TypeScript errors in domain.tsx and date.ts * chore: trigger Vercel redeploy * fix: pin Node.js version to 20.x for Vercel compatibility * chore: disable vercel deployment checks Disable performance/reliability checks that exceed hobby plan limit. The site works correctly, this is just a quota limitation. * revert: remove invalid vercel.json
1 parent a67899f commit 51409ab

26 files changed

+1001
-182
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ next-env.d.ts
4040
.vscode
4141

4242
#others
43-
backups
43+
backups
44+
45+
# playwright
46+
.playwright-mcp/

app/[lang]/contact.tsx

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getDataWithLocal } from '@/lib/graphql-client';
22
import { gql } from 'graphql-request';
33
import { Locale } from 'i18n-config';
44
import React from 'react';
5+
import ContactDisplay from '@/components/ContactDisplay';
56

67
const query = gql`
78
query getContact($lang: SiteLocale) {
@@ -17,31 +18,24 @@ const query = gql`
1718
}
1819
`;
1920

20-
export default async function contact(locale: Locale) {
21+
export default async function Contact(locale: Locale) {
2122
const data: any = await getDataWithLocal(locale, query);
23+
const contactData = {
24+
title: data?.contact?.title,
25+
phoneTitle: data?.contact?.phoneTitle || '',
26+
phone: data?.contact?.phone || '',
27+
emailTitle: data?.contact?.emailTitle || '',
28+
email: data?.contact?.email || '',
29+
locationTitle: data?.contact?.locationTitle || '',
30+
location: data?.contact?.location || '',
31+
};
32+
2233
return (
23-
<section id="contact" className="mt-10 ">
34+
<section id="contact" className="mt-10">
2435
<h2 className="border-b pb-1 text-2xl font-semibold text-pink-300">
25-
Contact
36+
{contactData.title || 'Contact'}
2637
</h2>
27-
<ul className="mb-10 mr-1 mt-4">
28-
<li className="mt-1 text-pink-200">
29-
<strong>{data?.contact?.phoneTitle}</strong>
30-
<a href={`tel:${data?.contact?.phone}`} className="block">
31-
{data?.contact?.phone}
32-
</a>
33-
</li>
34-
<li className="mt-1 text-pink-200">
35-
<strong>{data?.contact?.emailTitle}</strong>
36-
<a href={`mailto:${data?.contact?.email}`} className="block">
37-
{data?.contact?.email}
38-
</a>
39-
</li>
40-
<li className="mt-1 text-pink-200">
41-
<strong>{data?.contact?.locationTitle}</strong>
42-
<span className="block">{data?.contact?.location}</span>
43-
</li>
44-
</ul>
38+
<ContactDisplay contact={contactData} />
4539
</section>
4640
);
4741
}

app/[lang]/frameworks.tsx.backup

Lines changed: 0 additions & 34 deletions
This file was deleted.

app/[lang]/header.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import LocaleSwitcher from '@/components/locale-switcher';
22
import Logos from '@/components/logos';
3+
import HeaderContent from '@/components/HeaderContent';
34
import { getDataWithLocal } from '@/lib/graphql-client';
45
import { gql } from 'graphql-request';
56
import { i18n, Locale } from 'i18n-config';
@@ -18,22 +19,13 @@ const query = gql`
1819
export default async function Header(locale: Locale) {
1920
const data: any = await getDataWithLocal(locale, query);
2021
return (
21-
<header>
22-
<div className="flex flex-row justify-between">
22+
<header className="print:mb-2">
23+
<div className="flex flex-row justify-between print:hidden">
2324
<LocaleSwitcher lang={locale} />
2425
<Logos />
2526
</div>
2627

27-
<div className="flex justify-between py-14 md:py-20">
28-
<div className="grid justify-items-end">
29-
<h1 className="text-4xl font-extrabold text-blue-600 md:text-5xl lg:text-7xl">
30-
{data?.header?.name}
31-
</h1>
32-
<p className="mt-5 text-2xl text-teal-300 md:text-3xl">
33-
{data?.header?.role}
34-
</p>
35-
</div>
36-
</div>
28+
<HeaderContent name={data?.header?.name} role={data?.header?.role} />
3729
</header>
3830
);
3931
}

app/[lang]/jobs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export default async function jobs(locale: Locale) {
4141
{data?.jobsTitle?.title}
4242
</h2>
4343
<ul className="mt-4">
44-
{data?.allJobsModels?.map((job: any) => (
45-
<li key={job.id} className="py-4">
44+
{data?.allJobsModels?.map((job: any, index: number) => (
45+
<li key={job.client + index} className="py-4">
4646
<Job job={job} />
4747
</li>
4848
))}

app/[lang]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default async function RootLayout({
2020
return (
2121
<html lang="fr">
2222
<body>
23-
<div className="container mx-auto min-h-screen p-8">
23+
<div className="container mx-auto min-h-screen p-8 print:p-4">
2424
<main>{children}</main>
2525
</div>
2626
{enableAnalitycs ? <Analytics /> : ''}

app/[lang]/page.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import '../../styles/globals.css';
22

3-
import { setTimeout } from 'timers/promises';
43
import About from '@/app/[lang]/about';
54
import Headers from '@/app/[lang]/header';
65
import { Locale } from '../../i18n-config';
@@ -12,38 +11,71 @@ import Learnings from './learnings';
1211
import Hobbies from './hobbies';
1312
import Jobs from './jobs';
1413
import Projects from './projects';
14+
import { getDataWithLocal } from '@/lib/graphql-client';
15+
import { gql } from 'graphql-request';
16+
import type { Metadata } from 'next';
1517

16-
const waitFunction = async () => {
17-
await setTimeout(2000);
18-
};
18+
// Query to fetch header for metadata
19+
const headerQuery = gql`
20+
query getHeader($lang: SiteLocale) {
21+
header(locale: $lang) {
22+
name
23+
}
24+
}
25+
`;
1926

20-
export default function Page({
27+
// Helper to generate document title (developer style: lowercase with underscores)
28+
function generateDocumentTitle(name: string, lang: string, mode: 'full' | 'short'): string {
29+
const prefix = lang === 'fr' ? 'cv' : 'resume';
30+
const modeLabel = lang === 'fr'
31+
? (mode === 'full' ? 'complet' : 'court')
32+
: mode;
33+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
34+
const safeName = name.toLowerCase().replace(/\s+/g, '_');
35+
return `${prefix}_${safeName}_${modeLabel}_${date}`;
36+
}
37+
38+
// Generate dynamic metadata for PDF title
39+
export async function generateMetadata({
40+
params: { lang },
41+
}: {
42+
params: { lang: Locale };
43+
}): Promise<Metadata> {
44+
const data: any = await getDataWithLocal({ locale: lang } as any, headerQuery);
45+
const name = data?.header?.name || 'CV';
46+
47+
return {
48+
title: generateDocumentTitle(name, lang, 'full'),
49+
};
50+
}
51+
52+
export default async function Page({
2153
params: { lang },
2254
}: {
2355
params: { lang: Locale };
2456
}) {
25-
console.debug('new lang clicked : ' + lang);
2657
return (
2758
<>
2859
{/* @ts-expect-error Server Component */}
2960
<Headers locale={lang} />
61+
3062
{/* @ts-expect-error Server Component */}
3163
<About locale={lang} />
3264
{/* @ts-expect-error Server Component */}
3365
<Domains locale={lang} />
3466

35-
<div className="mt-10 flex columns-1 flex-col md:columns-2 md:flex-row">
36-
<div id="left" className="order-last md:order-first md:w-1/3 md:pr-10">
67+
<div className="mt-10 flex columns-1 flex-col md:columns-2 md:flex-row print:mt-4 print:flex-row">
68+
<div id="left" className="order-last md:order-first md:w-1/3 md:pr-10 print:order-first print:w-1/3 print:pr-4">
3769
{/* @ts-expect-error Server Component */}
3870
<Contact locale={lang} />
3971
{/* @ts-expect-error Server Component */}
4072
<Skills locale={lang} />
4173
{/* @ts-expect-error Server Component */}
4274
<Learnings locale={lang} />
43-
{/* @ts-expect-error Server Compone nt */}
75+
{/* @ts-expect-error Server Component */}
4476
<Hobbies locale={lang} />
4577
</div>
46-
<div id="main" className="md:w-2/3 md:pr-10">
78+
<div id="main" className="md:w-2/3 md:pr-10 print:w-2/3 print:pr-4">
4779
{/* @ts-expect-error Server Component */}
4880
<Jobs locale={lang} />
4981
{/* @ts-expect-error Server Component */}

0 commit comments

Comments
 (0)