Kampagnen-Kacheln: gegliederte Texte, Geschenke mit Preis, aufklappbar
Filipe, mit einem Bildschirmfoto des Regelblocks: "man soll die titel
in den texten besser erkennen und sehen was zu was passt. ich will
auch dass man sieht welche geschenke bei dem event zaehlen und so.
ich will alle informationen die moeglich sind wenn man den link
hochlaedt. ... auch wenn man teilt soll es einfach perfekt sein. man
soll alle infos sehen die man braucht. die kacheln von den events
soll auch nicht so lang sein, die soll man aufklappen koennen. genau
wie eine trennung fuer heute, letzte woche letzten monat und so."
DER FEHLER LAG NICHT IM TEXT, SONDERN IN SEINER ANZEIGE. Im Feld
stand die ganze Zeit eine Gliederung -- Hinweise mit "* ", eine
Ueberschrift mit Untertitel hinter einem Gedankenstrich, Absaetze
ohne Zeichen, Aufzaehlungen mit "- ". Die Kachel hat daraus mit
punkteAusText() EINE flache Liste gemacht: jede Zeile derselbe
Strich, alle gleich gross.
gliederung() liest die Gliederung, die schon dasteht, statt
den Text umzuschreiben. Die beiden Kampagnen, die
heute in der Datenbank liegen, sehen dadurch
sofort richtig aus -- ohne Umstellung.
Geschenke kommen aus der gelesenen LISTE, nicht aus dem
Text, und stehen OHNE Aufklappen auf der Kachel.
Aufklappen alles Uebrige hinter einem Druck: gemessen 555 px
weniger je Kachel bei Gipfelstuermer.
Gruppen je Spalte laeuft / kommt / vorbei, Vergangenes
nach diese Woche, dieser Monat, aelter.
NEU GELESEN (stand in allen vier gespeicherten Seiten und wurde nie
angesehen): die Rangliste mit ihren Gruppen ("Superstars" bis
"5. Liga", bei Glow Up "Team Glow"/"Team Shine"), die Beschreibung je
Missionsgruppe, der Steckbrief (Kurznummer, Markt, Sprache, Zone),
der zweite Bildbaustein, und Ueberschriften INNERHALB der Klapptexte
-- "WELCHE LIGA BIN ICH?" hat jetzt sechs eingerueckte Unterpunkte
statt zwoelf gleichrangiger Zeilen.
EVENT_TEXT_MAX 6000 -> 20000. Der Regeltext von Gipfelstuermer ist
7679 Zeichen lang; es fielen bisher jedes Mal rund 1600 Zeichen
echter Inhalt weg. Die Zahl steht jetzt an EINER Stelle.
TEILEN: Was die KAMPAGNE sagt, geht hinaus (Aufgaben, Geschenke,
Regeln) -- wer WIR sind, nicht (Name, Rolle, Haus, Kampagnennummer,
Steckbrief). Und keine Hinweiszeile: "Die Preise konnten nicht
gelesen werden -- bitte nachtragen" ist eine Notiz an DogFather und
hat auf einer Seite, die er in Discord postet, nichts zu suchen.
BERICHTIGT, WEIL NACHGEMESSEN:
- Number(null) ist 0 und 0 ist endlich -- "Feuerwerk" bekam an drei
Stellen "0 Coins" angeschrieben, also eine falsche Preisangabe.
- Ein Kommentar im Leser behauptete, der Wertungszeitraum weiche
vom Kampagnenzeitraum ab. Nachgemessen ist er in allen drei
Seiten mit Rangliste auf die Millisekunde gleich; ich hatte zwei
verschiedene Kampagnen verglichen.
- bild-kampagne.mjs verlangte "jede Kachel traegt den Ton ihrer
Seite" und war damit seit dem 07.10. rot, weil an dem Tag bewusst
das Gegenteil entschieden wurde (Zustandston, Seitenkante). Nicht
aufgefallen, weil nach dem Lauf nur die Zahl der MESSUNGEN notiert
wurde, nicht die der FEHLER.
- .kv-seite > .eintrag-karte wird .kv-seite .eintrag-karte: Die
Karten haengen durch die Gruppen eine Ebene tiefer. Das waere der
fuenfte Direktkind-Selektor gewesen, der nach einem Umbau still
ausfaellt -- diesmal vorher gesehen.
GEPRUEFT: 186 (lesen, +58), 148 (Route/Teilen, +25), 158 (Kachel,
+64), 67 (Agentur, +5), 67 Messungen (Bild, +14), dazu struktur,
css-klassen, zeichen, deutsche-texte, tippziele, lesbarkeit und
haus-trennung (107, unveraendert). Sechs Gegenproben: alle sechs
Sabotagen werden bemerkt, alle Dateien byte-gleich wiederhergestellt.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+252
-6
@@ -66,6 +66,14 @@ import {
|
||||
db, protokolliere, echteIp, sitzungLesen, istLeitung,
|
||||
DATEN_ORDNER } from "./workspace.js";
|
||||
import { liefereDatei } from "./helfer-ausliefern.mjs";
|
||||
/* DIESELBE ZERLEGUNG WIE DIE KACHEL, nicht eine zweite.
|
||||
|
||||
`gliederung` macht aus dem Regeltext Abschnitte, `punkteAusText`
|
||||
aus den Aufgaben eine Liste. Beides hier nachzubauen hiesse: Die
|
||||
geteilte Seite zeigt irgendwann etwas anderes als die Kachel,
|
||||
und niemand koennte sagen, welche von beiden recht hat. */
|
||||
import { gliederung } from "./kampagne-lesen.mjs";
|
||||
import { punkteAusText } from "./workspace-bereiche.js";
|
||||
|
||||
export const teilenRouter = express.Router();
|
||||
|
||||
@@ -193,7 +201,14 @@ function eventZu(token) {
|
||||
if (!/^[A-Za-z0-9_-]{10,64}$/.test(String(token || ""))) return null;
|
||||
try {
|
||||
return db().prepare(
|
||||
`SELECT id, titel, text, datum, event_ende, kampagne_id, teilen_seit
|
||||
/* NEU SEIT DEM 07.10.2026: event_aufgaben, event_regeln,
|
||||
kampagne_daten. Siehe die Begruendung bei `seitePlakat`,
|
||||
wo die Grenze gezogen wird -- was die KAMPAGNE sagt, geht
|
||||
hinaus; was ueber UNS etwas sagt, nicht. Deshalb steht
|
||||
`erstellt_von` hier weiterhin nicht, und `haus` auch
|
||||
nicht. */
|
||||
`SELECT id, titel, text, datum, event_ende, kampagne_id, teilen_seit,
|
||||
event_aufgaben, event_regeln, kampagne_daten
|
||||
FROM eintraege
|
||||
WHERE teilen_token = ? AND bereich = 'agentur' AND art = 'kampagne'`)
|
||||
.get(String(token)) || null;
|
||||
@@ -240,12 +255,77 @@ teilenRouter.get("/e/:token", (req, res) => {
|
||||
const bis = tagText(e.event_ende);
|
||||
const zeitraum = von && bis && bis !== von ? `${von} – ${bis}` : (von || "");
|
||||
const text = String(e.text || "").replace(/\s+/g, " ").trim();
|
||||
/* Discord zeigt rund 300 Zeichen. Ein Satz, der mitten im Wort
|
||||
endet, sieht nach einem Fehler aus -- also an einer Wortgrenze. */
|
||||
const kurz = text.length > 280
|
||||
? text.slice(0, text.lastIndexOf(" ", 277)) + " …"
|
||||
/* DISCORD ZEIGT RUND 300 ZEICHEN -- UND DER ZEITRAUM STEHT MIT
|
||||
DAVOR (berichtigt 07.10.2026).
|
||||
|
||||
Hier stand 280. Gemessen kam `og:description` auf 304
|
||||
Zeichen heraus: Die Beschreibung ist `zeitraum · kurz`, und
|
||||
der Zeitraum („04.10.2026 – 07.10.2026 · ") braucht allein
|
||||
26 Zeichen. Wer nur den Text begrenzt, begrenzt nicht die
|
||||
Angabe, die wirklich hinausgeht.
|
||||
|
||||
250 statt 280: 250 + 26 = 276, mit Luft fuer einen laengeren
|
||||
Zeitraum. Eine Wortgrenze bleibt es -- ein Satz, der mitten
|
||||
im Wort endet, sieht nach einem Fehler aus. */
|
||||
const kurz = text.length > 250
|
||||
? text.slice(0, text.lastIndexOf(" ", 247)) + " …"
|
||||
: text;
|
||||
const beschreibung = [zeitraum, kurz].filter(Boolean).join(" · ");
|
||||
/* ---- WAS DIE KAMPAGNE SAGT (07.10.2026) ------------------
|
||||
|
||||
Filipe: „auch wenn man teilt soll es einfach perfekt sein.
|
||||
man soll alle infos sehen die man braucht."
|
||||
|
||||
Bis heute standen hier Titel, Zeitraum, Einleitung und Banner
|
||||
-- also gerade so viel, dass Discord eine Vorschau bauen kann.
|
||||
Wer dem Link folgte, erfuhr nicht, was zu tun ist.
|
||||
|
||||
DIE GRENZE LIEGT NICHT BEI DER MENGE, SONDERN BEIM GEGENSTAND:
|
||||
Was die KAMPAGNE sagt, darf hinaus -- Aufgaben, Geschenke,
|
||||
Regeln. Was ueber UNS etwas sagt, nicht: kein Name, keine
|
||||
Rolle, kein Haken, kein Haus, keine Kampagnennummer, kein
|
||||
Steckbrief. Der Grund ist nicht Geheimhaltung um ihrer selbst
|
||||
willen: Wer den Link weitergibt, gibt eine EINLADUNG weiter,
|
||||
und eine Einladung nennt die Sache, nicht die Einladenden.
|
||||
|
||||
DIE HINWEISE BLEIBEN DRINNEN, und das ist die wichtigste
|
||||
Zeile hier. „Die Preise konnten nicht gelesen werden -- bitte
|
||||
nachtragen" ist eine Notiz AN DOGFATHER. Auf einer Seite, die
|
||||
er in Discord postet, stuende damit vor seinen Creatorn, dass
|
||||
seine Kachel unfertig ist. Dasselbe gilt fuer die Fussnote
|
||||
ueber einen gekuerzten Regeltext. Beides sind Hinweiszeilen,
|
||||
beide fallen hier weg -- an einer Stelle, mit einer Regel.
|
||||
|
||||
DER STECKBRIEF BLEIBT EBENFALLS DRINNEN. Kurznummer, Markt
|
||||
und Zeitzone sind Buchhaltung, keine Einladung; sie stehen
|
||||
auf der Kachel, wo sie gebraucht werden. */
|
||||
let auszug = null;
|
||||
if (e.kampagne_daten) {
|
||||
try { auszug = JSON.parse(e.kampagne_daten); } catch { auszug = null; }
|
||||
}
|
||||
const aufgaben = punkteAusText(e.event_aufgaben).map((p) => p.text);
|
||||
const abschnitte = gliederung(e.event_regeln,
|
||||
{ geschenke: auszug?.geschenke || [] })
|
||||
/* Kein Hinweisabschnitt, und auch keine Hinweiszeile in einem
|
||||
anderen Abschnitt. */
|
||||
.filter((a) => a.art !== "hinweise")
|
||||
.map((a) => ({ ...a, zeilen: a.zeilen.filter((z) => z.art !== "hinweis") }))
|
||||
.filter((a, i, alle) => a.zeilen.length
|
||||
|| (alle[i + 1] && alle[i + 1].stufe > a.stufe));
|
||||
|
||||
/* Die Geschenke werden aus den Abschnitten herausgezogen -- wie
|
||||
auf der Kachel: Sie stehen oben, nicht mitten im Regeltext. */
|
||||
const gabenAb = abschnitte.find((a) => a.art === "geschenke") || null;
|
||||
const rest = abschnitte.filter((a) => a !== gabenAb);
|
||||
const gaben = (gabenAb?.zeilen || []).filter((z) => z.art === "geschenk");
|
||||
|
||||
/* DIE VORSCHAU IN DISCORD SOLL AUCH OHNE EINLEITUNG ETWAS SAGEN.
|
||||
Gemessen an „LIVE Glow Up": Die Kampagne hat gar keinen
|
||||
Einleitungstext (der Inhalt steht in den Klapptexten) -- in der
|
||||
Vorschau stand damit nur ein Zeitraum. Die ersten Aufgaben
|
||||
sagen in derselben Zeile mehr als jede Umschreibung. */
|
||||
const aufgabenKurz = aufgaben.slice(0, 3).join(" · ");
|
||||
const beschreibung = [zeitraum, kurz || aufgabenKurz]
|
||||
.filter(Boolean).join(" · ");
|
||||
const basis = `${TEILEN_BASIS}/e/${encodeURIComponent(req.params.token)}`;
|
||||
const hatBild = (() => {
|
||||
try {
|
||||
@@ -262,6 +342,11 @@ teilenRouter.get("/e/:token", (req, res) => {
|
||||
basis,
|
||||
bild: hatBild ? `${basis}/bild` : null,
|
||||
vonTikTok: !!e.kampagne_id,
|
||||
aufgaben,
|
||||
gaben,
|
||||
gabenTitel: gabenAb?.titel || null,
|
||||
gabenUnter: gabenAb?.unter || null,
|
||||
abschnitte: rest,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -312,8 +397,166 @@ const STIL = `
|
||||
color: #7f95ad; font-size: .85rem;
|
||||
}
|
||||
a { color: #7fd2fb; }
|
||||
|
||||
/* ---- Alles, was seit dem 07.10.2026 dazugekommen ist ----
|
||||
Dieselbe Gliederung wie auf der Kachel, nur ohne Aufklappen:
|
||||
Wer einem geteilten Link folgt, will lesen und nicht
|
||||
bedienen. Augenschonend bleibt es trotzdem -- der
|
||||
Unterschied laeuft ueber Groesse, Gewicht und Sperrung,
|
||||
nicht ueber grelle Farben. */
|
||||
.k__block {
|
||||
margin: 20px 0 0; padding-top: 16px;
|
||||
border-top: 1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
.k__schild2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: .74rem; font-weight: 700;
|
||||
letter-spacing: .1em; text-transform: uppercase;
|
||||
color: #9fb2c6;
|
||||
}
|
||||
.k__liste { margin: 0 0 8px; padding-left: 20px; color: #c5d4e3; }
|
||||
.k__liste li { margin: 3px 0; font-size: .93rem; }
|
||||
.k__liste:last-child { margin-bottom: 0; }
|
||||
.k__woher { margin: 0 0 10px; color: #c5d4e3; font-size: .9rem; }
|
||||
.k__unter { display: block; margin-top: 2px; color: #9fb2c6; font-size: .88rem; }
|
||||
|
||||
.k__gaben { display: flex; flex-wrap: wrap; gap: 7px; margin: 0; padding: 0; list-style: none; }
|
||||
.k__gabe {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 5px 11px; border-radius: 999px;
|
||||
border: 1px solid rgba(226,193,90,.28);
|
||||
background: rgba(226,193,90,.08);
|
||||
font-size: .9rem; max-width: 100%;
|
||||
}
|
||||
.k__gname { font-weight: 600; overflow-wrap: anywhere; }
|
||||
.k__coins {
|
||||
padding: 1px 8px; border-radius: 999px;
|
||||
background: rgba(226,193,90,.18); color: #e8d08f;
|
||||
font-size: .82rem; font-weight: 700; white-space: nowrap;
|
||||
}
|
||||
.k__gzusatz { color: #9fb2c6; font-size: .84rem; overflow-wrap: anywhere; }
|
||||
|
||||
.k__abschnitt { margin: 0 0 14px; }
|
||||
.k__abschnitt:last-child { margin-bottom: 0; }
|
||||
.k__abschnitt[data-stufe="3"] {
|
||||
margin-left: 2px; padding-left: 12px;
|
||||
border-left: 2px solid rgba(255,255,255,.1);
|
||||
}
|
||||
.k__titel {
|
||||
margin: 0 0 6px;
|
||||
font-size: .8rem; font-weight: 700;
|
||||
letter-spacing: .08em; text-transform: uppercase;
|
||||
color: #d7e3f0;
|
||||
}
|
||||
.k__abschnitt[data-stufe="3"] > .k__titel {
|
||||
font-size: .92rem; font-weight: 650;
|
||||
letter-spacing: .01em; text-transform: none;
|
||||
}
|
||||
.k__absatz { margin: 0 0 7px; color: #c5d4e3; font-size: .93rem; }
|
||||
.k__absatz:last-child { margin-bottom: 0; }
|
||||
.k__frage { margin: 11px 0 5px; color: #e8f1fa; font-weight: 650; font-size: .93rem; }
|
||||
.k__frage:first-child { margin-top: 0; }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.k__abschnitt[data-stufe="3"] { padding-left: 9px; }
|
||||
}
|
||||
`;
|
||||
|
||||
/* ===================================================================
|
||||
DIE BAUTEILE DER OFFENEN SEITE (07.10.2026)
|
||||
|
||||
ALLES GEHT DURCH `sicher()`. Diese Seite ist die einzige im
|
||||
Haus ohne Anmeldung, und ihr Inhalt kommt aus einer FREMDEN
|
||||
Quelle (einer TikTok-Seite). Ein `<script>` im Titel einer
|
||||
Kampagne waere sonst ein Skript auf unserer Domain.
|
||||
|
||||
LEERE BLOECKE ERSCHEINEN NICHT. Eine Ueberschrift „Aufgaben"
|
||||
ueber einer leeren Flaeche sieht aus, als sei das Laden
|
||||
abgebrochen -- auf einer Seite, die jemand gerade in Discord
|
||||
gepostet hat, ist das das Letzte, was man will.
|
||||
=================================================================== */
|
||||
|
||||
/** Eine Zahl mit Tausenderpunkt, wie man sie liest. */
|
||||
function coinText(n) {
|
||||
/* `Number(null)` IST 0 -- und „0 Coins" waere eine falsche
|
||||
Preisangabe. Derselbe Fehler ist mir am 07.10.2026 an einem Tag
|
||||
zweimal passiert (Server und Kachel); hier steht er zum
|
||||
dritten Mal und deshalb mit derselben Sicherung. */
|
||||
if (n === null || n === undefined || n === "") return null;
|
||||
const z = Number(n);
|
||||
if (!Number.isFinite(z)) return null;
|
||||
return `${z.toLocaleString("de-DE")} ${z === 1 ? "Coin" : "Coins"}`;
|
||||
}
|
||||
|
||||
function blockAufgaben(liste) {
|
||||
const a = (liste || []).filter(Boolean);
|
||||
if (!a.length) return "";
|
||||
return `<section class="k__block">
|
||||
<h2 class="k__schild2">${a.length === 1 ? "Aufgabe" : "Aufgaben"}</h2>
|
||||
<ul class="k__liste">${a.map((x) => `<li>${sicher(x)}</li>`).join("")}</ul>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function blockGaben(d) {
|
||||
const g = (d.gaben || []).filter(Boolean);
|
||||
if (!g.length) return "";
|
||||
const kopf = d.gabenTitel
|
||||
? `<p class="k__woher">${sicher(d.gabenTitel)}${
|
||||
d.gabenUnter ? ` <span class="k__unter">${sicher(d.gabenUnter)}</span>` : ""}</p>`
|
||||
: "";
|
||||
return `<section class="k__block">
|
||||
<h2 class="k__schild2">Diese Geschenke zählen</h2>
|
||||
${kopf}
|
||||
<ul class="k__gaben">${g.map((x) => {
|
||||
const c = coinText(x.coins);
|
||||
return `<li class="k__gabe"><span class="k__gname">${sicher(x.text)}</span>${
|
||||
c ? `<span class="k__coins">${sicher(c)}</span>` : ""}${
|
||||
x.zusatz ? `<span class="k__gzusatz">${sicher(x.zusatz)}</span>` : ""}</li>`;
|
||||
}).join("")}</ul>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/** Die Zeilen eines Abschnitts in ihrer Reihenfolge.
|
||||
* Aufeinanderfolgende Punkte kommen in EINE Liste; ein Absatz
|
||||
* dazwischen beendet sie -- dieselbe Regel wie auf der Kachel, und
|
||||
* aus demselben Grund: Bei den Teilnahmebedingungen steht zwischen
|
||||
* zwei Aufzaehlungen ein Satz, der sich auf die ZWEITE bezieht. */
|
||||
function zeilenHtml(zeilen) {
|
||||
const teile = [];
|
||||
let liste = null;
|
||||
for (const z of zeilen || []) {
|
||||
if (z.art === "punkt" || z.art === "geschenk") {
|
||||
if (!liste) { liste = []; teile.push({ liste }); }
|
||||
liste.push(z);
|
||||
continue;
|
||||
}
|
||||
liste = null;
|
||||
teile.push({ zeile: z });
|
||||
}
|
||||
return teile.map((s) => {
|
||||
if (s.liste) {
|
||||
return `<ul class="k__liste">${s.liste.map((z) => {
|
||||
const c = coinText(z.coins);
|
||||
return `<li>${sicher(z.text)}${c ? ` <span class="k__coins">${sicher(c)}</span>` : ""}</li>`;
|
||||
}).join("")}</ul>`;
|
||||
}
|
||||
const klasse = s.zeile.art === "frage" ? "k__frage" : "k__absatz";
|
||||
return `<p class="${klasse}">${sicher(s.zeile.text)}</p>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function blockRegeln(abschnitte) {
|
||||
const a = (abschnitte || []).filter(Boolean);
|
||||
if (!a.length) return "";
|
||||
return `<section class="k__block">
|
||||
<h2 class="k__schild2">Regeln & Infos</h2>
|
||||
${a.map((s) => `<div class="k__abschnitt" data-stufe="${s.stufe === 3 ? 3 : 2}">${
|
||||
s.titel ? `<h3 class="k__titel">${sicher(s.titel)}</h3>` : ""}${
|
||||
s.unter ? `<p class="k__unter">${sicher(s.unter)}</p>` : ""}${
|
||||
zeilenHtml(s.zeilen)}</div>`).join("")}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function seitePlakat(d) {
|
||||
const bildMarke = d.bild
|
||||
? `<meta property="og:image" content="${sicher(d.bild)}" />
|
||||
@@ -345,6 +588,9 @@ ${d.bild ? `<img class="k__bild" src="${sicher(d.bild)}" alt="" />` : ""}
|
||||
<h1>${sicher(d.titel)}</h1>
|
||||
${d.zeitraum ? `<p class="k__zeit">${sicher(d.zeitraum)}</p>` : ""}
|
||||
${d.text ? `<p class="k__text">${sicher(d.text)}</p>` : ""}
|
||||
${blockAufgaben(d.aufgaben)}
|
||||
${blockGaben(d)}
|
||||
${blockRegeln(d.abschnitte)}
|
||||
<p class="k__fuss">Geteilt aus dem Creator Workspace von Spicy Media.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user