Files
hcrb/public/js/kooperationen.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

208 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { $, api, esc, showError, renderHeader, renderFooter, isAdmin, prepareImage } from './common.js';
renderHeader('kooperationen');
renderFooter();
let coops = [];
let admin = false; // nur der Admin-Zugang darf anlegen, ändern, löschen
let editingId = null;
let imageData = null; // data:-URL des neu gewählten Titelbildes
const imgUrl = (c) => `/koop-img/${encodeURIComponent(c.image)}`;
// ---------- Soziale Medien ----------
const SOCIALS = [
{ key: 'website', label: 'Website', icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z"/>' },
{ key: 'instagram', label: 'Instagram', icon: '<rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4.2"/><circle cx="17.5" cy="6.5" r=".9" fill="currentColor" stroke="none"/>' },
{ key: 'youtube', label: 'YouTube', icon: '<rect x="2" y="5" width="20" height="14" rx="4"/><path d="M10 9l5 3-5 3z" fill="currentColor"/>' },
{ key: 'tiktok', label: 'TikTok', icon: '<path d="M14 3v11.5a3.5 3.5 0 1 1-3.5-3.5"/><path d="M14 3c.4 2.6 2.2 4.4 5 4.6"/>' },
{ key: 'facebook', label: 'Facebook', icon: '<path d="M14.5 21v-8h3l.5-3.5h-3.5V7.6c0-1 .4-1.7 1.8-1.7H18V2.8c-.4 0-1.6-.2-2.9-.2-2.8 0-4.6 1.7-4.6 4.8v2.1H7.5V13h3v8"/>' },
{ key: 'discord', label: 'Discord', icon: '<path d="M7 7.5c3.3-1.3 6.7-1.3 10 0 1.7 2.5 2.6 5.3 2.5 8.5-1.5 1.2-3 1.9-4.6 2.3l-1-1.8M7 7.5C5.3 10 4.4 12.8 4.5 16c1.5 1.2 3 1.9 4.6 2.3l1-1.8M8 15.5c2.7 1.2 5.3 1.2 8 0"/><circle cx="9.5" cy="12" r="1.1" fill="currentColor" stroke="none"/><circle cx="14.5" cy="12" r="1.1" fill="currentColor" stroke="none"/>' },
];
const icon = (s) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${s.icon}</svg>`;
function socialsHtml(c) {
const links = SOCIALS.filter((s) => c.socials?.[s.key]);
return links.length
? links.map((s) => `<a class="button" href="${esc(c.socials[s.key])}" target="_blank" rel="noopener noreferrer">${icon(s)}${s.label}</a>`).join('')
: '<p class="muted">Keine Links eingetragen.</p>';
}
// Ohne Titelbild: Anfangsbuchstaben auf Flaggenblau
const initials = (name) => name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join('').toUpperCase();
// ---------- Übersicht ----------
function renderOverview() {
$('#coop-empty').hidden = coops.length > 0;
$('#coop-empty-hint').textContent = admin
? 'Trag über „Kooperation hinzufügen“ die erste Gruppe ein.'
: 'Sobald der Admin eine Kooperation einträgt, erscheint sie hier.';
$('#coop-grid').innerHTML = coops.map((c) => `
<a class="coop-card" href="#${c.id}">
<div class="coop-card-img">
${c.image ? `<img src="${imgUrl(c)}" alt="" loading="lazy">` : `<span class="coop-initials">${esc(initials(c.name))}</span>`}
</div>
<div class="coop-card-body">
<h2>${esc(c.name)}</h2>
${c.leader ? `<p class="muted">Anführer: ${esc(c.leader)}</p>` : ''}
<span class="go">Ansehen</span>
</div>
</a>`).join('');
}
// ---------- Einzelansicht (#id) ----------
function renderDetail(c) {
document.title = `${c.name} – Kooperationen – HCRB`;
$('#detail-name').textContent = c.name;
const img = $('#detail-img');
img.hidden = !c.image;
if (c.image) img.src = imgUrl(c);
$('#detail-hero').classList.toggle('no-image', !c.image);
$('#detail-hero').dataset.initials = initials(c.name);
$('#detail-description').textContent = c.description || 'Noch keine Beschreibung.';
$('#detail-leader').textContent = c.leader || 'Nicht angegeben';
$('#detail-socials').innerHTML = socialsHtml(c);
$('#detail-admin').hidden = !admin;
}
function route() {
const id = location.hash.slice(1);
const coop = coops.find((c) => c.id === id);
$('#overview').hidden = !!coop;
$('#detail').hidden = !coop;
if (coop) {
renderDetail(coop);
scrollTo(0, 0);
} else {
document.title = 'Kooperationen – HCRB';
renderOverview();
}
}
addEventListener('hashchange', () => {
closeForm();
route();
});
$('#back-link').addEventListener('click', (e) => {
e.preventDefault();
history.pushState(null, '', location.pathname);
route();
});
// ---------- Formular ----------
function showPreview(src) {
$('#coop-preview').hidden = !src;
if (src) $('#coop-preview').src = src;
$('#drop-text').innerHTML = src
? '<strong>Anderes Titelbild wählen</strong>'
: '<strong>Titelbild auswählen</strong><br>oder hierher ziehen · am besten ein breites Foto (Querformat)';
}
function openForm(coop) {
editingId = coop?.id ?? null;
imageData = null;
$('#coop-form').reset();
$('#coop-error').hidden = true;
$('#coop-form-title').textContent = coop ? 'Kooperation bearbeiten' : 'Kooperation hinzufügen';
$('#coop-name').value = coop?.name ?? '';
$('#coop-leader').value = coop?.leader ?? '';
$('#coop-description').value = coop?.description ?? '';
for (const s of SOCIALS) $(`#social-${s.key}`).value = coop?.socials?.[s.key] ?? '';
$('#remove-image-wrap').hidden = !coop?.image;
showPreview(coop?.image ? imgUrl(coop) : null);
$('#coop-form-card').hidden = false;
$('#coop-form-card').scrollIntoView({ behavior: 'smooth', block: 'start' });
$('#coop-name').focus({ preventScroll: true });
}
function closeForm() {
$('#coop-form-card').hidden = true;
editingId = null;
imageData = null;
}
async function pickFile(file) {
if (!file) return;
$('#coop-error').hidden = true;
try {
imageData = await prepareImage(file, 2000);
showPreview(imageData);
$('#coop-remove-image').checked = false;
} catch (err) {
imageData = null;
$('#coop-error').textContent = err.message || 'Das Bild konnte nicht gelesen werden.';
$('#coop-error').hidden = false;
}
}
$('#coop-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));
$('#coop-cancel').addEventListener('click', closeForm);
$('#detail-edit').addEventListener('click', () => openForm(coops.find((c) => c.id === location.hash.slice(1))));
$('#detail-delete').addEventListener('click', async () => {
const c = coops.find((x) => x.id === location.hash.slice(1));
if (!c || !confirm(`Kooperation „${c.name}“ wirklich löschen?`)) return;
try {
await api('DELETE', `/kooperationen/${c.id}`);
history.pushState(null, '', location.pathname);
await load();
} catch (err) {
showError(err);
}
});
$('#coop-form').addEventListener('submit', async (e) => {
e.preventDefault();
const error = $('#coop-error');
error.hidden = true;
const button = $('#coop-submit');
button.disabled = true;
try {
const body = {
name: $('#coop-name').value,
leader: $('#coop-leader').value,
description: $('#coop-description').value,
socials: Object.fromEntries(SOCIALS.map((s) => [s.key, $(`#social-${s.key}`).value])),
};
if (imageData) body.image = imageData;
else if ($('#coop-remove-image').checked) body.removeImage = true;
const saved = editingId
? await api('PUT', `/kooperationen/${editingId}`, body)
: await api('POST', '/kooperationen', body);
closeForm();
coops = await api('GET', '/kooperationen');
// Nach dem Speichern die Seite der Kooperation zeigen
if (location.hash.slice(1) === saved.id) route();
else location.hash = saved.id;
} catch (err) {
error.textContent = err.message;
error.hidden = false;
} finally {
button.disabled = false;
}
});
// ---------- Laden ----------
async function load() {
coops = await api('GET', '/kooperationen');
route();
}
admin = await isAdmin();
$('#add-btn').hidden = !admin;
showPreview(null);
await load().catch(showError);