Filipe zu dieser Seite: "mach was anderes daraus oder mach es viel krass und geiler ... erstell was was mich von den socken schmeisst. es soll wirklich sogar ein wow effekt fuer die community ergeben." Dazu die eine Auflage: die Kachel bleibt. WAS SIE WAR: zwei Karten mit zwei Adressen. Sauber gebaut, richtig beschriftet -- und niemand kam ein zweites Mal wieder. Wer im Treff ist, kennt die Website. Den Rabattcode merkt man sich beim ersten Mal. DIE FRAGE, DIE WIRKLICH JEMAND HAT, IST EINE ANDERE: Laeuft er gerade? Und wenn nicht -- wie lange noch? Die Antwort kennt das Haus seit dem 20.08.: postfach.dogfather-universe.com/live-status, derselbe Dienst, von dem der oeffentliche Streamplan lebt. Im Treff gab es sie bisher nicht. Jetzt steht oben ein Puls: ein Ring, der sich ueber den Tag bis zur naechsten Sendezeit fuellt, darin der Countdown auf die Sekunde. Sendet er, wird die Karte rot, atmet im Ruhepuls und traegt den Titel des Streams plus einen Weg hin. Darunter die drei Accounts und -- wenn die Verwaltung eines eintraegt -- das besondere Event mit eigener Uhr. DREI AUSGAENGE, NICHT ZWEI. Das ist der eigentliche Unterschied zur oeffentlichen Seite: streamplan.js faengt dort jeden Fehler ab und setzt `istLive = false`. Aus "wir konnten nicht fragen" wird "er sendet nicht" -- und wer das liest, macht die Seite zu und verpasst den Stream. Der Dienst liefert `autoHealthy` sogar mit; gelesen wird es draussen nicht. Hier schon: Bei einer Stoerung steht "Konnten wir gerade nicht nachsehen", dazu der Hinweis, dass das NICHT heisst, dass kein Stream laeuft. Der Countdown laeuft dabei weiter -- er braucht kein Netz. NICHTS IST ABGESCHRIEBEN. Die drei Accounts kommen ueber /workspace/api/draussen aus derselben KANAELE-Liste, an der die Aufgabenverteilung haengt; die Adressen baut der Steckbrief. Die Sendezeit steht im Server, und pruef-draussen haelt sie gegen FIX_STUNDE in streamplan.js -- verschiebt jemand den Termin und aendert nur einen Ort, wird die Pruefung rot. Gemessen, bevor gebaut wurde: der Dienst antwortet und meldet sich gesund, CORS steht auf *, und `connect-src` der Treff-Seiten fuehrt postfach bereits. Ohne das Letzte waere der Abruf still blockiert worden und haette ausgesehen wie ein toter Dienst. Die Kachel heisst jetzt "Draussen" statt "Unsere Seiten" -- drei Dinge statt zwei Adressen. Ziel und Datei bleiben gleich. pruef-wege-nach-draussen schreibt den Namen nicht mehr ab, sondern verlangt, dass Kachel und Seitenueberschrift uebereinstimmen. Zwei Namen fuer denselben Ort waren schon einmal ein echter Fehler. Nebenbefund, dort gleich behoben: Dieselbe Pruefung verlangte, dass die Community-Kacheln restlos in Dreierreihen aufgehen -- obwohl ihr eigener Kommentar sagt, eine freie Zelle am Ende sei erlaubt. Sie war rot bei einwandfreiem Raster. Gemessen wird jetzt, was wirklich schiefgeht: eine doppelt breite Kachel, die in der letzten Spalte beginnt und die Zelle davor leer laesst. Mit Gegenprobe. pruef-draussen: 39 Pruefungen, alle vier Lagen im echten Browser -- die Stoerung wird dafuer absichtlich hergestellt. pruef-wege-nach-draussen: 67 statt 66, keine Fehler mehr. Co-Authored-By: Claude Opus 5 <[email protected]>
439 lines
17 KiB
JavaScript
439 lines
17 KiB
JavaScript
/* =====================================================================
|
||
UNSERE SEITEN (19.09.2026)
|
||
|
||
Zwei Dinge macht diese Datei, und beide gehen auf denselben Satz von
|
||
Filipe zurück: „egal wo man drauf drückt ... sobald man drauf drückt
|
||
weiter geleitet".
|
||
|
||
1. Die GANZE Karte ist ein Weg nach draußen, nicht nur der Knopf.
|
||
2. Der Rabattcode wandert mit einem Tippen in die Zwischenablage.
|
||
|
||
WARUM DIE KARTE KEIN <a> IST, DAS ALLES UMSCHLIESST: Darin steckt ein
|
||
Knopf (der Code) und ein weiterer Link (der Knopf „Shop öffnen").
|
||
Ein Link in einem Link ist kein gültiges HTML — Browser reparieren
|
||
das still, indem sie den äußeren beenden, und dann ist die halbe
|
||
Karte tot. Deshalb: ein Klick-Zuhörer auf der Karte, der Kinder mit
|
||
eigener Bedeutung in Ruhe lässt.
|
||
|
||
UND DER LINK BLEIBT TROTZDEM EIN LINK. Wer mit der Tastatur
|
||
unterwegs ist, bekommt weiterhin den Knopf „Seite öffnen" als
|
||
Haltepunkt. Die Karte selbst wird NICHT fokussierbar gemacht: Das
|
||
gäbe einen zweiten Tab-Halt mit demselben Ziel — für jemanden, der
|
||
sich durchtabbt, sieht das aus wie ein Fehler.
|
||
===================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* ---- 1. DIE GANZE KARTE IST DER WEG ------------------------------ */
|
||
|
||
/** Hat der Klick schon eine eigene Bedeutung getroffen?
|
||
*
|
||
* Ein Klick auf „Shop öffnen" soll den Shop öffnen (und nicht
|
||
* zusätzlich die Karte auslösen), ein Klick auf den Code soll
|
||
* kopieren. Alles andere — Bild, Überschrift, Text, leere Fläche —
|
||
* ist Karte. */
|
||
function hatEigeneBedeutung(ziel) {
|
||
return !!ziel.closest('a, button, input, textarea, select, label, [role="button"]');
|
||
}
|
||
|
||
/** Markiert gerade jemand Text? Dann will er lesen, nicht weg.
|
||
*
|
||
* Ohne diese Abfrage reißt es einen beim Loslassen der Maus aus der
|
||
* Seite — der häufigste Ärger an klickbaren Karten überhaupt. */
|
||
function markiertGerade() {
|
||
const s = window.getSelection?.();
|
||
return !!s && !s.isCollapsed && String(s).trim().length > 0;
|
||
}
|
||
|
||
for (const karte of document.querySelectorAll('.aussen[data-weg]')) {
|
||
const weg = karte.dataset.weg;
|
||
if (!weg) continue;
|
||
|
||
karte.addEventListener('click', (e) => {
|
||
if (e.button !== 0) return; /* nur die linke Taste */
|
||
if (hatEigeneBedeutung(e.target)) return;
|
||
if (markiertGerade()) return;
|
||
/* `noopener` ist Pflicht: Ohne das könnte die geöffnete Seite
|
||
über `window.opener` auf unsere zugreifen. */
|
||
window.open(weg, '_blank', 'noopener,noreferrer');
|
||
});
|
||
|
||
/* Mittlere Maustaste = „in neuem Tab" — dieselbe Erwartung wie bei
|
||
einem echten Link, also dieselbe Antwort. */
|
||
karte.addEventListener('auxclick', (e) => {
|
||
if (e.button !== 1) return;
|
||
if (hatEigeneBedeutung(e.target)) return;
|
||
e.preventDefault();
|
||
window.open(weg, '_blank', 'noopener,noreferrer');
|
||
});
|
||
}
|
||
|
||
/* ---- 2. DER RABATTCODE ------------------------------------------- */
|
||
|
||
const knopf = document.getElementById('code-knopf');
|
||
const hinweis = document.getElementById('code-hinweis');
|
||
|
||
if (knopf && hinweis) {
|
||
const urtext = hinweis.textContent;
|
||
let zurueck = 0;
|
||
|
||
/** Kopieren mit Auffanglösung.
|
||
*
|
||
* `navigator.clipboard` gibt es nur auf gesicherten Verbindungen,
|
||
* und auf einigen älteren Android-Browsern gar nicht. Scheitert es,
|
||
* wird der Code wenigstens MARKIERT — dann reicht ein langes
|
||
* Antippen. Ein Knopf, der nichts tut und nichts sagt, ist
|
||
* schlimmer als gar keiner. */
|
||
async function kopieren(text) {
|
||
try {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return 'kopiert';
|
||
}
|
||
} catch { /* weiter zur Auffanglösung */ }
|
||
|
||
try {
|
||
const feld = document.createElement('textarea');
|
||
feld.value = text;
|
||
feld.setAttribute('readonly', '');
|
||
feld.style.position = 'fixed';
|
||
feld.style.opacity = '0';
|
||
document.body.append(feld);
|
||
feld.select();
|
||
const ging = document.execCommand?.('copy');
|
||
feld.remove();
|
||
if (ging) return 'kopiert';
|
||
} catch { /* auch das ging nicht */ }
|
||
|
||
/* Letzter Ausweg: den Code im Knopf markieren. */
|
||
try {
|
||
const wort = knopf.querySelector('.gutschein__wort');
|
||
if (wort) {
|
||
const bereich = document.createRange();
|
||
bereich.selectNodeContents(wort);
|
||
const auswahl = window.getSelection();
|
||
auswahl.removeAllRanges();
|
||
auswahl.addRange(bereich);
|
||
return 'markiert';
|
||
}
|
||
} catch { /* dann eben gar nicht */ }
|
||
return 'nein';
|
||
}
|
||
|
||
knopf.addEventListener('click', async () => {
|
||
const code = knopf.dataset.code || '';
|
||
if (!code) return;
|
||
const wie = await kopieren(code);
|
||
|
||
hinweis.textContent =
|
||
wie === 'kopiert' ? 'kopiert ✓' :
|
||
wie === 'markiert' ? 'markiert – jetzt kopieren' :
|
||
'bitte von Hand abschreiben';
|
||
knopf.dataset.kopiert = wie === 'kopiert' ? 'ja' : 'nein';
|
||
|
||
/* Nach ein paar Sekunden steht wieder da, was zu tun ist —
|
||
sonst sieht die Karte beim nächsten Besuch so aus, als sei
|
||
gerade eben etwas passiert. */
|
||
clearTimeout(zurueck);
|
||
zurueck = setTimeout(() => {
|
||
hinweis.textContent = urtext;
|
||
delete knopf.dataset.kopiert;
|
||
}, 4000);
|
||
});
|
||
}
|
||
|
||
/* ==================================================================
|
||
3. DER PULS -- laeuft er gerade? (21.09.2026)
|
||
==================================================================
|
||
|
||
Die Auskunft kommt von postfach.dogfather-universe.com/live-status,
|
||
demselben Dienst, von dem der oeffentliche Streamplan lebt. Er wird
|
||
direkt vom Browser gefragt; nachgemessen vor dem Bauen:
|
||
"access-control-allow-origin: *", und `connect-src` der
|
||
Treff-Seiten fuehrt postfach bereits -- sonst haette die
|
||
Inhaltsrichtlinie den Abruf still abgewiesen, und das haette
|
||
ausgesehen wie ein toter Dienst.
|
||
|
||
------------------------------------------------------------------
|
||
DREI AUSGAENGE, NICHT ZWEI -- UND DAS IST HIER KEINE FEINHEIT
|
||
|
||
`streamplan.js` macht an derselben Stelle etwas anderes: Sein
|
||
`.catch()` setzt `istLive = false` und zeigt den Countdown. Aus
|
||
"wir konnten nicht fragen" wird damit "er sendet nicht". Das ist
|
||
genau der Fehler, den das Haus seit dem 03.09. kennt -- eine
|
||
Pruefung ohne dritten Ausgang -- nur hier mit einer Folge, die
|
||
jemand merkt: Wer die Seite waehrend einer Stoerung aufmacht,
|
||
verpasst den Stream, weil eine Anzeige so getan hat, als haette
|
||
sie nachgesehen.
|
||
|
||
Der Dienst SAGT sogar, ob er nachsehen konnte: `autoHealthy` steht
|
||
in derselben Antwort und wird draussen nicht gelesen. Hier schon.
|
||
|
||
In der Lage "unklar" laeuft der Countdown trotzdem weiter -- er
|
||
braucht kein Netz. Eine Stoerung nimmt der Seite nur die eine
|
||
Auskunft, die wirklich an ihr haengt. */
|
||
|
||
const POSTFACH = 'https://postfach.dogfather-universe.com';
|
||
const puls = document.getElementById('puls');
|
||
|
||
if (puls) {
|
||
const eLage = document.getElementById('puls-lage');
|
||
const eZahl = document.getElementById('puls-zahl');
|
||
const eKlein = document.getElementById('puls-klein');
|
||
const eKnopf = document.getElementById('puls-knopf');
|
||
const eBogen = document.getElementById('puls-bogen');
|
||
|
||
/* DIE SENDEZEIT KOMMT VOM SERVER, DAMIT ES SIE NUR EINMAL GIBT.
|
||
Sie steht auch in assets/js/streamplan.js der oeffentlichen
|
||
Seite; zwei Zahlen, die dasselbe bedeuten, laufen irgendwann
|
||
auseinander. Bis die Antwort da ist, gilt derselbe Wert --
|
||
sonst stuende beim Laden gar nichts. Eine Pruefung haelt beide
|
||
Orte gegeneinander. */
|
||
let sendeStunde = 20;
|
||
let lage = 'laedt';
|
||
let uhrLaeuft = 0;
|
||
|
||
/* Der Ring: Umfang aus dem Radius gerechnet, nicht abgemessen.
|
||
Wird r im HTML geaendert, stimmt er weiter. */
|
||
const UMFANG = 2 * Math.PI * Number(eBogen.getAttribute('r') || 52);
|
||
eBogen.style.strokeDasharray = String(UMFANG);
|
||
|
||
const zwei = (n) => String(n).padStart(2, '0');
|
||
|
||
function naechsteSendezeit() {
|
||
const jetzt = new Date();
|
||
const ziel = new Date(jetzt);
|
||
ziel.setHours(sendeStunde, 0, 0, 0);
|
||
if (ziel <= jetzt) ziel.setDate(ziel.getDate() + 1);
|
||
return ziel;
|
||
}
|
||
|
||
function countdownZeichnen() {
|
||
const restMs = Math.max(0, naechsteSendezeit() - new Date());
|
||
const sek = Math.floor(restMs / 1000);
|
||
eZahl.textContent =
|
||
zwei(Math.floor(sek / 3600)) + ':'
|
||
+ zwei(Math.floor((sek % 3600) / 60)) + ':'
|
||
+ zwei(sek % 60);
|
||
|
||
/* Voll = gleich. Der Ring sagt dasselbe wie die Zahl, nur ohne
|
||
Lesen -- fuer den Blick im Vorbeigehen. */
|
||
const anteil = 1 - restMs / 86400000;
|
||
eBogen.style.strokeDashoffset = String(UMFANG * (1 - anteil));
|
||
}
|
||
|
||
/* Die Sekundenuhr laeuft NUR, solange jemand hinsieht. Ein
|
||
Zeitgeber, der im Hintergrund weiterlaeuft, kostet auf dem Handy
|
||
Akku, ohne etwas anzuzeigen. */
|
||
function uhrAn() {
|
||
if (uhrLaeuft || lage === 'live') return;
|
||
countdownZeichnen();
|
||
uhrLaeuft = setInterval(countdownZeichnen, 1000);
|
||
}
|
||
function uhrAus() {
|
||
if (!uhrLaeuft) return;
|
||
clearInterval(uhrLaeuft);
|
||
uhrLaeuft = 0;
|
||
}
|
||
|
||
function setzeLage(neueLage, titel) {
|
||
lage = neueLage;
|
||
puls.dataset.lage = neueLage;
|
||
|
||
if (neueLage === 'live') {
|
||
uhrAus();
|
||
eLage.textContent = 'Er ist JETZT live';
|
||
eZahl.textContent = titel || 'Der Stream läuft';
|
||
eKlein.textContent = 'Läuft gerade auf TikTok.';
|
||
eKnopf.hidden = false;
|
||
eKnopf.textContent = 'Jetzt zuschauen →';
|
||
eBogen.style.strokeDashoffset = '0';
|
||
return;
|
||
}
|
||
|
||
eKnopf.hidden = true;
|
||
if (neueLage === 'wartet') {
|
||
eLage.textContent = 'Gerade nicht live';
|
||
eKlein.textContent = 'Der nächste feste Termin ist '
|
||
+ sendeStunde + ':00 Uhr — in deiner Ortszeit.';
|
||
} else if (neueLage === 'unklar') {
|
||
/* DER SATZ SAGT, WAS ER WEISS, UND WAS NICHT. "Nicht live"
|
||
waere hier eine Behauptung, die niemand geprueft hat. */
|
||
eLage.textContent = 'Konnten wir gerade nicht nachsehen';
|
||
eKlein.textContent = 'Der Live-Dienst antwortet nicht. Das heißt NICHT, '
|
||
+ 'dass kein Stream läuft — schau im Zweifel direkt auf TikTok.';
|
||
eKnopf.hidden = false;
|
||
eKnopf.textContent = 'Auf TikTok nachsehen →';
|
||
} else {
|
||
eLage.textContent = 'Wir sehen gerade nach …';
|
||
eKlein.textContent = 'Alle Zeiten in deiner Ortszeit.';
|
||
}
|
||
uhrAn();
|
||
}
|
||
|
||
async function lageHolen() {
|
||
try {
|
||
const antwort = await fetch(POSTFACH + '/live-status', { cache: 'no-store' });
|
||
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
|
||
const d = await antwort.json();
|
||
if (!d || d.ok !== true) throw new Error('keine Auskunft');
|
||
|
||
if (d.live) { setzeLage('live', d.title); return; }
|
||
|
||
/* HIER STEHT DER UNTERSCHIED ZUR OEFFENTLICHEN SEITE. Der
|
||
Dienst schaut automatisch nach; klappt das gerade nicht,
|
||
sagt er es selbst. Ein "live: false" aus einem gestoerten
|
||
Pruefer ist keine Aussage ueber den Stream, sondern eine
|
||
ueber den Pruefer.
|
||
|
||
Nur wenn die Quelle "automatisch" ist -- hat jemand den
|
||
Status von Hand gesetzt, gilt der, und die Gesundheit des
|
||
automatischen Pruefers spielt keine Rolle. */
|
||
if (d.source === 'automatisch' && d.autoHealthy === false) {
|
||
setzeLage('unklar');
|
||
return;
|
||
}
|
||
setzeLage('wartet');
|
||
} catch {
|
||
setzeLage('unklar');
|
||
}
|
||
}
|
||
|
||
setzeLage('laedt');
|
||
lageHolen();
|
||
setInterval(lageHolen, 30000);
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.hidden) { uhrAus(); return; }
|
||
uhrAn();
|
||
lageHolen();
|
||
});
|
||
|
||
/* ---- DAS BESONDERE EVENT -------------------------------------- */
|
||
const eventKasten = document.getElementById('event');
|
||
if (eventKasten) {
|
||
const evTitel = document.getElementById('event-titel');
|
||
const evZahl = document.getElementById('event-zahl');
|
||
const evKnopf = document.getElementById('event-knopf');
|
||
let evZiel = null;
|
||
let evUhr = 0;
|
||
|
||
const evZeichnen = () => {
|
||
if (!evZiel) return;
|
||
const restMs = evZiel - new Date();
|
||
if (restMs <= 0) {
|
||
evZahl.textContent = 'Es geht los!';
|
||
eventKasten.dataset.jetzt = 'ja';
|
||
clearInterval(evUhr);
|
||
evUhr = 0;
|
||
return;
|
||
}
|
||
const sek = Math.floor(restMs / 1000);
|
||
const tage = Math.floor(sek / 86400);
|
||
const rest = zwei(Math.floor((sek % 86400) / 3600)) + ':'
|
||
+ zwei(Math.floor((sek % 3600) / 60)) + ':' + zwei(sek % 60);
|
||
evZahl.textContent = tage > 0
|
||
? 'noch ' + tage + ' Tage · ' + rest
|
||
: 'noch ' + rest;
|
||
};
|
||
|
||
const eventHolen = async () => {
|
||
try {
|
||
const antwort = await fetch(POSTFACH + '/special-event', { cache: 'no-store' });
|
||
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
|
||
const d = await antwort.json();
|
||
const e = d && d.ok ? d.event : null;
|
||
if (evUhr) { clearInterval(evUhr); evUhr = 0; }
|
||
if (!e || !e.zieldatum) { eventKasten.hidden = true; evZiel = null; return; }
|
||
|
||
evZiel = new Date(e.zieldatum);
|
||
if (Number.isNaN(evZiel.getTime())) {
|
||
eventKasten.hidden = true; evZiel = null; return;
|
||
}
|
||
|
||
/* Der Titel kommt mehrsprachig; auf dieser Seite wird
|
||
deutsch gesprochen. `textContent`, nicht `innerHTML` --
|
||
der Text stammt aus der Verwaltung und ist damit
|
||
Eingabe, auch wenn sie aus dem eigenen Haus kommt. */
|
||
evTitel.textContent =
|
||
(e.titel && (e.titel.de || e.titel)) || 'Besonderes Event';
|
||
eventKasten.hidden = false;
|
||
delete eventKasten.dataset.jetzt;
|
||
evZeichnen();
|
||
evUhr = setInterval(evZeichnen, 1000);
|
||
|
||
if (e.link) { evKnopf.href = e.link; evKnopf.hidden = false; }
|
||
else evKnopf.hidden = true;
|
||
} catch {
|
||
/* HIER IST VERSTECKEN RICHTIG -- anders als beim Puls oben.
|
||
Der Puls beantwortet die Frage, wegen der man die Seite
|
||
oeffnet; schweigt er, muss das dastehen. Ein besonderes
|
||
Event gibt es an den meisten Tagen gar nicht. "Wir
|
||
konnten nicht nachsehen, ob es ein Event gibt, das es
|
||
vermutlich nicht gibt" ist keine Auskunft, sondern
|
||
Rauschen -- und Rauschen bringt Menschen dazu, auch die
|
||
echten Hinweise zu ueberlesen. */
|
||
eventKasten.hidden = true;
|
||
}
|
||
};
|
||
|
||
eventHolen();
|
||
setInterval(eventHolen, 60000);
|
||
}
|
||
|
||
/* ---- DIE KANAELE ---------------------------------------------- */
|
||
const kanaeleKasten = document.getElementById('kanaele');
|
||
if (kanaeleKasten) {
|
||
const reihe = document.getElementById('kanaele-reihe');
|
||
const fehlerText = document.getElementById('kanaele-fehler');
|
||
|
||
(async () => {
|
||
try {
|
||
const antwort = await fetch('/workspace/api/draussen',
|
||
{ credentials: 'same-origin' });
|
||
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
|
||
const d = await antwort.json();
|
||
const liste = Array.isArray(d.kanaele) ? d.kanaele : [];
|
||
|
||
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Eine leere Antwort
|
||
wuerde sonst eine leere Reihe ergeben, die aussieht, als
|
||
gaebe es keine Kanaele -- statt zu sagen, dass etwas
|
||
schiefging. */
|
||
if (!liste.length) throw new Error('leere Liste');
|
||
|
||
if (Number.isFinite(d.sendeStunde)) {
|
||
sendeStunde = d.sendeStunde;
|
||
if (lage !== 'live') setzeLage(lage);
|
||
}
|
||
|
||
for (const k of liste) {
|
||
const a = document.createElement('a');
|
||
a.className = 'kanal';
|
||
a.href = k.adresse;
|
||
a.target = '_blank';
|
||
a.rel = 'noopener noreferrer';
|
||
|
||
const name = document.createElement('span');
|
||
name.className = 'kanal__name';
|
||
name.textContent = k.name;
|
||
|
||
const handle = document.createElement('span');
|
||
handle.className = 'kanal__handle';
|
||
handle.textContent = k.handle;
|
||
|
||
const satz = document.createElement('span');
|
||
satz.className = 'kanal__satz';
|
||
satz.textContent = k.satz || '';
|
||
|
||
a.append(name, handle, satz);
|
||
reihe.append(a);
|
||
}
|
||
kanaeleKasten.hidden = false;
|
||
} catch {
|
||
fehlerText.hidden = false;
|
||
kanaeleKasten.hidden = false;
|
||
}
|
||
})();
|
||
}
|
||
}
|
||
})();
|