642 lines
24 KiB
Plaintext
642 lines
24 KiB
Plaintext
---
|
||
import { getCVById, getProfileById } from "../../lib/db";
|
||
import { getLang, useT } from "../../lib/i18n";
|
||
import { TEMPLATES } from "../../types";
|
||
import Template1 from "../../components/templates/Template1.astro";
|
||
import Template2 from "../../components/templates/Template2.astro";
|
||
import Template3 from "../../components/templates/Template3.astro";
|
||
import Template4 from "../../components/templates/Template4.astro";
|
||
|
||
const { id } = Astro.params;
|
||
const user = Astro.locals.user;
|
||
const lang = getLang(Astro.request);
|
||
const t = useT(lang);
|
||
|
||
const cv = getCVById(id!, user.id);
|
||
if (!cv) return Astro.redirect("/dashboard");
|
||
|
||
const profile = getProfileById(cv.profile_id, user.id);
|
||
if (!profile) return Astro.redirect("/dashboard");
|
||
|
||
const d = profile.data;
|
||
const s = cv.settings;
|
||
const de = lang === "de";
|
||
|
||
function j(v: any) {
|
||
return JSON.stringify(v).replace(/</g, "\\u003c");
|
||
}
|
||
---
|
||
|
||
<!doctype html>
|
||
<html lang={lang}>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{cv.title} – {t("app.name")}</title>
|
||
<link rel="stylesheet" href="/styles/global.css" />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Lora:ital,wght@0,400;0,600;1,400&family=Merriweather:wght@400;700&family=Montserrat:wght@300;400;600;700&family=Outfit:wght@300;400;600&family=Oswald:wght@400;600&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Roboto:wght@300;400;700&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
</head>
|
||
<body>
|
||
<header class="app-header">
|
||
<div class="flex items-center gap-3">
|
||
<a href="/dashboard" class="logo" style="font-size:.95rem"
|
||
>Lebenslauf<span>App</span></a
|
||
>
|
||
<span style="color:rgba(255,255,255,.3)">›</span>
|
||
<span
|
||
id="cv-title-display"
|
||
style="color:rgba(255,255,255,.9);font-size:.9rem">{cv.title}</span
|
||
>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<a
|
||
href={`/profile/${profile.id}`}
|
||
class="btn btn-ghost btn-sm"
|
||
style="color:#fff"
|
||
>
|
||
{de ? "✏️ Daten bearbeiten" : "✏️ Edit Data"}
|
||
</a>
|
||
<span id="save-status" class="save-status hidden"
|
||
>✓ {t("editor.save")}</span
|
||
>
|
||
<button
|
||
id="toggle-preview"
|
||
class="btn btn-ghost btn-sm"
|
||
style="display:none"
|
||
>
|
||
{de ? "👁 Vorschau" : "👁 Preview"}
|
||
</button>
|
||
<div style="position:relative">
|
||
<button id="export-btn" class="btn btn-accent btn-sm"
|
||
>↓ {t("editor.export")}</button
|
||
>
|
||
<div
|
||
id="export-menu"
|
||
class="hidden"
|
||
style="position:absolute;right:0;top:calc(100% + 4px);background:white;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.15);min-width:200px;z-index:50"
|
||
>
|
||
<div
|
||
style="padding:8px 16px 4px;font-size:.7rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.5px"
|
||
>
|
||
PDF
|
||
</div>
|
||
<button
|
||
onclick="exportPDFPrint()"
|
||
style="width:100%;text-align:left;padding:8px 16px;border:none;background:none;font-size:.875rem;cursor:pointer;display:flex;align-items:center;gap:8px"
|
||
>
|
||
🖨️ <span>
|
||
<strong>{de ? "Browser-Druck" : "Browser Print"}</strong><br />
|
||
<span style="font-size:.75rem;color:#888"
|
||
>{
|
||
de
|
||
? "Empfohlen, Icons korrekt"
|
||
: "Recommended, icons correct"
|
||
}</span
|
||
>
|
||
</span>
|
||
</button>
|
||
<button
|
||
id="btn-puppeteer"
|
||
onclick="exportPDFCanvas()"
|
||
style="width:100%;text-align:left;padding:8px 16px;border:none;background:none;font-size:.875rem;cursor:pointer;display:flex;align-items:center;gap:8px"
|
||
>
|
||
📄 <span>
|
||
<strong>{de ? "PDF Download" : "PDF Download"}</strong><br />
|
||
<span style="font-size:.75rem;color:#888"
|
||
>{
|
||
de
|
||
? "Serverseitig, beste Qualität"
|
||
: "Server-side, best quality"
|
||
}</span
|
||
>
|
||
</span>
|
||
</button>
|
||
<div style="border-top:1px solid #eee;margin:4px 0"></div>
|
||
<button
|
||
onclick="exportHTML()"
|
||
style="width:100%;text-align:left;padding:8px 16px;border:none;background:none;font-size:.875rem;cursor:pointer"
|
||
>🌐 HTML</button
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="editor-layout" id="editor-layout">
|
||
<!-- SIDEBAR -->
|
||
<div class="editor-sidebar">
|
||
<div class="editor-tabs" id="editor-tabs">
|
||
<button class="editor-tab active" data-tab="settings"
|
||
>{t("editor.tab.settings")}</button
|
||
>
|
||
</div>
|
||
|
||
<div class="editor-content" id="editor-content">
|
||
<div class="tab-pane" id="tab-settings">
|
||
<div class="form-group">
|
||
<label class="form-label">{t("settings.template")}</label>
|
||
<div class="template-grid" id="template-grid">
|
||
{
|
||
TEMPLATES.map((tpl) => (
|
||
<div
|
||
class={`template-option ${cv.template === tpl.id ? "selected" : ""}`}
|
||
data-id={tpl.id}
|
||
onclick="selectTemplate(this)"
|
||
>
|
||
<div class="template-thumb">
|
||
<span style="color:white;font-size:1.3rem">
|
||
T{tpl.id}
|
||
</span>
|
||
</div>
|
||
<div class="template-label">
|
||
<>
|
||
<strong>{tpl.name}</strong>
|
||
<br />
|
||
</>
|
||
<span style="font-size:.7rem;color:#888">
|
||
{tpl.desc}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))
|
||
}
|
||
</div>
|
||
</div>
|
||
<hr class="divider" />
|
||
<div class="form-group">
|
||
<label class="form-label">{t("settings.colors")}</label>
|
||
<div class="color-row mt-1">
|
||
<div class="color-swatch">
|
||
<input
|
||
type="color"
|
||
id="color-primary"
|
||
value={s.primaryColor}
|
||
/>
|
||
</div>
|
||
<span class="text-sm">{t("settings.primaryColor")}</span>
|
||
</div>
|
||
<div class="color-row mt-2">
|
||
<div class="color-swatch">
|
||
<input type="color" id="color-accent" value={s.accentColor} />
|
||
</div>
|
||
<span class="text-sm">{t("settings.accentColor")}</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>{de ? "Text Schriftart" : "Text Font"}</label
|
||
>
|
||
<select id="font-family" class="form-input form-select">
|
||
{
|
||
[
|
||
["'Inter', sans-serif", "Inter"],
|
||
["'Roboto', sans-serif", "Roboto"],
|
||
["'Montserrat', sans-serif", "Montserrat"],
|
||
["'Outfit', sans-serif", "Outfit"],
|
||
["'Lora', serif", "Lora"],
|
||
["Arial, sans-serif", "Arial"],
|
||
].map(([val, label]) => (
|
||
<option
|
||
value={val}
|
||
selected={
|
||
s.fontFamily?.replace(/"/g, "'") === val ||
|
||
s.fontFamily === val
|
||
}
|
||
>
|
||
{label}
|
||
</option>
|
||
))
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>{de ? "Überschriften Schriftart" : "Heading Font"}</label
|
||
>
|
||
<select id="font-heading" class="form-input form-select">
|
||
{
|
||
[
|
||
["'Inter', sans-serif", "Inter"],
|
||
["'Montserrat', sans-serif", "Montserrat"],
|
||
["'Playfair Display', serif", "Playfair Display"],
|
||
["'Oswald', sans-serif", "Oswald"],
|
||
["'Merriweather', serif", "Merriweather"],
|
||
["Arial, sans-serif", "Arial"],
|
||
].map(([val, label]) => (
|
||
<option
|
||
value={val}
|
||
selected={
|
||
(s.fontHeading || s.fontFamily)?.replace(/"/g, "'") ===
|
||
val || (s.fontHeading || s.fontFamily) === val
|
||
}
|
||
>
|
||
{label}
|
||
</option>
|
||
))
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">{t("settings.language")}</label>
|
||
<select id="cv-lang" class="form-input form-select">
|
||
<option value="de" selected={s.language === "de"}
|
||
>Deutsch</option
|
||
>
|
||
<option value="en" selected={s.language === "en"}
|
||
>English</option
|
||
>
|
||
</select>
|
||
</div>
|
||
<div
|
||
class="form-group flex items-center gap-2"
|
||
style="flex-direction:row;align-items:center"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
id="show-photo"
|
||
checked={s.showPhoto}
|
||
style="width:16px;height:16px"
|
||
/>
|
||
<label for="show-photo" class="form-label" style="margin:0"
|
||
>{t("settings.showPhoto")}</label
|
||
>
|
||
</div>
|
||
<hr class="divider" />
|
||
<div class="form-group">
|
||
<div
|
||
class="flex items-center gap-2"
|
||
style="flex-direction:row;align-items:center;margin-bottom:8px"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
id="cv-public"
|
||
checked={cv.public}
|
||
style="width:16px;height:16px"
|
||
/>
|
||
<label for="cv-public" class="form-label" style="margin:0"
|
||
>{t("settings.public")}</label
|
||
>
|
||
</div>
|
||
{
|
||
cv.public && (
|
||
<div class="flex gap-2">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
id="public-link"
|
||
value={`${Astro.url.origin}/cv/${cv.hash}`}
|
||
readonly
|
||
style="font-size:.75rem"
|
||
/>
|
||
<button class="btn btn-ghost btn-sm" onclick="copyLink()">
|
||
{t("settings.copy")}
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PREVIEW -->
|
||
<div class="editor-preview">
|
||
<div class="editor-preview-toolbar">
|
||
<span style="font-size:.8rem;color:var(--muted)"
|
||
>{de ? "Vorschau (A4)" : "Preview (A4)"}</span
|
||
>
|
||
<div class="flex gap-2">
|
||
<span id="preview-save-status" class="save-status hidden text-xs"
|
||
>✓</span
|
||
>
|
||
<button onclick="exportPDFPrint()" class="btn btn-ghost btn-sm"
|
||
>🖨️ {de ? "Drucken" : "Print"}</button
|
||
>
|
||
<button onclick="exportPDFCanvas()" class="btn btn-accent btn-sm"
|
||
>↓ PDF</button
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="preview-wrap" id="preview-wrap">
|
||
<div class="preview-scale-wrap" id="preview-scale">
|
||
<div id="cv-preview">
|
||
{cv.template === 1 && <Template1 data={d} settings={s} />}
|
||
{cv.template === 2 && <Template2 data={d} settings={s} />}
|
||
{cv.template === 3 && <Template3 data={d} settings={s} />}
|
||
{cv.template === 4 && <Template4 data={d} settings={s} />}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script
|
||
define:vars={{
|
||
cvId: cv.id,
|
||
cvHash: cv.hash,
|
||
cvTitle: cv.title,
|
||
profileId: profile.id,
|
||
initData: j(profile.data),
|
||
initSettings: j(cv.settings),
|
||
initTemplate: cv.template,
|
||
isPublic: cv.public,
|
||
de,
|
||
origin: Astro.url.origin,
|
||
}}
|
||
>
|
||
let cvSettings = JSON.parse(initSettings);
|
||
let cvData = JSON.parse(initData);
|
||
let template = initTemplate;
|
||
let saveTimer = null;
|
||
let isDirty = false;
|
||
|
||
// ── Settings ──────────────────────────────────────────────────
|
||
document
|
||
.getElementById("color-primary")
|
||
.addEventListener("input", function () {
|
||
cvSettings.primaryColor = this.value;
|
||
markDirty();
|
||
});
|
||
document
|
||
.getElementById("color-accent")
|
||
.addEventListener("input", function () {
|
||
cvSettings.accentColor = this.value;
|
||
markDirty();
|
||
});
|
||
document
|
||
.getElementById("font-family")
|
||
.addEventListener("change", function () {
|
||
cvSettings.fontFamily = this.value;
|
||
markDirty();
|
||
});
|
||
document
|
||
.getElementById("font-heading")
|
||
.addEventListener("change", function () {
|
||
cvSettings.fontHeading = this.value;
|
||
markDirty();
|
||
});
|
||
document
|
||
.getElementById("cv-lang")
|
||
.addEventListener("change", function () {
|
||
cvSettings.language = this.value;
|
||
markDirty();
|
||
});
|
||
document
|
||
.getElementById("show-photo")
|
||
.addEventListener("change", function () {
|
||
cvSettings.showPhoto = this.checked;
|
||
markDirty();
|
||
});
|
||
document
|
||
.getElementById("cv-public")
|
||
.addEventListener("change", async function () {
|
||
await saveNow({ public: this.checked });
|
||
if (this.checked && !document.getElementById("public-link")) {
|
||
document
|
||
.getElementById("cv-public")
|
||
.parentElement.insertAdjacentHTML(
|
||
"afterend",
|
||
`<div class="flex gap-2"><input type="text" class="form-input" id="public-link" value="${origin}/cv/${cvHash}" readonly style="font-size:.75rem"/><button class="btn btn-ghost btn-sm" onclick="copyLink()">${de ? "Kopieren" : "Copy"}</button></div>`,
|
||
);
|
||
}
|
||
});
|
||
|
||
window.selectTemplate = function (el) {
|
||
document
|
||
.querySelectorAll(".template-option")
|
||
.forEach((e) => e.classList.remove("selected"));
|
||
el.classList.add("selected");
|
||
template = parseInt(el.dataset.id);
|
||
markDirty();
|
||
};
|
||
|
||
window.copyLink = function () {
|
||
const link = document.getElementById("public-link");
|
||
if (link) navigator.clipboard.writeText(link.value);
|
||
};
|
||
|
||
// ── Save & Preview ────────────────────────────────────────────
|
||
function markDirty() {
|
||
isDirty = true;
|
||
clearTimeout(saveTimer);
|
||
saveTimer = setTimeout(saveNow, 2000);
|
||
const s = document.getElementById("save-status");
|
||
s.className = "save-status saving";
|
||
s.textContent = de ? "⏳ Speichert..." : "⏳ Saving...";
|
||
s.classList.remove("hidden");
|
||
refreshPreview();
|
||
}
|
||
|
||
async function saveNow(extra = {}) {
|
||
const res = await fetch(`/api/cv/${cvId}/update`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
title: cvTitle,
|
||
template,
|
||
settings: cvSettings,
|
||
...extra,
|
||
}),
|
||
});
|
||
if (res.ok) {
|
||
isDirty = false;
|
||
const s = document.getElementById("save-status");
|
||
s.className = "save-status";
|
||
s.textContent = "✓ " + (de ? "Gespeichert" : "Saved");
|
||
setTimeout(() => s.classList.add("hidden"), 3000);
|
||
}
|
||
}
|
||
|
||
async function refreshPreview() {
|
||
const res = await fetch(`/api/cv/${cvId}/preview`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
template,
|
||
settings: cvSettings,
|
||
profile_id: profileId,
|
||
}),
|
||
});
|
||
if (res.ok) {
|
||
document.getElementById("cv-preview").innerHTML = await res.text();
|
||
}
|
||
}
|
||
|
||
function scalePreview() {
|
||
const wrap = document.getElementById("preview-wrap");
|
||
const scale = document.getElementById("preview-scale");
|
||
if (!wrap || !scale) return;
|
||
const factor = Math.min(1, (wrap.clientWidth - 48) / 794);
|
||
scale.style.transform = `scale(${factor})`;
|
||
scale.style.transformOrigin = "top center";
|
||
scale.style.marginBottom = `${(1 - factor) * -1123}px`;
|
||
}
|
||
window.addEventListener("resize", scalePreview);
|
||
scalePreview();
|
||
|
||
// ── Export Menu ───────────────────────────────────────────────
|
||
document.getElementById("export-btn").addEventListener("click", () => {
|
||
document.getElementById("export-menu").classList.toggle("hidden");
|
||
});
|
||
document.addEventListener("click", (e) => {
|
||
if (
|
||
!e.target.closest("#export-btn") &&
|
||
!e.target.closest("#export-menu")
|
||
) {
|
||
document.getElementById("export-menu").classList.add("hidden");
|
||
}
|
||
});
|
||
|
||
// ── printCV (inline, kein externer Script nötig) ─────────────
|
||
async function printCV(el, title, settings, tpl) {
|
||
let globalCss = "";
|
||
try {
|
||
globalCss = await fetch("/styles/global.css").then((r) => r.text());
|
||
} catch (e) {}
|
||
|
||
const inlineStyles = Array.from(document.querySelectorAll("style"))
|
||
.map((s) => s.textContent)
|
||
.join("\n");
|
||
|
||
const fontUrl =
|
||
"https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Lora:ital,wght@0,400;0,600;1,400&family=Merriweather:wght@400;700&family=Montserrat:wght@300;400;600;700&family=Outfit:wght@300;400;600&family=Oswald:wght@400;600&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Roboto:wght@300;400;700&display=swap";
|
||
|
||
const SIDEBAR_WIDTH = { 1: "220px", 2: "0", 3: "160px", 4: "240px" };
|
||
const sw = SIDEBAR_WIDTH[tpl] || "0";
|
||
const color = (settings && settings.primaryColor) || "#1B2A5E";
|
||
const sidebarCss =
|
||
sw !== "0"
|
||
? `
|
||
body {
|
||
background: linear-gradient(to right, ${color} ${sw}, white ${sw}) !important;
|
||
-webkit-print-color-adjust: exact !important;
|
||
print-color-adjust: exact !important;
|
||
}
|
||
.t1-sidebar, .t3-left, .t4-left { background: transparent !important; }
|
||
`
|
||
: "";
|
||
|
||
const html = `<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="UTF-8">
|
||
<title>${title || "Lebenslauf"}</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="${fontUrl}" rel="stylesheet">
|
||
<style>
|
||
${globalCss}
|
||
${inlineStyles}
|
||
@page { size: A4 portrait; margin: 0; }
|
||
html { margin: 0; padding: 0; }
|
||
body { margin: 0 !important; padding: 0 !important; display: flex; justify-content: center; background: white; }
|
||
${sidebarCss}
|
||
.cv-a4 { width: 210mm !important; min-height: 297mm !important; box-shadow: none !important; margin: 0 !important; }
|
||
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||
.t1-entry, .t1-sec, .t2-tl-item, .t2-sec, .t3-entry, .t3-sec, .t4-entry, .t4-sec { break-inside: avoid; }
|
||
</style>
|
||
</head><body>${el.innerHTML}</body></html>`;
|
||
|
||
const blob = new Blob([html], { type: "text/html; charset=utf-8" });
|
||
const blobUrl = URL.createObjectURL(blob);
|
||
const win = window.open(blobUrl, "_blank", "width=900,height=1200");
|
||
if (!win) {
|
||
alert("Popup blockiert – bitte Popup-Blocker deaktivieren.");
|
||
URL.revokeObjectURL(blobUrl);
|
||
return;
|
||
}
|
||
win.addEventListener("load", () => {
|
||
URL.revokeObjectURL(blobUrl);
|
||
setTimeout(() => {
|
||
win.focus();
|
||
win.print();
|
||
win.addEventListener("afterprint", () => win.close());
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
// ── PDF via Browser-Druck ─────────────────────────────────────
|
||
window.exportPDFPrint = function () {
|
||
document.getElementById("export-menu").classList.add("hidden");
|
||
const el = document.getElementById("cv-preview");
|
||
printCV(el, cvTitle, cvSettings, template);
|
||
};
|
||
|
||
// ── PDF via Puppeteer (serverseitig) ──────────────────────────
|
||
window.exportPDFCanvas = async function () {
|
||
document.getElementById("export-menu").classList.add("hidden");
|
||
const btn = document.getElementById("btn-puppeteer");
|
||
const origHtml = btn?.innerHTML;
|
||
if (btn)
|
||
btn.innerHTML = "⏳ " + (de ? "Generiert..." : "Generating...");
|
||
|
||
try {
|
||
const res = await fetch(`/api/cv/${cvId}/pdf`);
|
||
if (!res.ok) throw new Error(await res.text());
|
||
const blob = await res.blob();
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = (cvTitle || "lebenslauf") + ".pdf";
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
} catch (err) {
|
||
alert((de ? "PDF-Fehler: " : "PDF error: ") + err.message);
|
||
} finally {
|
||
if (btn && origHtml) btn.innerHTML = origHtml;
|
||
}
|
||
};
|
||
|
||
window.exportPDF = window.exportPDFPrint;
|
||
|
||
// ── HTML Export ───────────────────────────────────────────────
|
||
window.exportHTML = async function () {
|
||
document.getElementById("export-menu").classList.add("hidden");
|
||
const el = document.getElementById("cv-preview");
|
||
const globalCss = await fetch("/styles/global.css")
|
||
.then((r) => r.text())
|
||
.catch(() => "");
|
||
const inlineStyles = Array.from(document.querySelectorAll("style"))
|
||
.map((s) => s.textContent)
|
||
.join("\n");
|
||
const fontLinks = Array.from(
|
||
document.querySelectorAll('link[rel="stylesheet"]'),
|
||
)
|
||
.map((l) => l.outerHTML)
|
||
.join("\n");
|
||
const html = `<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>${cvTitle}</title>
|
||
${fontLinks}
|
||
<style>${globalCss}\n${inlineStyles}</style>
|
||
</head>
|
||
<body style="margin:0;background:#E8ECF0;display:flex;justify-content:center;padding:32px">
|
||
${el.outerHTML}
|
||
</body>
|
||
</html>`;
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
||
a.download = (cvTitle || "lebenslauf") + ".html";
|
||
a.click();
|
||
};
|
||
|
||
// ── Mobile toggle ─────────────────────────────────────────────
|
||
if (window.innerWidth <= 900) {
|
||
document.getElementById("toggle-preview").style.display = "inline-flex";
|
||
}
|
||
document
|
||
.getElementById("toggle-preview")
|
||
?.addEventListener("click", () => {
|
||
document
|
||
.getElementById("editor-layout")
|
||
.classList.toggle("show-preview");
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|