This commit is contained in:
2026-09-07 13:00:59 +02:00
parent cbe4726c07
commit e0326285d3
12 changed files with 478 additions and 143 deletions

View File

@@ -1,5 +1,6 @@
let friends = [];
let currentIndex = 0;
let bookTitle = 'Unser Freundesbuch';
let currentIndex = -1; // -1 = Deckblatt, 0..n-1 = Freunde
const STECKBRIEF_LABELS = {
spitzname: 'Spitzname',
@@ -17,6 +18,8 @@ const STECKBRIEF_LABELS = {
sonstiges: 'Sonstiges',
};
const COVER_MAX_STICKERS = 24;
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str || '';
@@ -28,29 +31,86 @@ function randomRotation(seed) {
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();
// 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)}`;
}
function renderPage() {
const f = friends[currentIndex];
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
@@ -77,7 +137,7 @@ function renderPage() {
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) : 'Sprachnachricht'}</div>
<div>🎙️${b.caption ? ' ' + escapeHtml(b.caption) : ''}</div>
<audio controls src="${uploadUrl(f.folderName, b.filename)}"></audio>
</div>`;
}
@@ -104,14 +164,31 @@ function renderPage() {
</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;
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 > 0) { currentIndex--; renderPage(); window.scrollTo({ top: 0, behavior: 'smooth' }); }
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' }); }