mirror of
https://github.com/guyg1474/hcrb.git
synced 2026-10-16 07: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
+37
-3
@@ -81,6 +81,7 @@ const NAV = [
|
||||
{ href: '/gruppe', key: 'gruppe', label: 'Gruppe' },
|
||||
{ href: '/statistik', key: 'statistik', label: 'Statistik' },
|
||||
{ href: '/patches', key: 'patches', label: 'Patches' },
|
||||
{ href: '/kooperationen', key: 'kooperationen', label: 'Kooperationen' },
|
||||
];
|
||||
|
||||
// overlay: Kopfzeile liegt durchsichtig über dem Hero und wird erst beim Scrollen fest (Startseite)
|
||||
@@ -129,13 +130,20 @@ export function renderFooter() {
|
||||
document.body.append(footer);
|
||||
}
|
||||
|
||||
// Einmal pro Seite abfragen, ob jemand angemeldet ist
|
||||
// Einmal pro Seite abfragen, wer angemeldet ist: { name, role } oder null.
|
||||
// role: 'admin' oder 'mitglied'. Die Rechte prüft der Server – hier geht es nur darum, was angezeigt wird.
|
||||
let mePromise;
|
||||
export function currentUser() {
|
||||
mePromise ??= api('GET', '/me').then(({ user }) => user || null).catch(() => null);
|
||||
mePromise ??= api('GET', '/me')
|
||||
.then(({ user, role }) => (user ? { name: user, role } : null))
|
||||
.catch(() => null);
|
||||
return mePromise;
|
||||
}
|
||||
|
||||
export async function isAdmin() {
|
||||
return (await currentUser())?.role === 'admin';
|
||||
}
|
||||
|
||||
async function renderAuthLink(nav) {
|
||||
const user = await currentUser();
|
||||
if (!user) {
|
||||
@@ -145,7 +153,7 @@ async function renderAuthLink(nav) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = 'Abmelden';
|
||||
btn.title = `Angemeldet als ${user}`;
|
||||
btn.title = `Angemeldet als ${user.name} (${user.role === 'admin' ? 'Admin' : 'Mitglied'})`;
|
||||
btn.addEventListener('click', async () => {
|
||||
await api('POST', '/logout').catch(() => {});
|
||||
location.href = '/';
|
||||
@@ -158,3 +166,29 @@ export function formatTime(ev) {
|
||||
if (!ev.time) return '';
|
||||
return `${ev.time}${ev.endTime ? `–${ev.endTime}` : ''} Uhr`;
|
||||
}
|
||||
|
||||
// ---------- Bild-Upload ----------
|
||||
// Bild im Browser auf höchstens maxSize px verkleinern; Transparenz bleibt (WebP bzw. PNG).
|
||||
// Liefert eine data:-URL für die API.
|
||||
export async function prepareImage(file, maxSize = 1600) {
|
||||
// SVG bleibt unverändert (Vektorgrafik, gestochen scharf in jeder Größe)
|
||||
if (file.type === 'image/svg+xml' || /\.svg$/i.test(file.name)) {
|
||||
if (file.size > 2 * 1024 * 1024) throw new Error('SVG ist zu groß (max. 2 MB).');
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
let bin = '';
|
||||
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||||
return `data:image/svg+xml;base64,${btoa(bin)}`;
|
||||
}
|
||||
if (!/^image\/(png|jpeg|webp)$/.test(file.type)) throw new Error('Bitte ein PNG-, JPEG-, WebP- oder SVG-Bild wählen.');
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const scale = Math.min(1, maxSize / Math.max(bitmap.width, bitmap.height));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.round(bitmap.width * scale);
|
||||
canvas.height = Math.round(bitmap.height * scale);
|
||||
canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
bitmap.close?.();
|
||||
let url = canvas.toDataURL('image/webp', 0.9);
|
||||
// Browser ohne WebP-Export (ältere Safari) liefern PNG – für Fotos ist JPEG dann kleiner
|
||||
if (!url.startsWith('data:image/webp') && file.type === 'image/jpeg') url = canvas.toDataURL('image/jpeg', 0.88);
|
||||
return url;
|
||||
}
|
||||
Reference in new issue
Block a user