From ea23835e2a02f3f6708518f3fc50404e5e2199ae Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Mon, 8 Jun 2026 17:55:07 -0400 Subject: [PATCH] feat(resume): render resume as html --- biome.json | 3 +- external/resume | 2 +- scripts/copy-resume.js | 8 +- src/app/[locale]/resume/page.tsx | 19 ++++- src/components/resume/Resume.tsx | 72 ++++++++++++++---- src/components/resume/ResumeHtml.tsx | 107 +++++++++++++++++++++++++++ src/i18n/messages/en.json | 4 +- src/i18n/messages/fr.json | 4 +- 8 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 src/components/resume/ResumeHtml.tsx diff --git a/biome.json b/biome.json index 86c9555..345fd55 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,8 @@ "useIgnoreFile": true }, "files": { - "ignoreUnknown": false + "ignoreUnknown": false, + "includes": ["src/**", "scripts/**"] }, "formatter": { "enabled": true, diff --git a/external/resume b/external/resume index a7eb45b..1a781e0 160000 --- a/external/resume +++ b/external/resume @@ -1 +1 @@ -Subproject commit a7eb45beff967202c6e88c91ca42e3dd3f33f165 +Subproject commit 1a781e0566d0ac6f73d7e0d5d04c6b668aefc7d8 diff --git a/scripts/copy-resume.js b/scripts/copy-resume.js index 3937457..f99618a 100644 --- a/scripts/copy-resume.js +++ b/scripts/copy-resume.js @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); const srcDir = 'external/resume/out'; const destDir = 'public/resume'; @@ -7,10 +7,10 @@ const destDir = 'public/resume'; // Ensure destination directory exists fs.mkdirSync(destDir, { recursive: true }); -// Copy only PDF files +// Copy PDF and HTML files const files = fs.readdirSync(srcDir); for (const file of files) { - if (file.endsWith('.pdf')) { + if (file.endsWith('.pdf') || file.endsWith('.html')) { fs.copyFileSync(path.join(srcDir, file), path.join(destDir, file)); console.log(`Copied ${file}`); } diff --git a/src/app/[locale]/resume/page.tsx b/src/app/[locale]/resume/page.tsx index 9188419..34acb8a 100644 --- a/src/app/[locale]/resume/page.tsx +++ b/src/app/[locale]/resume/page.tsx @@ -1,3 +1,5 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; import Resume from '@/components/resume/Resume'; import StaticOutput from '@/components/StaticOutput'; import TerminalEmulator from '@/components/terminal/TerminalEmulator'; @@ -12,12 +14,27 @@ interface ResumePageProps { export const generateMetadata = async (routeData: RouteData) => await setPageMeta(routeData, 'resume'); +async function getResumeHtml() { + const publicDir = join(process.cwd(), 'public', 'resume'); + const [htmlEn, htmlFr] = await Promise.all([ + readFile(join(publicDir, 'Xavier-SALINIERE_resume.EN.html'), 'utf-8').catch( + () => '', + ), + readFile(join(publicDir, 'Xavier-SALINIERE_resume.FR.html'), 'utf-8').catch( + () => '', + ), + ]); + return { htmlEn, htmlFr }; +} + export default async function ResumePage({ params }: ResumePageProps) { await params; + const { htmlEn, htmlFr } = await getResumeHtml(); + return ( <> - + diff --git a/src/components/resume/Resume.tsx b/src/components/resume/Resume.tsx index c9ec901..b0bd27d 100644 --- a/src/components/resume/Resume.tsx +++ b/src/components/resume/Resume.tsx @@ -1,19 +1,57 @@ 'use client'; -import { Badge, Button, Typography } from '@nipsys/lsd'; +import { Badge, Button, ScrollArea, Typography } from '@nipsys/lsd'; import { useLocale, useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; +import { ResumeHtml } from './ResumeHtml'; const RESUME_PATHS = { - en: '/resume/Xavier-SALINIERE_resume.EN.pdf', - fr: '/resume/Xavier-SALINIERE_resume.FR.pdf', + en: { + pdf: '/resume/Xavier-SALINIERE_resume.EN.pdf', + html: '/resume/Xavier-SALINIERE_resume.EN.html', + }, + fr: { + pdf: '/resume/Xavier-SALINIERE_resume.FR.pdf', + html: '/resume/Xavier-SALINIERE_resume.FR.html', + }, }; -export default function Resume() { +interface ResumeProps { + htmlEn?: string; + htmlFr?: string; +} + +async function fetchHtml(locale: 'en' | 'fr'): Promise { + const response = await fetch(RESUME_PATHS[locale].html); + if (!response.ok) { + throw new Error(`Failed to fetch resume HTML: ${response.status}`); + } + return response.text(); +} + +export default function Resume({ htmlEn, htmlFr }: ResumeProps) { const locale = useLocale(); const t = useTranslations('Terminal.cmds.resume'); const currentLocale = locale === 'fr' ? 'fr' : 'en'; - const currentPdfPath = RESUME_PATHS[currentLocale]; + + const [html, setHtml] = useState(''); + const [error, setError] = useState(null); + + useEffect(() => { + const initialHtml = currentLocale === 'fr' ? htmlFr : htmlEn; + if (initialHtml) { + setHtml(initialHtml); + return; + } + + fetchHtml(currentLocale) + .then(setHtml) + .catch((err) => { + console.error('Failed to load resume:', err); + setError('Failed to load resume'); + }); + }, [currentLocale, htmlEn, htmlFr]); return (
@@ -32,24 +70,32 @@ export default function Resume() {
-
-