2026-09-07 12:21:53 +02:00
|
|
|
|
let friends = [];
|
|
|
|
|
|
let shareTargetId = null;
|
2026-09-07 12:46:58 +02:00
|
|
|
|
let settings = { publicBaseUrl: '' };
|
2026-09-07 12:21:53 +02:00
|
|
|
|
|
|
|
|
|
|
async function checkSession() {
|
|
|
|
|
|
const res = await fetch('/api/session');
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.role !== 'admin') {
|
|
|
|
|
|
window.location.href = '/';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 13:00:59 +02:00
|
|
|
|
// Einstellungen werden im Hintergrund geladen (für den Freigabe-Link),
|
|
|
|
|
|
// bearbeitet werden sie im separaten "Einstellungen"-Tab.
|
2026-09-07 12:46:58 +02:00
|
|
|
|
async function loadSettings() {
|
|
|
|
|
|
const res = await fetch('/api/settings');
|
|
|
|
|
|
if (!res.ok) return;
|
|
|
|
|
|
const data = await res.json();
|
2026-09-07 13:00:59 +02:00
|
|
|
|
settings = data.settings || { publicBaseUrl: '', bookTitle: '' };
|
2026-09-07 12:46:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function baseUrl() {
|
|
|
|
|
|
return settings.publicBaseUrl && settings.publicBaseUrl.length
|
|
|
|
|
|
? settings.publicBaseUrl
|
|
|
|
|
|
: window.location.origin;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 12:21:53 +02:00
|
|
|
|
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';
|
|
|
|
|
|
|
2026-09-07 13:00:59 +02:00
|
|
|
|
friends.forEach((f, idx) => {
|
2026-09-07 12:21:53 +02:00
|
|
|
|
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>
|
2026-09-07 13:00:59 +02:00
|
|
|
|
<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>
|
2026-09-07 12:21:53 +02:00
|
|
|
|
${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>
|
2026-09-07 12:46:58 +02:00
|
|
|
|
<button class="secondary" data-export="${f.id}">⬇️ Export</button>
|
2026-09-07 12:21:53 +02:00
|
|
|
|
<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));
|
|
|
|
|
|
});
|
2026-09-07 12:46:58 +02:00
|
|
|
|
grid.querySelectorAll('[data-export]').forEach((btn) => {
|
|
|
|
|
|
btn.addEventListener('click', () => window.location.href = `/api/friends/${btn.dataset.export}/export`);
|
|
|
|
|
|
});
|
2026-09-07 13:00:59 +02:00
|
|
|
|
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();
|
2026-09-07 12:21:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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';
|
2026-09-07 12:46:58 +02:00
|
|
|
|
document.getElementById('shareLinkInput').value = `${baseUrl()}/share/${id}`;
|
2026-09-07 12:21:53 +02:00
|
|
|
|
} 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';
|
2026-09-07 12:46:58 +02:00
|
|
|
|
document.getElementById('shareLinkInput').value = `${baseUrl()}/share/${shareTargetId}`;
|
2026-09-07 12:21:53 +02:00
|
|
|
|
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();
|
2026-09-07 12:46:58 +02:00
|
|
|
|
loadSettings();
|
2026-09-07 12:21:53 +02:00
|
|
|
|
loadFriends();
|