First
This commit is contained in:
52
backend/public/book/book.css
Normal file
52
backend/public/book/book.css
Normal 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
147
backend/public/book/book.js
Normal 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();
|
||||
32
backend/public/book/index.html
Normal file
32
backend/public/book/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user