358 lines
14 KiB
JavaScript
358 lines
14 KiB
JavaScript
const STECKBRIEF_FIELDS = [
|
|
['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 pathParts = window.location.pathname.split('/').filter(Boolean); // ['friend', ':id'] or ['share', ':id']
|
|
const mode = pathParts[0]; // 'friend' | 'share'
|
|
const friendId = pathParts[1];
|
|
|
|
let currentFriend = null;
|
|
let currentFolder = null;
|
|
let selectedType = 'image';
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str || '';
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function init() {
|
|
const sessionRes = await fetch('/api/session');
|
|
const session = await sessionRes.json();
|
|
|
|
const isAdmin = session.role === 'admin';
|
|
const isMatchingFriend = session.role === 'friend' && session.friendId === friendId;
|
|
|
|
document.getElementById('adminBackWrap').style.display = isAdmin ? 'block' : 'none';
|
|
|
|
if (isAdmin || isMatchingFriend) {
|
|
loadFriend();
|
|
} else if (mode === 'share') {
|
|
showLoginGate();
|
|
} else {
|
|
// /friend/:id ohne Admin-Session -> zur Startseite
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
|
|
function showLoginGate() {
|
|
document.getElementById('loginGate').style.display = 'block';
|
|
document.getElementById('editorRoot').style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('gateForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const password = document.getElementById('gatePassword').value;
|
|
const errBox = document.getElementById('gateErr');
|
|
errBox.innerHTML = '';
|
|
const res = await fetch(`/api/share/${friendId}/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
errBox.innerHTML = `<p class="error-msg">${data.error}</p>`;
|
|
return;
|
|
}
|
|
loadFriend();
|
|
});
|
|
|
|
async function loadFriend() {
|
|
const res = await fetch(`/api/friends/${friendId}`);
|
|
if (res.status === 401) { showLoginGate(); return; }
|
|
if (res.status === 404) { document.body.innerHTML = '<div class="container"><p class="error-msg">Diesen Freund gibt es nicht (mehr).</p></div>'; return; }
|
|
const data = await res.json();
|
|
currentFriend = data.friend;
|
|
currentFolder = currentFriend.folderName;
|
|
|
|
document.getElementById('loginGate').style.display = 'none';
|
|
document.getElementById('editorRoot').style.display = 'block';
|
|
document.getElementById('pageTitle').textContent = `📖 ${currentFriend.name}`;
|
|
|
|
renderProfileImage();
|
|
renderSteckbrief();
|
|
renderBlocks();
|
|
}
|
|
|
|
function uploadUrl(filename) {
|
|
return `/uploads/${encodeURIComponent(currentFolder)}/${encodeURIComponent(filename)}`;
|
|
}
|
|
|
|
function renderProfileImage() {
|
|
const img = document.getElementById('profileImg');
|
|
const placeholder = document.getElementById('profilePlaceholder');
|
|
if (currentFriend.profileImage) {
|
|
img.src = uploadUrl(currentFriend.profileImage) + `?t=${Date.now()}`;
|
|
img.style.display = 'block';
|
|
placeholder.style.display = 'none';
|
|
} else {
|
|
img.style.display = 'none';
|
|
placeholder.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
document.getElementById('profileInput').addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const fd = new FormData();
|
|
fd.append('image', file);
|
|
const res = await fetch(`/api/friends/${friendId}/profileimage`, { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
currentFriend = data.friend;
|
|
renderProfileImage();
|
|
} else {
|
|
alert(data.error || 'Upload fehlgeschlagen.');
|
|
}
|
|
});
|
|
|
|
function renderSteckbrief() {
|
|
const form = document.getElementById('steckbriefForm');
|
|
form.innerHTML = '';
|
|
STECKBRIEF_FIELDS.forEach(([key, label]) => {
|
|
const wrap = document.createElement('div');
|
|
const val = (currentFriend.steckbrief && currentFriend.steckbrief[key]) || '';
|
|
wrap.innerHTML = `<label>${label}</label><input type="text" data-key="${key}" value="${escapeHtml(val)}" />`;
|
|
form.appendChild(wrap);
|
|
});
|
|
}
|
|
|
|
document.getElementById('steckbriefForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const inputs = document.querySelectorAll('#steckbriefForm input');
|
|
const payload = {};
|
|
inputs.forEach((inp) => { payload[inp.dataset.key] = inp.value; });
|
|
const res = await fetch(`/api/friends/${friendId}/steckbrief`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
currentFriend = data.friend;
|
|
const savedEl = document.getElementById('sbSaved');
|
|
savedEl.textContent = 'Gespeichert ✓';
|
|
savedEl.classList.add('show');
|
|
setTimeout(() => savedEl.classList.remove('show'), 1800);
|
|
} else {
|
|
alert(data.error || 'Speichern fehlgeschlagen.');
|
|
}
|
|
});
|
|
|
|
// --- Block-Typ-Auswahl ---
|
|
document.querySelectorAll('.tab-btn').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
selectedType = btn.dataset.type;
|
|
document.querySelectorAll('.tab-btn').forEach((b) => b.classList.toggle('active', b === btn));
|
|
document.getElementById('blockType').value = selectedType;
|
|
const fileWrap = document.getElementById('blockFileWrap');
|
|
const textWrap = document.getElementById('blockTextWrap');
|
|
const coverWrap = document.getElementById('blockCoverWrap');
|
|
const fileLabel = document.getElementById('blockFileLabel');
|
|
const fileInput = document.getElementById('blockFile');
|
|
if (selectedType === 'text') {
|
|
fileWrap.style.display = 'none';
|
|
textWrap.style.display = 'block';
|
|
coverWrap.style.display = 'none';
|
|
} else {
|
|
fileWrap.style.display = 'block';
|
|
textWrap.style.display = 'none';
|
|
fileLabel.textContent = selectedType === 'image' ? 'Bild auswählen' : 'MP3 auswählen';
|
|
fileInput.accept = selectedType === 'image' ? 'image/*' : 'audio/*';
|
|
coverWrap.style.display = selectedType === 'audio' ? 'block' : 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById('blockForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData();
|
|
fd.append('type', selectedType);
|
|
fd.append('caption', document.getElementById('blockCaption').value);
|
|
if (selectedType === 'text') {
|
|
fd.append('content', document.getElementById('blockContent').value);
|
|
} else {
|
|
const file = document.getElementById('blockFile').files[0];
|
|
if (!file) { alert('Bitte eine Datei auswählen.'); return; }
|
|
fd.append('file', file);
|
|
if (selectedType === 'audio') {
|
|
const coverFile = document.getElementById('blockCover').files[0];
|
|
if (coverFile) fd.append('cover', coverFile);
|
|
}
|
|
}
|
|
const res = await fetch(`/api/friends/${friendId}/blocks`, { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
currentFriend = data.friend;
|
|
document.getElementById('blockForm').reset();
|
|
document.getElementById('blockCoverWrap').style.display = selectedType === 'audio' ? 'block' : 'none';
|
|
renderBlocks();
|
|
} else {
|
|
alert(data.error || 'Hinzufügen fehlgeschlagen.');
|
|
}
|
|
});
|
|
|
|
function renderBlocks() {
|
|
const list = document.getElementById('blocksList');
|
|
list.innerHTML = '';
|
|
const blocks = [...(currentFriend.blocks || [])].sort((a, b) => a.order - b.order);
|
|
blocks.forEach((b, idx) => {
|
|
const el = document.createElement('div');
|
|
el.className = 'block-item';
|
|
const tag = { image: '🖼️ Bild', text: '📝 Text', audio: '🎙️ Audio' }[b.type] || b.type;
|
|
let inner = `<div class="block-type-tag">${tag}</div>`;
|
|
if (b.type === 'image') {
|
|
inner += `<img src="${uploadUrl(b.filename)}" />`;
|
|
} else if (b.type === 'audio') {
|
|
inner += b.coverFilename
|
|
? `<img src="${uploadUrl(b.coverFilename)}" style="margin-bottom:0.4em;" />`
|
|
: '';
|
|
inner += `<audio controls src="${uploadUrl(b.filename)}"></audio>`;
|
|
} else {
|
|
inner += `<textarea data-content-for="${b.id}">${escapeHtml(b.content)}</textarea>`;
|
|
}
|
|
inner += `<input type="text" class="block-caption" placeholder="Bildunterschrift" data-caption-for="${b.id}" value="${escapeHtml(b.caption)}" />`;
|
|
inner += `<div class="block-actions">
|
|
<button type="button" class="secondary" data-save="${b.id}">💾</button>
|
|
<button type="button" class="secondary" data-up="${b.id}" ${idx === 0 ? 'disabled' : ''}>↑</button>
|
|
<button type="button" class="secondary" data-down="${b.id}" ${idx === blocks.length - 1 ? 'disabled' : ''}>↓</button>
|
|
<button type="button" class="danger" data-del="${b.id}">🗑️</button>
|
|
</div>`;
|
|
el.innerHTML = inner;
|
|
list.appendChild(el);
|
|
});
|
|
|
|
list.querySelectorAll('[data-save]').forEach((btn) => btn.addEventListener('click', () => saveBlock(btn.dataset.save)));
|
|
list.querySelectorAll('[data-del]').forEach((btn) => btn.addEventListener('click', () => deleteBlock(btn.dataset.del)));
|
|
list.querySelectorAll('[data-up]').forEach((btn) => btn.addEventListener('click', () => moveBlock(btn.dataset.up, -1)));
|
|
list.querySelectorAll('[data-down]').forEach((btn) => btn.addEventListener('click', () => moveBlock(btn.dataset.down, 1)));
|
|
}
|
|
|
|
async function saveBlock(id) {
|
|
const captionInput = document.querySelector(`[data-caption-for="${id}"]`);
|
|
const contentInput = document.querySelector(`[data-content-for="${id}"]`);
|
|
const payload = { caption: captionInput ? captionInput.value : '' };
|
|
if (contentInput) payload.content = contentInput.value;
|
|
const res = await fetch(`/api/friends/${friendId}/blocks/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) { currentFriend = data.friend; renderBlocks(); }
|
|
}
|
|
|
|
async function deleteBlock(id) {
|
|
if (!confirm('Diesen Block wirklich löschen?')) return;
|
|
const res = await fetch(`/api/friends/${friendId}/blocks/${id}`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (res.ok) { currentFriend = data.friend; renderBlocks(); }
|
|
}
|
|
|
|
async function moveBlock(id, dir) {
|
|
const blocks = [...(currentFriend.blocks || [])].sort((a, b) => a.order - b.order);
|
|
const idx = blocks.findIndex((b) => b.id === id);
|
|
const swapIdx = idx + dir;
|
|
if (swapIdx < 0 || swapIdx >= blocks.length) return;
|
|
[blocks[idx], blocks[swapIdx]] = [blocks[swapIdx], blocks[idx]];
|
|
const order = blocks.map((b) => b.id);
|
|
const res = await fetch(`/api/friends/${friendId}/blocks/reorder`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order }),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) { currentFriend = data.friend; renderBlocks(); }
|
|
}
|
|
|
|
// --- Vorschau: rendert die Seite genauso wie später im Freundesbuch-Viewer ---
|
|
function randomRotation(seed) {
|
|
const angles = [-6, -4, -2, 2, 4, 6, -3, 3, 5, -5];
|
|
return angles[seed % angles.length];
|
|
}
|
|
|
|
function renderPreview() {
|
|
const f = currentFriend;
|
|
const container = document.getElementById('previewContent');
|
|
|
|
const profileHtml = f.profileImage
|
|
? `<img class="page-profile-pic" src="${uploadUrl(f.profileImage)}" />`
|
|
: `<div class="page-profile-pic placeholder">${(f.name || '?')[0].toUpperCase()}</div>`;
|
|
|
|
const sbEntries = STECKBRIEF_FIELDS
|
|
.map(([key, label]) => [label, (f.steckbrief || {})[key]])
|
|
.filter(([, v]) => v && String(v).trim());
|
|
const sbHtml = sbEntries.length
|
|
? `<div class="sb-grid">${sbEntries.map(([label, v]) => `
|
|
<div class="sb-row"><span class="sb-key">${escapeHtml(label)}</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(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(b.coverFilename)}" />` : ''}
|
|
<div>🎙️${b.caption ? ' ' + escapeHtml(b.caption) : ''}</div>
|
|
<audio controls src="${uploadUrl(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>';
|
|
|
|
container.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>
|
|
`;
|
|
}
|
|
|
|
document.getElementById('previewBtn').addEventListener('click', () => {
|
|
renderPreview();
|
|
document.getElementById('previewModal').style.display = 'flex';
|
|
});
|
|
document.getElementById('previewCloseBtn').addEventListener('click', () => {
|
|
document.getElementById('previewModal').style.display = 'none';
|
|
});
|
|
document.getElementById('previewModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'previewModal') document.getElementById('previewModal').style.display = 'none';
|
|
});
|
|
|
|
init();
|