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,46 @@
.inline-form { display: flex; gap: 0.6em; flex-wrap: wrap; margin-top: 0.6em; }
.inline-form input { flex: 1; min-width: 220px; }
.friend-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.2em;
margin-top: 1em;
}
.friend-card {
background: var(--paper-2);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.2em;
position: relative;
text-align: center;
}
.friend-card .tape { top: -10px; left: 50%; transform: translateX(-50%) rotate(-3deg); }
.friend-thumb {
width: 84px; height: 84px; border-radius: 50%;
object-fit: cover; border: 4px solid #fff; box-shadow: var(--shadow);
margin-bottom: 0.5em;
}
.friend-thumb.placeholder {
display: flex; align-items: center; justify-content: center;
background: var(--rose); color: #fff; font-size: 1.8rem; font-family: 'Caveat', cursive;
}
.friend-card h3 { margin-bottom: 0.15em; }
.badge {
display: inline-block; font-size: 0.72rem; font-weight: 700; border-radius: 999px;
padding: 0.15em 0.7em; margin-bottom: 0.6em;
}
.badge.on { background: var(--sage); color: #245a3a; }
.badge.off { background: #eee; color: #888; }
.friend-actions { display: flex; gap: 0.4em; justify-content: center; flex-wrap: wrap; margin-top: 0.6em; }
.friend-actions button, .friend-actions a.btn { font-size: 0.85rem; padding: 0.5em 1em; }
.modal-backdrop {
position: fixed; inset: 0; background: rgba(75,59,107,0.35);
display: flex; align-items: center; justify-content: center; z-index: 50; padding: 1em;
}
.modal { width: 100%; max-width: 420px; }

View File

@@ -0,0 +1,158 @@
let friends = [];
let shareTargetId = null;
async function checkSession() {
const res = await fetch('/api/session');
const data = await res.json();
if (data.role !== 'admin') {
window.location.href = '/';
}
}
async function loadFriends() {
const res = await fetch('/api/friends');
if (res.status === 401) { window.location.href = '/'; return; }
const data = await res.json();
friends = data.friends || [];
renderFriends();
}
function renderFriends() {
const grid = document.getElementById('friendGrid');
const emptyMsg = document.getElementById('emptyMsg');
grid.innerHTML = '';
emptyMsg.style.display = friends.length ? 'none' : 'block';
friends.forEach((f) => {
const el = document.createElement('div');
el.className = 'friend-card';
const thumbHtml = f.profileImage
? `<img class="friend-thumb" src="/uploads/${encodeURIComponent(folderFor(f))}/${encodeURIComponent(f.profileImage)}" />`
: `<div class="friend-thumb placeholder">${(f.name || '?')[0].toUpperCase()}</div>`;
el.innerHTML = `
<div class="tape"></div>
${thumbHtml}
<h3>${escapeHtml(f.name)}</h3>
<span class="badge ${f.shareEnabled ? 'on' : 'off'}">${f.shareEnabled ? 'Freigegeben' : 'Nicht freigegeben'}</span>
<div class="friend-actions">
<a class="btn" href="/friend/${f.id}">✏️ Bearbeiten</a>
<button class="secondary" data-share="${f.id}">🔗 Freigabe</button>
<button class="danger" data-delete="${f.id}">🗑️</button>
</div>
`;
grid.appendChild(el);
});
grid.querySelectorAll('[data-share]').forEach((btn) => {
btn.addEventListener('click', () => openShareModal(btn.dataset.share));
});
grid.querySelectorAll('[data-delete]').forEach((btn) => {
btn.addEventListener('click', () => deleteFriend(btn.dataset.delete));
});
}
// wir brauchen den vollen Ordnernamen für Bild-URLs; da wir ihn nicht separat liefern,
// nutzen wir /uploads/<id>-Lookup über eine kleine Konvention: Backend akzeptiert auch
// reine IDs nicht direkt, daher holen wir folderName aus friend-detail bei Bedarf.
// Einfachere Lösung: Backend liefert profileImage-Pfad bereits vollständig auf.
function folderFor(f) {
return f.folderName || f.id;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
}
document.getElementById('createForm').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('newName').value.trim();
const errBox = document.getElementById('createErr');
errBox.innerHTML = '';
if (!name) return;
const res = await fetch('/api/friends', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
const data = await res.json();
if (!res.ok) {
errBox.innerHTML = `<p class="error-msg">${data.error}</p>`;
return;
}
document.getElementById('newName').value = '';
loadFriends();
});
async function deleteFriend(id) {
const f = friends.find((x) => x.id === id);
if (!confirm(`"${f ? f.name : 'Freund'}" wirklich löschen? Alle Fotos und Einträge gehen dabei verloren.`)) return;
const res = await fetch(`/api/friends/${id}`, { method: 'DELETE' });
if (res.ok) loadFriends();
}
function openShareModal(id) {
shareTargetId = id;
const f = friends.find((x) => x.id === id);
document.getElementById('shareFriendName').textContent = f ? f.name : '';
document.getElementById('sharePw').value = '';
document.getElementById('shareErr').innerHTML = '';
const linkBox = document.getElementById('shareLinkBox');
if (f && f.shareEnabled) {
linkBox.style.display = 'block';
document.getElementById('shareLinkInput').value = `${window.location.origin}/share/${id}`;
} else {
linkBox.style.display = 'none';
}
document.getElementById('shareModal').style.display = 'flex';
}
document.getElementById('shareCloseBtn').addEventListener('click', () => {
document.getElementById('shareModal').style.display = 'none';
});
document.getElementById('shareEnableBtn').addEventListener('click', async () => {
const pw = document.getElementById('sharePw').value;
const errBox = document.getElementById('shareErr');
errBox.innerHTML = '';
const res = await fetch(`/api/friends/${shareTargetId}/share`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true, password: pw }),
});
const data = await res.json();
if (!res.ok) {
errBox.innerHTML = `<p class="error-msg">${data.error}</p>`;
return;
}
document.getElementById('shareLinkBox').style.display = 'block';
document.getElementById('shareLinkInput').value = `${window.location.origin}/share/${shareTargetId}`;
await loadFriends();
});
document.getElementById('shareDisableBtn').addEventListener('click', async () => {
const res = await fetch(`/api/friends/${shareTargetId}/share`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: false }),
});
if (res.ok) {
document.getElementById('shareLinkBox').style.display = 'none';
await loadFriends();
}
});
document.getElementById('copyLinkBtn').addEventListener('click', () => {
const input = document.getElementById('shareLinkInput');
input.select();
navigator.clipboard.writeText(input.value).catch(() => {});
});
document.getElementById('logoutBtn').addEventListener('click', async () => {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/';
});
checkSession();
loadFriends();

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Freundesbuch Admin</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>
<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 />
<button type="submit">+ Anlegen</button>
</form>
<div id="createErr"></div>
</section>
<section style="margin-top:2em;">
<h2>Deine Freunde</h2>
<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>
</div>
<!-- Share-Dialog -->
<div id="shareModal" class="modal-backdrop" style="display:none;">
<div class="card modal">
<h3>Freigabe für <span id="shareFriendName"></span></h3>
<p class="muted">Vergib ein eigenes Passwort. Dein Freund kann sich über den Link damit anmelden und nur seine eigene Seite bearbeiten.</p>
<label for="sharePw">Freigabe-Passwort</label>
<input type="text" id="sharePw" placeholder="z. B. Sommerferien2026" />
<div style="display:flex; gap:0.6em; margin-top:1em; flex-wrap:wrap;">
<button id="shareEnableBtn">Freigabe aktivieren / Passwort setzen</button>
<button class="danger" id="shareDisableBtn">Freigabe beenden</button>
<button class="secondary" id="shareCloseBtn">Schließen</button>
</div>
<div id="shareLinkBox" style="margin-top:1em; display:none;">
<label>Link zum Teilen</label>
<div style="display:flex; gap:0.5em;">
<input type="text" id="shareLinkInput" readonly />
<button type="button" id="copyLinkBtn" class="secondary">Kopieren</button>
</div>
</div>
<div id="shareErr"></div>
</div>
</div>
<script src="/admin/admin.js"></script>
</body>
</html>