Unsere Seiten, klickbare Video-Karten -- und coturn, das nie vermittelt hat

Drei Sachen von Filipe, und eine davon war ein Fehler von mir.

1. DER VERMITTLUNGSSERVER HAT NIE EINEN ANRUF GETRAGEN.

Am 18.09. habe ich gemeldet, coturn laufe und sei "von aussen
nachgewiesen". Das war falsch. Im Protokoll: null Zuteilungen, jemals.
Beim ersten echten Versuch "check_stun_auth: Cannot find credentials"
-- in /etc/turnserver.conf fehlte `static-auth-secret`, also suchte
coturn die Zugangsdaten in seiner eigenen Datenbank und wies jeden ab.

Und mein eigenes Werkzeug meldete die ganze Zeit gruen:
"Eingetragen, Geheimnis lesbar, Server antwortet." Jedes Wort wahr --
und keins beantwortete die Frage, um die es geht. Eine STUN-Bindung
fragt "bist du da?", eine Zuteilung fragt "traegst du meinen Anruf?".

`tools/turn-sprache.mjs` (neu) versucht jetzt eine ECHTE Zuteilung und
schickt ein Paket darueber. `turn-einrichten.mjs` benutzt sie und hat
drei Ausgaenge statt zwei: traegt / traegt nicht / konnte nicht
nachsehen. Nach Filipes Reparatur auf dem Server gemessen: Zuteilung
auf Port 49199, Paket angekommen -- von Luxemburg durch Deutschland.

2. UNSERE SEITEN -- die erste Kachel, die HINAUSFUEHRT.

Website und VanVans Shop, mit dem Rabattcode DOGI10. Der Code wurde
nachgesehen, nicht abgeschrieben: partnercodes.json, 10 Prozent,
aktiv, "zum weitergeben an Community" -- und giltAufSale: false,
weshalb der Satz zu reduzierten Artikeln danebensteht. Der zweite Code
dort ist als "nur fuer Filipe persoenlich" vermerkt und steht nirgends.

Auch Name und Beschreibung des Shops stammen von der Seite selbst.
Meine erste Fassung hiess "Van's DIY Bastelbedarf" und nannte "Perlen,
Anhaenger, Werkzeug" -- ausgedacht und falsch. Er heisst mit "&" und
verkauft Haekelwerke, Plushies, Schmuck.

Der Ton der Kachel wurde GESUCHT, nicht gewaehlt: sieben geratene
Blautoene schafften den noetigen Abstand nicht, also 372 600
Kombinationen abgesucht. #087ce7, Abstand 0.310, Kontrast 4.51:1.

3. DIE GANZE VIDEO-KARTE KLICKT -- "egal wo man drauf drückt".

Und hier steckte der lehrreiche Fehler: Zuerst stand der Klick-Block
unten vor `return k`. Diese Funktion hat DREI Rueckgabepunkte, und der
erste lautet `if (!darfEintragen()) { ...; return k; }` -- also genau
fuer die Mitglieder, fuer die Filipe es wollte. Beim Team ging es, bei
der Community nicht. Jetzt haengt er dort, wo `videoWeg` entsteht.

Nicht klickbar bleiben Karten ohne Video und solche, deren Video bei
TikTok geloescht wurde -- sonst fuehrt der Klick auf eine Fehlerseite,
und wir sehen kaputt aus.

GEMESSEN: pruef-wege-nach-draussen (neu) 63 Pruefungen, 0 Fehler, auf
1400 und 412 px. Mit Gegenproben: der Kopierknopf oeffnet nichts, der
eigene Knopf wirkt genau einmal, auf einer Karte ohne Video passiert
nichts. pruef-community-sicht 10/0 auf jetzt 11 Seiten, 0 tote Wege.

