/* =================================================================== Der eigene Steckbrief -- Bild, ein Satz ueber sich, die Kanaele. Laeuft auf ZWEI Seiten: steckbrief.html fuer Scouts, Manager und DogFather -- dort ist er der ganze Inhalt profil.html fuer Creator -- dort steht er ueber der eigenen Betreuungsakte Warum eine eigene Datei: Vorher hing er hinten an profil.js. Damit war er an die Creator-Akte gekettet, und auf der Profilseite standen zwei Personen gleichzeitig -- oben "Profil: SpongBobSchwammKopf" und mittendrin "MEIN PROFIL: Dogfather". Er gehoert aber nicht zur Akte, sondern zur angemeldeten Person. Der Block laeuft fuer sich: Faellt die Akte aus, steht der Steckbrief weiterhin -- und umgekehrt. =================================================================== */ (() => { 'use strict'; const $ = (id) => document.getElementById(id); const block = $('steckbrief'); if (!block) return; /* Auf profil.html traegt der Block data-nur-creator: Dort steht die Akte eines ANDEREN Menschen, und der eigene Steckbrief hat daneben nichts zu suchen -- ausser bei einem Creator, der nur diese eine Seite hat. Wer ihn nicht sehen soll, bekommt ihn gar nicht erst geladen; die Abfrage unterbleibt ebenfalls. */ let nurCreator = block.dataset.nurCreator === 'ja'; let stand = null; async function hole(pfad, optionen = {}) { const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } return a; } const melde = (t) => { $('sb-fehler').textContent = t || ''; }; const sage = (t) => { $('sb-stand').textContent = t || ''; if (t) setTimeout(() => { if ($('sb-stand').textContent === t) $('sb-stand').textContent = ''; }, 4000); }; function bildZeigen(quelle, name) { const feld = $('sb-bild'); feld.textContent = ''; if (quelle) { const b = document.createElement('img'); b.src = quelle; b.alt = ''; /* Fällt das Bild aus, erscheint der Buchstabe -- nicht das kaputte Bildsymbol des Browsers. */ b.addEventListener('error', () => { b.remove(); feld.textContent = anfang(name); }); feld.append(b); $('sb-bild-weg').hidden = false; } else { feld.textContent = anfang(name); $('sb-bild-weg').hidden = true; } } const anfang = (n) => (n || '?').trim().charAt(0).toUpperCase(); function kanaeleBauen(kanaele, vorhanden) { const ziel = $('sb-kanaele'); ziel.textContent = ''; for (const k of kanaele) { const zeile = document.createElement('div'); zeile.className = 'kanal'; zeile.dataset.art = k.art; const schild = document.createElement('label'); schild.className = 'kanal__name'; schild.setAttribute('for', 'sb-' + k.art); schild.textContent = k.name; const feld = document.createElement('div'); feld.className = 'kanal__feld'; const at = document.createElement('span'); at.className = 'kanal__at'; at.setAttribute('aria-hidden', 'true'); at.textContent = '@'; const eingabe = document.createElement('input'); eingabe.id = 'sb-' + k.art; eingabe.type = 'text'; eingabe.maxLength = 40; eingabe.autocomplete = 'off'; eingabe.placeholder = 'dein Name'; const da = vorhanden.find((v) => v.art === k.art); eingabe.value = da ? da.handle : ''; feld.append(at, eingabe); zeile.append(schild, feld); /* Ein Verweis erscheint erst, wenn wirklich ein Name da ist -- ein Knopf, der ins Leere führt, ist schlimmer als keiner. */ if (da) { const a = document.createElement('a'); a.className = 'kanal__auf'; a.href = da.adresse; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = 'öffnen'; zeile.append(a); } ziel.append(zeile); } } function zeichne(sb, kanaele) { stand = sb; $('sb-name').textContent = sb.name; $('sb-rolle').textContent = sb.rolle_name; $('sb-rolle').dataset.rolle = sb.rolle; $('sb-ueber').value = sb.ueber_mich || ''; bildZeigen(sb.bild, sb.name); kanaeleBauen(kanaele, sb.kanaele); block.hidden = false; } /* ---------- Bild hochladen --------------------------------------------- Roh im Rumpf statt als multipart -- dasselbe Verfahren wie in der Dateiablage. Kein Parsen von multipart im Server, keine zusätzliche Abhängigkeit. */ $('sb-datei').addEventListener('change', async (e) => { const datei = e.target.files?.[0]; if (!datei) return; melde(''); if (datei.size > 4 * 1024 * 1024) { melde('Das Bild ist größer als 4 MB. Bitte ein kleineres nehmen.'); e.target.value = ''; return; } sage('Bild wird hochgeladen …'); try { const a = await hole('/workspace/api/steckbrief/bild', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: datei, }); const antwort = await a.json().catch(() => ({})); if (!a.ok) { melde(antwort.fehler || 'Das Hochladen ging nicht.'); sage(''); return; } bildZeigen(antwort.bild, stand?.name); sage('Bild gespeichert.'); /* Auch oben rechts in der Kopfleiste soll sofort das neue Bild stehen -- sonst müsste man die Seite neu laden, um es zu sehen. */ const oben = document.querySelector('.wer__zeichen'); if (oben) { oben.querySelector('.wer__bild')?.remove(); const b = document.createElement('img'); b.className = 'wer__bild'; b.src = antwort.bild; b.alt = ''; oben.append(b); } } catch { /* umgeleitet */ } finally { e.target.value = ''; } }); $('sb-bild-weg').addEventListener('click', async () => { if (!confirm('Profilbild entfernen?')) return; try { const a = await hole('/workspace/api/steckbrief/bild', { method: 'DELETE' }); if (!a.ok) { melde('Entfernen ging nicht.'); return; } bildZeigen(null, stand?.name); document.querySelector('.wer__bild')?.remove(); sage('Bild entfernt.'); } catch { /* umgeleitet */ } }); /* ---------- Speichern ---------------------------------------------------- */ $('sb-speichern').addEventListener('click', async () => { melde(''); const knopf = $('sb-speichern'); const koerper = { ueber_mich: $('sb-ueber').value }; for (const zeile of document.querySelectorAll('#sb-kanaele .kanal')) { koerper[zeile.dataset.art] = zeile.querySelector('input').value; } knopf.disabled = true; try { const a = await hole('/workspace/api/steckbrief/mein', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(koerper), }); const antwort = await a.json().catch(() => ({})); if (!a.ok) { melde(antwort.fehler || 'Speichern ging nicht.'); return; } /* Neu zeichnen, damit die "öffnen"-Verweise zu den frisch eingetragenen Namen sofort erscheinen. */ zeichne(antwort.steckbrief, KANAELE); sage('Profil gespeichert.'); } catch { /* umgeleitet */ } finally { knopf.disabled = false; } }); let KANAELE = []; /* ---------- Das Team ------------------------------------------------------ Wunsch vom 01.09.2026: "ich will, dass wir Manager, Scouts, DogFather auch die Fotos, Namen und so alles sehen, alles was die in ihrem Profil eintragen." Der Steckbrief war bis dahin eine Einbahnstraße: Jeder pflegte seinen, und niemand bekam ihn je zu sehen. Die Schnittstelle dafür lag fertig da und wurde von keiner Seite aufgerufen -- gebaut, dokumentiert, nie angeschlossen. NACH ROLLEN GRUPPIERT und nicht als eine lange Liste: "Wer kann mir helfen?" ist eine andere Frage als "wer ist alles da?", und die Antwort steht in der Rolle. DogFather und die Manager stehen dabei in DERSELBEN Gruppe -- er ist gleichwertiges Teammitglied und wird hier nicht als Kopf über die anderen gestellt. Wer wen sehen darf, entscheidet weiterhin der Server. Diese Seite zeigt nur, was sie bekommt. */ const ROLLEN_GRUPPEN = [ { name: 'Management', rollen: ['admin', 'manager'], text: 'Ansprechpartner für alles, was Freigaben oder Entscheidungen braucht.' }, { name: 'Scouting', rollen: ['scout'], text: 'Sie finden neue Creator und begleiten den Anfang.' }, { name: 'Creator', rollen: ['creator'], text: 'Die Leute, um die es geht.' }, ]; async function teamBauen(ich) { const block = document.getElementById('team-block'); if (!block) return; let liste; try { const a = await hole('/workspace/api/steckbrief'); if (!a.ok) return; liste = (await a.json()).steckbriefe || []; } catch { return; } if (!liste.length) return; const ziel = document.getElementById('team'); ziel.textContent = ''; document.getElementById('team-zahl').textContent = String(liste.length); document.getElementById('team-text').textContent = 'Bild, Name und was jeder über sich geschrieben hat. Jeder pflegt seinen ' + 'eigenen Eintrag – schreiben kann hier niemand für einen anderen.'; for (const gruppe of ROLLEN_GRUPPEN) { const drin = liste.filter((p) => gruppe.rollen.includes(p.rolle)); if (!drin.length) continue; const kopf = document.createElement('div'); kopf.className = 'team-gruppe'; const h = document.createElement('h3'); h.className = 'team-gruppe__name'; h.textContent = gruppe.name; kopf.append(h); const t = document.createElement('p'); t.className = 'team-gruppe__text'; t.textContent = gruppe.text; kopf.append(t); ziel.append(kopf); const reihe = document.createElement('div'); reihe.className = 'team-reihe'; for (const p of drin) reihe.append(karteBauen(p, ich)); ziel.append(reihe); } block.hidden = false; } function karteBauen(p, ich) { const k = document.createElement('article'); k.className = 'team-karte'; k.dataset.rolle = p.rolle; /* Die eigene Karte wird gekennzeichnet. Ohne das sucht man sich in einer Liste von zwanzig Gesichtern selbst. */ if (ich && p.id === ich.id) k.dataset.ich = 'ja'; const bild = document.createElement('span'); bild.className = 'team-karte__bild'; bild.setAttribute('aria-hidden', 'true'); if (p.bild) { const b = document.createElement('img'); b.src = p.bild; b.alt = ''; b.loading = 'lazy'; b.addEventListener('error', () => { b.remove(); bild.textContent = anfang(p.name); }); bild.append(b); } else { bild.textContent = anfang(p.name); } k.append(bild); const text = document.createElement('div'); text.className = 'team-karte__text'; const zeile = document.createElement('div'); zeile.className = 'team-karte__zeile'; const name = document.createElement('h4'); name.className = 'team-karte__name'; name.textContent = p.name; zeile.append(name); if (ich && p.id === ich.id) { const marke = document.createElement('span'); marke.className = 'team-karte__ich'; marke.textContent = 'du'; zeile.append(marke); } text.append(zeile); const rolle = document.createElement('p'); rolle.className = 'team-karte__rolle'; rolle.textContent = p.rolle_name || p.rolle; text.append(rolle); /* Steht nichts da, steht auch nichts da -- kein Platzhaltersatz. "Noch nichts eingetragen" auf zwanzig Karten liest niemand, und es macht die Karten künstlich gleich hoch. */ if (p.ueber_mich) { const u = document.createElement('p'); u.className = 'team-karte__ueber'; u.textContent = p.ueber_mich; text.append(u); } if (p.kanaele?.length) { const kan = document.createElement('div'); kan.className = 'team-karte__kanaele'; for (const c of p.kanaele) { const a = document.createElement('a'); a.className = 'team-kanal'; a.dataset.art = c.art; a.href = c.adresse; a.target = '_blank'; /* noopener: Ohne das könnte die geöffnete fremde Seite auf das Fenster hier zugreifen. */ a.rel = 'noopener noreferrer'; a.textContent = c.name; a.title = `${c.name}: ${c.handle}`; kan.append(a); } text.append(kan); } k.append(text); return k; } (async () => { try { const ich = await (await hole('/workspace/api/ich')).json().catch(() => null); /* Die Kopfleiste fuellen -- VOR dem moeglichen Ausstieg unten. Diese Seite war die einzige im ganzen Workspace, die das nie getan hat: Oben rechts stand dauerhaft "…" statt des eigenen Namens. Aufgefallen ist es erst, als der Sicht-Umschalter dort fehlte, denn der haengt an derselben Stelle. Ein Fehler, den man monatelang ansehen kann, ohne ihn zu sehen. */ if (ich) window.werZeigen?.(ich); if (nurCreator && ich && ich.rolle !== 'creator') return; const a = await hole('/workspace/api/steckbrief/mein'); if (!a.ok) return; const { steckbrief, kanaele } = await a.json(); KANAELE = kanaele; zeichne(steckbrief, kanaele); /* Erst NACH dem eigenen Steckbrief -- und in einem eigenen try/catch weiter innen, damit ein Ausfall des Teams nicht das eigene Profil mitnimmt. */ await teamBauen(ich); } catch { /* umgeleitet oder nicht verfügbar -- die Seite bleibt nutzbar */ } })(); })();