mirror of
https://github.com/guyg1474/hcrb.git
synced 2026-10-15 23:13:28 +02:00
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>
This commit is contained in:
1 parent
f46016487d
commit
819219fb2d
18 files changed
+661
-106
No files matched your search
@@ -0,0 +1,207 @@
|
||||
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);
|
||||
Reference in new issue
Block a user