From d6b4c4bcd4c7fd1b26b6d7f868e989de19bff285 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Wed, 1 Jul 2026 11:41:51 +0800 Subject: [PATCH 1/5] chore: update resume submodule --- external/resume | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/resume b/external/resume index 1a781e0..40bdfde 160000 --- a/external/resume +++ b/external/resume @@ -1 +1 @@ -Subproject commit 1a781e0566d0ac6f73d7e0d5d04c6b668aefc7d8 +Subproject commit 40bdfde2d2461bb0d530d84c32cc1a7531d7b344 From 6640dacb610cd62ccb13793083d389587f2c75b1 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Wed, 1 Jul 2026 11:44:33 +0800 Subject: [PATCH 2/5] feat: display pdf instead of html, add credit subtitle --- scripts/copy-resume.js | 4 +- src/components/cmd-outputs/ResumeOutput.tsx | 223 +++++--------------- src/i18n/messages/en.json | 9 +- src/i18n/messages/fr.json | 9 +- 4 files changed, 75 insertions(+), 170 deletions(-) diff --git a/scripts/copy-resume.js b/scripts/copy-resume.js index f99618a..d22349b 100644 --- a/scripts/copy-resume.js +++ b/scripts/copy-resume.js @@ -7,10 +7,10 @@ const destDir = 'public/resume'; // Ensure destination directory exists fs.mkdirSync(destDir, { recursive: true }); -// Copy PDF and HTML files +// Copy PDF files const files = fs.readdirSync(srcDir); for (const file of files) { - if (file.endsWith('.pdf') || file.endsWith('.html')) { + if (file.endsWith('.pdf')) { fs.copyFileSync(path.join(srcDir, file), path.join(destDir, file)); console.log(`Copied ${file}`); } diff --git a/src/components/cmd-outputs/ResumeOutput.tsx b/src/components/cmd-outputs/ResumeOutput.tsx index 2766af2..0d7c9be 100644 --- a/src/components/cmd-outputs/ResumeOutput.tsx +++ b/src/components/cmd-outputs/ResumeOutput.tsx @@ -1,163 +1,20 @@ 'use client'; -import { useStore } from '@nanostores/react'; -import { Badge, Button, ScrollArea, Typography } from '@nipsys/lsd'; +import { Badge, Button, Typography } from '@nipsys/lsd'; import { useLocale, useTranslations } from 'next-intl'; -import { useEffect, useRef, useState } from 'react'; -import { $isDarkMode } from '@/stores/theme-store'; +import Link from 'next/link'; const RESUME_PATHS = { - 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', - }, + en: '/resume/Xavier-SALINIERE_resume.EN.pdf', + fr: '/resume/Xavier-SALINIERE_resume.FR.pdf', }; -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(); -} - -function extractContent(html: string): { - body: string; - style: string; - script: string; -} { - const styleMatch = html.match(/]*>([\s\S]*?)<\/style>/i); - const scriptMatch = html.match( - /]*type="module"[^>]*>([\s\S]*?)<\/script>/i, - ); - const bodyMatch = html.match(/]*>([\s\S]*?)<\/body>/i); - - return { - style: styleMatch?.[1] || '', - script: scriptMatch?.[1] || '', - body: bodyMatch?.[1] || '', - }; -} - -function ResumeHtml({ htmlContent }: { htmlContent: string }) { - const containerRef = useRef(null); - const shadowRef = useRef(null); - const isDarkMode = useStore($isDarkMode); - const [initialized, setInitialized] = useState(false); - - useEffect(() => { - if (!containerRef.current) return; - - if (!shadowRef.current) { - shadowRef.current = containerRef.current.attachShadow({ mode: 'open' }); - } - const shadow = shadowRef.current; - - // Clear previous content before re-rendering - shadow.innerHTML = ''; - - const { style, body, script } = extractContent(htmlContent); - - const hostStyles = ` - :host { - display: block; - width: 100%; - min-height: 100%; - } - :host(.dark) { - --color-background: var(--color-background-dark, #191e23); - --color-dimmed: var(--color-dimmed-dark, #23282d); - --color-primary: var(--color-primary-dark, #fbfbfc); - --color-secondary: var(--color-secondary-dark, #ccd0d4); - --color-accent: var(--color-accent-dark, #00a0d2); - } - :host(.light) { - --color-background: var(--color-background-light, #ffffff); - --color-dimmed: var(--color-dimmed-light, #f3f4f5); - --color-primary: var(--color-primary-light, #191e23); - --color-secondary: var(--color-secondary-light, #6c7781); - --color-accent: var(--color-accent-light, #0073aa); - } - .resume-wrapper { - background: var(--color-background); - padding: 1.5rem; - min-height: 100%; - } - `; - - const styleEl = document.createElement('style'); - styleEl.textContent = hostStyles + style; - shadow.appendChild(styleEl); - - const wrapper = document.createElement('div'); - wrapper.className = 'resume-wrapper'; - wrapper.innerHTML = body; - shadow.appendChild(wrapper); - - if (script) { - // SECURITY: Only allow trusted scripts from bundled resume HTML assets. - // The script content comes from our own /public/resume/*.html files, - // not from user input. This is safe because we control the source. - const wrappedScript = ` - const originalDefine = customElements.define.bind(customElements); - customElements.define = (name, constructor, options) => { - if (!customElements.get(name)) { - originalDefine(name, constructor, options); - } - }; - ${script} - `; - const scriptEl = document.createElement('script'); - scriptEl.type = 'module'; - scriptEl.textContent = wrappedScript; - shadow.appendChild(scriptEl); - } - - setInitialized(true); - - // Cleanup: Clear shadow root content on unmount to prevent memory leak - return () => { - if (shadowRef.current) { - shadowRef.current.innerHTML = ''; - } - }; - }, [htmlContent]); - - useEffect(() => { - if (!containerRef.current || !initialized) return; - - const host = containerRef.current; - if (isDarkMode) { - host.classList.add('dark'); - host.classList.remove('light'); - } else { - host.classList.add('light'); - host.classList.remove('dark'); - } - }, [isDarkMode, initialized]); - - return
; -} - export default function ResumeOutput() { const locale = useLocale(); const t = useTranslations('Resume'); - const [html, setHtml] = useState(''); - const [error, setError] = useState(null); - - useEffect(() => { - fetchHtml(locale as 'en' | 'fr') - .then(setHtml) - .catch((err) => { - console.error('Failed to load resume:', err); - setError('Failed to load resume'); - }); - }, [locale]); + const currentLocale = locale === 'fr' ? 'fr' : 'en'; + const currentPdfPath = RESUME_PATHS[currentLocale]; return (
@@ -170,7 +27,7 @@ export default function ResumeOutput() {
- {t('viewing')} {locale.toUpperCase()} {t('version')} + {t('viewing')} {currentLocale.toUpperCase()} {t('version')}
@@ -181,12 +38,17 @@ export default function ResumeOutput() { asChild data-prevent-terminal-focus > - + {t('downloadEN')} - @@ -197,21 +59,50 @@ export default function ResumeOutput() { className="w-full max-w-6xl h-[600px] border border-(--lsd-color-border) rounded-(--lsd-shape-sm) overflow-hidden" data-prevent-terminal-focus > - - {error ? ( -
- {error} -
- ) : html ? ( - - ) : ( -
- Loading... -
- )} -
+