Filipe: "Es darf vor allem keine Stellen geben, an denen ein Benutzer
etwas falsch machen kann, nur weil die Seite es nicht verstaendlich
genug erklaert."
Gemessen: 30 Stellen in 16 Dateien benutzten confirm() oder prompt().
Das Haus hatte die richtige Bauweise laengst -- einen <dialog>, in
aufgaben.html sogar ausfuehrlich begruendet -- aber sie stand IN EINER
SEITE. Wer anderswo etwas loeschen liess, hatte sie nicht.
confirm('Wirklich loeschen?') stellt die falsche Frage: Es fragt, ob
man sicher ist, und nennt nicht, WAS passiert, was BLEIBT und ob es
ZURUECK geht. Jetzt beantwortet jeder der 41 Dialoge alle drei.
Neu: workspace/assets/js/nachfrage.js -- window.frageNach() mit
Pflichtgrund, Zahlenfeld, einzeiliger Eingabe und Abtippsicherung.
Drei Ausgaenge: <dialog> / confirm()-Notnagel fuer Safari vor 15.4 /
Abbruch (Esc, Klick daneben, "Doch nicht" -- immer false).
DREIMAL DERSELBE FALLSTRICK, dreimal nachgemessen statt vermutet:
.dialog stand in aufgaben.css und leistung.css -> auf dateien.html
waere der Dialog ein weisser Systemkasten gewesen. 14 Regeln
klammergenau nach module.css verschoben (Klammern gezaehlt, nicht
per Muster geschnitten -- heute frueh hat ein nicht-gieriges
Muster schon einmal CSS zerrissen).
Das Formular trug .neu neu--blank -- und .neu gibt seine Abstaende
nur in aufgaben.css. Gemessen: padding 0px, und die Felder
verloren ihre height:44px. Jetzt steht alles unter
.nachfrage__form in module.css; der Dialog borgt nichts mehr.
Die erste Fassung der Pruefung zaehlte nachfrage.js SELBST als
Nutzer -- damit war jede Seite trivialerweise "Nutzer" und die
Pruefung gruen ohne Inhalt. Jetzt ausdruecklich ausgenommen.
ZWEI FUNDE NEBENBEI:
hilfeAufraeumen() wird im Betrieb NIE aufgerufen. Der Kommentar
behauptete "wird beim Start aufgerufen (siehe index.js)" -- das
war nie wahr; einziger Aufrufer ist die eigene Pruefung. Folge:
geschlossene vertrauliche Faelle bleiben unbegrenzt stehen. NICHT
eingeschaltet (das loescht echte Daten und ist Filipes
Entscheidung), sondern der Kommentar richtiggestellt.
Einen Hilfe-Fall zu schliessen ist endgueltig -- es gibt keine
Route, die ihn wieder oeffnet. Vorher stand darueber nur die
Frage nach einem Schlusswort. Jetzt sagt der Dialog es.
pruef-struktur hat meine eigene Pruefung von heute Nachmittag
erwischt: Sie bildete ihr Datum aus UTC. Beim Beheben erst
heuteLokal(datum) genommen -- die Funktion nimmt gar kein Argument
und haette still "heute" statt "+3 Tage" geliefert. Jetzt tagLokal(3),
nachgerechnet: Abstand 3 Tage.
Am Bildschirm angesehen (Rechner 1280, Handy 390): passt rein, Esc
ergibt false, Fokus liegt auf dem harmlosen Knopf, Knoepfe 44px auf
Touch. Der Platzhalter im Abtippfeld zeigte den erwarteten Namen --
das sah aus wie ein schon ausgefuelltes Feld, entfernt.
Neu: server/pruef-nachfrage.mjs -- 17/0, mit sechs Gegenproben und
beiden Richtungen (wer fragt, laedt die Datei; wer nie fragt, laedt
sie nicht -- sonst truege die Anmeldewand 4,8 KB fuer nichts).
pruef-meldungen 8/0, pruef-css-klassen gruen, pruef-struktur gruen,
pruef-leistung gruen.
403 lines
15 KiB
JavaScript
403 lines
15 KiB
JavaScript
/* ===================================================================
|
||
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(sagWas(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 (!await frageNach({
|
||
titel: 'Profilbild entfernen?',
|
||
was: 'Dein Bild verschwindet überall – aus der Liste, aus dem Chat, '
|
||
+ 'aus deinem Steckbrief.',
|
||
bleibt: 'Alles andere an deinem Steckbrief bleibt stehen. Du kannst '
|
||
+ 'jederzeit ein neues Bild hochladen.',
|
||
ja: 'Bild 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(sagWas(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. */
|
||
|
||
/* DIE ABSCHNITTE KOMMEN JETZT VOM SERVER (19.09.2026).
|
||
|
||
Hier stand eine Liste aus drei Gruppen — Management, Scouting,
|
||
Creator — und wer in keine davon passte, fiel LAUTLOS heraus.
|
||
Betroffen waren die rechte Hand, die Modis und (seit heute) die
|
||
Community: Ein Modi, der diese Seite öffnete, sah seine eigenen
|
||
Leute nicht. Kein Fehler, keine leere Liste, sie waren einfach
|
||
nicht da.
|
||
|
||
Zwei Fliegen: Die Zuordnung kann nicht mehr veralten, und in
|
||
dieser Datei — die jeder Besucher herunterlädt — steht keine
|
||
Liste von Rollennamen mehr.
|
||
|
||
Die REIHENFOLGE der Abschnitte ist die, in der der Server die
|
||
Steckbriefe schickt (er sortiert nach Rolle). Eine zweite
|
||
Sortierung hier wäre die, die damit auseinanderläuft. */
|
||
function abschnitteBilden(liste) {
|
||
const nach = new Map();
|
||
for (const p of liste) {
|
||
const name = p.abschnitt || 'Weitere';
|
||
if (!nach.has(name)) {
|
||
nach.set(name, { name, text: p.abschnitt_text || '', leute: [] });
|
||
}
|
||
nach.get(name).leute.push(p);
|
||
}
|
||
return [...nach.values()];
|
||
}
|
||
|
||
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 abschnitteBilden(liste)) {
|
||
if (!gruppe.leute.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);
|
||
/* Ohne Text kein leerer Absatz -- der macht sonst eine Lücke,
|
||
die aussieht, als fehle etwas. */
|
||
if (gruppe.text) {
|
||
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 gruppe.leute) 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 */ }
|
||
})();
|
||
})();
|