Nebenbei: Der Pfeil klebte bei langen Untertiteln am Text (auf dem
Bildschirmfoto gesehen). 0.45rem Abstand.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-19 01:17:09 +02:00
co-authored by Claude Opus 5
parent d766be57b6
commit f30227c873
46 changed files with 2096 additions and 411 deletions
+37
View File
@@ -14,6 +14,43 @@
.eintrag-karte[data-dringlich="niedrig"] { border-left-color: #3c4a5a; }
.eintrag-karte[data-status="erledigt"] { opacity: .55; }
/* DIE KLICKBARE KARTE (19.09.2026) -- nur dort, wo ein Video dahinter
liegt.
Sie muss SAGEN, dass sie klickbar ist, bevor jemand darauf tippt.
Eine Fläche, die erst beim Berühren verrät, dass sie ein Weg war,
ist auf dem Handy unsichtbar -- dort gibt es kein Darüberfahren.
Deshalb der ruhige, dauerhaft sichtbare Rand links: Er steht schon
da, bevor die Maus kommt.
Bewusst KEIN Farbwechsel der Fläche und kein Leuchten. Zwischen
siebzehn Karten untereinander wäre das Unruhe, und was hier gefragt
ist, ist eine Einladung, kein Blinken. */
.eintrag-karte[data-klickbar="ja"] {
cursor: pointer;
border-left-color: color-mix(in srgb, var(--akzent, #5aa0e0) 55%, var(--rand));
transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease;
}
.eintrag-karte[data-klickbar="ja"]:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--akzent, #5aa0e0) 34%, var(--rand));
border-left-color: color-mix(in srgb, var(--akzent, #5aa0e0) 85%, transparent);
box-shadow: 0 10px 22px -16px rgba(0, 0, 0, .9);
}
/* Wer über den Knopf fährt, soll den Knopf spüren und nicht die ganze
Karte wackeln sehen. */
.eintrag-karte[data-klickbar="ja"]:has(.videozeile__weg:hover) {
transform: none;
}
/* Das Standbild bekommt denselben Hinweis: Der Zeiger sagt „hier geht
es weiter", schon bevor man liest. */
.eintrag-karte[data-klickbar="ja"] .bildstreifen__feld { cursor: pointer; }
@media (prefers-reduced-motion: reduce) {
.eintrag-karte[data-klickbar="ja"] { transition: none; }
.eintrag-karte[data-klickbar="ja"]:hover { transform: none; }
}
.eintrag-kopf {
display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px;
}
+27
View File
@@ -1098,6 +1098,24 @@ body.start > main { position: relative; z-index: 1; }
ist die nächste Frage nicht „noch ein Ton", sondern „braucht diese
Kachel wirklich eine eigene Farbe". */
[data-ton="37"] { --ton: #61ff09; } /* Wie geht's dir? */
/* UNSERE SEITEN (19.09.2026) -- GESUCHT, NICHT GEWAEHLT.
Die Treff-Gruppe trug Grün (28, 30, 32), Magenta (29), Orange (31)
und zweimal Rosa (33, 34). Ein achter Ton von Hand danebengesetzt
wäre mit hoher Wahrscheinlichkeit einem davon zu nah gekommen — beim
ersten Versuch war genau das der Fall: sieben geratene Blautöne, und
keiner schaffte den nötigen Abstand.
Deshalb abgesucht statt geschätzt: 372 600 Kombinationen aus
Helligkeit, Buntheit und Farbton, davon 213 260 überhaupt
darstellbar und 140 973 mit ausreichendem Kontrast auf #0a121e.
Gewonnen hat der mit dem GRÖSSTEN Mindestabstand zu allen sieben —
und bei Gleichstand die leisere Fassung, damit kein Neon entsteht.
Ergebnis: OKLCH L=0.59 C=0.185 H=254. Abstand 0.310 zum nächsten
(Ton 33), Kontrast 4.51:1. Blau ist die einzige Ecke, die im Treff
noch frei war — das ist kein Zufall, sondern das Ergebnis. */
[data-ton="38"] { --ton: #087ce7; } /* Unsere Seiten */
/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat.
Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der
@@ -1739,6 +1757,15 @@ body.start > main { position: relative; z-index: 1; }
.kachel__pfeil {
position: relative; z-index: 1;
flex: none; margin-left: auto;
/* ABSTAND ZUM UNTERTITEL (19.09.2026, auf dem Bildschirmfoto
gesehen). Bei langen Untertiteln — „Wie du dazugehören kannst",
„Website, Shop & dein Rabatt" — stieß der Text direkt an den
Pfeil, ohne eine Lücke dazwischen. Kaputt war nichts: Der
Untertitel kürzt sich selbst mit „…". Es sah nur gedrängt aus,
und zwar genau auf den Kacheln mit dem meisten zu sagen.
Der Zuschlag nimmt dem Text ein paar Pixel, die er ohnehin
abschneidet. */
padding-left: .45rem;
color: var(--text-still); font-size: 1rem;
transition: color var(--tempo), transform var(--tempo);
}
+288
View File
@@ -0,0 +1,288 @@
/* =====================================================================
UNSERE SEITEN (19.09.2026)
Zwei Karten, die aus dem Treff hinausführen: die Website und
VanVans Shop.
WARUM SIE ANDERS AUSSEHEN ALS DIE BRETTER-KACHELN: Sie tun etwas
anderes. Eine Brett-Kachel öffnet eine Liste im selben Fenster; diese
hier verlassen den Treff. Dass man das vorher sieht, ist kein
Schmuck, sondern die Ansage — deshalb der eigene Zuschnitt, der
deutlich sichtbare Adressname und der Pfeil nach außen.
AUGENSCHONEND, WIE ÜBERALL IM HAUS: Die Farbe liegt im Verlauf und
im Rand, nie in einer leuchtenden Fläche hinter Text. Bewegung gibt
es nur beim Darüberfahren, und `prefers-reduced-motion` schaltet sie
ganz ab.
===================================================================== */
.seiten-gitter {
display: grid;
gap: 1.1rem;
/* EINE REGEL STATT EINER ZAHL. Zwei Spalten, sobald zweimal 21rem
plus Abstand hineinpassen — und sonst eine. Kommt eine dritte
Karte dazu, ordnet sich das von selbst, ohne dass jemand eine
Schwelle nachziehen muss. */
grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
margin: 1.4rem 0 0;
}
.aussen {
position: relative;
isolation: isolate;
overflow: hidden;
display: flex;
flex-direction: column;
gap: .75rem;
padding: 1.35rem 1.35rem 1.2rem;
border-radius: 1.1rem;
border: 1px solid color-mix(in srgb, var(--aussen-ton) 30%, transparent);
background:
radial-gradient(120% 100% at 8% 0%,
color-mix(in srgb, var(--aussen-ton) 15%, transparent), transparent 62%),
linear-gradient(160deg,
color-mix(in srgb, var(--aussen-ton) 7%, #0d1523),
#0a111d 68%);
box-shadow: 0 16px 34px -22px color-mix(in srgb, var(--aussen-ton) 75%, transparent);
cursor: pointer;
transition: transform .22s ease, border-color .22s ease, box-shadow .22s ease;
}
/* Die Töne der beiden Karten. Sie stehen hier und nicht im HTML,
damit eine dritte Karte eine Zeile braucht und nicht zehn. */
.aussen--dogi { --aussen-ton: #087ce7; }
.aussen--shop { --aussen-ton: #e0559b; }
/* Der stille Schein im Hintergrund. Liegt UNTER allem (`z-index: -1`)
und ist so schwach, dass er bei Tageslicht kaum auffällt — er soll
Tiefe geben, nicht leuchten. */
.aussen__schein {
position: absolute;
inset: -40% -20% auto -20%;
height: 78%;
z-index: -1;
background: radial-gradient(60% 60% at 50% 50%,
color-mix(in srgb, var(--aussen-ton) 22%, transparent), transparent 70%);
filter: blur(26px);
opacity: .55;
pointer-events: none;
}
.aussen:hover,
.aussen:focus-within {
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--aussen-ton) 52%, transparent);
box-shadow: 0 22px 40px -20px color-mix(in srgb, var(--aussen-ton) 85%, transparent);
}
.aussen__kopf {
display: flex;
align-items: center;
gap: .85rem;
}
.aussen__zeichen {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
border-radius: .8rem;
border: 1px solid color-mix(in srgb, var(--aussen-ton) 38%, transparent);
background: linear-gradient(150deg,
color-mix(in srgb, var(--aussen-ton) 20%, transparent), transparent 74%);
}
.aussen__zeichen svg {
width: 1.65rem;
height: 1.65rem;
fill: none;
stroke: color-mix(in srgb, var(--aussen-ton) 78%, #dceaf8);
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.aussen__marke {
margin: 0 0 .1rem;
font-size: .74rem;
letter-spacing: .12em;
text-transform: uppercase;
color: color-mix(in srgb, var(--aussen-ton) 55%, #9fb4c9);
}
.aussen__titel {
margin: 0;
font-size: 1.32rem;
line-height: 1.2;
/* Text bleibt hell und ruhig. Die Farbe steckt im Rahmen, nicht in
der Schrift — farbige Überschriften auf dunklem Grund sind der
schnellste Weg zu müden Augen. */
color: #e9f2fb;
}
.aussen__satz {
margin: 0;
color: var(--text-leise, #93a7bd);
font-size: .95rem;
line-height: 1.5;
}
/* Was es auf der Seite gibt. Ruhig gesetzt: Der Punkt links ist ein
kleiner Strich in der Kartenfarbe, kein Aufzählungszeichen — vier
bunte Punkte untereinander wären mehr Unruhe als Information. */
.aussen__liste {
margin: .1rem 0 .2rem;
padding: 0;
list-style: none;
display: grid;
gap: .42rem;
}
.aussen__liste li {
position: relative;
padding-left: 1.05rem;
font-size: .89rem;
line-height: 1.45;
color: var(--text-leise, #93a7bd);
}
.aussen__liste li::before {
content: "";
position: absolute;
left: 0;
top: .62em;
width: .5rem;
height: 1px;
background: color-mix(in srgb, var(--aussen-ton) 60%, transparent);
}
.aussen__weg {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: .6rem;
margin: auto 0 0;
padding-top: .35rem;
}
.aussen__adresse {
font-size: .82rem;
color: color-mix(in srgb, var(--aussen-ton) 45%, #8ea3b9);
word-break: break-all;
}
.aussen__knopf {
flex: 0 0 auto;
display: inline-block;
padding: .55rem 1rem;
border-radius: .6rem;
border: 1px solid color-mix(in srgb, var(--aussen-ton) 45%, transparent);
background: color-mix(in srgb, var(--aussen-ton) 14%, transparent);
color: #eaf3fc;
font-weight: 600;
font-size: .92rem;
text-decoration: none;
/* 44 px hoch, damit ein Daumen ihn trifft. */
min-height: 2.75rem;
display: inline-flex;
align-items: center;
transition: background-color .18s ease, border-color .18s ease;
}
.aussen__knopf:hover,
.aussen__knopf:focus-visible {
background: color-mix(in srgb, var(--aussen-ton) 26%, transparent);
border-color: color-mix(in srgb, var(--aussen-ton) 68%, transparent);
}
/* ---- DER GUTSCHEIN ---------------------------------------------------
Sieht aus wie ein abgerissener Abschnitt, weil genau das gemeint
ist: etwas zum Mitnehmen. Die gestrichelte Linie ist das einzige
Stilmittel — sie sagt "hier abtrennen", ohne ein Wort dafür zu
brauchen. */
.gutschein {
margin: .2rem 0 .1rem;
padding: .9rem 1rem 1rem;
border-radius: .8rem;
border: 1px dashed color-mix(in srgb, var(--aussen-ton) 42%, transparent);
background: color-mix(in srgb, var(--aussen-ton) 8%, transparent);
}
.gutschein__zeile {
display: flex;
align-items: baseline;
gap: .55rem;
margin: 0 0 .6rem;
}
.gutschein__prozent {
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
color: #f2f7fc;
}
.gutschein__wofuer {
font-size: .88rem;
color: var(--text-leise, #93a7bd);
}
.gutschein__code {
display: flex;
align-items: center;
justify-content: space-between;
gap: .7rem;
width: 100%;
min-height: 3rem;
padding: .6rem .9rem;
border-radius: .6rem;
border: 1px solid color-mix(in srgb, var(--aussen-ton) 50%, transparent);
background: rgba(255, 255, 255, .04);
color: #f2f7fc;
cursor: pointer;
text-align: left;
transition: background-color .18s ease, border-color .18s ease;
}
.gutschein__code:hover,
.gutschein__code:focus-visible {
background: rgba(255, 255, 255, .08);
border-color: color-mix(in srgb, var(--aussen-ton) 72%, transparent);
}
.gutschein__wort {
font-family: ui-monospace, "SFMono-Regular", "Cascadia Code", monospace;
font-size: 1.22rem;
font-weight: 700;
letter-spacing: .1em;
}
.gutschein__hinweis {
font-size: .78rem;
color: var(--text-leise, #93a7bd);
text-align: right;
}
/* Nach dem Kopieren steht dort die Bestätigung — in Grün, aber in
einem gedeckten, das auf Dunkel nicht sticht. */
.gutschein__code[data-kopiert="ja"] .gutschein__hinweis { color: #74c79a; }
.gutschein__klein {
margin: .6rem 0 0;
font-size: .78rem;
line-height: 1.45;
color: var(--text-leise, #93a7bd);
}
.seiten-fuss {
margin: 1.4rem 0 0;
font-size: .85rem;
color: var(--text-leise, #93a7bd);
}
/* Wer Bewegung ausgeschaltet hat, bekommt keine. Die Karte bleibt
trotzdem erkennbar bedienbar — über Rand und Schatten. */
@media (prefers-reduced-motion: reduce) {
.aussen,
.aussen__knopf,
.gutschein__code { transition: none; }
.aussen:hover,
.aussen:focus-within { transform: none; }
}
+99 -3
View File
@@ -355,6 +355,82 @@
k.dataset.dringlich = e.dringlichkeit;
k.dataset.status = e.status;
/* DER GANZE KASTEN IST DER WEG ZUM VIDEO (19.09.2026).
Filipe: „die community soll sofort auf das video geschickt werden
sobald sie auf die kachel drücken, also die ganze kacheln will
ich egal wo man drauf drückt, foto text.... egal wo sobald man
drauf drückt weiter geleitet".
Er hat recht, und der Grund ist handfest: Auf dem Handy ist der
Knopf „Auf TikTok ansehen" ein Ziel von rund 150 × 44 px in
einer Karte von 350 × 600. Wer das Bild antippt — die größte und
einladendste Fläche — erwartet, dass etwas passiert, und es
passiert nichts. Das fühlt sich an wie eine kaputte Seite.
NUR DORT, WO ES AUCH EIN VIDEO GIBT. Auf den anderen Brettern
bleibt die Karte eine Karte: Eine Fläche, die manchmal klickt und
manchmal nicht, ist schlimmer als eine, die es nie tut.
UND NUR, SOLANGE DAS VIDEO NOCH DA IST. Ist es bei TikTok
gelöscht (`quelle_fehlt_seit`, Stufe 5), führt der Weg auf eine
Fehlerseite — und wer darauf tippt, hält UNS für kaputt. Dann
bleibt die Karte still, und der Hinweis „Bei TikTok nicht mehr
da" steht weiterhin da, wo er hingehört. */
const videoWeg = (e.quelle_url && !e.quelle_fehlt_seit) ? String(e.quelle_url) : '';
/* ---- UND DER KLICK HAENGT GLEICH HIER DRAN ---------------------
ER STAND ZUERST GANZ UNTEN, VOR `return k` -- und war fuer die
Community wirkungslos. Diese Funktion hat DREI Rueckgabepunkte,
und der erste davon lautet:
if (!darfEintragen()) { k.append(fuss); return k; }
Wer nicht schreiben darf, bekommt die Karte also frueher zurueck
-- und genau das sind die Mitglieder, fuer die Filipe es
ueberhaupt wollte. Beim Team funktionierte es, bei der Community
nicht: der unangenehmste Fehler ueberhaupt, weil er beim
Selbsttest unsichtbar ist.
Deshalb steht es jetzt dort, wo `videoWeg` entsteht: „Diese
Karte ist ein Weg" ist eine Eigenschaft der Daten, kein
Nachtrag am Ende. Damit gilt sie fuer jeden Rueckgabeweg, auch
fuer den naechsten, den jemand einbaut.
WARUM KEIN <a> UM ALLES: In der Karte stecken schon Links (der
TikTok-Knopf, „Daraus wurde", die Bilder) und Knoepfe (loeschen,
melden, zur Aufgabe). Ein Link in einem Link ist ungueltiges
HTML -- der Browser beendet den aeusseren still, und danach ist
ein Teil der Karte tot, ohne Fehlermeldung.
WARUM DIE KARTE NICHT FOKUSSIERBAR WIRD: Der Knopf „Auf TikTok
ansehen" ist bereits der Tastaturweg. Ein zweiter Halt mit
demselben Ziel saehe fuer jemanden, der sich durchtabbt, nach
einem Fehler aus. Die Flaeche ist eine Bequemlichkeit fuer den
Daumen, nicht der eigentliche Weg. */
if (videoWeg) {
k.dataset.klickbar = 'ja';
const eigeneBedeutung = (ziel) =>
!!ziel.closest('a, button, input, textarea, select, label, [role="button"]');
k.addEventListener('click', (ev) => {
if (ev.button !== 0) return;
if (eigeneBedeutung(ev.target)) return;
/* Wer Text markiert, will lesen und nicht weg -- der haeufigste
Aerger an klickbaren Karten ueberhaupt. */
const auswahl = window.getSelection?.();
if (auswahl && !auswahl.isCollapsed && String(auswahl).trim()) return;
window.open(videoWeg, '_blank', 'noopener,noreferrer');
});
/* Mittlere Maustaste: dieselbe Erwartung wie bei einem Link. */
k.addEventListener('auxclick', (ev) => {
if (ev.button !== 1) return;
if (eigeneBedeutung(ev.target)) return;
ev.preventDefault();
window.open(videoWeg, '_blank', 'noopener,noreferrer');
});
}
const kopf = el('div', 'eintrag-kopf');
const marke = el('span', 'marke-art', einstellung.arten[e.art] || e.art);
/* Die Art als Merkmal am Element -- daran haengt der Ton (siehe
@@ -454,17 +530,36 @@
if (e.bilder.length > 1) bg.dataset.mehrere = 'ja';
for (const b of e.bilder) {
const a = el('a', 'bildstreifen__feld');
a.href = `/workspace/api/dateien/${b.id}/bild`;
/* GIBT ES EIN VIDEO, FÜHRT AUCH DAS BILD DORTHIN (19.09.2026).
Vorher öffnete das Bild sich selbst in voller Größe. Bei
einem Clip ist das Bild aber das Standbild des Videos — wer
darauf tippt, will das Video sehen und nicht das Standbild
größer. Genau das meinte Filipe mit „foto".
Ohne Video bleibt es beim Vollbild: Auf den anderen Brettern
ist ein Bild wirklich nur ein Bild. */
a.href = videoWeg || `/workspace/api/dateien/${b.id}/bild`;
a.target = '_blank';
a.rel = 'noopener';
a.rel = videoWeg ? 'noopener noreferrer' : 'noopener';
const bild = el('img', 'bildstreifen__bild');
bild.src = `/workspace/api/dateien/${b.id}/bild`;
bild.loading = 'lazy';
bild.decoding = 'async';
/* Der Titel des Beitrags ist die beste Beschreibung, die es
hier gibt -- ein Dateiname wie "IMG_2831.jpg" sagt niemandem
etwas. */
etwas.
UND DAS ZIEL GEHOERT DAZU (19.09.2026): Seit das Bild zum
Video führt, wäre „Bild zu ..." eine Falschauskunft für jeden,
der die Seite vorlesen lässt -- er würde ein Bild erwarten
und in einer fremden App landen. */
bild.alt = e.titel ? `Bild zu „${e.titel}“` : (b.name || 'Bild');
if (videoWeg) {
a.setAttribute('aria-label',
e.titel ? `Video ansehen: ${e.titel}` : 'Video ansehen');
a.title = 'Öffnet das Video in einem neuen Fenster';
}
/* Ist es doch kein Bild (oder die Datei fehlt), verschwindet
der Rahmen, statt ein kaputtes Symbol zu zeigen. */
bild.addEventListener('error', () => { a.remove(); if (!bg.childNodes.length) bg.remove(); });
@@ -1055,6 +1150,7 @@
}
fuss.append(knoepfe);
k.append(fuss);
return k;
}
+14
View File
@@ -283,6 +283,20 @@
'M3.5 12.6h17',
'M10.9 11.6h2.2v2h-2.2z'],
},
/* HINAUS (19.09.2026) -- fuer "Unsere Seiten".
Jedes andere Zeichen im Haus zeigt auf etwas INNERHALB des
Workspace. Dieses eine muss sagen: hier geht es hinaus. Das
Sinnbild dafuer ist ueberall dasselbe -- ein angefangener Rahmen
mit einem Pfeil, der ihn nach oben rechts verlaesst. Es zu
erfinden waere ein Fehler; wer es kennt, weiss sofort, dass sich
ein neues Fenster oeffnet. */
hinaus: {
/* Nur der Pfeilkopf ist Flaeche -- er ist die Aussage. */
fuellung: ['M13.8 4H20v6.2z'],
linien: ['M11 4.8H5.4A1.4 1.4 0 0 0 4 6.2v12.4a1.4 1.4 0 0 0 1.4 1.4h12.4a1.4 1.4 0 0 0 1.4-1.4V13',
'M13.8 4H20v6.2',
'M19.4 4.6 12 12'],
},
zahlen: {
fuellung: ['M4 16 9.5 11.5 13 14 20 6.5 20 19.5 4 19.5z'],
/* Der Linienzug selbst, die Grundlinie darunter und ein Punkt am
+144
View File
@@ -0,0 +1,144 @@
/* =====================================================================
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);
});
}
})();