226 lines
8.2 KiB
JavaScript
226 lines
8.2 KiB
JavaScript
let friends = [];
|
|
let bookTitle = 'Unser Freundesbuch';
|
|
let currentIndex = -1; // -1 = Deckblatt, 0..n-1 = Freunde
|
|
|
|
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',
|
|
};
|
|
|
|
const COVER_MAX_STICKERS = 24;
|
|
|
|
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];
|
|
}
|
|
|
|
// Einfacher, deterministischer Pseudo-Zufallsgenerator (gleiche Seed -> gleiches Ergebnis),
|
|
// damit das Deckblatt bei jedem Aufruf ähnlich aussieht statt bei jedem Reload zu "springen".
|
|
function seededRandom(seed) {
|
|
let t = (seed += 0x6d2b79f5);
|
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
}
|
|
|
|
function uploadUrl(folder, filename) {
|
|
return `/uploads/${encodeURIComponent(folder)}/${encodeURIComponent(filename)}`;
|
|
}
|
|
|
|
async function load() {
|
|
const [bookRes, settingsRes] = await Promise.all([fetch('/api/book'), fetch('/api/settings')]);
|
|
if (bookRes.status === 401 || settingsRes.status === 401) { window.location.href = '/'; return; }
|
|
const bookData = await bookRes.json();
|
|
const settingsData = await settingsRes.json();
|
|
friends = bookData.friends || [];
|
|
bookTitle = (settingsData.settings && settingsData.settings.bookTitle) || 'Unser Freundesbuch';
|
|
|
|
document.getElementById('bookNav').style.display = 'flex';
|
|
renderPage();
|
|
}
|
|
|
|
// --- Deckblatt: Titel + "sticker-gebombte" Foto-Collage ---
|
|
function collectStickerImages() {
|
|
const imgs = [];
|
|
friends.forEach((f) => {
|
|
if (f.profileImage) imgs.push({ folder: f.folderName, filename: f.profileImage });
|
|
(f.blocks || []).forEach((b) => {
|
|
if (b.type === 'image' && b.filename) imgs.push({ folder: f.folderName, filename: b.filename });
|
|
if (b.type === 'audio' && b.coverFilename) imgs.push({ folder: f.folderName, filename: b.coverFilename });
|
|
});
|
|
});
|
|
return imgs;
|
|
}
|
|
|
|
function renderCoverPage() {
|
|
const root = document.getElementById('bookRoot');
|
|
let imgs = collectStickerImages();
|
|
|
|
if (imgs.length > COVER_MAX_STICKERS) {
|
|
const step = imgs.length / COVER_MAX_STICKERS;
|
|
imgs = Array.from({ length: COVER_MAX_STICKERS }, (_, i) => imgs[Math.floor(i * step)]);
|
|
}
|
|
|
|
let stickersHtml = '';
|
|
if (imgs.length) {
|
|
const cols = Math.min(6, Math.max(3, Math.ceil(Math.sqrt(imgs.length * 1.6))));
|
|
const rows = Math.ceil(imgs.length / cols);
|
|
stickersHtml = imgs
|
|
.map((img, i) => {
|
|
const cellX = i % cols;
|
|
const cellY = Math.floor(i / cols);
|
|
const rx = seededRandom(i * 13 + 1);
|
|
const ry = seededRandom(i * 17 + 2);
|
|
const rr = seededRandom(i * 7 + 3);
|
|
const rs = seededRandom(i * 5 + 4);
|
|
const leftPct = ((cellX + 0.1 + rx * 0.8) / cols) * 100;
|
|
const topPct = ((cellY + 0.1 + ry * 0.8) / rows) * 100;
|
|
const rot = (rr * 50 - 25).toFixed(1);
|
|
const size = Math.round(85 + rs * 65);
|
|
return `<img class="cover-sticker" style="left:${leftPct}%; top:${topPct}%; width:${size}px; transform: rotate(${rot}deg);" src="${uploadUrl(img.folder, img.filename)}" />`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
root.innerHTML = `
|
|
<div class="cover-page">
|
|
<div class="cover-stickers">${stickersHtml}</div>
|
|
<div class="cover-title-wrap">
|
|
<div class="tape" style="position:static; margin: 0 auto 0.4em; transform: rotate(-4deg);"></div>
|
|
<h1 class="cover-title">${escapeHtml(bookTitle)}</h1>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderFriendPage(f) {
|
|
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);">
|
|
${b.coverFilename ? `<img class="audio-cover" src="${uploadUrl(f.folderName, b.coverFilename)}" />` : ''}
|
|
<div>🎙️${b.caption ? ' ' + escapeHtml(b.caption) : ''}</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>
|
|
`;
|
|
}
|
|
|
|
function renderPage() {
|
|
const root = document.getElementById('bookRoot');
|
|
|
|
if (currentIndex === -1) {
|
|
renderCoverPage();
|
|
} else if (!friends.length) {
|
|
root.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>`;
|
|
} else {
|
|
renderFriendPage(friends[currentIndex]);
|
|
}
|
|
|
|
const indicator = document.getElementById('pageIndicator');
|
|
indicator.textContent = currentIndex === -1 ? 'Deckblatt' : `Seite ${currentIndex + 1} von ${friends.length}`;
|
|
document.getElementById('prevBtn').disabled = currentIndex === -1;
|
|
document.getElementById('nextBtn').disabled = currentIndex >= friends.length - 1;
|
|
}
|
|
|
|
document.getElementById('prevBtn').addEventListener('click', () => {
|
|
if (currentIndex > -1) { 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();
|