layout
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import node from '@astrojs/node';
|
import node from '@astrojs/node';
|
||||||
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'server',
|
output: 'server',
|
||||||
adapter: node({ mode: 'standalone' }),
|
adapter: node({ mode: 'standalone' }),
|
||||||
server: { port: 4321, host: true },
|
server: { port: 4321, host: true },
|
||||||
vite: {
|
vite: {
|
||||||
optimizeDeps: { exclude: ['better-sqlite3'] },
|
optimizeDeps: {
|
||||||
ssr: { external: ['better-sqlite3'] }
|
exclude: ['better-sqlite3', 'puppeteer'] // ← puppeteer ergänzen
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
external: ['better-sqlite3', 'puppeteer'] // ← puppeteer ergänzen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
971
package-lock.json
generated
971
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,12 @@
|
|||||||
"start": "node dist/server/entry.mjs"
|
"start": "node dist/server/entry.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^4.5.0",
|
|
||||||
"@astrojs/node": "^8.2.0",
|
"@astrojs/node": "^8.2.0",
|
||||||
|
"astro": "^4.5.0",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
|
"marked": "^18.0.2",
|
||||||
"nodemailer": "^6.9.13",
|
"nodemailer": "^6.9.13",
|
||||||
|
"puppeteer": "^24.42.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
121
public/scripts/printExport.js
Normal file
121
public/scripts/printExport.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// printExport.js
|
||||||
|
// Einbinden: <script src="/scripts/printExport.js"></script>
|
||||||
|
// Aufruf: window.printCV(element, cvTitle, cvSettings, template)
|
||||||
|
|
||||||
|
window.printCV = async function (el, cvTitle, cvSettings, template) {
|
||||||
|
|
||||||
|
// ── 1. Lokale CSS-Datei direkt fetchen (kein CORS-Problem) ──────
|
||||||
|
let globalCss = '';
|
||||||
|
try {
|
||||||
|
globalCss = await fetch('/styles/global.css').then(r => r.text());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('global.css nicht geladen', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Inline <style> Tags sammeln (Astro Scoped CSS) ───────────
|
||||||
|
const inlineStyles = Array.from(document.querySelectorAll('style'))
|
||||||
|
.map(s => s.textContent).join('\n');
|
||||||
|
|
||||||
|
// ── 3. Google Fonts URL ─────────────────────────────────────────
|
||||||
|
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';
|
||||||
|
|
||||||
|
// ── 4. Sidebar-Body-Gradient (wiederholt sich auf jeder Seite) ──
|
||||||
|
const SIDEBAR_WIDTH = { 1: '220px', 2: '0', 3: '160px', 4: '240px' };
|
||||||
|
const sw = SIDEBAR_WIDTH[template] || '0';
|
||||||
|
const color = (cvSettings && cvSettings.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;
|
||||||
|
}
|
||||||
|
/* Sidebar transparent – Farbe kommt von body-Hintergrund */
|
||||||
|
.t1-sidebar { background: transparent !important; }
|
||||||
|
.t3-left { background: transparent !important; }
|
||||||
|
.t4-left { background: transparent !important; }
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
// ── 5. Komplettes HTML zusammenbauen ────────────────────────────
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>${cvTitle || '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>
|
||||||
|
/* ── global.css ── */
|
||||||
|
${globalCss}
|
||||||
|
|
||||||
|
/* ── Astro Scoped Styles ── */
|
||||||
|
${inlineStyles}
|
||||||
|
|
||||||
|
/* ── Print Overrides (am Ende = höchste Priorität) ── */
|
||||||
|
@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>`;
|
||||||
|
|
||||||
|
// ── 6. Blob-URL statt document.write ────────────────────────────
|
||||||
|
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 wurde blockiert. Bitte Popup-Blocker deaktivieren.');
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
win.addEventListener('load', () => {
|
||||||
|
// Blob-URL freigeben sobald geladen
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
|
||||||
|
// Fonts + base64-PNGs (Icons) etwas Zeit geben
|
||||||
|
setTimeout(() => {
|
||||||
|
win.focus();
|
||||||
|
win.print();
|
||||||
|
win.addEventListener('afterprint', () => win.close());
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
611
src/components/templates/Template1 copy.astro
Normal file
611
src/components/templates/Template1 copy.astro
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
---
|
||||||
|
import type { CVData, CVSettings } from "../../types";
|
||||||
|
import {
|
||||||
|
IcoEmail,
|
||||||
|
IcoPhone,
|
||||||
|
IcoCal,
|
||||||
|
IcoFlag,
|
||||||
|
IcoHome,
|
||||||
|
IcoPerson,
|
||||||
|
IcoEdu,
|
||||||
|
IcoExp,
|
||||||
|
} from "../../lib/icons_svg";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: CVData;
|
||||||
|
settings: CVSettings;
|
||||||
|
}
|
||||||
|
const { data: d, settings: s } = Astro.props;
|
||||||
|
const p = d.personal;
|
||||||
|
const T = s.language === "en";
|
||||||
|
|
||||||
|
function date(str: string) {
|
||||||
|
return str || (T ? "present" : "heute");
|
||||||
|
}
|
||||||
|
function datRange(from: string, to: string, cur: boolean) {
|
||||||
|
return `${from} – ${cur ? (T ? "present" : "Aktuell") : to}`;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cv-a4 t1"
|
||||||
|
style={`--p:${s.primaryColor};--a:${s.accentColor};font-family:${s.fontFamily?.replace(/"/g, "'")}`}
|
||||||
|
>
|
||||||
|
<div class="t1-layout">
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside class="t1-sidebar">
|
||||||
|
{
|
||||||
|
s.showPhoto && (
|
||||||
|
<div class="t1-photo-wrap">
|
||||||
|
{p.photo ? (
|
||||||
|
<img src={p.photo} class="t1-photo" alt="" />
|
||||||
|
) : (
|
||||||
|
<div class="t1-photo-ph">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
>
|
||||||
|
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Personal -->
|
||||||
|
<div class="t1-section-head">{T ? "PERSONAL" : "PERSÖNLICH"}</div>
|
||||||
|
<div class="t1-contact-list">
|
||||||
|
{
|
||||||
|
p.firstName && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoPerson} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "NAME" : "NAME"}</div>
|
||||||
|
<div class="t1-cv">
|
||||||
|
{p.firstName} {p.lastName}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(p.address || p.city) && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<fragment set:html={IcoHome} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "ADDRESS" : "ADRESSE"}</div>
|
||||||
|
<div class="t1-cv">
|
||||||
|
{p.address}
|
||||||
|
{p.address && p.city ? ", " : ""}
|
||||||
|
{p.city}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.phone && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoPhone} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "PHONE" : "TELEFON"}</div>
|
||||||
|
<div class="t1-cv">{p.phone}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.email && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoEmail} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">E-MAIL</div>
|
||||||
|
<div class="t1-cv">{p.email}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.birthDate && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoCal} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "BIRTH DATE" : "GEBURTSDATUM"}</div>
|
||||||
|
<div class="t1-cv">
|
||||||
|
{p.birthDate}
|
||||||
|
{p.birthPlace ? `, ${p.birthPlace}` : ""}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.nationality && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoFlag} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">
|
||||||
|
{T ? "NATIONALITY" : "NATIONALITÄT"}
|
||||||
|
</div>
|
||||||
|
<div class="t1-cv">{p.nationality}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.maritalStatus && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoPerson} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">
|
||||||
|
{T ? "MARITAL STATUS" : "FAMILIENSTAND"}
|
||||||
|
</div>
|
||||||
|
<div class="t1-cv t1-cv-sm">{p.maritalStatus}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Languages -->
|
||||||
|
{
|
||||||
|
d.languages.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div class="t1-section-head">{T ? "LANGUAGES" : "SPRACHEN"}</div>
|
||||||
|
<div class="t1-lang-grid">
|
||||||
|
{d.languages.map((l) => (
|
||||||
|
<>
|
||||||
|
<span class="t1-lang-name">{l.name.toUpperCase()}</span>
|
||||||
|
<span class="t1-lang-level">{l.level}</span>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Skills / Interests -->
|
||||||
|
{
|
||||||
|
d.interests.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div class="t1-section-head">{T ? "INTERESTS" : "INTERESSEN"}</div>
|
||||||
|
<ul class="t1-list">
|
||||||
|
{d.interests.map((i) => (
|
||||||
|
<li>{i.trim()}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.skills.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div class="t1-section-head">{T ? "SKILLS" : "KENNTNISSE"}</div>
|
||||||
|
<ul class="t1-list">
|
||||||
|
{d.skills.map((sk) => (
|
||||||
|
<li>
|
||||||
|
<span class="t1-sk-name">{sk.name}</span>{" "}
|
||||||
|
<span class="t1-sk-lvl">
|
||||||
|
{["", "•", "••", "•••", "••••", "•••••"][sk.level] || ""}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
<main class="t1-main">
|
||||||
|
<div class="t1-name">{p.firstName} {p.lastName}</div>
|
||||||
|
{p.jobTitle && <div class="t1-jobtitle">{p.jobTitle}</div>}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.profile && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">📋</span>
|
||||||
|
{T ? "PROFILE" : "PROFIL"}
|
||||||
|
</div>
|
||||||
|
<p class="t1-profile-text">{d.profile}</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.education.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">🎓</span>
|
||||||
|
{T ? "EDUCATION & QUALIFICATIONS" : "BILDUNG UND QUALIFIKATION"}
|
||||||
|
</div>
|
||||||
|
{d.education.map((e) => (
|
||||||
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">
|
||||||
|
{datRange(e.dateFrom, e.dateTo, e.current)}
|
||||||
|
</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-etitle">{e.degree}</div>
|
||||||
|
{e.school && (
|
||||||
|
<div class="t1-ecompany">
|
||||||
|
{e.school}
|
||||||
|
{e.location ? `, ${e.location}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{e.description && <div class="t1-edesc">{e.description}</div>}
|
||||||
|
{e.bullets.filter(Boolean).length > 0 && (
|
||||||
|
<ul class="t1-bullets">
|
||||||
|
{e.bullets.filter(Boolean).map((b) => (
|
||||||
|
<li>{b}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.experience.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">💼</span>
|
||||||
|
{T ? "WORK EXPERIENCE" : "ARBEITSERFAHRUNG"}
|
||||||
|
</div>
|
||||||
|
{d.experience.map((e) => (
|
||||||
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">
|
||||||
|
{datRange(e.dateFrom, e.dateTo, e.current)}
|
||||||
|
</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-etitle">{e.jobTitle}</div>
|
||||||
|
{e.company && (
|
||||||
|
<div class="t1-ecompany">
|
||||||
|
{e.company}
|
||||||
|
{e.location ? `, ${e.location}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{e.description && <div class="t1-edesc">{e.description}</div>}
|
||||||
|
{e.bullets.filter(Boolean).length > 0 && (
|
||||||
|
<ul class="t1-bullets">
|
||||||
|
{e.bullets.filter(Boolean).map((b) => {
|
||||||
|
const [bold, rest] = b.includes(":")
|
||||||
|
? [b.split(":")[0], b.split(":").slice(1).join(":")]
|
||||||
|
: [null, b];
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{bold ? (
|
||||||
|
<>
|
||||||
|
<strong>{bold}</strong>:{rest}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
rest
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.skills.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">🔧</span>
|
||||||
|
{T ? "COMPETENCIES" : "KOMPETENZEN"}
|
||||||
|
</div>
|
||||||
|
{Object.entries(
|
||||||
|
d.skills.reduce((acc: any, sk) => {
|
||||||
|
const cat = sk.category || (T ? "Other" : "Sonstiges");
|
||||||
|
if (!acc[cat]) acc[cat] = [];
|
||||||
|
acc[cat].push(sk);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
).map(([cat, skills]: any) => (
|
||||||
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">{cat}</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-edesc">
|
||||||
|
{skills
|
||||||
|
.map(
|
||||||
|
(sk: any) =>
|
||||||
|
`${sk.name}: ${["", "Grundkenntnisse", "Gut", "Sehr gut", "Experte", "Meister"][sk.level] || sk.level}`,
|
||||||
|
)
|
||||||
|
.join(" · ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.certifications.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">📜</span>
|
||||||
|
{T ? "COURSES, TRAINING" : "WEITERBILDUNGEN, KURSE"}
|
||||||
|
</div>
|
||||||
|
{d.certifications.map((c) => (
|
||||||
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">
|
||||||
|
{c.dateFrom}
|
||||||
|
{c.dateTo && c.dateTo !== c.dateFrom ? ` – ${c.dateTo}` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-etitle">{c.name}</div>
|
||||||
|
{c.issuer && (
|
||||||
|
<div class="t1-ecompany">
|
||||||
|
{c.issuer}
|
||||||
|
{c.location ? `, ${c.location}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.achievements.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">🏆</span>
|
||||||
|
{T ? "ACHIEVEMENTS" : "ERFOLGE"}
|
||||||
|
</div>
|
||||||
|
<ul class="t1-bullets">
|
||||||
|
{d.achievements.filter(Boolean).map((a) => (
|
||||||
|
<li>{a}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.t1-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
min-height: 1123px;
|
||||||
|
}
|
||||||
|
.t1-sidebar {
|
||||||
|
background: var(--p, #1b2a5e);
|
||||||
|
color: white;
|
||||||
|
padding: 0 0 24px 0;
|
||||||
|
}
|
||||||
|
.t1-photo-wrap {
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.t1-photo {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
.t1-photo-ph {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
.t1-section-head {
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 10px 16px 6px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
.t1-contact-list {
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
}
|
||||||
|
.t1-ci {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
}
|
||||||
|
.t1-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.t1-cl {
|
||||||
|
font-size: 7.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.t1-cv {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.t1-cv-sm {
|
||||||
|
font-size: 9.5px;
|
||||||
|
}
|
||||||
|
.t1-lang-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 4px 12px;
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.t1-lang-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.t1-lang-level {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
.t1-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
}
|
||||||
|
.t1-list li {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
padding: 2px 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.t1-list li::before {
|
||||||
|
content: "▪";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 8px;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
.t1-sk-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.t1-sk-lvl {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main */
|
||||||
|
.t1-main {
|
||||||
|
padding: 28px 28px 28px 24px;
|
||||||
|
}
|
||||||
|
.t1-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--p, #1b2a5e);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.t1-jobtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.t1-sec {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.t1-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2.5px;
|
||||||
|
color: var(--p, #1b2a5e);
|
||||||
|
border-bottom: 2px solid var(--p, #1b2a5e);
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.t1-hicon {
|
||||||
|
font-size: 13px;
|
||||||
|
filter: brightness(0);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.t1-entry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.t1-edate {
|
||||||
|
font-size: 9.5px;
|
||||||
|
color: #888;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
.t1-etitle {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
.t1-ecompany {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--a, #4a7bc5);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.t1-edesc {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #444;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.t1-profile-text {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.t1-bullets {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.t1-bullets li {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #333;
|
||||||
|
padding: 2px 0 2px 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.t1-bullets li::before {
|
||||||
|
content: "▪";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--p, #1b2a5e);
|
||||||
|
font-size: 8px;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
---
|
---
|
||||||
import type { CVData, CVSettings } from '../../types';
|
import type { CVData, CVSettings } from "../../types";
|
||||||
|
import {
|
||||||
|
IcoEmail,
|
||||||
|
IcoPhone,
|
||||||
|
IcoCal,
|
||||||
|
IcoFlag,
|
||||||
|
IcoHome,
|
||||||
|
IcoPerson,
|
||||||
|
IcoEdu,
|
||||||
|
IcoExp,
|
||||||
|
} from "../../lib/icons_svg";
|
||||||
|
// markdown
|
||||||
|
import { marked } from "marked";
|
||||||
|
marked.setOptions({ breaks: true });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: CVData;
|
data: CVData;
|
||||||
@@ -7,68 +20,215 @@ interface Props {
|
|||||||
}
|
}
|
||||||
const { data: d, settings: s } = Astro.props;
|
const { data: d, settings: s } = Astro.props;
|
||||||
const p = d.personal;
|
const p = d.personal;
|
||||||
const T = s.language === 'en';
|
const T = s.language === "en";
|
||||||
|
|
||||||
function date(str: string) { return str || (T ? 'present' : 'heute'); }
|
function date(str: string) {
|
||||||
|
return str || (T ? "present" : "heute");
|
||||||
|
}
|
||||||
function datRange(from: string, to: string, cur: boolean) {
|
function datRange(from: string, to: string, cur: boolean) {
|
||||||
return `${from} – ${cur ? (T ? 'present' : 'Aktuell') : to}`;
|
return `${from} – ${cur ? (T ? "present" : "Aktuell") : to}`;
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
<div class="cv-a4 t1" style={`--p:${s.primaryColor};--a:${s.accentColor};font-family:${s.fontFamily?.replace(/"/g, "'")}`}>
|
|
||||||
|
<div
|
||||||
|
class="cv-a4 t1"
|
||||||
|
style={`--p:${s.primaryColor};--a:${s.accentColor};font-family:${s.fontFamily?.replace(/"/g, "'")}`}
|
||||||
|
>
|
||||||
<div class="t1-layout">
|
<div class="t1-layout">
|
||||||
<!-- SIDEBAR -->
|
<!-- SIDEBAR -->
|
||||||
<aside class="t1-sidebar">
|
<aside class="t1-sidebar" style={`--p:${s.primaryColor};`}>
|
||||||
{s.showPhoto && (
|
{
|
||||||
<div class="t1-photo-wrap">
|
s.showPhoto && (
|
||||||
{p.photo
|
<div class="t1-photo-wrap">
|
||||||
? <img src={p.photo} class="t1-photo" alt="" />
|
{p.photo ? (
|
||||||
: <div class="t1-photo-ph"><svg viewBox="0 0 24 24" fill="currentColor" width="48" height="48"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg></div>
|
<img src={p.photo} class="t1-photo" alt="" />
|
||||||
}
|
) : (
|
||||||
</div>
|
<div class="t1-photo-ph">
|
||||||
)}
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
>
|
||||||
|
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Personal -->
|
<!-- Personal -->
|
||||||
<div class="t1-section-head">{T ? 'PERSONAL' : 'PERSÖNLICH'}</div>
|
<div class="t1-section-head">{T ? "PERSONAL" : "PERSÖNLICH"}</div>
|
||||||
<div class="t1-contact-list">
|
<div class="t1-contact-list">
|
||||||
{p.firstName && <div class="t1-ci"><span class="t1-icon">👤</span><div><div class="t1-cl">{T ? 'NAME' : 'NAME'}</div><div class="t1-cv">{p.firstName} {p.lastName}</div></div></div>}
|
{
|
||||||
{(p.address || p.city) && <div class="t1-ci"><span class="t1-icon">🏠</span><div><div class="t1-cl">{T ? 'ADDRESS' : 'ADRESSE'}</div><div class="t1-cv">{p.address}{p.address && p.city ? ', ' : ''}{p.city}</div></div></div>}
|
p.firstName && (
|
||||||
{p.phone && <div class="t1-ci"><span class="t1-icon">📞</span><div><div class="t1-cl">{T ? 'PHONE' : 'TELEFON'}</div><div class="t1-cv">{p.phone}</div></div></div>}
|
<div class="t1-ci">
|
||||||
{p.email && <div class="t1-ci"><span class="t1-icon">✉️</span><div><div class="t1-cl">E-MAIL</div><div class="t1-cv">{p.email}</div></div></div>}
|
<>
|
||||||
{p.birthDate && <div class="t1-ci"><span class="t1-icon">📅</span><div><div class="t1-cl">{T ? 'BIRTH DATE' : 'GEBURTSDATUM'}</div><div class="t1-cv">{p.birthDate}{p.birthPlace ? `, ${p.birthPlace}` : ''}</div></div></div>}
|
<span set:html={IcoPerson} class="t1-icon" />
|
||||||
{p.nationality && <div class="t1-ci"><span class="t1-icon">🏳️</span><div><div class="t1-cl">{T ? 'NATIONALITY' : 'NATIONALITÄT'}</div><div class="t1-cv">{p.nationality}</div></div></div>}
|
<div>
|
||||||
{p.maritalStatus && <div class="t1-ci"><span class="t1-icon">👥</span><div><div class="t1-cl">{T ? 'MARITAL STATUS' : 'FAMILIENSTAND'}</div><div class="t1-cv t1-cv-sm">{p.maritalStatus}</div></div></div>}
|
<>
|
||||||
|
<div class="t1-cl">{T ? "NAME" : "NAME"}</div>
|
||||||
|
<div class="t1-cv">
|
||||||
|
{p.firstName} {p.lastName}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(p.address || p.city) && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoHome} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "ADDRESS" : "ADRESSE"}</div>
|
||||||
|
<div class="t1-cv">
|
||||||
|
{p.address}
|
||||||
|
{p.address && p.city ? ", " : ""}
|
||||||
|
{p.city}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.phone && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoPhone} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "PHONE" : "TELEFON"}</div>
|
||||||
|
<div class="t1-cv">{p.phone}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.email && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoEmail} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">E-MAIL</div>
|
||||||
|
<div class="t1-cv">{p.email}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.birthDate && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoCal} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">{T ? "BIRTH DATE" : "GEBURTSDATUM"}</div>
|
||||||
|
<div class="t1-cv">
|
||||||
|
{p.birthDate}
|
||||||
|
{p.birthPlace ? `, ${p.birthPlace}` : ""}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.nationality && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoFlag} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">
|
||||||
|
{T ? "NATIONALITY" : "NATIONALITÄT"}
|
||||||
|
</div>
|
||||||
|
<div class="t1-cv">{p.nationality}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
p.maritalStatus && (
|
||||||
|
<div class="t1-ci">
|
||||||
|
<>
|
||||||
|
<span set:html={IcoPerson} class="t1-icon" />
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<div class="t1-cl">
|
||||||
|
{T ? "MARITAL STATUS" : "FAMILIENSTAND"}
|
||||||
|
</div>
|
||||||
|
<div class="t1-cv t1-cv-sm">{p.maritalStatus}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Languages -->
|
<!-- Languages -->
|
||||||
{d.languages.length > 0 && (
|
{
|
||||||
<div>
|
d.languages.length > 0 && (
|
||||||
<div class="t1-section-head">{T ? 'LANGUAGES' : 'SPRACHEN'}</div>
|
<div>
|
||||||
<div class="t1-lang-grid">
|
<div class="t1-section-head">{T ? "LANGUAGES" : "SPRACHEN"}</div>
|
||||||
{d.languages.map(l => (
|
<div class="t1-lang-grid">
|
||||||
<><span class="t1-lang-name">{l.name.toUpperCase()}</span><span class="t1-lang-level">{l.level}</span></>
|
{d.languages.map((l) => (
|
||||||
))}
|
<>
|
||||||
|
<span class="t1-lang-name">{l.name.toUpperCase()}</span>
|
||||||
|
<span class="t1-lang-level">{l.level}</span>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
<!-- Skills / Interests -->
|
<!-- Skills / Interests -->
|
||||||
{d.interests.length > 0 && (
|
{
|
||||||
<div>
|
d.interests.length > 0 && (
|
||||||
<div class="t1-section-head">{T ? 'INTERESTS' : 'INTERESSEN'}</div>
|
<div>
|
||||||
<ul class="t1-list">
|
<div class="t1-section-head">{T ? "INTERESTS" : "INTERESSEN"}</div>
|
||||||
{d.interests.map(i => <li>{i.trim()}</li>)}
|
<ul class="t1-list">
|
||||||
</ul>
|
{d.interests.map((i) => (
|
||||||
</div>
|
<li>{i.trim()}</li>
|
||||||
)}
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{d.skills.length > 0 && (
|
{
|
||||||
<div>
|
d.skills.length > 0 && (
|
||||||
<div class="t1-section-head">{T ? 'SKILLS' : 'KENNTNISSE'}</div>
|
<div>
|
||||||
<ul class="t1-list">
|
<div class="t1-section-head">{T ? "SKILLS" : "KENNTNISSE"}</div>
|
||||||
{d.skills.map(sk => <li><span class="t1-sk-name">{sk.name}</span> <span class="t1-sk-lvl">{['','•','••','•••','••••','•••••'][sk.level] || ''}</span></li>)}
|
<ul class="t1-list">
|
||||||
</ul>
|
{d.skills.map((sk) => (
|
||||||
</div>
|
<li>
|
||||||
)}
|
<span class="t1-sk-name">{sk.name}</span>{" "}
|
||||||
|
<span class="t1-sk-lvl">
|
||||||
|
{["", "•", "••", "•••", "••••", "•••••"][sk.level] || ""}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- MAIN -->
|
<!-- MAIN -->
|
||||||
@@ -76,150 +236,483 @@ function datRange(from: string, to: string, cur: boolean) {
|
|||||||
<div class="t1-name">{p.firstName} {p.lastName}</div>
|
<div class="t1-name">{p.firstName} {p.lastName}</div>
|
||||||
{p.jobTitle && <div class="t1-jobtitle">{p.jobTitle}</div>}
|
{p.jobTitle && <div class="t1-jobtitle">{p.jobTitle}</div>}
|
||||||
|
|
||||||
{d.profile && (
|
{
|
||||||
<section class="t1-sec">
|
d.profile && (
|
||||||
<div class="t1-heading"><span class="t1-hicon">📋</span>{T ? 'PROFILE' : 'PROFIL'}</div>
|
<section class="t1-sec">
|
||||||
<p class="t1-profile-text">{d.profile}</p>
|
<div class="t1-heading">
|
||||||
</section>
|
<span class="t1-hicon">📋</span>
|
||||||
)}
|
{T ? "PROFILE" : "PROFIL"}
|
||||||
|
|
||||||
{d.education.length > 0 && (
|
|
||||||
<section class="t1-sec">
|
|
||||||
<div class="t1-heading"><span class="t1-hicon">🎓</span>{T ? 'EDUCATION & QUALIFICATIONS' : 'BILDUNG UND QUALIFIKATION'}</div>
|
|
||||||
{d.education.map(e => (
|
|
||||||
<div class="t1-entry">
|
|
||||||
<div class="t1-edate">{datRange(e.dateFrom, e.dateTo, e.current)}</div>
|
|
||||||
<div class="t1-eright">
|
|
||||||
<div class="t1-etitle">{e.degree}</div>
|
|
||||||
{e.school && <div class="t1-ecompany">{e.school}{e.location ? `, ${e.location}` : ''}</div>}
|
|
||||||
{e.description && <div class="t1-edesc">{e.description}</div>}
|
|
||||||
{e.bullets.filter(Boolean).length > 0 && (
|
|
||||||
<ul class="t1-bullets">{e.bullets.filter(Boolean).map(b => <li>{b}</li>)}</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div
|
||||||
</section>
|
class="t1-profile-text markdown"
|
||||||
)}
|
set:html={marked.parse(d.profile)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{d.experience.length > 0 && (
|
{
|
||||||
<section class="t1-sec">
|
d.education.length > 0 && (
|
||||||
<div class="t1-heading"><span class="t1-hicon">💼</span>{T ? 'WORK EXPERIENCE' : 'ARBEITSERFAHRUNG'}</div>
|
<section class="t1-sec">
|
||||||
{d.experience.map(e => (
|
<div class="t1-heading">
|
||||||
<div class="t1-entry">
|
<span class="t1-hicon">🎓</span>
|
||||||
<div class="t1-edate">{datRange(e.dateFrom, e.dateTo, e.current)}</div>
|
{T ? "EDUCATION & QUALIFICATIONS" : "BILDUNG UND QUALIFIKATION"}
|
||||||
<div class="t1-eright">
|
|
||||||
<div class="t1-etitle">{e.jobTitle}</div>
|
|
||||||
{e.company && <div class="t1-ecompany">{e.company}{e.location ? `, ${e.location}` : ''}</div>}
|
|
||||||
{e.description && <div class="t1-edesc">{e.description}</div>}
|
|
||||||
{e.bullets.filter(Boolean).length > 0 && (
|
|
||||||
<ul class="t1-bullets">{e.bullets.filter(Boolean).map(b => {
|
|
||||||
const [bold, rest] = b.includes(':') ? [b.split(':')[0], b.split(':').slice(1).join(':')] : [null, b];
|
|
||||||
return <li>{bold ? <><strong>{bold}</strong>:{rest}</> : rest}</li>;
|
|
||||||
})}</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
{d.education.map((e) => (
|
||||||
</section>
|
<div class="t1-entry">
|
||||||
)}
|
<div class="t1-edate">
|
||||||
|
{datRange(e.dateFrom, e.dateTo, e.current)}
|
||||||
{d.skills.length > 0 && (
|
</div>
|
||||||
<section class="t1-sec">
|
<div class="t1-eright">
|
||||||
<div class="t1-heading"><span class="t1-hicon">🔧</span>{T ? 'COMPETENCIES' : 'KOMPETENZEN'}</div>
|
<div class="t1-etitle">{e.degree}</div>
|
||||||
{Object.entries(d.skills.reduce((acc: any, sk) => {
|
{e.school && (
|
||||||
const cat = sk.category || (T ? 'Other' : 'Sonstiges');
|
<div class="t1-ecompany">
|
||||||
if (!acc[cat]) acc[cat] = [];
|
{e.school}
|
||||||
acc[cat].push(sk);
|
{e.location ? `, ${e.location}` : ""}
|
||||||
return acc;
|
</div>
|
||||||
}, {})).map(([cat, skills]: any) => (
|
)}
|
||||||
<div class="t1-entry">
|
{e.description && <div class="t1-edesc">{e.description}</div>}
|
||||||
<div class="t1-edate">{cat}</div>
|
{e.bullets.filter(Boolean).length > 0 && (
|
||||||
<div class="t1-eright">
|
<ul class="t1-bullets">
|
||||||
<div class="t1-edesc">{skills.map((sk: any) => `${sk.name}: ${['','Grundkenntnisse','Gut','Sehr gut','Experte','Meister'][sk.level] || sk.level}`).join(' · ')}</div>
|
{e.bullets.filter(Boolean).map((b) => (
|
||||||
|
<li>{b}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</section>
|
||||||
</section>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{d.certifications.length > 0 && (
|
{
|
||||||
<section class="t1-sec">
|
d.experience.length > 0 && (
|
||||||
<div class="t1-heading"><span class="t1-hicon">📜</span>{T ? 'COURSES, TRAINING' : 'WEITERBILDUNGEN, KURSE'}</div>
|
<section class="t1-sec">
|
||||||
{d.certifications.map(c => (
|
<div class="t1-heading">
|
||||||
<div class="t1-entry">
|
<span class="t1-hicon">💼</span>
|
||||||
<div class="t1-edate">{c.dateFrom}{c.dateTo && c.dateTo !== c.dateFrom ? ` – ${c.dateTo}` : ''}</div>
|
{T ? "WORK EXPERIENCE" : "ARBEITSERFAHRUNG"}
|
||||||
<div class="t1-eright">
|
</div>
|
||||||
<div class="t1-etitle">{c.name}</div>
|
{d.experience.map((e) => (
|
||||||
{c.issuer && <div class="t1-ecompany">{c.issuer}{c.location ? `, ${c.location}` : ''}</div>}
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">
|
||||||
|
{datRange(e.dateFrom, e.dateTo, e.current)}
|
||||||
|
</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-etitle">{e.jobTitle}</div>
|
||||||
|
{e.company && (
|
||||||
|
<div class="t1-ecompany">
|
||||||
|
{e.company}
|
||||||
|
{e.location ? `, ${e.location}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{e.description && <div class="t1-edesc">{e.description}</div>}
|
||||||
|
{e.bullets.filter(Boolean).length > 0 && (
|
||||||
|
<ul class="t1-bullets">
|
||||||
|
{e.bullets.filter(Boolean).map((b) => {
|
||||||
|
const [bold, rest] = b.includes(":")
|
||||||
|
? [b.split(":")[0], b.split(":").slice(1).join(":")]
|
||||||
|
: [null, b];
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{bold ? (
|
||||||
|
<>
|
||||||
|
<strong>{bold}</strong>:{rest}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
rest
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</section>
|
||||||
</section>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
{d.achievements.length > 0 && (
|
{
|
||||||
<section class="t1-sec">
|
d.skills.length > 0 && (
|
||||||
<div class="t1-heading"><span class="t1-hicon">🏆</span>{T ? 'ACHIEVEMENTS' : 'ERFOLGE'}</div>
|
<section class="t1-sec">
|
||||||
<ul class="t1-bullets">{d.achievements.filter(Boolean).map(a => <li>{a}</li>)}</ul>
|
<div class="t1-heading">
|
||||||
</section>
|
<span class="t1-hicon">🔧</span>
|
||||||
)}
|
{T ? "COMPETENCIES" : "KOMPETENZEN"}
|
||||||
|
</div>
|
||||||
|
{Object.entries(
|
||||||
|
d.skills.reduce((acc: any, sk) => {
|
||||||
|
const cat = sk.category || (T ? "Other" : "Sonstiges");
|
||||||
|
if (!acc[cat]) acc[cat] = [];
|
||||||
|
acc[cat].push(sk);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
).map(([cat, skills]: any) => (
|
||||||
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">{cat}</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-edesc">
|
||||||
|
{skills
|
||||||
|
.map(
|
||||||
|
(sk: any) =>
|
||||||
|
`${sk.name}: ${["", "Grundkenntnisse", "Gut", "Sehr gut", "Experte", "Meister"][sk.level] || sk.level}`,
|
||||||
|
)
|
||||||
|
.join(" · ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.certifications.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">📜</span>
|
||||||
|
{T ? "COURSES, TRAINING" : "WEITERBILDUNGEN, KURSE"}
|
||||||
|
</div>
|
||||||
|
{d.certifications.map((c) => (
|
||||||
|
<div class="t1-entry">
|
||||||
|
<div class="t1-edate">
|
||||||
|
{c.dateFrom}
|
||||||
|
{c.dateTo && c.dateTo !== c.dateFrom ? ` – ${c.dateTo}` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="t1-eright">
|
||||||
|
<div class="t1-etitle">{c.name}</div>
|
||||||
|
{c.issuer && (
|
||||||
|
<div class="t1-ecompany">
|
||||||
|
{c.issuer}
|
||||||
|
{c.location ? `, ${c.location}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
d.achievements.length > 0 && (
|
||||||
|
<section class="t1-sec">
|
||||||
|
<div class="t1-heading">
|
||||||
|
<span class="t1-hicon">🏆</span>
|
||||||
|
{T ? "ACHIEVEMENTS" : "ERFOLGE"}
|
||||||
|
</div>
|
||||||
|
<ul class="t1-bullets">
|
||||||
|
{d.achievements.filter(Boolean).map((a) => (
|
||||||
|
<li>{a}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.t1-layout { display: grid; grid-template-columns: 220px 1fr; min-height: 1123px; }
|
.t1-layout {
|
||||||
.t1-sidebar {
|
/* Hintergrund auf Grid-Parent → wiederholt sich auf jeder Druckseite */
|
||||||
background: var(--p, #1B2A5E);
|
/*background: linear-gradient(to right, var(--p, #1b2a5e) 220px, white 220px);*/
|
||||||
color: white;
|
/*background-color: var(--p, #1b2a5e);*/
|
||||||
padding: 0 0 24px 0;
|
-webkit-print-color-adjust: exact;
|
||||||
}
|
print-color-adjust: exact;
|
||||||
.t1-photo-wrap { padding: 24px; display: flex; justify-content: center; }
|
display: grid;
|
||||||
.t1-photo { width: 130px; height: 130px; border-radius: 50%; object-fit: cover; border: 3px solid rgba(255,255,255,.3); }
|
grid-template-columns: 220px 1fr;
|
||||||
.t1-photo-ph { width: 130px; height: 130px; border-radius: 50%; background: rgba(255,255,255,.15); display: grid; place-items: center; color: rgba(255,255,255,.6); }
|
min-height: 100%; /* ← nicht 1123px */
|
||||||
.t1-section-head {
|
position: relative;
|
||||||
font-size: 9.5px; font-weight: 700; letter-spacing: 2px;
|
z-index: 2;
|
||||||
padding: 10px 16px 6px;
|
}
|
||||||
border-bottom: 1px solid rgba(255,255,255,.2);
|
.t1-sidebar {
|
||||||
margin-bottom: 8px;
|
/*background: var(--p, #1b2a5e);*/
|
||||||
color: rgba(255,255,255,.9);
|
color: white;
|
||||||
}
|
padding: 0 0 24px 0;
|
||||||
.t1-contact-list { padding: 0 12px 8px; }
|
/*background: var(--p, #1b2a5e);*/
|
||||||
.t1-ci { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 9px; }
|
/*box-shadow: -100vw 0 0 100vw transparent;*/
|
||||||
.t1-icon { font-size: 12px; margin-top: 1px; flex-shrink: 0; filter: brightness(0) invert(1); opacity: 0.9; }
|
}
|
||||||
.t1-cl { font-size: 7.5px; font-weight: 700; letter-spacing: 1px; color: rgba(255,255,255,.6); text-transform: uppercase; }
|
.t1-photo-wrap {
|
||||||
.t1-cv { font-size: 10px; color: rgba(255,255,255,.9); margin-top: 1px; }
|
padding: 24px;
|
||||||
.t1-cv-sm { font-size: 9.5px; }
|
display: flex;
|
||||||
.t1-lang-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; padding: 0 12px 8px; font-size: 10px; }
|
justify-content: center;
|
||||||
.t1-lang-name { font-weight: 700; color: white; }
|
}
|
||||||
.t1-lang-level { color: rgba(255,255,255,.75); }
|
.t1-photo {
|
||||||
.t1-list { list-style: none; padding: 0 12px 8px; }
|
width: 130px;
|
||||||
.t1-list li { font-size: 10px; color: rgba(255,255,255,.85); padding: 2px 0; padding-left: 10px; position: relative; }
|
height: 130px;
|
||||||
.t1-list li::before { content: '▪'; position: absolute; left: 0; color: rgba(255,255,255,.5); font-size: 8px; top: 3px; }
|
border-radius: 50%;
|
||||||
.t1-sk-name { color: white; }
|
object-fit: cover;
|
||||||
.t1-sk-lvl { color: rgba(255,255,255,.5); font-size: 9px; }
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
.t1-photo-ph {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
.t1-section-head {
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 10px 16px 6px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
.t1-contact-list {
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
}
|
||||||
|
.t1-ci {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
}
|
||||||
|
.t1-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.t1-cl {
|
||||||
|
font-size: 7.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.t1-cv {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.t1-cv-sm {
|
||||||
|
font-size: 9.5px;
|
||||||
|
}
|
||||||
|
.t1-lang-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 4px 12px;
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.t1-lang-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.t1-lang-level {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
.t1-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
}
|
||||||
|
.t1-list li {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
padding: 2px 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.t1-list li::before {
|
||||||
|
content: "▪";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 8px;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
.t1-sk-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.t1-sk-lvl {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Main */
|
/* ── Main ─────────────────────────────────────────────────────── */
|
||||||
.t1-main { padding: 28px 28px 28px 24px; }
|
.t1-main {
|
||||||
.t1-name { font-size: 28px; font-weight: 700; color: var(--p, #1B2A5E); letter-spacing: -0.5px; margin-bottom: 2px; }
|
padding: 28px 28px 28px 24px;
|
||||||
.t1-jobtitle { font-size: 11px; color: #666; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 16px; }
|
background-color: transparent;
|
||||||
.t1-sec { margin-bottom: 16px; }
|
}
|
||||||
.t1-heading {
|
.t1-sidebar:before {
|
||||||
display: flex; align-items: center; gap: 6px;
|
background-color: var(--p, #1b2a5e);
|
||||||
font-size: 10px; font-weight: 700; letter-spacing: 2.5px;
|
position: fixed;
|
||||||
color: var(--p, #1B2A5E);
|
top: 0;
|
||||||
border-bottom: 2px solid var(--p, #1B2A5E);
|
left: 0;
|
||||||
padding-bottom: 4px; margin-bottom: 10px;
|
right: 0;
|
||||||
}
|
bottom: 0;
|
||||||
.t1-hicon { font-size: 13px; filter: brightness(0); opacity: 0.8; }
|
z-index: -1;
|
||||||
.t1-entry { display: grid; grid-template-columns: 100px 1fr; gap: 8px; margin-bottom: 8px; }
|
width: 220px;
|
||||||
.t1-edate { font-size: 9.5px; color: #888; padding-top: 1px; }
|
content: " ";
|
||||||
.t1-etitle { font-size: 11px; font-weight: 700; color: #1a1a1a; }
|
}
|
||||||
.t1-ecompany { font-size: 10px; color: var(--a, #4A7BC5); margin-top: 1px; }
|
.t1-name {
|
||||||
.t1-edesc { font-size: 10px; color: #444; margin-top: 3px; }
|
font-size: 28px;
|
||||||
.t1-profile-text { font-size: 10.5px; color: #444; line-height: 1.55; }
|
font-weight: 700;
|
||||||
.t1-bullets { list-style: none; margin-top: 4px; }
|
color: var(--p, #1b2a5e);
|
||||||
.t1-bullets li { font-size: 10px; color: #333; padding: 2px 0 2px 12px; position: relative; }
|
letter-spacing: -0.5px;
|
||||||
.t1-bullets li::before { content: '▪'; position: absolute; left: 0; color: var(--p, #1B2A5E); font-size: 8px; top: 4px; }
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.t1-jobtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.t1-sec {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.t1-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2.5px;
|
||||||
|
color: var(--p, #1b2a5e);
|
||||||
|
border-bottom: 2px solid var(--p, #1b2a5e);
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.t1-hicon {
|
||||||
|
font-size: 13px;
|
||||||
|
filter: brightness(0);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.t1-entry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.t1-edate {
|
||||||
|
font-size: 9.5px;
|
||||||
|
color: #888;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
.t1-etitle {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
.t1-ecompany {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--a, #4a7bc5);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.t1-edesc {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #444;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Profile / Markdown ───────────────────────────────────────── */
|
||||||
|
.t1-profile-text {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.t1-profile-text.markdown :global(p) {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
}
|
||||||
|
.t1-profile-text.markdown :global(ul),
|
||||||
|
.t1-profile-text.markdown :global(ol) {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
.t1-profile-text.markdown :global(li) {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.t1-profile-text.markdown :global(strong) {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.t1-profile-text.markdown :global(em) {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.t1-profile-text.markdown :global(*:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Bullets ──────────────────────────────────────────────────── */
|
||||||
|
.t1-bullets {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.t1-bullets li {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #333;
|
||||||
|
padding: 2px 0 2px 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.t1-bullets li::before {
|
||||||
|
content: "▪";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--p, #1b2a5e);
|
||||||
|
font-size: 8px;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar-Hintergrund via cv-a4 Gradient ──────────────────── */
|
||||||
|
/* Liegt auf dem äußersten Container → läuft durch alle Druckseiten */
|
||||||
|
:global(.cv-aaa4.t1) {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--p, #1b2a5e) 220px,
|
||||||
|
white 220px
|
||||||
|
) !important;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Print ────────────────────────────────────────────────────── */
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: A4 portrait;
|
||||||
|
margin: 10mm 0 0 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
@page :first {
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t1-layout {
|
||||||
|
border: 0px solid red;
|
||||||
|
width: 210mm;
|
||||||
|
height: 297mm;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
border: 0px solid blue;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.cv-aaa4.t1) {
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--p, #1b2a5e) 220px,
|
||||||
|
white 220px
|
||||||
|
) !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
}
|
||||||
|
.t1-entry,
|
||||||
|
.t1-sec {
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,43 @@
|
|||||||
export const IcoEmail = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>';
|
/***
|
||||||
export const IcoPhone = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>';
|
<!-- 1. Fertiger Tag direkt -->
|
||||||
export const IcoCal = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>';
|
<Fragment set:html={IcoPersonTag} />
|
||||||
export const IcoFlag = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>';
|
|
||||||
export const IcoHome = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>';
|
<!-- 2. Eigene Größe -->
|
||||||
export const IcoPerson = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>';
|
<Fragment set:html={icoTag(IcoPerson, '14px')} />
|
||||||
export const IcoEdu = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><path d="M22 10v6M2 10l10-5 10 5-10 5z"></path><path d="M6 12v5c3 3 9 3 12 0v-5"></path></svg>';
|
|
||||||
export const IcoExp = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;vertical-align:-0.15em"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>';
|
<!-- 3. Nur die Data-URL, eigenes img -->
|
||||||
|
<img src={IcoPerson} style="width:14px;height:14px" />
|
||||||
|
***/
|
||||||
|
|
||||||
|
// Auto-generated PNG icons (pure Node.js, no dependencies)
|
||||||
|
// All icons: 20×20px transparent PNG, dark (#444) strokes
|
||||||
|
|
||||||
|
export const IcoEmail = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAh0lEQVR4nO3TwQ2AIAwFUFZgBVboAF7Yy9EYTcMBU0pbS+vRJj9RiU/yIyn988nknI9a6xUJALQHjGIjpZSTBfuCJX1XJnDa/kZNC4i/qqEYw9cs2O81lGLc++sDAeUwM0hRCdsCMSph2yBGOcwFjv6kNReozeuPHT56tHhvplqs51eK1nFobtcpL2DD8YzgAAAAAElFTkSuQmCC';
|
||||||
|
export const IcoPhone = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAk0lEQVR4nL2U0QnAIAxEXcEVsoID9Cd7dbSO1pKPgyPEDz2pINgWX8+7mNb+GL33y93fmGOMRwYChhk/OAIMdTKMgWZ2y7BKoexj9lAOp4JJUPYQoUjQWSjbYVUbY433ywqr48mlBChuCiuMb+zvNpSft8uJlUFpBi/bwN2HN0u+Vi1NDoqh0pHzyB4eaXGhqiqdD7k1qABJQoCkAAAAAElFTkSuQmCC';
|
||||||
|
export const IcoCal = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAd0lEQVR4nGNgoBcQFBS0NzY23k+sOEHg4uLyH4SVlJTqiRHHsBWmkFyM4mpKDcNwNbItIEEQxiaGSxyngcjhAtIAEgMFB3LwYBMnykBY2OIKc7wRRbMwpGmkkIKJDkNiwaiBQ8lAqqVDWP6kFKNkSXISNDKGGQYAXgddF8yb6SQAAAAASUVORK5CYII=';
|
||||||
|
export const IcoFlag = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAbUlEQVR4nGNgoCUQFBS0d3Fx+W9sbLxfSUmpHoQpMhBkGC4MswBkKVUMxGcJQQORFcE0ETIcFExEGYgOQBpxGYoRHNgMBCkC8fEZBJLDGrbICgh5k6gIIhRGJCcjslxBjIEUJ+hRA0cNHG4GAgCFC737QwHrBwAAAABJRU5ErkJggg==';
|
||||||
|
export const IcoHome = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAi0lEQVR4nN3TwQ2AIAwFUFZgBVZgAC/s5WiOpvkHkvqh2ArxIMlPBORRiIbwixZj3HLOxzKslHIi0yhjU6jE8Iyx12gPq82NjjA3asHMqAd7RCWGpJR2CyjX3NC60wzYnExWWUGunCuR73eviUHGtHn1RNoCVIWxZaC1/x3ojQry52PN8EfAbp4wdgHqUfXzO48S3QAAAABJRU5ErkJggg==';
|
||||||
|
export const IcoPerson = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAkElEQVR4nGNgGNJAUFDQ3tjYeL+Li8t/GFZSUqon2zBkg5AxyBKKDAO5CoSRXUuSocga0eVABsPkQBYTZSCh8CI5PGlmILZwQg5fog1EDidkTejJiCjDYAA9/aFjoiOEGEOJNgxfYiY5kZNjGF5DYd4D0bCcQQzGGgzIriM1sJEdgl+QSIDVMZQWTRj6B7WBAIJ63VJfa/AIAAAAAElFTkSuQmCC';
|
||||||
|
export const IcoEdu = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAmUlEQVR4nO2T0Q2AMAhEWaErdIUO4E/3cjRH02BSQ/Aq15r4JQkfWngnVxT545PIOa+llC2ltLwG1Vp3m1NgbUAg/0yBfKN+JXtOg4bq/XgjHnmPTxus0ozhHnq+nDEcXdwFDH0h/LsBWyBVbUAgKxoCUTM6o4Gt0I5jL83XNZEQaL2L6uCG9Mbs5dMPIAJWKEpqb1WVSQQ7AMPb/9EAxbXuAAAAAElFTkSuQmCC';
|
||||||
|
export const IcoExp = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAWklEQVR4nGNgGAXIQFBQ0N7FxeU/MjY2Nt5PtoHohsGwkpJSPUUGglwFMoRoA7F5jVSMEhSUGobhcnSvoWNi5TEMxBU2JMuPGjhq4GAykGpZD5SlqGEgqJABABvuMFINme7XAAAAAElFTkSuQmCC';
|
||||||
|
export const IcoRings = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAhklEQVR4nGNgGAWDHwgKCtobGxvvd3Fx+Q/DSkpK9bjECRqGrIEYDLKEKMNAtoMwNlehi2M1FFkBLnEQBlkMkwMZjE0cDHCFC7oXccljhCfNDEQPD3wGIoc7hoHI4YEsiSyOHL7oyQgjUrBFALEYI0KIMRSXOF7DkL0DS2/oYYZNfBRQFwAAfjjIRhgi+hMAAAAASUVORK5CYII=';
|
||||||
|
export const IcoLink = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAd0lEQVR4nGNgGJZAUFDQ3sXF5T8IGxsb76eaYTCspKRUT7FhIEPIMhCbi0BiyOIgNtEGgsII3XvIYiQZBgPohlBkGC6XUmQYemRQzTCqGUR1wygyEN0w5MggOZuhhxdybiDLpegxicwnK3bRXTO4Ei8MkO09WgEA2VWQrb3SXjUAAAAASUVORK5CYII=';
|
||||||
|
export const IcoGlobe = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAn0lEQVR4nL2U0Q3AIAhEXcEVXIEB+uNeHa2jtfFDc7GHoKY1ucSIPBHQEP4aMcYjpXTmnG+UiFxlfQrGQAxcDjVhZWPvyOZVQyhz8IhCPdcc6VWAPukor41Gx1JRbay61Jee4gTi7Zp9J3eo74Es6VZR0N6A2H87RWn9SBedQDWY1VeiBoLln5XWbhRqfQ4qbCVSEzaTU9dfyEbfcxboAUk6gd3N0EXVAAAAAElFTkSuQmCC';
|
||||||
|
|
||||||
|
// ── Helper: PNG-Data-URL als fertiges <img>-Tag ───────────────────────
|
||||||
|
export function icoTag(src: string, size = '1em'): string {
|
||||||
|
return `<img src="${src}" style="width:${size};height:${size};vertical-align:-0.15em;display:inline-block;flex-shrink:0"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fertige <img>-Tags für direkte Verwendung mit set:html ────────────
|
||||||
|
export const IcoEmailTag = icoTag(IcoEmail);
|
||||||
|
export const IcoPhoneTag = icoTag(IcoPhone);
|
||||||
|
export const IcoCalTag = icoTag(IcoCal);
|
||||||
|
export const IcoFlagTag = icoTag(IcoFlag);
|
||||||
|
export const IcoHomeTag = icoTag(IcoHome);
|
||||||
|
export const IcoPersonTag = icoTag(IcoPerson);
|
||||||
|
export const IcoEduTag = icoTag(IcoEdu);
|
||||||
|
export const IcoExpTag = icoTag(IcoExp);
|
||||||
|
export const IcoRingsTag = icoTag(IcoRings);
|
||||||
|
export const IcoLinkTag = icoTag(IcoLink);
|
||||||
|
export const IcoGlobeTag = icoTag(IcoGlobe);
|
||||||
|
|||||||
80
src/lib/icons_svg.ts
Normal file
80
src/lib/icons_svg.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Wraps SVG string as base64 img tag → PDF-kompatibel
|
||||||
|
// Buffer.from() statt btoa() → Node/Astro-SSR-kompatibel
|
||||||
|
// Farbe ist fest kodiert (kein currentColor) → funktioniert in data:image/svg+xml
|
||||||
|
|
||||||
|
/***
|
||||||
|
---
|
||||||
|
import { IcoPersonW, IcoEmailW, makeIco } from '../lib/icons';
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--Weiß für dunkle Sidebar-- >
|
||||||
|
<Fragment set: html = { IcoPersonW } />
|
||||||
|
|
||||||
|
<!--Beliebige Farbe-- >
|
||||||
|
<Fragment set: html = { makeIco('email', '#1B2A5E') } />
|
||||||
|
***/
|
||||||
|
function ico(svg: string): string {
|
||||||
|
const b64 = Buffer.from(svg, 'utf-8').toString('base64');
|
||||||
|
return `<img src="data:image/svg+xml;base64,${b64}" style="width:14px;height:14px;vertical-align:-0.15em;display:inline-block;flex-shrink:0"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IcoEmail = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>');
|
||||||
|
|
||||||
|
export const IcoPhone = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>');
|
||||||
|
|
||||||
|
export const IcoCal = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>');
|
||||||
|
|
||||||
|
export const IcoFlag = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>');
|
||||||
|
|
||||||
|
export const IcoHome = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>');
|
||||||
|
|
||||||
|
export const IcoPerson = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>');
|
||||||
|
|
||||||
|
export const IcoEdu = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>');
|
||||||
|
|
||||||
|
export const IcoExp = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>');
|
||||||
|
|
||||||
|
export const IcoRings = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="12" r="4"/><circle cx="16" cy="12" r="4"/></svg>');
|
||||||
|
|
||||||
|
export const IcoLink = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>');
|
||||||
|
|
||||||
|
export const IcoGlobe = ico('<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>');
|
||||||
|
|
||||||
|
// ── Weiß-Varianten für dunkle Sidebar ────────────────────────────────
|
||||||
|
function icoColor(svg: string, color: string): string {
|
||||||
|
const colored = svg.replace(/stroke="#444"/g, `stroke="${color}"`);
|
||||||
|
const b64 = Buffer.from(colored, 'utf-8').toString('base64');
|
||||||
|
return `<img src="data:image/svg+xml;base64,${b64}" style="width:1em;height:1em;vertical-align:-0.15em;display:inline-block;flex-shrink:0"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _svgs: Record<string, string> = {
|
||||||
|
email: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>',
|
||||||
|
phone: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>',
|
||||||
|
cal: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
||||||
|
flag: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>',
|
||||||
|
home: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
||||||
|
person: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
||||||
|
edu: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>',
|
||||||
|
exp: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>',
|
||||||
|
rings: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="12" r="4"/><circle cx="16" cy="12" r="4"/></svg>',
|
||||||
|
link: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
|
||||||
|
globe: '<svg viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Weiß-Varianten
|
||||||
|
export const IcoEmailW = icoColor(_svgs.email, '#fff');
|
||||||
|
export const IcoPhoneW = icoColor(_svgs.phone, '#fff');
|
||||||
|
export const IcoCalW = icoColor(_svgs.cal, '#fff');
|
||||||
|
export const IcoFlagW = icoColor(_svgs.flag, '#fff');
|
||||||
|
export const IcoHomeW = icoColor(_svgs.home, '#fff');
|
||||||
|
export const IcoPersonW = icoColor(_svgs.person, '#fff');
|
||||||
|
export const IcoEduW = icoColor(_svgs.edu, '#fff');
|
||||||
|
export const IcoExpW = icoColor(_svgs.exp, '#fff');
|
||||||
|
export const IcoRingsW = icoColor(_svgs.rings, '#fff');
|
||||||
|
export const IcoLinkW = icoColor(_svgs.link, '#fff');
|
||||||
|
export const IcoGlobeW = icoColor(_svgs.globe, '#fff');
|
||||||
|
|
||||||
|
// ── Factory für beliebige Farben ──────────────────────────────────────
|
||||||
|
export function makeIco(name: keyof typeof _svgs, color = '#444'): string {
|
||||||
|
return icoColor(_svgs[name], color);
|
||||||
|
}
|
||||||
59
src/pages/api/cv/[id]/pdf.ts
Normal file
59
src/pages/api/cv/[id]/pdf.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { getCVById, getProfileById } from '../../../../lib/db';
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params, locals, request }) => {
|
||||||
|
const user = locals.user;
|
||||||
|
const cv = getCVById(params.id!, user?.id);
|
||||||
|
if (!cv) return new Response('Not Found', { status: 404 });
|
||||||
|
|
||||||
|
const profile = getProfileById(cv.profile_id, user?.id);
|
||||||
|
if (!profile) return new Response('Profile Not Found', { status: 404 });
|
||||||
|
|
||||||
|
const name = [profile.data.personal.firstName, profile.data.personal.lastName]
|
||||||
|
.filter(Boolean).join(' ') || cv.title;
|
||||||
|
|
||||||
|
const origin = new URL(request.url).origin;
|
||||||
|
const targetUrl = `${origin}/cv/${cv.hash}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dynamic import – verhindert Vite-Verarbeitung
|
||||||
|
const { default: puppeteer } = await import('puppeteer');
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 794, height: 1123 });
|
||||||
|
await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 20000 });
|
||||||
|
|
||||||
|
// Toolbar ausblenden
|
||||||
|
await page.addStyleTag({
|
||||||
|
content: `
|
||||||
|
.cv-public-bar { display: none !important; }
|
||||||
|
body { background: white !important; padding: 0 !important; }
|
||||||
|
.cv-public-wrap { padding: 0 !important; background: white !important; }
|
||||||
|
`
|
||||||
|
});
|
||||||
|
await page.emulateMediaType('print');
|
||||||
|
const pdf = await page.pdf({
|
||||||
|
format: 'A4',
|
||||||
|
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
||||||
|
printBackground: true,
|
||||||
|
scale: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return new Response(pdf, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(name)}.pdf"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[PDF]', err.message);
|
||||||
|
return new Response(err.message, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
57
src/pages/api/pdf/[hash].ts
Normal file
57
src/pages/api/pdf/[hash].ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { getCVByHash, getProfileById } from '../../../lib/db';
|
||||||
|
|
||||||
|
// Öffentlicher Endpunkt – kein Login nötig, CV muss public=1 sein
|
||||||
|
export const GET: APIRoute = async ({ params, request }) => {
|
||||||
|
const cv = getCVByHash(params.hash!);
|
||||||
|
if (!cv) return new Response('Not Found', { status: 404 });
|
||||||
|
|
||||||
|
const profile = getProfileById(cv.profile_id);
|
||||||
|
if (!profile) return new Response('Profile Not Found', { status: 404 });
|
||||||
|
|
||||||
|
const name = [profile.data.personal.firstName, profile.data.personal.lastName]
|
||||||
|
.filter(Boolean).join(' ') || cv.title;
|
||||||
|
|
||||||
|
const origin = new URL(request.url).origin;
|
||||||
|
const targetUrl = `${origin}/cv/${cv.hash}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { default: puppeteer } = await import('puppeteer');
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 794, height: 1123 });
|
||||||
|
await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 20000 });
|
||||||
|
|
||||||
|
await page.addStyleTag({
|
||||||
|
content: `
|
||||||
|
.cv-public-bar { display: none !important; }
|
||||||
|
body { background: white !important; padding: 0 !important; }
|
||||||
|
.cv-public-wrap { padding: 0 !important; background: white !important; }
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdf = await page.pdf({
|
||||||
|
format: 'A4',
|
||||||
|
margin: { top: '0mm', right: '0mm', bottom: '0mm', left: '0mm' },
|
||||||
|
printBackground: true,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return new Response(pdf, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(name)}.pdf"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[PDF Hash]', err.message);
|
||||||
|
return new Response(err.message, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,126 +1,398 @@
|
|||||||
---
|
---
|
||||||
import { getCVByHash, getProfileById } from '../../lib/db';
|
import { getCVByHash, getProfileById } from "../../lib/db";
|
||||||
import { useT } from '../../lib/i18n';
|
import { useT } from "../../lib/i18n";
|
||||||
import Template1 from '../../components/templates/Template1.astro';
|
import Template1 from "../../components/templates/Template1.astro";
|
||||||
import Template2 from '../../components/templates/Template2.astro';
|
import Template2 from "../../components/templates/Template2.astro";
|
||||||
import Template3 from '../../components/templates/Template3.astro';
|
import Template3 from "../../components/templates/Template3.astro";
|
||||||
import Template4 from '../../components/templates/Template4.astro';
|
import Template4 from "../../components/templates/Template4.astro";
|
||||||
|
|
||||||
const { hash } = Astro.params;
|
const { hash } = Astro.params;
|
||||||
const cv = getCVByHash(hash!);
|
const cv = getCVByHash(hash!);
|
||||||
if (!cv) return new Response('Not Found', { status: 404 });
|
if (!cv) return new Response("Not Found", { status: 404 });
|
||||||
|
|
||||||
const profile = getProfileById(cv.profile_id);
|
const profile = getProfileById(cv.profile_id);
|
||||||
if (!profile) return new Response('Profile Not Found', { status: 404 });
|
if (!profile) return new Response("Profile Not Found", { status: 404 });
|
||||||
|
|
||||||
const lang = cv.settings.language || 'de';
|
const lang = cv.settings.language || "de";
|
||||||
const t = useT(lang as any);
|
const t = useT(lang as any);
|
||||||
const name = `${profile.data.personal.firstName} ${profile.data.personal.lastName}`.trim() || cv.title;
|
const name =
|
||||||
const appUrl = import.meta.env.APP_URL || Astro.url.origin;
|
`${profile.data.personal.firstName} ${profile.data.personal.lastName}`.trim() ||
|
||||||
|
cv.title;
|
||||||
|
const de = lang === "de";
|
||||||
---
|
---
|
||||||
<!DOCTYPE html>
|
|
||||||
|
<!doctype html>
|
||||||
<html lang={lang}>
|
<html lang={lang}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{name} – Lebenslauf</title>
|
<title>{name} – Lebenslauf</title>
|
||||||
<meta name="description" content={`Lebenslauf von ${name}`} />
|
<meta name="description" content={`Lebenslauf von ${name}`} />
|
||||||
<meta property="og:title" content={name} />
|
<meta property="og:title" content={name} />
|
||||||
<meta property="og:description" content={profile.data.personal.jobTitle || ''} />
|
<meta
|
||||||
<link rel="stylesheet" href="/styles/global.css" />
|
property="og:description"
|
||||||
|
content={profile.data.personal.jobTitle || ""}
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"
|
||||||
|
defer></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #e8ecf0;
|
||||||
|
}
|
||||||
|
.cv-public-wrap {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 24px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.cv-public-bar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 794px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.cv-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.cv-info-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.cv-info-sub {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.cv-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.pdf-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.pdf-dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 200px;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
.pdf-dropdown-menu.open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.pdf-dropdown-menu button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 0.83rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
.pdf-dropdown-menu button:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.pdf-dropdown-menu button span {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.pdf-dropdown-menu hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 840px) {
|
||||||
|
.cv-public-bar {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.cv-a4 {
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.cv-public-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.cv-public-wrap {
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cv-public-wrap">
|
||||||
|
<div class="cv-public-bar">
|
||||||
|
<div class="cv-info">
|
||||||
|
<span class="cv-info-name">{name}</span>
|
||||||
|
<span class="cv-info-sub">{profile.data.personal.jobTitle || ""}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="cv-actions">
|
||||||
|
<div class="pdf-dropdown">
|
||||||
|
<button onclick="togglePdfMenu()" class="btn btn-primary btn-sm"
|
||||||
|
>↓ PDF ▾</button
|
||||||
|
>
|
||||||
|
<div class="pdf-dropdown-menu" id="pdf-menu">
|
||||||
|
<button onclick="exportPDFPrint()">
|
||||||
|
🖨️ {de ? "Browser-Druck" : "Browser Print"}
|
||||||
|
<span
|
||||||
|
>{
|
||||||
|
de
|
||||||
|
? "Empfohlen – Icons & Farben korrekt"
|
||||||
|
: "Recommended – icons & colors correct"
|
||||||
|
}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<hr />
|
||||||
|
<button id="btn-puppeteer" onclick="exportPDFPuppeteer(event)">
|
||||||
|
📄 {de ? "PDF Download" : "PDF Download"}
|
||||||
|
<span
|
||||||
|
>{
|
||||||
|
de
|
||||||
|
? "Serverseitig, beste Qualität"
|
||||||
|
: "Server-side, best quality"
|
||||||
|
}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="exportHTML()" class="btn btn-ghost btn-sm"
|
||||||
|
>🌐 HTML</button
|
||||||
|
>
|
||||||
|
<button onclick="exportJSON()" class="btn btn-ghost btn-sm"
|
||||||
|
>{} JSON</button
|
||||||
|
>
|
||||||
|
<a href="/" class="btn btn-ghost btn-sm">🏠 App</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cv-content">
|
||||||
|
{
|
||||||
|
cv.template === 1 && (
|
||||||
|
<Template1 data={profile.data} settings={cv.settings} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cv.template === 2 && (
|
||||||
|
<Template2 data={profile.data} settings={cv.settings} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cv.template === 3 && (
|
||||||
|
<Template3 data={profile.data} settings={cv.settings} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cv.template === 4 && (
|
||||||
|
<Template4 data={profile.data} settings={cv.settings} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script
|
||||||
|
define:vars={{
|
||||||
|
cvTitle: name,
|
||||||
|
cvData: profile.data,
|
||||||
|
cvSettings: cv.settings,
|
||||||
|
cvTemplate: cv.template,
|
||||||
|
cvHash: cv.hash,
|
||||||
|
de,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
window.togglePdfMenu = function () {
|
||||||
|
document.getElementById("pdf-menu").classList.toggle("open");
|
||||||
|
};
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!e.target.closest(".pdf-dropdown")) {
|
||||||
|
document.getElementById("pdf-menu").classList.remove("open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── printCV inline ────────────────────────────────────────────
|
||||||
|
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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link href="${fontUrl}" rel="stylesheet">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" defer></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js" defer></script>
|
|
||||||
<style>
|
<style>
|
||||||
body { background: #E8ECF0; }
|
${globalCss}
|
||||||
.cv-public-wrap { min-height: 100vh; padding: 24px 16px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
|
${inlineStyles}
|
||||||
.cv-public-bar {
|
html { margin: 0; padding: 0; }
|
||||||
width: 100%;
|
body { margin: 0 !important; padding: 0 !important; display: flex; justify-content: center; background: white; }
|
||||||
max-width: 794px;
|
${sidebarCss}
|
||||||
background: white;
|
.cv-a4 { width: 210mm !important; min-height: 297mm !important; box-shadow: none !important; border: none !important; margin: 0 !important; }
|
||||||
border-radius: 10px;
|
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||||
padding: 12px 20px;
|
.t1-entry, .t1-sec, .t2-tl-item, .t2-sec, .t3-entry, .t3-sec, .t4-entry, .t4-sec { break-inside: avoid; }
|
||||||
display: flex;
|
@page { size: A4 portrait; margin: 0; }
|
||||||
align-items: center;
|
@page :first { margin: 0; }
|
||||||
justify-content: space-between;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,.08);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.cv-info { display: flex; flex-direction: column; }
|
|
||||||
.cv-info-name { font-weight: 700; font-size: .95rem; }
|
|
||||||
.cv-info-sub { font-size: .75rem; color: #888; }
|
|
||||||
.cv-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
||||||
@media (max-width: 840px) {
|
|
||||||
.cv-public-bar { max-width: 100%; }
|
|
||||||
.cv-a4 { width: 100% !important; min-height: auto !important; }
|
|
||||||
}
|
|
||||||
@media print {
|
|
||||||
.cv-public-bar { display: none !important; }
|
|
||||||
body { background: white; }
|
|
||||||
.cv-public-wrap { padding: 0; background: white; }
|
|
||||||
}
|
|
||||||
</style>
|
</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("pdf-menu").classList.remove("open");
|
||||||
|
const el = document.getElementById("cv-content");
|
||||||
|
printCV(el, cvTitle, cvSettings, cvTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── PDF via Puppeteer (serverseitig) ──────────────────────────
|
||||||
|
window.exportPDFPuppeteer = async function (e) {
|
||||||
|
document.getElementById("pdf-menu").classList.remove("open");
|
||||||
|
const btn = document.getElementById("btn-puppeteer");
|
||||||
|
const origHtml = btn?.innerHTML;
|
||||||
|
if (btn)
|
||||||
|
btn.innerHTML = "⏳ " + (de ? "Generiert..." : "Generating...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/pdf/${cvHash}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = await res.text();
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
const blob = await res.blob();
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = cvTitle + ".pdf";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
} catch (err) {
|
||||||
|
alert((de ? "PDF-Fehler: " : "PDF error: ") + err.message);
|
||||||
|
} finally {
|
||||||
|
if (btn && origHtml) btn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HTML Export ───────────────────────────────────────────────
|
||||||
|
window.exportHTML = async function () {
|
||||||
|
const el = document.getElementById("cv-content");
|
||||||
|
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>
|
</head>
|
||||||
<body>
|
<body style="margin:0;background:#E8ECF0;display:flex;justify-content:center;padding:32px">
|
||||||
<div class="cv-public-wrap">
|
${el.innerHTML}
|
||||||
<div class="cv-public-bar">
|
|
||||||
<div class="cv-info">
|
|
||||||
<span class="cv-info-name">{name}</span>
|
|
||||||
<span class="cv-info-sub">{profile.data.personal.jobTitle || ''}</span>
|
|
||||||
</div>
|
|
||||||
<div class="cv-actions">
|
|
||||||
<button onclick="exportPDF()" class="btn btn-primary btn-sm">↓ PDF</button>
|
|
||||||
<button onclick="exportHTML()" class="btn btn-ghost btn-sm">🌐 HTML</button>
|
|
||||||
<button onclick="exportJSON()" class="btn btn-ghost btn-sm">{ } JSON</button>
|
|
||||||
<button onclick="window.print()" class="btn btn-ghost btn-sm">🖨️ {lang==='de'?'Drucken':'Print'}</button>
|
|
||||||
<a href="/" class="btn btn-ghost btn-sm">🏠 App</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cv-content">
|
|
||||||
{cv.template === 1 && <Template1 data={profile.data} settings={cv.settings} />}
|
|
||||||
{cv.template === 2 && <Template2 data={profile.data} settings={cv.settings} />}
|
|
||||||
{cv.template === 3 && <Template3 data={profile.data} settings={cv.settings} />}
|
|
||||||
{cv.template === 4 && <Template4 data={profile.data} settings={cv.settings} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script define:vars={{ cvTitle: name, cvData: profile.data, cvSettings: cv.settings }}>
|
|
||||||
window.exportPDF = function() {
|
|
||||||
const el = document.getElementById('cv-content');
|
|
||||||
window.html2pdf().set({
|
|
||||||
margin: 0,
|
|
||||||
filename: cvTitle + '.pdf',
|
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
|
||||||
html2canvas: { scale: 2, useCORS: true },
|
|
||||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
|
||||||
}).from(el).save();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.exportHTML = function() {
|
|
||||||
const el = document.getElementById('cv-content');
|
|
||||||
const styles = Array.from(document.styleSheets).map(ss => {
|
|
||||||
try { return Array.from(ss.cssRules).map(r => r.cssText).join('\n'); } catch { return ''; }
|
|
||||||
}).join('\n');
|
|
||||||
const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${cvTitle}</title><style>${styles}</style></head><body style="margin:0;background:#E8ECF0;display:flex;justify-content:center;padding:32px">${el.innerHTML}</body></html>`;
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
|
|
||||||
a.download = cvTitle + '.html';
|
|
||||||
a.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.exportJSON = function() {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = URL.createObjectURL(new Blob(
|
|
||||||
[JSON.stringify({ title: cvTitle, settings: cvSettings, data: cvData }, null, 2)],
|
|
||||||
{ type: 'application/json' }
|
|
||||||
));
|
|
||||||
a.download = cvTitle + '.json';
|
|
||||||
a.click();
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
</html>`;
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
||||||
|
a.download = cvTitle + ".html";
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── JSON Export ───────────────────────────────────────────────
|
||||||
|
window.exportJSON = function () {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(
|
||||||
|
new Blob(
|
||||||
|
[
|
||||||
|
JSON.stringify(
|
||||||
|
{ title: cvTitle, settings: cvSettings, data: cvData },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
{ type: "application/json" },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
a.download = cvTitle + ".json";
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
import { getCVById, getProfileById } from '../../lib/db';
|
import { getCVById, getProfileById } from "../../lib/db";
|
||||||
import { getLang, useT } from '../../lib/i18n';
|
import { getLang, useT } from "../../lib/i18n";
|
||||||
import { TEMPLATES } from '../../types';
|
import { TEMPLATES } from "../../types";
|
||||||
import Template1 from '../../components/templates/Template1.astro';
|
import Template1 from "../../components/templates/Template1.astro";
|
||||||
import Template2 from '../../components/templates/Template2.astro';
|
import Template2 from "../../components/templates/Template2.astro";
|
||||||
import Template3 from '../../components/templates/Template3.astro';
|
import Template3 from "../../components/templates/Template3.astro";
|
||||||
import Template4 from '../../components/templates/Template4.astro';
|
import Template4 from "../../components/templates/Template4.astro";
|
||||||
|
|
||||||
const { id } = Astro.params;
|
const { id } = Astro.params;
|
||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
@@ -13,322 +13,629 @@ const lang = getLang(Astro.request);
|
|||||||
const t = useT(lang);
|
const t = useT(lang);
|
||||||
|
|
||||||
const cv = getCVById(id!, user.id);
|
const cv = getCVById(id!, user.id);
|
||||||
if (!cv) return Astro.redirect('/dashboard');
|
if (!cv) return Astro.redirect("/dashboard");
|
||||||
|
|
||||||
const profile = getProfileById(cv.profile_id, user.id);
|
const profile = getProfileById(cv.profile_id, user.id);
|
||||||
if (!profile) return Astro.redirect('/dashboard');
|
if (!profile) return Astro.redirect("/dashboard");
|
||||||
|
|
||||||
const d = profile.data;
|
const d = profile.data;
|
||||||
const s = cv.settings;
|
const s = cv.settings;
|
||||||
const de = lang === 'de';
|
const de = lang === "de";
|
||||||
|
|
||||||
function j(v: any) { return JSON.stringify(v).replace(/</g, '\\u003c'); }
|
function j(v: any) {
|
||||||
|
return JSON.stringify(v).replace(/</g, "\\u003c");
|
||||||
|
}
|
||||||
---
|
---
|
||||||
<!DOCTYPE html>
|
|
||||||
|
<!doctype html>
|
||||||
<html lang={lang}>
|
<html lang={lang}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{cv.title} – {t('app.name')}</title>
|
<title>{cv.title} – {t("app.name")}</title>
|
||||||
<link rel="stylesheet" href="/styles/global.css" />
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link href="${fontUrl}" rel="stylesheet">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" defer></script>
|
<style>
|
||||||
</head>
|
${globalCss}
|
||||||
<body>
|
${inlineStyles}
|
||||||
<header class="app-header">
|
@page { size: A4 portrait; margin: 0; }
|
||||||
<div class="flex items-center gap-3">
|
html { margin: 0; padding: 0; }
|
||||||
<a href="/dashboard" class="logo" style="font-size:.95rem">Lebenslauf<span>App</span></a>
|
body { margin: 0 !important; padding: 0 !important; display: flex; justify-content: center; background: white; }
|
||||||
<span style="color:rgba(255,255,255,.3)">›</span>
|
${sidebarCss}
|
||||||
<span id="cv-title-display" style="color:rgba(255,255,255,.9);font-size:.9rem">{cv.title}</span>
|
.cv-a4 { width: 210mm !important; min-height: 297mm !important; box-shadow: none !important; margin: 0 !important; }
|
||||||
</div>
|
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||||
<div class="flex items-center gap-2">
|
.t1-entry, .t1-sec, .t2-tl-item, .t2-sec, .t3-entry, .t3-sec, .t4-entry, .t4-sec { break-inside: avoid; }
|
||||||
<a href={`/profile/${profile.id}`} class="btn btn-ghost btn-sm" style="color:#fff">{de ? '✏️ Daten bearbeiten' : '✏️ Edit Data'}</a>
|
</style>
|
||||||
<span id="save-status" class="save-status hidden">✓ {t('editor.save')}</span>
|
</head><body>${el.innerHTML}</body></html>`;
|
||||||
<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:160px;z-index:50">
|
|
||||||
<button onclick="exportPDF()" style="width:100%;text-align:left;padding:10px 16px;border:none;background:none;font-size:.875rem;cursor:pointer">📄 PDF</button>
|
|
||||||
<button onclick="exportHTML()" style="width:100%;text-align:left;padding:10px 16px;border:none;background:none;font-size:.875rem;cursor:pointer">🌐 HTML</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="editor-layout" id="editor-layout">
|
const blob = new Blob([html], { type: "text/html; charset=utf-8" });
|
||||||
<!-- SIDEBAR -->
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
<div class="editor-sidebar">
|
const win = window.open(blobUrl, "_blank", "width=900,height=1200");
|
||||||
<div class="editor-tabs" id="editor-tabs">
|
if (!win) {
|
||||||
<button class="editor-tab active" data-tab="settings">{t('editor.tab.settings')}</button>
|
alert("Popup blockiert – bitte Popup-Blocker deaktivieren.");
|
||||||
</div>
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
return;
|
||||||
<div class="editor-content" id="editor-content">
|
}
|
||||||
<!-- SETTINGS -->
|
win.addEventListener("load", () => {
|
||||||
<div class="tab-pane" id="tab-settings">
|
URL.revokeObjectURL(blobUrl);
|
||||||
<div class="form-group">
|
setTimeout(() => {
|
||||||
<label class="form-label">{t('settings.template')}</label>
|
win.focus();
|
||||||
<div class="template-grid" id="template-grid">
|
win.print();
|
||||||
{TEMPLATES.map(tpl => (
|
win.addEventListener("afterprint", () => win.close());
|
||||||
<div class={`template-option ${cv.template === tpl.id ? 'selected' : ''}`} data-id={tpl.id} onclick="selectTemplate(this)">
|
}, 1000);
|
||||||
<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="exportPDF()" 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,
|
|
||||||
}}>
|
|
||||||
// ── State ────────────────────────────────────────────────────────
|
|
||||||
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 });
|
|
||||||
const linkRow = document.getElementById('public-link');
|
|
||||||
if (this.checked) {
|
|
||||||
if (!linkRow) {
|
|
||||||
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) {
|
// ── PDF via Browser-Druck ─────────────────────────────────────
|
||||||
document.querySelectorAll('.template-option').forEach(e => e.classList.remove('selected'));
|
window.exportPDFPrint = function () {
|
||||||
el.classList.add('selected');
|
document.getElementById("export-menu").classList.add("hidden");
|
||||||
template = parseInt(el.dataset.id);
|
const el = document.getElementById("cv-preview");
|
||||||
markDirty();
|
printCV(el, cvTitle, cvSettings, template);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.copyLink = function() {
|
// ── PDF via Puppeteer (serverseitig) ──────────────────────────
|
||||||
const link = document.getElementById('public-link');
|
window.exportPDFCanvas = async function () {
|
||||||
if (link) { navigator.clipboard.writeText(link.value); }
|
document.getElementById("export-menu").classList.add("hidden");
|
||||||
};
|
const btn = document.getElementById("btn-puppeteer");
|
||||||
|
const origHtml = btn?.innerHTML;
|
||||||
|
if (btn)
|
||||||
|
btn.innerHTML = "⏳ " + (de ? "Generiert..." : "Generating...");
|
||||||
|
|
||||||
// ── Save & Preview ───────────────────────────────────────────────
|
try {
|
||||||
function markDirty() {
|
const res = await fetch(`/api/cv/${cvId}/pdf`);
|
||||||
isDirty = true;
|
if (!res.ok) throw new Error(await res.text());
|
||||||
clearTimeout(saveTimer);
|
const blob = await res.blob();
|
||||||
saveTimer = setTimeout(saveNow, 2000);
|
const a = document.createElement("a");
|
||||||
const s = document.getElementById('save-status');
|
a.href = URL.createObjectURL(blob);
|
||||||
s.className = 'save-status saving';
|
a.download = (cvTitle || "lebenslauf") + ".pdf";
|
||||||
s.textContent = de ? '⏳ Speichert...' : '⏳ Saving...';
|
a.click();
|
||||||
s.classList.remove('hidden');
|
URL.revokeObjectURL(a.href);
|
||||||
refreshPreview();
|
} catch (err) {
|
||||||
}
|
alert((de ? "PDF-Fehler: " : "PDF error: ") + err.message);
|
||||||
|
} finally {
|
||||||
|
if (btn && origHtml) btn.innerHTML = origHtml;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function saveNow(extra = {}) {
|
window.exportPDF = window.exportPDFPrint;
|
||||||
const body = {
|
|
||||||
title: cvTitle,
|
|
||||||
template,
|
|
||||||
settings: cvSettings,
|
|
||||||
...extra
|
|
||||||
};
|
|
||||||
const res = await fetch(`/api/cv/${cvId}/update`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
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() {
|
// ── HTML Export ───────────────────────────────────────────────
|
||||||
const preview = document.getElementById('cv-preview');
|
window.exportHTML = async function () {
|
||||||
// Using a new parameter format: profile_id
|
document.getElementById("export-menu").classList.add("hidden");
|
||||||
const res = await fetch(`/api/cv/${cvId}/preview`, {
|
const el = document.getElementById("cv-preview");
|
||||||
method: 'POST',
|
const globalCss = await fetch("/styles/global.css")
|
||||||
headers: { 'Content-Type': 'application/json' },
|
.then((r) => r.text())
|
||||||
body: JSON.stringify({ template, settings: cvSettings, profile_id: profileId })
|
.catch(() => "");
|
||||||
});
|
const inlineStyles = Array.from(document.querySelectorAll("style"))
|
||||||
if (res.ok) {
|
.map((s) => s.textContent)
|
||||||
preview.innerHTML = await res.text();
|
.join("\n");
|
||||||
}
|
const fontLinks = Array.from(
|
||||||
}
|
document.querySelectorAll('link[rel="stylesheet"]'),
|
||||||
|
)
|
||||||
// Scale preview
|
.map((l) => l.outerHTML)
|
||||||
function scalePreview() {
|
.join("\n");
|
||||||
const wrap = document.getElementById('preview-wrap');
|
const html = `<!DOCTYPE html>
|
||||||
const scale = document.getElementById('preview-scale');
|
<html>
|
||||||
if (!wrap || !scale) return;
|
<head>
|
||||||
const available = wrap.clientWidth - 48;
|
<meta charset="UTF-8">
|
||||||
const factor = Math.min(1, available / 794);
|
<title>${cvTitle}</title>
|
||||||
scale.style.transform = `scale(${factor})`;
|
${fontLinks}
|
||||||
scale.style.transformOrigin = 'top center';
|
<style>${globalCss}\n${inlineStyles}</style>
|
||||||
scale.style.marginBottom = `${(1 - factor) * -1123}px`;
|
</head>
|
||||||
}
|
<body style="margin:0;background:#E8ECF0;display:flex;justify-content:center;padding:32px">
|
||||||
window.addEventListener('resize', scalePreview);
|
${el.outerHTML}
|
||||||
scalePreview();
|
|
||||||
|
|
||||||
// ── Export ───────────────────────────────────────────────────────
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.exportPDF = function() {
|
|
||||||
const el = document.getElementById('cv-preview');
|
|
||||||
const opt = {
|
|
||||||
margin: 0,
|
|
||||||
filename: (cvTitle || 'lebenslauf') + '.pdf',
|
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
|
||||||
html2canvas: { scale: 2, useCORS: true },
|
|
||||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
|
||||||
};
|
|
||||||
window.html2pdf().set(opt).from(el).save();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.exportHTML = function() {
|
|
||||||
const el = document.getElementById('cv-preview');
|
|
||||||
const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${cvTitle}</title><style>${getAllStyles()}</style></head><body style="margin:0;background:#E8ECF0;display:flex;justify-content:center;padding:32px">${el.outerHTML}</body></html>`;
|
|
||||||
download(html, cvTitle + '.html', 'text/html');
|
|
||||||
};
|
|
||||||
|
|
||||||
function getAllStyles() {
|
|
||||||
return Array.from(document.styleSheets).map(ss => {
|
|
||||||
try { return Array.from(ss.cssRules).map(r => r.cssText).join('\n'); } catch { return ''; }
|
|
||||||
}).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function download(content, filename, mime) {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = URL.createObjectURL(new Blob([content], { type: mime }));
|
|
||||||
a.download = filename; 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>
|
</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>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
/* ── Reset & Base ─────────────────────────────────────────────────── */
|
/* ── Reset & Base ─────────────────────────────────────────────────── */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--navy: #1B2A5E;
|
--navy: #1B2A5E;
|
||||||
@@ -12,16 +18,40 @@
|
|||||||
--danger: #E53E3E;
|
--danger: #E53E3E;
|
||||||
--success: #38A169;
|
--success: #38A169;
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--shadow: 0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06);
|
--shadow: 0 1px 3px rgba(0, 0, 0, .1), 0 1px 2px rgba(0, 0, 0, .06);
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06);
|
||||||
}
|
}
|
||||||
|
|
||||||
html { font-size: 16px; }
|
html {
|
||||||
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
|
font-size: 16px;
|
||||||
a { color: var(--accent); text-decoration: none; }
|
}
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
button { cursor: pointer; font-family: inherit; }
|
body {
|
||||||
input, textarea, select { font-family: inherit; }
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Layout ───────────────────────────────────────────────────────── */
|
/* ── Layout ───────────────────────────────────────────────────────── */
|
||||||
.app-header {
|
.app-header {
|
||||||
@@ -37,13 +67,37 @@ input, textarea, select { font-family: inherit; }
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
.app-header .logo { font-size: 1.1rem; font-weight: 700; color: white; letter-spacing: -0.3px; }
|
|
||||||
.app-header .logo span { color: var(--accent); }
|
.app-header .logo {
|
||||||
.app-header nav { display: flex; align-items: center; gap: 16px; }
|
font-size: 1.1rem;
|
||||||
.app-header nav a { color: rgba(255,255,255,.8); font-size: .875rem; transition: color .2s; }
|
font-weight: 700;
|
||||||
.app-header nav a:hover { color: white; text-decoration: none; }
|
color: white;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .logo span {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header nav a {
|
||||||
|
color: rgba(255, 255, 255, .8);
|
||||||
|
font-size: .875rem;
|
||||||
|
transition: color .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header nav a:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.app-header .btn-logout {
|
.app-header .btn-logout {
|
||||||
background: rgba(255,255,255,.12);
|
background: rgba(255, 255, 255, .12);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
@@ -51,56 +105,182 @@ input, textarea, select { font-family: inherit; }
|
|||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
transition: background .2s;
|
transition: background .2s;
|
||||||
}
|
}
|
||||||
.app-header .btn-logout:hover { background: rgba(255,255,255,.22); }
|
|
||||||
|
|
||||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
|
.app-header .btn-logout:hover {
|
||||||
.page { padding: 32px 24px; max-width: 1200px; margin: 0 auto; }
|
background: rgba(255, 255, 255, .22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding: 32px 24px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Buttons ──────────────────────────────────────────────────────── */
|
/* ── Buttons ──────────────────────────────────────────────────────── */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
display: inline-flex;
|
||||||
padding: 8px 16px; border-radius: var(--radius);
|
align-items: center;
|
||||||
font-size: .875rem; font-weight: 500;
|
gap: 6px;
|
||||||
border: none; transition: all .15s;
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
transition: all .15s;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.btn-primary { background: var(--navy); color: white; }
|
|
||||||
.btn-primary:hover { background: #243572; text-decoration: none; }
|
.btn-primary {
|
||||||
.btn-accent { background: var(--accent); color: white; }
|
background: var(--navy);
|
||||||
.btn-accent:hover { background: #3a6ab5; text-decoration: none; }
|
color: white;
|
||||||
.btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--border); }
|
}
|
||||||
.btn-ghost:hover { background: var(--bg); text-decoration: none; }
|
|
||||||
.btn-danger { background: var(--danger); color: white; border: none; }
|
.btn-primary:hover {
|
||||||
.btn-danger:hover { background: #c53030; }
|
background: #243572;
|
||||||
.btn-sm { padding: 5px 10px; font-size: .8rem; }
|
text-decoration: none;
|
||||||
.btn-lg { padding: 12px 24px; font-size: 1rem; }
|
}
|
||||||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
||||||
|
.btn-accent {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent:hover {
|
||||||
|
background: #3a6ab5;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: .5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Cards ────────────────────────────────────────────────────────── */
|
/* ── Cards ────────────────────────────────────────────────────────── */
|
||||||
.card {
|
.card {
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
background: var(--surface);
|
||||||
border-radius: var(--radius); box-shadow: var(--shadow);
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.card-body { padding: 24px; }
|
|
||||||
.card-header { padding: 16px 24px; border-bottom: 1px solid var(--border); background: var(--bg); }
|
.card-body {
|
||||||
.card-footer { padding: 16px 24px; border-top: 1px solid var(--border); background: var(--bg); }
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Forms ────────────────────────────────────────────────────────── */
|
/* ── Forms ────────────────────────────────────────────────────────── */
|
||||||
.form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
|
.form-group {
|
||||||
.form-label { font-size: .875rem; font-weight: 500; color: var(--text); }
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius);
|
padding: 8px 12px;
|
||||||
font-size: .875rem; background: var(--surface); color: var(--text);
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: .875rem;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
transition: border-color .15s, box-shadow .15s;
|
transition: border-color .15s, box-shadow .15s;
|
||||||
}
|
}
|
||||||
.form-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(74,123,197,.15); }
|
|
||||||
.form-textarea { min-height: 80px; resize: vertical; }
|
.form-input:focus {
|
||||||
.form-select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; padding-right: 32px; }
|
outline: none;
|
||||||
.form-hint { font-size: .75rem; color: var(--muted); }
|
border-color: var(--accent);
|
||||||
.form-error { font-size: .75rem; color: var(--danger); }
|
box-shadow: 0 0 0 3px rgba(74, 123, 197, .15);
|
||||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
}
|
||||||
@media (max-width: 600px) { .form-row { grid-template-columns: 1fr; } }
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Auth Page ────────────────────────────────────────────────────── */
|
/* ── Auth Page ────────────────────────────────────────────────────── */
|
||||||
.auth-page {
|
.auth-page {
|
||||||
@@ -110,24 +290,50 @@ input, textarea, select { font-family: inherit; }
|
|||||||
background: linear-gradient(135deg, var(--navy) 0%, #2d4a9e 100%);
|
background: linear-gradient(135deg, var(--navy) 0%, #2d4a9e 100%);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-card {
|
.auth-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 40px 36px;
|
padding: 40px 36px;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,.3);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, .3);
|
||||||
}
|
}
|
||||||
.auth-logo { font-size: 1.5rem; font-weight: 800; color: var(--navy); margin-bottom: 8px; }
|
|
||||||
.auth-logo span { color: var(--accent); }
|
.auth-logo {
|
||||||
.auth-subtitle { color: var(--muted); font-size: .875rem; margin-bottom: 32px; }
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--navy);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-logo span {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-subtitle {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .875rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.otp-input {
|
.otp-input {
|
||||||
width: 100%; text-align: center; font-size: 2rem; font-weight: 700;
|
width: 100%;
|
||||||
letter-spacing: 12px; padding: 16px; border: 2px solid var(--border);
|
text-align: center;
|
||||||
border-radius: var(--radius); font-family: monospace;
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-family: monospace;
|
||||||
transition: border-color .15s;
|
transition: border-color .15s;
|
||||||
}
|
}
|
||||||
.otp-input:focus { outline: none; border-color: var(--accent); }
|
|
||||||
|
.otp-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Dashboard ────────────────────────────────────────────────────── */
|
/* ── Dashboard ────────────────────────────────────────────────────── */
|
||||||
.cv-grid {
|
.cv-grid {
|
||||||
@@ -136,13 +342,21 @@ input, textarea, select { font-family: inherit; }
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-card {
|
.cv-card {
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
background: var(--surface);
|
||||||
border-radius: var(--radius); overflow: hidden;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
transition: box-shadow .2s, transform .2s;
|
transition: box-shadow .2s, transform .2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.cv-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
|
|
||||||
|
.cv-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
.cv-card-preview {
|
.cv-card-preview {
|
||||||
height: 180px;
|
height: 180px;
|
||||||
background: linear-gradient(135deg, var(--navy) 0%, #243572 40%, #3a6ab5 100%);
|
background: linear-gradient(135deg, var(--navy) 0%, #243572 40%, #3a6ab5 100%);
|
||||||
@@ -151,30 +365,50 @@ input, textarea, select { font-family: inherit; }
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-card-preview .t-badge {
|
.cv-card-preview .t-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
background: rgba(255,255,255,.2);
|
background: rgba(255, 255, 255, .2);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: .7rem;
|
font-size: .7rem;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-card-preview .preview-mini {
|
.cv-card-preview .preview-mini {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 127px;
|
height: 127px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,.3);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, .3);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.cv-card-body { padding: 16px; }
|
|
||||||
.cv-card-title { font-weight: 600; font-size: .95rem; margin-bottom: 4px; }
|
.cv-card-body {
|
||||||
.cv-card-meta { font-size: .75rem; color: var(--muted); }
|
padding: 16px;
|
||||||
.cv-card-actions { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border); }
|
}
|
||||||
|
|
||||||
|
.cv-card-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: .95rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-card-meta {
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Editor ───────────────────────────────────────────────────────── */
|
/* ── Editor ───────────────────────────────────────────────────────── */
|
||||||
.editor-layout {
|
.editor-layout {
|
||||||
@@ -183,6 +417,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-sidebar {
|
.editor-sidebar {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
@@ -190,6 +425,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-tabs {
|
.editor-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -197,7 +433,11 @@ input, textarea, select { font-family: inherit; }
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
.editor-tabs::-webkit-scrollbar { display: none; }
|
|
||||||
|
.editor-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-tab {
|
.editor-tab {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
@@ -210,13 +450,22 @@ input, textarea, select { font-family: inherit; }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all .15s;
|
transition: all .15s;
|
||||||
}
|
}
|
||||||
.editor-tab.active { color: var(--navy); border-bottom-color: var(--navy); }
|
|
||||||
.editor-tab:hover { color: var(--text); }
|
.editor-tab.active {
|
||||||
|
color: var(--navy);
|
||||||
|
border-bottom-color: var(--navy);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tab:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.editor-content {
|
.editor-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview {
|
.editor-preview {
|
||||||
background: #E8ECF0;
|
background: #E8ECF0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -224,6 +473,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview-toolbar {
|
.editor-preview-toolbar {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
@@ -234,6 +484,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-wrap {
|
.preview-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -242,11 +493,13 @@ input, textarea, select { font-family: inherit; }
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-scale-wrap {
|
.preview-scale-wrap {
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
width: 794px;
|
width: 794px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-status {
|
.save-status {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
@@ -254,7 +507,10 @@ input, textarea, select { font-family: inherit; }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.save-status.saving { color: var(--muted); }
|
|
||||||
|
.save-status.saving {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Section Items ────────────────────────────────────────────────── */
|
/* ── Section Items ────────────────────────────────────────────────── */
|
||||||
.item-card {
|
.item-card {
|
||||||
@@ -263,6 +519,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-card-header {
|
.item-card-header {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -272,29 +529,79 @@ input, textarea, select { font-family: inherit; }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.item-card-title { font-size: .875rem; font-weight: 500; }
|
|
||||||
.item-card-body { padding: 14px; display: none; }
|
.item-card-title {
|
||||||
.item-card.open .item-card-body { display: block; }
|
font-size: .875rem;
|
||||||
.item-card-actions { display: flex; gap: 6px; }
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-body {
|
||||||
|
padding: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card.open .item-card-body {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Skill level ──────────────────────────────────────────────────── */
|
/* ── Skill level ──────────────────────────────────────────────────── */
|
||||||
.skill-dots { display: flex; gap: 4px; align-items: center; }
|
.skill-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.skill-dot {
|
.skill-dot {
|
||||||
width: 14px; height: 14px; border-radius: 50%;
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
border: 2px solid var(--accent);
|
border: 2px solid var(--accent);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .15s;
|
transition: background .15s;
|
||||||
}
|
}
|
||||||
.skill-dot.filled { background: var(--accent); }
|
|
||||||
|
.skill-dot.filled {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Color picker ─────────────────────────────────────────────────── */
|
/* ── Color picker ─────────────────────────────────────────────────── */
|
||||||
.color-row { display: flex; align-items: center; gap: 10px; }
|
.color-row {
|
||||||
.color-swatch { width: 36px; height: 36px; border-radius: 6px; border: 2px solid var(--border); cursor: pointer; overflow: hidden; padding: 0; }
|
display: flex;
|
||||||
.color-swatch input[type=color] { width: 200%; height: 200%; margin: -50%; border: none; cursor: pointer; }
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch input[type=color] {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
margin: -50%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Template selector ────────────────────────────────────────────── */
|
/* ── Template selector ────────────────────────────────────────────── */
|
||||||
.template-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
.template-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.template-option {
|
.template-option {
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
@@ -302,15 +609,28 @@ input, textarea, select { font-family: inherit; }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color .15s;
|
transition: border-color .15s;
|
||||||
}
|
}
|
||||||
.template-option.selected { border-color: var(--accent); }
|
|
||||||
.template-option:hover { border-color: var(--accent); }
|
.template-option.selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-option:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.template-thumb {
|
.template-thumb {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
background: linear-gradient(135deg, #1B2A5E, #4A7BC5);
|
background: linear-gradient(135deg, #1B2A5E, #4A7BC5);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
.template-label { padding: 8px; font-size: .75rem; font-weight: 500; text-align: center; }
|
|
||||||
|
.template-label {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: .75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── CV Templates (A4) ────────────────────────────────────────────── */
|
/* ── CV Templates (A4) ────────────────────────────────────────────── */
|
||||||
.cv-a4 {
|
.cv-a4 {
|
||||||
@@ -320,7 +640,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
position: relative;
|
position: relative;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,.15);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, .15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Public CV Page ───────────────────────────────────────────────── */
|
/* ── Public CV Page ───────────────────────────────────────────────── */
|
||||||
@@ -333,6 +653,7 @@ input, textarea, select { font-family: inherit; }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-public-actions {
|
.cv-public-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -341,45 +662,183 @@ input, textarea, select { font-family: inherit; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Alerts ───────────────────────────────────────────────────────── */
|
/* ── Alerts ───────────────────────────────────────────────────────── */
|
||||||
.alert { padding: 12px 16px; border-radius: var(--radius); font-size: .875rem; margin-bottom: 16px; }
|
.alert {
|
||||||
.alert-success { background: #f0fff4; border: 1px solid #9ae6b4; color: #276749; }
|
padding: 12px 16px;
|
||||||
.alert-error { background: #fff5f5; border: 1px solid #fed7d7; color: #9b2c2c; }
|
border-radius: var(--radius);
|
||||||
.alert-info { background: #ebf8ff; border: 1px solid #bee3f8; color: #2a4a6b; }
|
font-size: .875rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #f0fff4;
|
||||||
|
border: 1px solid #9ae6b4;
|
||||||
|
color: #276749;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fff5f5;
|
||||||
|
border: 1px solid #fed7d7;
|
||||||
|
color: #9b2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #ebf8ff;
|
||||||
|
border: 1px solid #bee3f8;
|
||||||
|
color: #2a4a6b;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Utils ────────────────────────────────────────────────────────── */
|
/* ── Utils ────────────────────────────────────────────────────────── */
|
||||||
.flex { display: flex; }
|
.flex {
|
||||||
.items-center { align-items: center; }
|
display: flex;
|
||||||
.justify-between { justify-content: space-between; }
|
}
|
||||||
.gap-2 { gap: 8px; }
|
|
||||||
.gap-3 { gap: 12px; }
|
.items-center {
|
||||||
.mt-1 { margin-top: 4px; }
|
align-items: center;
|
||||||
.mt-2 { margin-top: 8px; }
|
}
|
||||||
.mt-4 { margin-top: 16px; }
|
|
||||||
.mt-6 { margin-top: 24px; }
|
.justify-between {
|
||||||
.mb-2 { margin-bottom: 8px; }
|
justify-content: space-between;
|
||||||
.mb-4 { margin-bottom: 16px; }
|
}
|
||||||
.text-sm { font-size: .875rem; }
|
|
||||||
.text-xs { font-size: .75rem; }
|
.gap-2 {
|
||||||
.text-muted { color: var(--muted); }
|
gap: 8px;
|
||||||
.font-bold { font-weight: 700; }
|
}
|
||||||
.font-semibold { font-weight: 600; }
|
|
||||||
.w-full { width: 100%; }
|
.gap-3 {
|
||||||
.hidden { display: none !important; }
|
gap: 12px;
|
||||||
.divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive ───────────────────────────────────────────────────── */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.editor-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-layout.show-preview .editor-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-layout.show-preview .editor-preview {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-scale-wrap {
|
||||||
|
width: 100%;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-a4 {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Print ────────────────────────────────────────────────────────── */
|
/* ── Print ────────────────────────────────────────────────────────── */
|
||||||
@media print {
|
@media print {
|
||||||
body { background: white !important; }
|
@page {
|
||||||
.cv-public-actions, .app-header { display: none !important; }
|
size: A4 portrait;
|
||||||
.cv-public-wrap { padding: 0 !important; background: white !important; }
|
margin: 10mm 0 0 0;
|
||||||
.cv-a4 { box-shadow: none !important; }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@page :first {
|
||||||
.editor-layout { grid-template-columns: 1fr; }
|
margin: 0 0 0 0;
|
||||||
.editor-preview { display: none; }
|
}
|
||||||
.editor-layout.show-preview .editor-sidebar { display: none; }
|
|
||||||
.editor-layout.show-preview .editor-preview { display: flex; }
|
body,
|
||||||
.preview-scale-wrap { width: 100%; transform: none !important; }
|
.cv-public-wrap,
|
||||||
.cv-a4 { width: 100% !important; }
|
.editor-preview {
|
||||||
}
|
background: white !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header,
|
||||||
|
.cv-public-bar,
|
||||||
|
.cv-public-actions,
|
||||||
|
.editor-sidebar,
|
||||||
|
.editor-preview-toolbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-a4 {
|
||||||
|
width: 210mm !important;
|
||||||
|
box-shadow: none !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;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user