Files
dogfather-universe/workspace/assets/js/gate.js
T
DogFatherGit 19f969f36f Creator Workspace: Anmeldeseite unter /workspace
Erstes sichtbares Stueck des Creator-Workspace-Konzepts. Reine statische
Dateien in einem neuen Ordner workspace/ -- express.static liefert den
Repo-Ordner aus, die Seite ist damit ohne Servercode-Aenderung und ohne
Neustart unter /workspace/ erreichbar. Rein additiv: an bestehenden
Dateien wurde nichts geaendert.

Motiv: Dogfather und Hasi Dog stehen rechts im Bild, deshalb liegt die
Code-Tafel links ueber der ruhigen Flaeche (bei VanVans Buchhaltung ist es
gespiegelt, weil dort das Emblem links steht). Drei Layoutfaelle, damit die
Figuren in keiner Fenstergroesse von der Tafel ueberdeckt werden.

Bewusst nur WebP, kein AVIF: send 0.19.2 (Express 4) kennt AVIF nicht und
liefert es als application/octet-stream aus -- wegen des nosniff-Headers
verweigert der Browser das Bild dann komplett. Lokal gegen den echten
Express-Server geprueft: 0 Konsolenfehler, 0 CSP-Verstoesse.

Anmelden funktioniert noch nicht (kein Endpunkt); das Formular sagt das
jetzt ehrlich statt "Code stimmt nicht".
2026-08-27 18:13:05 +02:00

156 lines
5.5 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.
/* ===================================================================
Creator Workspace - Anmeldeseite
Kein Framework, keine externen Abhängigkeiten.
=================================================================== */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const rollen = Array.from(document.querySelectorAll('.rolle'));
const formular = $('formular');
const code = $('code');
const auge = $('auge');
const fehler = $('fehler');
const hinweis = $('hinweis');
const knopf = $('knopf');
const fuer = $('fuer');
const NAMEN = { admin: 'Management', creator: 'Creator', scout: 'Scout' };
const SPEICHER = 'dfw.rolle';
let gewaehlt = null;
/* ---------- Rolle wählen ------------------------------------------ */
function waehle(rolle, fokus = false) {
gewaehlt = rolle;
rollen.forEach((b) => b.setAttribute('aria-checked', String(b.dataset.rolle === rolle)));
fuer.textContent = `· ${NAMEN[rolle]}`;
fuer.hidden = false;
melde('');
try { localStorage.setItem(SPEICHER, rolle); } catch { /* Privater Modus */ }
if (fokus) code.focus();
}
rollen.forEach((b) => {
b.addEventListener('click', () => waehle(b.dataset.rolle, true));
// Pfeiltasten wie bei echten Radio-Feldern
b.addEventListener('keydown', (e) => {
const i = rollen.indexOf(b);
let ziel = null;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') ziel = rollen[(i + 1) % rollen.length];
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') ziel = rollen[(i - 1 + rollen.length) % rollen.length];
if (!ziel) return;
e.preventDefault();
ziel.focus();
waehle(ziel.dataset.rolle);
});
});
// Zuletzt genutzte Rolle vorauswählen - spart bei täglicher Nutzung einen Klick.
try {
const zuletzt = localStorage.getItem(SPEICHER);
if (zuletzt && NAMEN[zuletzt]) waehle(zuletzt);
} catch { /* egal */ }
/* ---------- Code anzeigen / verbergen ------------------------------ */
auge.addEventListener('click', () => {
const sichtbar = code.type === 'text';
code.type = sichtbar ? 'password' : 'text';
auge.setAttribute('aria-pressed', String(!sichtbar));
auge.setAttribute('aria-label', sichtbar ? 'Code anzeigen' : 'Code verbergen');
code.focus();
});
/* ---------- Feststelltaste ----------------------------------------- */
const pruefeFeststell = (e) => {
if (typeof e.getModifierState !== 'function') return;
hinweis.hidden = !e.getModifierState('CapsLock');
};
code.addEventListener('keyup', pruefeFeststell);
code.addEventListener('keydown', pruefeFeststell);
code.addEventListener('blur', () => { hinweis.hidden = true; });
/* ---------- Rückmeldung -------------------------------------------- */
function melde(text) { fehler.textContent = text; }
function laden(an) {
knopf.disabled = an;
knopf.querySelector('.knopf__text').textContent = an ? 'Prüfe …' : 'Anmelden';
}
/* ---------- Absenden ------------------------------------------------ */
formular.addEventListener('submit', async (e) => {
e.preventDefault();
const wert = code.value.trim();
if (!gewaehlt) { melde('Bitte zuerst auswählen, womit du dich anmeldest.'); rollen[0].focus(); return; }
if (!wert) { melde('Bitte den Zugangscode eingeben.'); code.focus(); return; }
melde('');
laden(true);
try {
const antwort = await fetch('/api/anmelden', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rolle: gewaehlt, code: wert }),
credentials: 'same-origin',
});
if (antwort.ok) {
const ziel = (await antwort.json().catch(() => ({}))).weiter || '/workspace';
document.body.classList.add('oeffnet');
setTimeout(() => { location.assign(ziel); }, 260);
return;
}
// 404 heißt: Es gibt noch gar keine Gegenstelle. Das ist derzeit der
// Normalfall - die Seite steht schon, der Login-Server wird noch gebaut.
// Eine ehrliche Meldung ist hier besser als "Code stimmt nicht".
if (antwort.status === 404) {
melde('Der Zugang wird gerade eingerichtet – Anmelden ist noch nicht möglich.');
return;
}
// Bewusst keine Auskunft darüber, ob Rolle oder Code falsch war.
melde(antwort.status === 429
? 'Zu viele Versuche. Bitte kurz warten.'
: 'Zugangscode stimmt nicht.');
code.select();
} catch {
melde('Keine Verbindung zum Server. Bitte erneut versuchen.');
} finally {
laden(false);
}
});
/* ---------- Sehr dezente Parallaxe ---------------------------------- */
const ruhig = matchMedia('(prefers-reduced-motion: reduce)');
const bild = document.querySelector('.buehne__bild');
const grob = matchMedia('(pointer: coarse)');
if (bild && !ruhig.matches && !grob.matches) {
let ziel = { x: 0, y: 0 }, ist = { x: 0, y: 0 }, laeuft = false;
addEventListener('pointermove', (e) => {
ziel.x = (e.clientX / innerWidth - 0.5) * -10; // max. 10 px
ziel.y = (e.clientY / innerHeight - 0.5) * -6;
if (!laeuft) { laeuft = true; requestAnimationFrame(schritt); }
}, { passive: true });
function schritt() {
ist.x += (ziel.x - ist.x) * 0.06;
ist.y += (ziel.y - ist.y) * 0.06;
bild.style.transform = `scale(1.03) translate3d(${ist.x.toFixed(2)}px, ${ist.y.toFixed(2)}px, 0)`;
if (Math.abs(ziel.x - ist.x) > 0.05 || Math.abs(ziel.y - ist.y) > 0.05) {
requestAnimationFrame(schritt);
} else { laeuft = false; }
}
}
})();