1.0
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
.inline-form { display: flex; gap: 0.6em; flex-wrap: wrap; margin-top: 0.6em; }
|
||||
.inline-form input { flex: 1; min-width: 220px; }
|
||||
|
||||
.tab-nav {
|
||||
display: flex; gap: 0.4em; padding: 0.6em 1.5em 0;
|
||||
background: var(--paper-2); border-bottom: 2px solid var(--line);
|
||||
}
|
||||
.tab-link {
|
||||
font-family: 'Nunito', sans-serif; font-weight: 700; text-decoration: none;
|
||||
color: var(--ink-soft); padding: 0.6em 1.1em; border-radius: 10px 10px 0 0;
|
||||
border: 2px solid transparent; border-bottom: none; margin-bottom: -2px;
|
||||
}
|
||||
.tab-link.active { background: var(--paper); border-color: var(--line); color: var(--rust); }
|
||||
.tab-link:hover:not(.active) { background: rgba(232,184,75,0.15); }
|
||||
|
||||
.friend-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
@@ -18,6 +30,13 @@
|
||||
}
|
||||
.friend-card .tape { top: -10px; left: 50%; transform: translateX(-50%) rotate(-3deg); }
|
||||
|
||||
.order-controls {
|
||||
display: flex; align-items: center; justify-content: center; gap: 0.5em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.order-controls button { font-size: 1rem; padding: 0.15em 0.5em; line-height: 1; }
|
||||
.order-num { font-family: 'Caveat', cursive; font-size: 1.2rem; color: var(--ink-soft); min-width: 1.4em; }
|
||||
|
||||
.friend-thumb {
|
||||
width: 84px; height: 84px; border-radius: 50%;
|
||||
object-fit: cover; border: 4px solid #fff; box-shadow: var(--shadow);
|
||||
|
||||
@@ -10,31 +10,15 @@ async function checkSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// Einstellungen werden im Hintergrund geladen (für den Freigabe-Link),
|
||||
// bearbeitet werden sie im separaten "Einstellungen"-Tab.
|
||||
async function loadSettings() {
|
||||
const res = await fetch('/api/settings');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
settings = data.settings || { publicBaseUrl: '' };
|
||||
document.getElementById('publicBaseUrl').value = settings.publicBaseUrl || '';
|
||||
settings = data.settings || { publicBaseUrl: '', bookTitle: '' };
|
||||
}
|
||||
|
||||
document.getElementById('saveSettingsBtn').addEventListener('click', async () => {
|
||||
const publicBaseUrl = document.getElementById('publicBaseUrl').value.trim();
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ publicBaseUrl }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
settings = data.settings;
|
||||
const msg = document.getElementById('settingsSaved');
|
||||
msg.textContent = 'Gespeichert ✓';
|
||||
msg.classList.add('show');
|
||||
setTimeout(() => msg.classList.remove('show'), 1800);
|
||||
}
|
||||
});
|
||||
|
||||
function baseUrl() {
|
||||
return settings.publicBaseUrl && settings.publicBaseUrl.length
|
||||
? settings.publicBaseUrl
|
||||
@@ -55,7 +39,7 @@ function renderFriends() {
|
||||
grid.innerHTML = '';
|
||||
emptyMsg.style.display = friends.length ? 'none' : 'block';
|
||||
|
||||
friends.forEach((f) => {
|
||||
friends.forEach((f, idx) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'friend-card';
|
||||
const thumbHtml = f.profileImage
|
||||
@@ -63,6 +47,11 @@ function renderFriends() {
|
||||
: `<div class="friend-thumb placeholder">${(f.name || '?')[0].toUpperCase()}</div>`;
|
||||
el.innerHTML = `
|
||||
<div class="tape"></div>
|
||||
<div class="order-controls">
|
||||
<button type="button" class="ghost" data-moveup="${f.id}" ${idx === 0 ? 'disabled' : ''} title="Nach vorn">↑</button>
|
||||
<span class="order-num">${idx + 1}</span>
|
||||
<button type="button" class="ghost" data-movedown="${f.id}" ${idx === friends.length - 1 ? 'disabled' : ''} title="Nach hinten">↓</button>
|
||||
</div>
|
||||
${thumbHtml}
|
||||
<h3>${escapeHtml(f.name)}</h3>
|
||||
<span class="badge ${f.shareEnabled ? 'on' : 'off'}">${f.shareEnabled ? 'Freigegeben' : 'Nicht freigegeben'}</span>
|
||||
@@ -85,6 +74,27 @@ function renderFriends() {
|
||||
grid.querySelectorAll('[data-export]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => window.location.href = `/api/friends/${btn.dataset.export}/export`);
|
||||
});
|
||||
grid.querySelectorAll('[data-moveup]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => moveFriend(btn.dataset.moveup, -1));
|
||||
});
|
||||
grid.querySelectorAll('[data-movedown]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => moveFriend(btn.dataset.movedown, 1));
|
||||
});
|
||||
}
|
||||
|
||||
async function moveFriend(id, dir) {
|
||||
const idx = friends.findIndex((f) => f.id === id);
|
||||
const swapIdx = idx + dir;
|
||||
if (idx === -1 || swapIdx < 0 || swapIdx >= friends.length) return;
|
||||
const newOrder = [...friends];
|
||||
[newOrder[idx], newOrder[swapIdx]] = [newOrder[swapIdx], newOrder[idx]];
|
||||
const order = newOrder.map((f) => f.id);
|
||||
const res = await fetch('/api/friends/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ order }),
|
||||
});
|
||||
if (res.ok) loadFriends();
|
||||
}
|
||||
|
||||
// wir brauchen den vollen Ordnernamen für Bild-URLs; da wir ihn nicht separat liefern,
|
||||
@@ -190,56 +200,6 @@ document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
// --- Sichern & Wiederherstellen ---
|
||||
function showTransferMsg(text, isError) {
|
||||
const box = document.getElementById('transferMsg');
|
||||
box.innerHTML = isError ? `<p class="error-msg">${text}</p>` : `<p class="saved-msg2 show">${text}</p>`;
|
||||
}
|
||||
|
||||
document.getElementById('bookExportBtn').addEventListener('click', () => {
|
||||
window.location.href = '/api/book/export';
|
||||
});
|
||||
|
||||
document.getElementById('bookImportBtn').addEventListener('click', async () => {
|
||||
const fileInput = document.getElementById('bookImportFile');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) { showTransferMsg('Bitte zuerst eine ZIP-Datei auswählen.', true); return; }
|
||||
const replaceAll = document.getElementById('bookImportReplace').checked;
|
||||
if (replaceAll && !confirm('Wirklich ALLE bestehenden Freunde vor dem Import löschen?')) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
fd.append('replaceAll', replaceAll ? 'true' : 'false');
|
||||
showTransferMsg('Importiere...', false);
|
||||
const res = await fetch('/api/book/import', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showTransferMsg(`${data.imported.length} Freund(e) importiert: ${data.imported.join(', ') || '–'}`, false);
|
||||
fileInput.value = '';
|
||||
loadFriends();
|
||||
} else {
|
||||
showTransferMsg(data.error || 'Import fehlgeschlagen.', true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('friendImportBtn').addEventListener('click', async () => {
|
||||
const fileInput = document.getElementById('friendImportFile');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) { showTransferMsg('Bitte zuerst eine ZIP-Datei auswählen.', true); return; }
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
showTransferMsg('Importiere...', false);
|
||||
const res = await fetch('/api/friends/import', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showTransferMsg(`"${data.friend.name}" wurde importiert.`, false);
|
||||
fileInput.value = '';
|
||||
loadFriends();
|
||||
} else {
|
||||
showTransferMsg(data.error || 'Import fehlgeschlagen.', true);
|
||||
}
|
||||
});
|
||||
|
||||
checkSession();
|
||||
loadSettings();
|
||||
loadFriends();
|
||||
|
||||
@@ -16,19 +16,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<section class="card" id="settingsCard">
|
||||
<h2>Einstellungen</h2>
|
||||
<label for="publicBaseUrl">Öffentliche URL (für Freigabe-Links hinter einem Reverse-Proxy)</label>
|
||||
<div class="inline-form">
|
||||
<input type="text" id="publicBaseUrl" placeholder="https://freundesbuch.example.com" />
|
||||
<button type="button" id="saveSettingsBtn">Speichern</button>
|
||||
</div>
|
||||
<p class="muted" style="margin-top:0.4em;">Leer lassen, um automatisch die aktuell aufgerufene Adresse zu verwenden.</p>
|
||||
<div id="settingsSaved" class="saved-msg2"></div>
|
||||
</section>
|
||||
<nav class="tab-nav">
|
||||
<a href="/admin" class="tab-link active">👥 Freunde</a>
|
||||
<a href="/admin/settings" class="tab-link">⚙️ Einstellungen & Sichern</a>
|
||||
</nav>
|
||||
|
||||
<section class="card" id="createCard" style="margin-top:1.4em;">
|
||||
<div class="container">
|
||||
<section class="card" id="createCard">
|
||||
<h2>Neuen Freund anlegen</h2>
|
||||
<form id="createForm" class="inline-form">
|
||||
<input type="text" id="newName" placeholder="Name des Freundes" required />
|
||||
@@ -37,35 +31,9 @@
|
||||
<div id="createErr"></div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="transferCard" style="margin-top:1.4em;">
|
||||
<h2>Sichern & Wiederherstellen</h2>
|
||||
<div class="transfer-row">
|
||||
<div>
|
||||
<h3 style="font-size:1.2rem;">Ganzes Buch</h3>
|
||||
<button type="button" id="bookExportBtn" class="secondary">⬇️ Buch exportieren (ZIP)</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="bookImportFile">Buch importieren</label>
|
||||
<input type="file" id="bookImportFile" accept=".zip" />
|
||||
<label style="font-weight:normal; display:flex; align-items:center; gap:0.4em; margin-top:0.4em;">
|
||||
<input type="checkbox" id="bookImportReplace" style="width:auto;" />
|
||||
Bestehende Freunde vorher löschen
|
||||
</label>
|
||||
<button type="button" id="bookImportBtn" class="secondary" style="margin-top:0.4em;">⬆️ Importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr style="border:none; border-top:1px solid var(--line); margin:1.2em 0;" />
|
||||
<div>
|
||||
<h3 style="font-size:1.2rem;">Einzelnen Freund importieren</h3>
|
||||
<p class="muted">Legt einen neuen Freund aus einer zuvor exportierten ZIP-Datei an.</p>
|
||||
<input type="file" id="friendImportFile" accept=".zip" />
|
||||
<button type="button" id="friendImportBtn" class="secondary" style="margin-top:0.4em;">⬆️ Importieren</button>
|
||||
</div>
|
||||
<div id="transferMsg"></div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top:2em;">
|
||||
<h2>Deine Freunde</h2>
|
||||
<p class="muted">Die Reihenfolge hier bestimmt die Seitenfolge im Freundesbuch (nach dem Deckblatt). Mit den Pfeilen verschieben.</p>
|
||||
<div id="friendGrid" class="friend-grid"></div>
|
||||
<p id="emptyMsg" class="muted" style="display:none;">Noch keine Freunde angelegt – leg oben den ersten an!</p>
|
||||
</section>
|
||||
|
||||
68
backend/public/admin/settings.html
Normal file
68
backend/public/admin/settings.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Freundesbuch – Einstellungen</title>
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
<link rel="stylesheet" href="/admin/admin.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;">
|
||||
<a class="btn" href="/book" target="_blank" rel="noopener">🎠 Freundesbuch anschauen</a>
|
||||
<button class="secondary" id="logoutBtn">Abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="tab-nav">
|
||||
<a href="/admin" class="tab-link">👥 Freunde</a>
|
||||
<a href="/admin/settings" class="tab-link active">⚙️ Einstellungen & Sichern</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<section class="card" id="settingsCard">
|
||||
<h2>Einstellungen</h2>
|
||||
<label for="bookTitle">Titel des Freundesbuchs (steht auf dem Deckblatt)</label>
|
||||
<input type="text" id="bookTitle" placeholder="Unser Freundesbuch" style="margin-bottom:1em;" />
|
||||
<label for="publicBaseUrl">Öffentliche URL (für Freigabe-Links hinter einem Reverse-Proxy)</label>
|
||||
<div class="inline-form">
|
||||
<input type="text" id="publicBaseUrl" placeholder="https://freundesbuch.example.com" />
|
||||
<button type="button" id="saveSettingsBtn">Speichern</button>
|
||||
</div>
|
||||
<p class="muted" style="margin-top:0.4em;">Leer lassen, um automatisch die aktuell aufgerufene Adresse zu verwenden.</p>
|
||||
<div id="settingsSaved" class="saved-msg2"></div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="transferCard" style="margin-top:1.4em;">
|
||||
<h2>Sichern & Wiederherstellen</h2>
|
||||
<div class="transfer-row">
|
||||
<div>
|
||||
<h3 style="font-size:1.2rem;">Ganzes Buch</h3>
|
||||
<button type="button" id="bookExportBtn" class="secondary">⬇️ Buch exportieren (ZIP)</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="bookImportFile">Buch importieren</label>
|
||||
<input type="file" id="bookImportFile" accept=".zip" />
|
||||
<label style="font-weight:normal; display:flex; align-items:center; gap:0.4em; margin-top:0.4em;">
|
||||
<input type="checkbox" id="bookImportReplace" style="width:auto;" />
|
||||
Bestehende Freunde vorher löschen
|
||||
</label>
|
||||
<button type="button" id="bookImportBtn" class="secondary" style="margin-top:0.4em;">⬆️ Importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr style="border:none; border-top:1px solid var(--line); margin:1.2em 0;" />
|
||||
<div>
|
||||
<h3 style="font-size:1.2rem;">Einzelnen Freund importieren</h3>
|
||||
<p class="muted">Legt einen neuen Freund aus einer zuvor exportierten ZIP-Datei an.</p>
|
||||
<input type="file" id="friendImportFile" accept=".zip" />
|
||||
<button type="button" id="friendImportBtn" class="secondary" style="margin-top:0.4em;">⬆️ Importieren</button>
|
||||
</div>
|
||||
<div id="transferMsg"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/admin/settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
92
backend/public/admin/settings.js
Normal file
92
backend/public/admin/settings.js
Normal file
@@ -0,0 +1,92 @@
|
||||
let settings = { publicBaseUrl: '', bookTitle: '' };
|
||||
|
||||
async function checkSession() {
|
||||
const res = await fetch('/api/session');
|
||||
const data = await res.json();
|
||||
if (data.role !== 'admin') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const res = await fetch('/api/settings');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
settings = data.settings || { publicBaseUrl: '', bookTitle: '' };
|
||||
document.getElementById('publicBaseUrl').value = settings.publicBaseUrl || '';
|
||||
document.getElementById('bookTitle').value = settings.bookTitle || '';
|
||||
}
|
||||
|
||||
document.getElementById('saveSettingsBtn').addEventListener('click', async () => {
|
||||
const publicBaseUrl = document.getElementById('publicBaseUrl').value.trim();
|
||||
const bookTitle = document.getElementById('bookTitle').value.trim();
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ publicBaseUrl, bookTitle }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
settings = data.settings;
|
||||
const msg = document.getElementById('settingsSaved');
|
||||
msg.textContent = 'Gespeichert ✓';
|
||||
msg.classList.add('show');
|
||||
setTimeout(() => msg.classList.remove('show'), 1800);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||||
await fetch('/api/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
// --- Sichern & Wiederherstellen ---
|
||||
function showTransferMsg(text, isError) {
|
||||
const box = document.getElementById('transferMsg');
|
||||
box.innerHTML = isError ? `<p class="error-msg">${text}</p>` : `<p class="saved-msg2 show">${text}</p>`;
|
||||
}
|
||||
|
||||
document.getElementById('bookExportBtn').addEventListener('click', () => {
|
||||
window.location.href = '/api/book/export';
|
||||
});
|
||||
|
||||
document.getElementById('bookImportBtn').addEventListener('click', async () => {
|
||||
const fileInput = document.getElementById('bookImportFile');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) { showTransferMsg('Bitte zuerst eine ZIP-Datei auswählen.', true); return; }
|
||||
const replaceAll = document.getElementById('bookImportReplace').checked;
|
||||
if (replaceAll && !confirm('Wirklich ALLE bestehenden Freunde vor dem Import löschen?')) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
fd.append('replaceAll', replaceAll ? 'true' : 'false');
|
||||
showTransferMsg('Importiere...', false);
|
||||
const res = await fetch('/api/book/import', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showTransferMsg(`${data.imported.length} Freund(e) importiert: ${data.imported.join(', ') || '–'}`, false);
|
||||
fileInput.value = '';
|
||||
} else {
|
||||
showTransferMsg(data.error || 'Import fehlgeschlagen.', true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('friendImportBtn').addEventListener('click', async () => {
|
||||
const fileInput = document.getElementById('friendImportFile');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) { showTransferMsg('Bitte zuerst eine ZIP-Datei auswählen.', true); return; }
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
showTransferMsg('Importiere...', false);
|
||||
const res = await fetch('/api/friends/import', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showTransferMsg(`"${data.friend.name}" wurde importiert.`, false);
|
||||
fileInput.value = '';
|
||||
} else {
|
||||
showTransferMsg(data.error || 'Import fehlgeschlagen.', true);
|
||||
}
|
||||
});
|
||||
|
||||
checkSession();
|
||||
loadSettings();
|
||||
@@ -51,3 +51,45 @@
|
||||
#pageIndicator { font-family: 'Caveat', cursive; font-size: 1.3rem; color: var(--ink-soft); }
|
||||
|
||||
.empty-book { text-align: center; margin-top: 3em; }
|
||||
|
||||
/* Deckblatt */
|
||||
.cover-page {
|
||||
position: relative;
|
||||
min-height: 640px;
|
||||
border-radius: 18px;
|
||||
background: var(--paper-2);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
border: 2px dashed var(--mustard);
|
||||
}
|
||||
.cover-stickers { position: absolute; inset: 0; }
|
||||
.cover-sticker {
|
||||
position: absolute;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1 / 1;
|
||||
border: 6px solid #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.22);
|
||||
}
|
||||
.cover-title-wrap {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
text-align: center;
|
||||
top: 42%;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.6em 1.2em;
|
||||
}
|
||||
.cover-title {
|
||||
display: inline-block;
|
||||
font-size: 3.6rem;
|
||||
background: rgba(255,253,247,0.92);
|
||||
padding: 0.25em 0.7em;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.18);
|
||||
transform: rotate(-2deg);
|
||||
max-width: 90%;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.cover-page { min-height: 460px; }
|
||||
.cover-title { font-size: 2.3rem; }
|
||||
}
|
||||
|
||||
@@ -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' }); }
|
||||
|
||||
Reference in New Issue
Block a user