{% extends 'application/whileresume/website/cv-public/base.html.twig' %}{% set show_loader = true %}{% block title %}{{ 'generate2.theme.page_title'|trans({}, 'whr-public') }} · WhileResume{% endblock %}{% block extra_css %}.theme-grid {display: grid;grid-template-columns: 1fr 1fr;gap: 14px;margin-bottom: 8px;}.theme-card {position: relative;padding: 24px 22px;border: 2px solid var(--ink-10);border-radius: var(--r-lg);cursor: pointer;transition: all 0.3s var(--ease);background: var(--white);}.theme-card:hover { border-color: var(--ink-20); transform: translateY(-2px); }.theme-card.selected {border-color: var(--violet-deep);background: var(--violet-ultra);box-shadow: 0 8px 28px var(--violet-glow);}.theme-card-ico {width: 44px; height: 44px;border-radius: var(--r-sm);background: var(--violet-soft);color: var(--violet-dark);display: flex;align-items: center;justify-content: center;margin-bottom: 14px;transition: all 0.25s;}.theme-card.selected .theme-card-ico { background: var(--violet-deep); color: white; }.theme-card-title { font-size: 1rem; font-weight: 600; margin-bottom: 4px; }.theme-card-desc { font-size: 0.82rem; color: var(--ink-60); line-height: 1.5; }.theme-check {position: absolute;top: 14px; right: 14px;width: 22px; height: 22px;border-radius: 50%;background: var(--violet-deep);color: white;display: flex;align-items: center;justify-content: center;opacity: 0;transform: scale(0.5);transition: all 0.3s var(--ease-spring);}.theme-card.selected .theme-check { opacity: 1; transform: scale(1); }/* ═══════════════════════════════════COULEURS EN CERCLES═══════════════════════════════════ */.colors-grid {display: grid;grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));gap: 14px;}.color-wrap {display: flex;flex-direction: column;align-items: center;gap: 6px;}.color-swatch {width: 52px;height: 52px;border-radius: 50%;cursor: pointer;position: relative;border: 3px solid transparent;box-shadow: 0 2px 8px rgba(0,0,0,0.08);transition: all 0.3s var(--ease-spring);display: flex;align-items: center;justify-content: center;}.color-swatch:hover {transform: scale(1.12);box-shadow: 0 6px 16px rgba(0,0,0,0.15);}.color-swatch.selected {transform: scale(1.15);border-color: var(--white);box-shadow: 0 0 0 3px var(--ink), 0 6px 18px rgba(0,0,0,0.2);}.color-swatch .check {opacity: 0;transform: scale(0.4);transition: all 0.25s var(--ease-spring);color: white;}.color-swatch.selected .check { opacity: 1; transform: scale(1); }.color-label {font-size: 0.72rem;color: var(--ink-60);font-weight: 500;text-align: center;}.color-swatch.selected + .color-label { color: var(--ink); font-weight: 600; }/* ═══════════════════════════════════LANGUES EN BOUTONS PILL═══════════════════════════════════ */.lang-grid {display: flex;flex-wrap: wrap;gap: 8px;}.lang-btn {padding: 10px 18px;border: 1.5px solid var(--ink-10);background: var(--white);border-radius: var(--r-full);font-family: var(--font-body);font-size: 0.86rem;font-weight: 500;color: var(--ink-60);cursor: pointer;transition: all 0.2s var(--ease);user-select: none;}.lang-btn:hover {border-color: var(--ink-20);color: var(--ink-80);transform: translateY(-1px);}.lang-btn.selected {background: var(--violet-deep);border-color: var(--violet-deep);color: white;box-shadow: 0 4px 14px var(--violet-glow);}.lang-btn.selected:hover {background: var(--violet-dark);transform: translateY(-1px);}{% endblock %}{% block body %}<div class="fade-up"><div class="badge"><span class="badge-num">2</span> {{ 'generate2.theme.badge'|trans({}, 'whr-public') }}</div><h1 class="stitle">{{ 'generate2.theme.title_part1'|trans({}, 'whr-public') }}<br><em>{{ 'generate2.theme.title_part2'|trans({}, 'whr-public') }}</em></h1><p class="sdesc">{{ 'generate2.theme.subtitle'|trans({}, 'whr-public') }}</p></div><div class="fade-up d1"><div class="section-label">{{ 'generate2.theme.section_template'|trans({}, 'whr-public') }}</div><div class="theme-grid"><div class="theme-card" data-theme="simple"><div class="theme-card-ico"><svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg></div><div class="theme-card-title">{{ 'generate2.theme.simple_title'|trans({}, 'whr-public') }}</div><div class="theme-card-desc">{{ 'generate2.theme.simple_desc'|trans({}, 'whr-public') }}</div><div class="theme-check"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div></div><div class="theme-card" data-theme="compact"><div class="theme-card-ico"><svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg></div><div class="theme-card-title">{{ 'generate2.theme.compact_title'|trans({}, 'whr-public') }}</div><div class="theme-card-desc">{{ 'generate2.theme.compact_desc'|trans({}, 'whr-public') }}</div><div class="theme-check"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div></div></div></div><div class="fade-up d2"><div class="section-label">{{ 'generate2.theme.section_color'|trans({}, 'whr-public') }}</div><div class="colors-grid" id="colorsGrid"></div></div><div class="fade-up d3"><div class="section-label">{{ 'generate2.theme.section_language'|trans({}, 'whr-public') }}</div><div class="lang-grid" id="langGrid"></div></div><div class="cta-row fade-up d4"><a href="{{ path('cv_public_choice', { slug: public_slug }) }}" class="btn-ghost"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>{{ 'generate2.common.back'|trans({}, 'whr-public') }}</a><button class="btn-primary" id="btnContinue" onclick="continueToForm()">{{ 'generate2.common.continue'|trans({}, 'whr-public') }}<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg></button></div>{% endblock %}{% block body_js %}<script>const COLORS = [{ id: 'bleu_ciel', label: 'Bleu ciel', primary: '#00AEEF', secondary: '#F59E0B' },{ id: 'charbon', label: 'Charbon', primary: '#374151', secondary: '#9CA3AF' },{ id: 'emeraude', label: 'Émeraude', primary: '#10B981', secondary: '#F59E0B' },{ id: 'navy', label: 'Navy', primary: '#1E3A5F', secondary: '#3B82F6' },{ id: 'or', label: 'Or', primary: '#B8860B', secondary: '#F59E0B' },{ id: 'rose', label: 'Rose', primary: '#EC4899', secondary: '#F9A8D4' },{ id: 'rouge', label: 'Rouge', primary: '#DC2626', secondary: '#F87171' },{ id: 'corail', label: 'Corail', primary: '#F97316', secondary: '#FED7AA' },{ id: 'vert', label: 'Vert', primary: '#16A34A', secondary: '#86EFAC' },{ id: 'bleu', label: 'Bleu', primary: '#2563EB', secondary: '#93C5FD' },{ id: 'violet', label: 'Violet', primary: '#7C3AED', secondary: '#C4B5FD' },];const CV_LANGUAGES = [{ code: 'fr', label: 'Français' }, { code: 'en', label: 'English' },{ code: 'de', label: 'Deutsch' }, { code: 'es', label: 'Español' },{ code: 'it', label: 'Italiano' }, { code: 'pt', label: 'Português' },{ code: 'nl', label: 'Nederlands' }, { code: 'pl', label: 'Polski' },{ code: 'ro', label: 'Română' }, { code: 'cs', label: 'Čeština' },{ code: 'sk', label: 'Slovenčina' }, { code: 'hu', label: 'Magyar' },{ code: 'sv', label: 'Svenska' }, { code: 'da', label: 'Dansk' },{ code: 'fi', label: 'Suomi' }, { code: 'nb', label: 'Norsk' },{ code: 'el', label: 'Ελληνικά' }, { code: 'bg', label: 'Български' },{ code: 'hr', label: 'Hrvatski' }, { code: 'sl', label: 'Slovenščina' },{ code: 'lt', label: 'Lietuvių' }, { code: 'lv', label: 'Latviešu' },{ code: 'et', label: 'Eesti' }, { code: 'uk', label: 'Українська' },{ code: 'tr', label: 'Türkçe' },];// État local synchronisé avec l'API via save-draftconst state = {theme: 'simple',primary: COLORS[0].primary,secondary: COLORS[0].secondary,cv_language: window.__CV_CTX__.locale || 'fr',data: null,};function renderColors() {const grid = document.getElementById('colorsGrid');grid.innerHTML = COLORS.map(c => `<div class="color-wrap"><div class="color-swatch ${state.primary === c.primary ? 'selected' : ''}"style="background:${c.primary}"data-color-id="${c.id}"data-primary="${c.primary}"data-secondary="${c.secondary}"onclick="selectColor(this)"><svg class="check" width="20" height="20" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><span class="color-label">${c.label}</span></div>`).join('');}function renderLangs() {const grid = document.getElementById('langGrid');grid.innerHTML = CV_LANGUAGES.map(l => `<button class="lang-btn ${state.cv_language === l.code ? 'selected' : ''}"data-lang="${l.code}"onclick="selectLang('${l.code}')">${l.label}</button>`).join('');}function renderTheme() {document.querySelectorAll('.theme-card').forEach(c =>c.classList.toggle('selected', c.dataset.theme === state.theme));}window.selectColor = (el) => {state.primary = el.dataset.primary;state.secondary = el.dataset.secondary;document.querySelectorAll('.color-swatch').forEach(s =>s.classList.toggle('selected', s === el));scheduleSave();};window.selectLang = (code) => {state.cv_language = code;document.querySelectorAll('.lang-btn').forEach(b =>b.classList.toggle('selected', b.dataset.lang === code));scheduleSave();};document.querySelectorAll('.theme-card').forEach(card => {card.addEventListener('click', () => {state.theme = card.dataset.theme;renderTheme();scheduleSave();});});// ═══════════════════════════════════// CHARGEMENT INITIAL (GET /data)// ═══════════════════════════════════async function loadExisting() {try {const res = await fetch(window.__CV_CTX__.apiBase + '/data' + window.publicQuery(), {headers: { 'Content-Type': 'application/json' }});if (!res.ok) {console.warn('[CV] GET /data a retourné HTTP ' + res.status);return;}const ct = (res.headers.get('content-type') || '').toLowerCase();if (!ct.includes('application/json')) {console.error('[CV] /data a renvoyé du non-JSON. Vérifiez le préfixe API (' + window.__CV_CTX__.apiBase + ').');return;}const json = await res.json();if (json.settings) {state.theme = json.settings.theme || state.theme;state.primary = json.settings.primary || state.primary;state.secondary = json.settings.secondary || state.secondary;state.cv_language = json.settings.cv_language || state.cv_language;}if (json.data) state.data = json.data;} catch (e) {console.error('[CV] Erreur chargement données :', e);}}// ═══════════════════════════════════// AUTOSAVE//// Stratégie identique à form.html.twig :// - debounce 2s sur chaque sélection (thème, couleur, langue)// - flush via fetch({keepalive:true}) sur beforeunload / visibilitychange// - flush au clic "Continuer" (déjà fait dans continueToForm)// Pas de flush sur blur ici : la page theme n'a pas de champ texte, tous// les contrôles sont des clics donc déjà capturés par le debounce court.// ═══════════════════════════════════let saveTimer = null, saveInFlight = false, dirty = false;function buildSavePayload() {return {data: state.data || { first_name: '', last_name: '', email: window.__CV_CTX__.user.email || '' },theme: state.theme,primary: state.primary,secondary: state.secondary,cv_language: state.cv_language,};}async function scheduleSave() {dirty = true;window.setSaveStatus('saving');if (saveTimer) clearTimeout(saveTimer);saveTimer = setTimeout(saveNow, 2000);}async function saveNow() {if (saveInFlight) return;if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }saveInFlight = true;try {const res = await fetch(window.__CV_CTX__.apiBase + '/save-draft', {method: 'POST',headers: window.authHeaders(),body: JSON.stringify(window.publicBody(buildSavePayload())),});if (!res.ok) throw new Error();dirty = false;window.setSaveStatus('saved');} catch (e) {window.setSaveStatus('error');} finally {saveInFlight = false;}}// Flush beforeunload : survit à la fermeture de l'onglet grâce à keepalivefunction installBeaconFlush() {const beaconSave = () => {if (!dirty || saveInFlight) return;try {fetch(window.__CV_CTX__.apiBase + '/save-draft', {method: 'POST',headers: window.authHeaders(),body: JSON.stringify(window.publicBody(buildSavePayload())),keepalive: true,});dirty = false;} catch (e) { /* best effort */ }};window.addEventListener('beforeunload', beaconSave);document.addEventListener('visibilitychange', () => {if (document.visibilityState === 'hidden') beaconSave();});}// ═══════════════════════════════════// CONTINUER// ═══════════════════════════════════window.continueToForm = async () => {const btn = document.getElementById('btnContinue');btn.disabled = true;if (saveTimer) { clearTimeout(saveTimer); await saveNow(); }const params = new URLSearchParams(window.location.search);const mode = params.get('mode') || 'scratch';window.location.href = window.__CV_CTX__.routes.form + '?mode=' + mode;};// ═══════════════════════════════════// INIT// ═══════════════════════════════════(async () => {const params = new URLSearchParams(window.location.search);const mode = params.get('mode') || 'scratch';const i18n = window.__CV_CTX__.i18n;if (mode === 'analyze') {window.cvLoader.show(i18n.loaderAnalyze, i18n.loaderAnalyzeSub);} else {window.cvLoader.show(i18n.loaderPrefs, i18n.loaderPrefsSub);}await loadExisting();renderTheme();renderColors();renderLangs();installBeaconFlush();window.cvLoader.hide();})();</script>{% endblock %}