reverseproxy
This commit is contained in:
@@ -44,3 +44,16 @@
|
||||
display: flex; align-items: center; justify-content: center; z-index: 50; padding: 1em;
|
||||
}
|
||||
.modal { width: 100%; max-width: 420px; }
|
||||
|
||||
.transfer-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1.4em;
|
||||
}
|
||||
.transfer-row input[type="file"] { margin-top: 0.3em; }
|
||||
|
||||
.saved-msg2 {
|
||||
margin-top: 0.6em; color: #3a8a5a; font-weight: 700; font-size: 0.9rem;
|
||||
opacity: 0; transition: opacity 0.3s;
|
||||
}
|
||||
.saved-msg2.show { opacity: 1; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let friends = [];
|
||||
let shareTargetId = null;
|
||||
let settings = { publicBaseUrl: '' };
|
||||
|
||||
async function checkSession() {
|
||||
const res = await fetch('/api/session');
|
||||
@@ -9,6 +10,37 @@ async function checkSession() {
|
||||
}
|
||||
}
|
||||
|
||||
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 || '';
|
||||
}
|
||||
|
||||
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
|
||||
: window.location.origin;
|
||||
}
|
||||
|
||||
async function loadFriends() {
|
||||
const res = await fetch('/api/friends');
|
||||
if (res.status === 401) { window.location.href = '/'; return; }
|
||||
@@ -37,6 +69,7 @@ function renderFriends() {
|
||||
<div class="friend-actions">
|
||||
<a class="btn" href="/friend/${f.id}">✏️ Bearbeiten</a>
|
||||
<button class="secondary" data-share="${f.id}">🔗 Freigabe</button>
|
||||
<button class="secondary" data-export="${f.id}">⬇️ Export</button>
|
||||
<button class="danger" data-delete="${f.id}">🗑️</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -49,6 +82,9 @@ function renderFriends() {
|
||||
grid.querySelectorAll('[data-delete]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => deleteFriend(btn.dataset.delete));
|
||||
});
|
||||
grid.querySelectorAll('[data-export]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => window.location.href = `/api/friends/${btn.dataset.export}/export`);
|
||||
});
|
||||
}
|
||||
|
||||
// wir brauchen den vollen Ordnernamen für Bild-URLs; da wir ihn nicht separat liefern,
|
||||
@@ -101,7 +137,7 @@ function openShareModal(id) {
|
||||
const linkBox = document.getElementById('shareLinkBox');
|
||||
if (f && f.shareEnabled) {
|
||||
linkBox.style.display = 'block';
|
||||
document.getElementById('shareLinkInput').value = `${window.location.origin}/share/${id}`;
|
||||
document.getElementById('shareLinkInput').value = `${baseUrl()}/share/${id}`;
|
||||
} else {
|
||||
linkBox.style.display = 'none';
|
||||
}
|
||||
@@ -127,7 +163,7 @@ document.getElementById('shareEnableBtn').addEventListener('click', async () =>
|
||||
return;
|
||||
}
|
||||
document.getElementById('shareLinkBox').style.display = 'block';
|
||||
document.getElementById('shareLinkInput').value = `${window.location.origin}/share/${shareTargetId}`;
|
||||
document.getElementById('shareLinkInput').value = `${baseUrl()}/share/${shareTargetId}`;
|
||||
await loadFriends();
|
||||
});
|
||||
|
||||
@@ -154,5 +190,56 @@ 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();
|
||||
|
||||
@@ -17,7 +17,18 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<section class="card" id="createCard">
|
||||
<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>
|
||||
|
||||
<section class="card" id="createCard" style="margin-top:1.4em;">
|
||||
<h2>Neuen Freund anlegen</h2>
|
||||
<form id="createForm" class="inline-form">
|
||||
<input type="text" id="newName" placeholder="Name des Freundes" required />
|
||||
@@ -26,6 +37,33 @@
|
||||
<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>
|
||||
<div id="friendGrid" class="friend-grid"></div>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
.scrap-item.audio-note { width: 220px; padding: 1.2em; background: #e8f3ec; text-align: center; }
|
||||
.scrap-item.audio-note audio { width: 100%; margin-top: 0.6em; }
|
||||
.audio-cover { width: 100%; height: 150px; object-fit: cover; border-radius: 6px; margin-bottom: 0.6em; }
|
||||
|
||||
.scrap-caption { font-family: 'Caveat', cursive; font-size: 1.2rem; text-align: center; margin-top: 0.4em; color: var(--ink-soft); }
|
||||
|
||||
|
||||
@@ -76,6 +76,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>
|
||||
<audio controls src="${uploadUrl(f.folderName, b.filename)}"></audio>
|
||||
</div>`;
|
||||
|
||||
@@ -160,16 +160,19 @@ document.querySelectorAll('.tab-btn').forEach((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';
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -185,12 +188,17 @@ document.getElementById('blockForm').addEventListener('submit', async (e) => {
|
||||
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.');
|
||||
@@ -209,6 +217,9 @@ function renderBlocks() {
|
||||
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>`;
|
||||
|
||||
@@ -67,6 +67,11 @@
|
||||
<label>Text</label>
|
||||
<textarea id="blockContent" placeholder="Schreib etwas Schönes..."></textarea>
|
||||
</div>
|
||||
<div id="blockCoverWrap" style="display:none; margin-top:0.8em;">
|
||||
<label>Cover-Bild (optional)</label>
|
||||
<input type="file" id="blockCover" accept="image/*" />
|
||||
<p class="muted" style="margin:0.3em 0 0; font-size:0.82rem;">Wird kein Cover ausgewählt, versuchen wir automatisch ein in der MP3 eingebettetes Cover zu verwenden.</p>
|
||||
</div>
|
||||
<label style="margin-top:0.8em;">Bildunterschrift (optional)</label>
|
||||
<input type="text" id="blockCaption" placeholder="z. B. Sommer 2026" />
|
||||
<button type="submit" style="margin-top:0.8em;">+ Hinzufügen</button>
|
||||
|
||||
Reference in New Issue
Block a user