This commit is contained in:
2026-09-07 12:21:53 +02:00
commit 93051f791d
25 changed files with 4226 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
.book-root { max-width: 1000px; margin: 0 auto; padding: 2em 1.2em 6em; min-height: 60vh; }
.friend-page { position: relative; }
.page-head { text-align: center; margin-bottom: 1.4em; position: relative; }
.page-head .tape { top: -10px; left: 50%; transform: translateX(-50%) rotate(-4deg); }
.page-profile-pic {
width: 150px; height: 150px; border-radius: 50%; object-fit: cover;
border: 8px solid #fff; box-shadow: var(--shadow); transform: rotate(-3deg);
}
.page-profile-pic.placeholder {
display: flex; align-items: center; justify-content: center;
background: var(--rose); color: #fff; font-size: 3rem; font-family: 'Caveat', cursive;
}
.page-head h1 { font-size: 3.2rem; margin-top: 0.3em; }
.steckbrief-card {
background: var(--paper-2); border: 2px dashed var(--mustard); border-radius: 14px;
padding: 1.2em 1.6em; margin: 0 auto 2em; max-width: 640px; transform: rotate(-0.4deg);
box-shadow: var(--shadow);
}
.steckbrief-card h2 { color: var(--rust); }
.sb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.6em 1.2em; }
.sb-row .sb-key { font-weight: 700; color: var(--ink-soft); font-size: 0.82rem; display: block; }
.sb-row .sb-val { font-size: 0.95rem; }
.scrap-grid {
display: flex; flex-wrap: wrap; gap: 2em 1.6em; justify-content: center; padding: 1em 0.5em;
}
.scrap-item { background: #fff; box-shadow: 0 6px 16px rgba(0,0,0,0.16); transition: transform 0.15s ease; }
.scrap-item:hover { transform: scale(1.04) rotate(0deg) !important; z-index: 2; }
.scrap-item.polaroid { padding: 10px 10px 30px; width: 220px; }
.scrap-item.polaroid img { width: 100%; height: 190px; object-fit: cover; display: block; }
.scrap-item.text-note { width: 220px; min-height: 160px; padding: 1.2em; background: #fef7d6; font-family: Georgia, serif; }
.scrap-item.audio-note { width: 220px; padding: 1.2em; background: #e8f3ec; text-align: center; }
.scrap-item.audio-note audio { width: 100%; margin-top: 0.6em; }
.scrap-caption { font-family: 'Caveat', cursive; font-size: 1.2rem; text-align: center; margin-top: 0.4em; color: var(--ink-soft); }
.book-nav {
position: fixed; bottom: 0; left: 0; right: 0;
background: var(--paper-2); border-top: 2px solid var(--line);
display: flex; align-items: center; justify-content: center; gap: 1.4em;
padding: 0.8em; box-shadow: 0 -4px 12px rgba(0,0,0,0.08);
}
#pageIndicator { font-family: 'Caveat', cursive; font-size: 1.3rem; color: var(--ink-soft); }
.empty-book { text-align: center; margin-top: 3em; }

147
backend/public/book/book.js Normal file
View File

@@ -0,0 +1,147 @@
let friends = [];
let currentIndex = 0;
const STECKBRIEF_LABELS = {
spitzname: 'Spitzname',
geburtstag: 'Geburtstag',
adresse: 'Adresse / Wohnort',
lieblingsfarbe: 'Lieblingsfarbe',
lieblingsessen: 'Lieblingsessen',
lieblingstier: 'Lieblingstier',
hobbys: 'Hobbys',
lieblingsmusik: 'Lieblingsmusik',
lieblingsfilm: 'Lieblingsfilm/-serie',
traumberuf: 'Traumberuf',
bestefreunde: 'Beste Freunde',
motto: 'Motto / Lebensweisheit',
sonstiges: 'Sonstiges',
};
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
}
function randomRotation(seed) {
const angles = [-6, -4, -2, 2, 4, 6, -3, 3, 5, -5];
return angles[seed % angles.length];
}
async function load() {
const res = await fetch('/api/book');
if (res.status === 401) { window.location.href = '/'; return; }
const data = await res.json();
friends = data.friends || [];
if (!friends.length) {
document.getElementById('bookRoot').innerHTML = `
<div class="empty-book">
<h2>Noch ist dieses Buch leer 📖</h2>
<p class="muted">Lege im Admin-Bereich Freunde an und teile den Link mit ihnen.</p>
</div>`;
return;
}
document.getElementById('bookNav').style.display = 'flex';
renderPage();
}
function uploadUrl(folder, filename) {
return `/uploads/${encodeURIComponent(folder)}/${encodeURIComponent(filename)}`;
}
function renderPage() {
const f = friends[currentIndex];
const root = document.getElementById('bookRoot');
const profileHtml = f.profileImage
? `<img class="page-profile-pic" src="${uploadUrl(f.folderName, f.profileImage)}" />`
: `<div class="page-profile-pic placeholder">${(f.name || '?')[0].toUpperCase()}</div>`;
const sbEntries = Object.entries(f.steckbrief || {}).filter(([, v]) => v && String(v).trim());
const sbHtml = sbEntries.length
? `<div class="sb-grid">${sbEntries.map(([k, v]) => `
<div class="sb-row"><span class="sb-key">${escapeHtml(STECKBRIEF_LABELS[k] || k)}</span><span class="sb-val">${escapeHtml(v)}</span></div>
`).join('')}</div>`
: `<p class="muted">Noch nichts eingetragen.</p>`;
const blocks = [...(f.blocks || [])].sort((a, b) => a.order - b.order);
const blocksHtml = blocks.length
? blocks.map((b, i) => {
const rot = randomRotation(i + f.id.length);
if (b.type === 'image') {
return `<div class="scrap-item polaroid" style="transform: rotate(${rot}deg);">
<img src="${uploadUrl(f.folderName, b.filename)}" />
${b.caption ? `<div class="scrap-caption">${escapeHtml(b.caption)}</div>` : ''}
</div>`;
}
if (b.type === 'audio') {
return `<div class="scrap-item audio-note" style="transform: rotate(${rot * 0.5}deg);">
<div>🎙️ ${b.caption ? escapeHtml(b.caption) : 'Sprachnachricht'}</div>
<audio controls src="${uploadUrl(f.folderName, b.filename)}"></audio>
</div>`;
}
return `<div class="scrap-item text-note" style="transform: rotate(${rot * 0.5}deg);">
<p>${escapeHtml(b.content).replace(/\n/g, '<br/>')}</p>
${b.caption ? `<div class="scrap-caption">${escapeHtml(b.caption)}</div>` : ''}
</div>`;
}).join('')
: '<p class="muted">Noch keine Erinnerungen hinzugefügt.</p>';
root.innerHTML = `
<div class="friend-page">
<div class="page-head">
<div class="tape"></div>
${profileHtml}
<h1>${escapeHtml(f.name)}</h1>
</div>
<div class="steckbrief-card">
<h2>Steckbrief</h2>
${sbHtml}
</div>
<div class="scrap-grid">
${blocksHtml}
</div>
</div>
`;
document.getElementById('pageIndicator').textContent = `Seite ${currentIndex + 1} von ${friends.length}`;
document.getElementById('prevBtn').disabled = currentIndex === 0;
document.getElementById('nextBtn').disabled = currentIndex === friends.length - 1;
}
document.getElementById('prevBtn').addEventListener('click', () => {
if (currentIndex > 0) { currentIndex--; renderPage(); window.scrollTo({ top: 0, behavior: 'smooth' }); }
});
document.getElementById('nextBtn').addEventListener('click', () => {
if (currentIndex < friends.length - 1) { currentIndex++; renderPage(); window.scrollTo({ top: 0, behavior: 'smooth' }); }
});
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') document.getElementById('prevBtn').click();
if (e.key === 'ArrowRight') document.getElementById('nextBtn').click();
});
document.getElementById('pdfBtn').addEventListener('click', async () => {
const btn = document.getElementById('pdfBtn');
btn.disabled = true;
btn.textContent = 'Erzeuge PDF...';
try {
const res = await fetch('/api/book/pdf');
if (!res.ok) throw new Error('PDF-Export fehlgeschlagen.');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'freundesbuch.pdf';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (err) {
alert(err.message);
} finally {
btn.disabled = false;
btn.textContent = '⬇️ Als PDF exportieren';
}
});
load();

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Freundesbuch Ansicht</title>
<link rel="stylesheet" href="/assets/style.css" />
<link rel="stylesheet" href="/book/book.css" />
</head>
<body>
<div class="top-bar card" style="border-radius:0;">
<h1 style="margin:0;">🎠 Freundesbuch</h1>
<div style="display:flex; gap:0.6em; flex-wrap:wrap;">
<button class="secondary" id="pdfBtn">⬇️ Als PDF exportieren</button>
<a class="btn secondary" href="/admin">← Zur Übersicht</a>
</div>
</div>
<div id="bookRoot" class="book-root">
<p class="muted" style="text-align:center; margin-top:3em;">Lädt...</p>
</div>
<div class="book-nav" id="bookNav" style="display:none;">
<button id="prevBtn" class="secondary">← Zurück</button>
<span id="pageIndicator"></span>
<button id="nextBtn" class="secondary">Weiter →</button>
</div>
<script src="/book/book.js"></script>
</body>
</html>