Files
hcrb/public/js/patches.js
T
guyg1474andClaude Opus 5.5 819219fb2d Rollen (Admin/Mitglied), Seite Kooperationen, Textanpassungen
- Gemeinsame Zugänge mit Rollen: Admin darf alles, Mitglied nutzt Kalender,
  Fahrtplaner, Treffpunkte und bearbeitet Mitglieder (kein Hinzufügen,
  Löschen, Import, keine Patches, keine Statistik-Korrektur)
- Server prüft die Rechte (403), Konten ohne Rolle gelten als Admin
- npm run user: add <name> [admin|mitglied], role <name> <rolle>
- Neue öffentliche Seite Kooperationen mit Titelbild, Anführer,
  Social-Media-Links und Beschreibung, Pflege durch Admin
- Kopfzeile bleibt bei mittlerer Breite einzeilig
- Texte auf Wunsch gekürzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:47:03 +02:00

180 lines
6.1 KiB
JavaScript

import { $, api, esc, showError, renderHeader, renderFooter, isAdmin, prepareImage } from './common.js';
renderHeader('patches');
renderFooter();
let patches = [];
let admin = false; // nur der Admin-Zugang darf Patches hochladen, ändern, löschen
let editingId = null;
let imageData = null; // data:-URL des gewählten (verkleinerten) Bildes
const imgUrl = (p) => `/patch-img/${encodeURIComponent(p.image)}`;
// ---------- Galerie ----------
function render() {
$('#patch-empty').hidden = patches.length > 0;
$('#patch-empty-hint').textContent = admin
? 'Lade über „Patch hochladen“ den ersten Patch hoch.'
: 'Sobald der Admin einen Patch hochlädt, erscheint er hier.';
for (const status of ['vorhanden', 'geplant']) {
const list = patches.filter((p) => p.status === status);
$(`#group-${status}`).hidden = list.length === 0;
$(`#count-${status}`).textContent = list.length === 1 ? '1 Patch' : `${list.length} Patches`;
$(`#grid-${status}`).innerHTML = list.map(patchHtml).join('');
}
}
function patchHtml(p) {
return `
<article class="patch ${p.status === 'geplant' ? 'planned' : ''}" id="${p.id}">
<button type="button" class="patch-img" data-zoom="${p.id}" aria-label="${esc(p.name)} groß anzeigen">
<img src="${imgUrl(p)}" alt="${esc(p.name)}" loading="lazy">
</button>
<div class="patch-body">
${p.status === 'geplant' ? '<span class="badge nogame">In Planung</span>' : ''}
<h3>${esc(p.name)}</h3>
${p.description ? `<p>${esc(p.description)}</p>` : ''}
${admin ? `
<div class="actions">
<button class="link" data-edit="${p.id}">Bearbeiten</button>
<button class="link danger" data-delete="${p.id}">Löschen</button>
</div>` : ''}
</div>
</article>`;
}
document.addEventListener('click', async (e) => {
if (!e.target.closest('.patch-grid')) return;
const zoom = e.target.closest('[data-zoom]');
if (zoom) return openLightbox(zoom.dataset.zoom);
const edit = e.target.closest('[data-edit]');
if (edit) return openForm(patches.find((p) => p.id === edit.dataset.edit));
const del = e.target.closest('[data-delete]');
if (del) {
const p = patches.find((x) => x.id === del.dataset.delete);
if (!confirm(`Patch „${p.name}“ wirklich löschen?`)) return;
try {
await api('DELETE', `/patches/${p.id}`);
await load();
} catch (err) {
showError(err);
}
}
});
function openLightbox(id) {
const p = patches.find((x) => x.id === id);
if (!p) return;
$('#lightbox-img').src = imgUrl(p);
$('#lightbox-img').alt = p.name;
$('#lightbox-name').textContent = p.name;
$('#lightbox-text').textContent = p.description;
$('#lightbox').showModal();
}
// Klick neben das Bild schließt
$('#lightbox').addEventListener('click', (e) => {
if (e.target === e.currentTarget) e.currentTarget.close();
});
// ---------- Formular ----------
function openForm(patch) {
editingId = patch?.id ?? null;
imageData = null;
$('#patch-form').reset();
$('#patch-error').hidden = true;
$('#patch-form-title').textContent = patch ? 'Patch bearbeiten' : 'Patch hochladen';
$('#patch-submit').textContent = patch ? 'Speichern' : 'Hochladen';
$('#patch-name').value = patch?.name ?? '';
$('#patch-description').value = patch?.description ?? '';
$(`#patch-status-${patch?.status ?? 'vorhanden'}`).checked = true;
showPreview(patch ? imgUrl(patch) : null);
$('#patch-form-card').hidden = false;
$('#patch-form-card').scrollIntoView({ behavior: 'smooth', block: 'start' });
$('#patch-name').focus({ preventScroll: true });
}
function closeForm() {
$('#patch-form-card').hidden = true;
editingId = null;
imageData = null;
}
function showPreview(src) {
$('#patch-preview').hidden = !src;
if (src) $('#patch-preview').src = src;
$('#drop-text').innerHTML = src
? '<strong>Anderes Bild wählen</strong>'
: '<strong>Bild auswählen</strong><br>oder hierher ziehen · PNG, JPEG, WebP oder SVG';
}
async function pickFile(file) {
if (!file) return;
$('#patch-error').hidden = true;
try {
imageData = await prepareImage(file);
showPreview(imageData);
if (!$('#patch-name').value) $('#patch-name').value = file.name.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').slice(0, 80);
} catch (err) {
imageData = null;
$('#patch-error').textContent = err.message || 'Das Bild konnte nicht gelesen werden.';
$('#patch-error').hidden = false;
}
}
$('#patch-file').addEventListener('change', (e) => pickFile(e.target.files[0]));
const drop = $('#dropzone');
drop.addEventListener('dragover', (e) => { e.preventDefault(); drop.classList.add('over'); });
drop.addEventListener('dragleave', () => drop.classList.remove('over'));
drop.addEventListener('drop', (e) => {
e.preventDefault();
drop.classList.remove('over');
pickFile(e.dataTransfer.files[0]);
});
$('#add-btn').addEventListener('click', () => openForm(null));
$('#patch-cancel').addEventListener('click', closeForm);
$('#patch-form').addEventListener('submit', async (e) => {
e.preventDefault();
const error = $('#patch-error');
error.hidden = true;
if (!editingId && !imageData) {
error.textContent = 'Bitte zuerst ein Bild auswählen.';
error.hidden = false;
return;
}
const button = $('#patch-submit');
button.disabled = true;
try {
const body = {
name: $('#patch-name').value,
description: $('#patch-description').value,
status: document.querySelector('input[name="patch-status"]:checked').value,
};
if (imageData) body.image = imageData;
if (editingId) await api('PUT', `/patches/${editingId}`, body);
else await api('POST', '/patches', body);
closeForm();
await load();
} catch (err) {
error.textContent = err.message;
error.hidden = false;
} finally {
button.disabled = false;
}
});
// ---------- Laden ----------
async function load() {
patches = await api('GET', '/patches');
render();
}
admin = await isAdmin();
$('#add-btn').hidden = !admin;
await load().catch(showError);
// Link von der Startseite (/patches#id) springt zum Patch
if (location.hash) document.getElementById(location.hash.slice(1))?.scrollIntoView({ block: 'center' });