Der erste Partnercode steht: DOGI10 auf einer gepraegten Muenze
Die Rabattcodeseite war ein Platzhalter -- "Codes folgen in Kuerze" und
darunter eine Vorschaukachel mit dem erfundenen Code "DOGFATHER". Jetzt
steht der erste echte drauf: DOGI10 fuer Van's DIY & Bastelbedarf,
verlinkt auf vans-diy-bastelbedarf.com.
DIE KONDITIONEN SIND ABGELESEN, NICHT GERATEN.
Aus "DOGI10" auf zehn Prozent zu schliessen, waere naheliegend gewesen,
haette zufaellig gestimmt und waere trotzdem falsch gewesen. Nachgesehen
im Shop selbst (src/content/partnercodes.json):
{ "code": "DOGI10", "prozent": 10, "bis": "2026-09-30",
"aktiv": true, "giltAufSale": false }
Zwei der drei Angaben stehen im Namen NICHT drin: dass der Code am
30.09.2026 auslaeuft und dass er auf bereits reduzierte Artikel nicht
gilt. Beides steht jetzt sichtbar auf der Karte. Eine falsche Zusage auf
einer Verkaufsseite kostet VanVan die Diskussion an der Kasse.
Gegengeprueft, dass die Datei auch wirklich gilt und kein Ueberbleibsel
ist: Sie wird an zwei Stellen ausgewertet, src/scripts/cart.ts fuer den
Warenkorb und server/lib/preis-berechnen.js fuer den Endpreis, beide mit
derselben Regel (aktiv !== false UND jetzt <= bis 23:59:59).
DAS ABLAUFDATUM IST EINE ZEITBOMBE, ALSO BEKOMMT ES EINEN ZUENDER.
Ein fest eingetippter Satz "gueltig bis 30.09.2026" stimmt, bis der
Kalender ihn ueberholt -- ab dem 01.10. verspraeche die Seite etwas, das
der Shop schon ablehnt. Genau dieselbe Falle hat am 06.09. den
Oeffnungstest im Shop umgeworfen. Das Datum steht deshalb nicht nur im
Text, sondern einmal als Zahl im Skript: Ist es vorbei, schaltet die
Karte selbsttaetig auf "abgelaufen", streicht den Code durch und sperrt
den Kopierknopf, statt weiter zu werben.
DAS LOGO: AUS EINEM SIEGEL WIRD EINE MUENZE.
Filipe hat das Logo als Bildschirmfoto aus TikTok geliefert, rundes
Siegel auf schwarzem Grund. Ungestellt waere daraus auf der dunklen
Karte ein sichtbarer schwarzer Kasten geworden -- derselbe Fehler wie
bei der Workspace-Marke, deren Zahlen damals tadellos aussahen.
Freigestellt wird per Flutfuellung vom Bildrand (tools/partner-siegel-
freistellen.mjs, 384 px WebP, 37 KB). Eine Kreismaske waere hier sogar
ausrechenbar gewesen -- Mitte 539,5/526,5, Radius 495 -- und haette
genau fuer dieses eine Bild funktioniert. Die Fuellung MISST die Form,
statt sie vorauszusetzen: Der naechste Partner ist ein Eintrag in
AUFTRAEGE und sonst nichts. Die Quelle liegt mit im Repo, sonst laesst
sich das Werkzeug genau einmal ausfuehren und ist danach Dekoration.
"Extrem speziell" fuehrt hier NICHT ueber mehr Farbe -- das Siegel ist
schwarz-weiss, jede Einfaerbung lackierte eine fremde Marke um. Es
fuehrt ueber mehr Material: sechs CSS-Lagen, kein zweites Bild.
Aura weicher Lichthof, atmet in 9 s
Raendel die geriffelte Muenzkante, 72 Zaehne. Sie steht STILL --
eine sich drehende Riffelung flimmert bei 148 px, und das
waere genau die grelle Optik, die die Hausregel ausschliesst
Glanz stattdessen wandert EIN Lichtpunkt in 22 s um die Kante.
Das ist die Bewegung, die eine Muenze im Licht macht
Schliff Praegekante nach innen, oben Licht, unten Schatten
Ablage elliptischer Schatten, damit die Muenze auf der Karte LIEGT
Bei prefers-reduced-motion steht alles davon still.
NEBENBEFUND, DER SONST NIEMANDEM AUFGEFALLEN WAERE: Der Text auf der
Sperrkachel endete auf "sobald die ersten Kooperationen live sind".
Seit heute IST die erste live -- der Satz haette jemanden dafuer zahlen
lassen, auf etwas zu warten, das schon hinter der Sperre liegt.
Ebenfalls nachgemessen statt vermutet: die Warengruppen im
Beschreibungssatz sind die echten Kategorien des Shops.
pruef-rabattcodes EXIT=0 (42 Pruefungen), darunter beide Richtungen der
Supporter-Sperre, die Bildpunkte des ausgelieferten Siegels (Ecken
durchsichtig, Mitte deckend, 69 % Flaeche), das Kopieren gegen die echte
Zwischenablage samt Gegenprobe davor, die Ablaufschaltung mit gestellter
Uhr an beiden Seiten des Stichtags und alle fuenf Sprachen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
+100
-23
@@ -1,6 +1,20 @@
|
||||
/* =====================================================================
|
||||
i18n-rabattcodes.js — Übersetzungs-Wörterbuch NUR für rabattcodes.html
|
||||
Alle 5 Sprachen vollständig gepflegt (de, de-CH, en, fr, pt).
|
||||
|
||||
⚠️ ZWEI EINSETZWEGE, ZWEI SCHREIBWEISEN FÜR "&" (siehe main.js,
|
||||
applyTranslations):
|
||||
|
||||
data-i18n="schluessel" -> el.innerHTML = text
|
||||
Dort gehört "&" hin.
|
||||
data-i18n-attr="alt:schluessel" -> el.setAttribute(...)
|
||||
Dort gehört ein blankes "&" hin;
|
||||
"&" stünde wörtlich im
|
||||
Vorlesetext.
|
||||
|
||||
Betroffen ist hier genau ein Eintrag: rc_logo_alt (Attribut, blankes
|
||||
"&"). Der Markenname selbst steht bewusst NICHT im Wörterbuch — "Van's
|
||||
DIY & Bastelbedarf" heißt in jeder Sprache gleich.
|
||||
===================================================================== */
|
||||
|
||||
window.I18N_PAGE = {
|
||||
@@ -23,39 +37,102 @@ window.I18N_PAGE = {
|
||||
de: "Nur für Dogfather-Supporter", "de-CH": "Nur für Dogfather-Supporter",
|
||||
en: "Dogfather supporters only", fr: "Réservé aux Supporters Dogfather", pt: "Somente para apoiadores do Dogfather",
|
||||
},
|
||||
/* Stand 08.09.2026 angepasst: Der Satz endete vorher auf "sobald die
|
||||
ersten Kooperationen live sind". Seit heute IST die erste live —
|
||||
der alte Text hätte einen Supporter dafür bezahlen lassen, auf etwas
|
||||
zu warten, das schon hinter der Sperre liegt. */
|
||||
rc_gesperrt_text: {
|
||||
de: "Rabattcodes sind ein exklusiver Vorteil der DogiCrew 🩵. Werde jetzt Teil davon, um sie freizuschalten, sobald die ersten Kooperationen live sind.",
|
||||
"de-CH": "Rabattcodes sind e exklusive Vorteil vo dr DogiCrew 🩵. Wird jetzt Teil devo, zum sie freizschalte, sobald die erste Kooperatione live sind.",
|
||||
en: "Discount codes are an exclusive perk of the DogiCrew 🩵. Join now to unlock them as soon as the first collaborations go live.",
|
||||
fr: "Les codes de réduction sont un avantage exclusif de la DogiCrew 🩵. Rejoins-la dès maintenant pour les débloquer dès que les premières collaborations seront lancées.",
|
||||
pt: "Os códigos de desconto são um benefício exclusivo da DogiCrew 🩵. Junte-se agora para desbloqueá-los assim que as primeiras parcerias forem lançadas.",
|
||||
de: "Rabattcodes sind ein exklusiver Vorteil der DogiCrew 🩵. Der erste Partnercode ist schon drin — werde jetzt Teil davon, um ihn freizuschalten.",
|
||||
"de-CH": "Rabattcodes sind e exklusive Vorteil vo dr DogiCrew 🩵. De erst Partnercode isch scho dinn — wird jetzt Teil devo, zum en freizschalte.",
|
||||
en: "Discount codes are an exclusive perk of the DogiCrew 🩵. The first partner code is already in there — join now to unlock it.",
|
||||
fr: "Les codes de réduction sont un avantage exclusif de la DogiCrew 🩵. Le premier code partenaire est déjà là — rejoins-la maintenant pour le débloquer.",
|
||||
pt: "Os códigos de desconto são um benefício exclusivo da DogiCrew 🩵. O primeiro código de parceiro já lá está — junta-te agora para o desbloquear.",
|
||||
},
|
||||
rc_btn_abo: {
|
||||
de: "Jetzt Supporter werden 👑", "de-CH": "Jetzt Supporter werde 👑",
|
||||
en: "Become a supporter now 👑", fr: "Devenir Supporter maintenant 👑", pt: "Tornar-se apoiador agora 👑",
|
||||
},
|
||||
rc_titel: {
|
||||
de: "Codes folgen in Kürze", "de-CH": "Codes chömed bald",
|
||||
en: "Codes coming soon", fr: "Codes bientôt disponibles", pt: "Códigos em breve",
|
||||
|
||||
/* ---- Erster Partner: Van's DIY & Bastelbedarf (DOGI10) ---------- */
|
||||
rc_band: {
|
||||
de: "Partner Nr. 1", "de-CH": "Partner Nr. 1",
|
||||
en: "Partner no. 1", fr: "Partenaire n° 1", pt: "Parceiro n.º 1",
|
||||
},
|
||||
rc_text: {
|
||||
de: "Als Creator bekommt DogFather eigene Rabattcodes bei ausgewählten Partnern — die gibt er direkt an Team Dogi weiter. Sobald die ersten Kooperationen live sind, stehen die Codes genau hier.",
|
||||
"de-CH": "Als Creator überchunnt DogFather eigeni Rabattcodes bi usgwählte Partner — die gitt er direkt a Team Dogi wyter. Sobald die erste Kooperatione live sind, stönd die Codes genau da.",
|
||||
en: "As a creator, DogFather gets his own discount codes from selected partners — and passes them straight on to Team Dogi. As soon as the first collaborations go live, the codes will show up right here.",
|
||||
fr: "En tant que créateur, DogFather reçoit ses propres codes de réduction chez des partenaires sélectionnés — et les transmet directement à Team Dogi. Dès que les premières collaborations seront lancées, les codes apparaîtront ici.",
|
||||
pt: "Como criador, DogFather recebe seus próprios códigos de desconto de parceiros selecionados — e os repassa direto para a Team Dogi. Assim que as primeiras parcerias forem lançadas, os códigos aparecerão bem aqui.",
|
||||
/* Attribut-Eintrag: blankes "&", siehe Kopf dieser Datei. */
|
||||
rc_logo_alt: {
|
||||
de: "Logo von Van's DIY & Bastelbedarf",
|
||||
"de-CH": "Logo vo Van's DIY & Bastelbedarf",
|
||||
en: "Logo of Van's DIY & Bastelbedarf",
|
||||
fr: "Logo de Van's DIY & Bastelbedarf",
|
||||
pt: "Logótipo de Van's DIY & Bastelbedarf",
|
||||
},
|
||||
rc_beispiel_marke: {
|
||||
de: "Partner-Marke", "de-CH": "Partner-Marke",
|
||||
en: "Partner brand", fr: "Marque partenaire", pt: "Marca parceira",
|
||||
/* Die aufgezählten Warengruppen sind die echten Kategorien des Shops
|
||||
(src/content/categories/), nicht das, was man bei "Bastelbedarf"
|
||||
vermuten würde: Bastelzubehör, Häkelzubehör, DIY Häkelwerke, DIY
|
||||
Plushies, DIY Modeschmuck & Accessoires, Mystery-Pakete. */
|
||||
rc_beschreibung: {
|
||||
de: "Häkelzubehör, Bastelbedarf, DIY-Plushies und Modeschmuck — VanVans eigener Shop.",
|
||||
"de-CH": "Häkelzubehör, Bastelbedarf, DIY-Plushies und Modeschmuck — VanVans eigene Shop.",
|
||||
en: "Crochet supplies, craft materials, DIY plushies and handmade jewellery — VanVan's own shop.",
|
||||
fr: "Accessoires de crochet, matériel de bricolage, peluches DIY et bijoux — la boutique de VanVan.",
|
||||
pt: "Acessórios de crochet, material de artesanato, plushies DIY e bijutaria — a loja da VanVan.",
|
||||
},
|
||||
rc_beispiel_code: {
|
||||
de: `Code „DOGFATHER" · –10 %`, "de-CH": `Code „DOGFATHER" · –10 %`,
|
||||
en: `Code "DOGFATHER" · –10%`, fr: `Code « DOGFATHER » · –10 %`, pt: `Código "DOGFATHER" · –10%`,
|
||||
rc_code_kopf: {
|
||||
de: "Dein Code", "de-CH": "Din Code",
|
||||
en: "Your code", fr: "Ton code", pt: "O teu código",
|
||||
},
|
||||
rc_btn_kontakt: {
|
||||
de: "Auf dem Laufenden bleiben", "de-CH": "Am Bau blibe",
|
||||
en: "Stay in the loop", fr: "Rester informé(e)", pt: "Fique por dentro",
|
||||
rc_code_fuss: {
|
||||
de: "Tippen zum Kopieren", "de-CH": "Tippe zum Kopiere",
|
||||
en: "Tap to copy", fr: "Touche pour copier", pt: "Toca para copiar",
|
||||
},
|
||||
rc_code_kopiert: {
|
||||
de: "Code kopiert ✓", "de-CH": "Code kopiert ✓",
|
||||
en: "Code copied ✓", fr: "Code copié ✓", pt: "Código copiado ✓",
|
||||
},
|
||||
rc_code_fehler: {
|
||||
de: "Kopieren ging nicht — bitte von Hand abtippen.",
|
||||
"de-CH": "Kopiere het nöd klappt — bitte vo Hand abtippe.",
|
||||
en: "Copying didn't work — please type it in by hand.",
|
||||
fr: "La copie n'a pas fonctionné — saisis-le à la main.",
|
||||
pt: "A cópia não funcionou — escreve-o à mão.",
|
||||
},
|
||||
/* Die drei Konditionen stehen 1:1 so im Shop (partnercodes.json,
|
||||
Eintrag DOGI10) — nicht aus dem Codenamen geschlossen. */
|
||||
rc_fakt_rabatt: {
|
||||
de: "<b>10 %</b> Rabatt auf deinen Einkauf",
|
||||
"de-CH": "<b>10 %</b> Rabatt uf din Iikauf",
|
||||
en: "<b>10%</b> off your order",
|
||||
fr: "<b>10 %</b> de remise sur ta commande",
|
||||
pt: "<b>10 %</b> de desconto na tua compra",
|
||||
},
|
||||
rc_fakt_bis: {
|
||||
de: "Gültig bis 30.09.2026", "de-CH": "Gültig bis 30.09.2026",
|
||||
en: "Valid until 30 September 2026", fr: "Valable jusqu'au 30.09.2026", pt: "Válido até 30.09.2026",
|
||||
},
|
||||
rc_fakt_sale: {
|
||||
de: "Nicht auf bereits reduzierte Artikel",
|
||||
"de-CH": "Nöd uf scho reduzierti Artikel",
|
||||
en: "Not valid on items that are already reduced",
|
||||
fr: "Non valable sur les articles déjà réduits",
|
||||
pt: "Não válido em artigos já reduzidos",
|
||||
},
|
||||
rc_abgelaufen: {
|
||||
de: "Dieser Code ist ausgelaufen. Dogi klärt gerade die Verlängerung — schau bald wieder rein.",
|
||||
"de-CH": "De Code isch abglaufe. Dogi klärt grad d Verlängerig — lueg bald wieder verbi.",
|
||||
en: "This code has expired. Dogi is sorting out an extension — check back soon.",
|
||||
fr: "Ce code a expiré. Dogi s'occupe d'une prolongation — repasse bientôt.",
|
||||
pt: "Este código expirou. O Dogi está a tratar da renovação — volta em breve.",
|
||||
},
|
||||
rc_btn_shop: {
|
||||
de: "Zum Shop von Van’s DIY ↗", "de-CH": "Zum Shop vo Van’s DIY ↗",
|
||||
en: "Go to the Van’s DIY shop ↗", fr: "Voir la boutique Van’s DIY ↗", pt: "Ir à loja Van’s DIY ↗",
|
||||
},
|
||||
rc_mehr: {
|
||||
de: "Weitere Codes folgen, sobald die nächsten Kooperationen stehen.",
|
||||
"de-CH": "Wyteri Codes chömed, sobald die nächste Kooperatione stönd.",
|
||||
en: "More codes will follow as soon as the next collaborations are in place.",
|
||||
fr: "D'autres codes suivront dès que les prochaines collaborations seront conclues.",
|
||||
pt: "Mais códigos seguem assim que as próximas parcerias estiverem fechadas.",
|
||||
},
|
||||
rc_btn_zurueck: {
|
||||
de: "← Zurück zu allen Links", "de-CH": "← Zrugg zu alle Links",
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
/* Prüft die Rabattcode-Seite mit dem ersten echten Partnercode (DOGI10).
|
||||
|
||||
WAS HIER SCHIEFGEHEN KANN, UND WARUM MAN ES NICHT SIEHT
|
||||
|
||||
Die Seite hat zwei Zustände, und man bekommt immer nur einen davon zu
|
||||
sehen: Ohne Supporter-Abo die Sperre, mit Abo die Codekarte. Wer im
|
||||
Browser nachschaut, prüft also die Hälfte. Beide Richtungen stehen
|
||||
deshalb hier drin — auch die Gegenprobe, dass die Karte OHNE Abo
|
||||
wirklich verschwunden ist und nicht bloss weiter unten steht.
|
||||
|
||||
Dazu vier Dinge, die im Bild richtig aussehen und trotzdem falsch
|
||||
sein können:
|
||||
|
||||
* Das Siegel. Ein nicht freigestelltes Logo ist auf einer dunklen
|
||||
Karte kaum zu erkennen — der schwarze Kasten geht im Hintergrund
|
||||
unter. Genau daran ist die Workspace-Marke schon einmal
|
||||
vorbeigerutscht ("60,8 % deckend" klang tadellos). Hier werden
|
||||
deshalb die Bildpunkte der AUSGELIEFERTEN Datei gemessen: Ecken
|
||||
durchsichtig, Mitte deckend.
|
||||
|
||||
* Der Code. "DOGI10" ist kurz genug, um ihn beim Abtippen zu
|
||||
verdrehen, und lang genug, dass es niemandem auffällt. Verglichen
|
||||
wird deshalb zeichengenau, nicht "enthält".
|
||||
|
||||
* Das Ablaufdatum. Es steht an zwei Stellen — sichtbar im Text und
|
||||
als Zahl im Skript. Laufen die auseinander, verspricht die Seite
|
||||
etwas, das sie selbst schon anders bewertet. Die Prüfung liest
|
||||
beide und vergleicht sie.
|
||||
|
||||
* Die Ablaufschaltung selbst. Sie wird erst am 01.10.2026 zum
|
||||
ersten Mal gebraucht — also genau dann, wenn niemand mehr
|
||||
hinschaut. Sie wird hier mit gestellter Uhr in beide Richtungen
|
||||
gefahren: am letzten gültigen Abend muss der Code GELTEN, am
|
||||
Morgen danach nicht mehr. Eine Prüfung, die nur bestätigen kann,
|
||||
bestätigt nichts.
|
||||
|
||||
Läuft rein lokal gegen die Dateien im Arbeitsverzeichnis, rund 20
|
||||
Sekunden. Kein Zugriff auf den Server, keine Bestellung, keine Daten.
|
||||
|
||||
Aufrufen mit: node pruef-rabattcodes.mjs
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import { createServer } from "node:http";
|
||||
import { readFileSync, existsSync, statSync } from "node:fs";
|
||||
import { join, extname } from "node:path";
|
||||
|
||||
const W = process.cwd();
|
||||
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
||||
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
|
||||
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
|
||||
/* Auf isFile() statt auf "endet nicht mit /" prüfen: main.js fragt auch
|
||||
Adressen ohne Schrägstrich am Ende ab, die im Arbeitsverzeichnis ein
|
||||
ORDNER sind. readFileSync bricht darauf mit EISDIR ab -- und weil das
|
||||
im Serverrückruf passiert, stirbt der ganze Prüflauf statt einer
|
||||
einzelnen Anfrage. */
|
||||
const srv = createServer((q, r) => {
|
||||
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
|
||||
if (existsSync(p) && statSync(p).isFile()) {
|
||||
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
|
||||
return r.end(readFileSync(p));
|
||||
}
|
||||
r.writeHead(404).end("");
|
||||
});
|
||||
await new Promise((r) => srv.listen(4331, r));
|
||||
const URL = "http://127.0.0.1:4331/rabattcodes.html";
|
||||
|
||||
/* Erwartungswerte. Sie stammen aus dem Shop, nicht aus dem Codenamen:
|
||||
/home/vandiy/vans-diy-bastelbedarf/src/content/partnercodes.json
|
||||
{ "code": "DOGI10", "prozent": 10, "bis": "2026-09-30",
|
||||
"aktiv": true, "giltAufSale": false }
|
||||
Ändert VanVan die Konditionen, muss hier UND auf der Seite nachgezogen
|
||||
werden — dass beides zusammen auffällt, ist der Zweck dieser Zeilen. */
|
||||
const CODE = "DOGI10";
|
||||
const SHOP = "https://vans-diy-bastelbedarf.com/";
|
||||
const BIS = "2026-09-30";
|
||||
const BIS_TEXT = "30.09.2026";
|
||||
|
||||
const ergebnisse = [];
|
||||
function pruefe(name, ok, zusatz) {
|
||||
ergebnisse.push({ name, ok });
|
||||
console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : ""));
|
||||
}
|
||||
|
||||
const browser = await chromium.launch();
|
||||
|
||||
/* Eine Seite im gewünschten Zustand öffnen. `abo` schaltet die Sperre,
|
||||
`zeit` stellt die Uhr (nur Date, die Zeitgeber laufen weiter — sonst
|
||||
stünde main.js still). */
|
||||
async function seiteOeffnen({ abo = true, zeit = null, sprache = "de", viewport, reducedMotion } = {}) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: viewport || { width: 1280, height: 1000 },
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
reducedMotion,
|
||||
});
|
||||
const p = await ctx.newPage();
|
||||
if (zeit) await p.clock.setFixedTime(zeit);
|
||||
await p.addInitScript(([a, s]) => {
|
||||
if (a) localStorage.setItem("supporterSubStatus", "active");
|
||||
else localStorage.removeItem("supporterSubStatus");
|
||||
localStorage.setItem("dogi-sprache", s);
|
||||
}, [abo, sprache]);
|
||||
await p.goto(URL, { waitUntil: "domcontentloaded" });
|
||||
await p.waitForTimeout(700);
|
||||
return { ctx, p };
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
1 — Die Sperre, in BEIDE Richtungen
|
||||
=================================================================== */
|
||||
console.log("\n=== Supporter-Sperre ===");
|
||||
for (const [abo, sichtbar] of [[true, "Karte"], [false, "Sperre"]]) {
|
||||
const { ctx, p } = await seiteOeffnen({ abo });
|
||||
const z = await p.evaluate(() => ({
|
||||
karte: !document.getElementById("rc-inhalt").hidden,
|
||||
sperre: !document.getElementById("rc-gesperrt").hidden,
|
||||
codeSichtbar: !!document.getElementById("rc-code")?.checkVisibility?.(),
|
||||
}));
|
||||
const lage = abo ? "Mit Abo" : "Ohne Abo";
|
||||
pruefe(`${lage}: ${sichtbar} steht da`,
|
||||
z.karte === abo && z.sperre === !abo, `Karte=${z.karte} Sperre=${z.sperre}`);
|
||||
/* Gegenprobe: "hidden" ist ein Attribut, kein Beweis. Ohne Abo darf
|
||||
der Code auch tatsächlich nicht auf dem Schirm sein. */
|
||||
pruefe(`${lage}: Code ist ${abo ? "sichtbar" : "unsichtbar"}`,
|
||||
z.codeSichtbar === abo, `sichtbar=${z.codeSichtbar}`);
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
2 — Das Siegel: geladen, gross genug, wirklich freigestellt
|
||||
=================================================================== */
|
||||
console.log("\n=== Partnersiegel ===");
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({});
|
||||
await p.waitForFunction(() => {
|
||||
const i = document.querySelector(".rc-medaillon img");
|
||||
return i && i.complete && i.naturalWidth > 0;
|
||||
}, null, { timeout: 8000 }).catch(() => {});
|
||||
|
||||
const b = await p.evaluate(() => {
|
||||
const i = document.querySelector(".rc-medaillon img");
|
||||
if (!i) return { fehler: "kein Bild" };
|
||||
const r = i.getBoundingClientRect();
|
||||
const K = 64;
|
||||
const c = document.createElement("canvas");
|
||||
c.width = K; c.height = K;
|
||||
const s = c.getContext("2d", { willReadFrequently: true });
|
||||
s.drawImage(i, 0, 0, K, K);
|
||||
const d = s.getImageData(0, 0, K, K).data;
|
||||
const alpha = (x, y) => d[(y * K + x) * 4 + 3];
|
||||
let deckend = 0;
|
||||
for (let k = 3; k < d.length; k += 4) if (d[k] > 200) deckend++;
|
||||
return {
|
||||
quelle: (i.currentSrc || i.src).split("/").pop(),
|
||||
natur: i.naturalWidth, breite: Math.round(r.width),
|
||||
ecken: [alpha(0, 0), alpha(K - 1, 0), alpha(0, K - 1), alpha(K - 1, K - 1)],
|
||||
mitte: alpha(K >> 1, K >> 1),
|
||||
anteil: deckend / (K * K),
|
||||
};
|
||||
});
|
||||
|
||||
pruefe("Siegel geladen (naturalWidth 384)", b.natur === 384, `${b.quelle} / ${b.natur}px`);
|
||||
pruefe("Siegel wird gross genug angezeigt (>= 110 px)", b.breite >= 110, `${b.breite} px`);
|
||||
/* DAS ist die Prüfung, die den schwarzen Kasten gefunden hätte. */
|
||||
pruefe("Ecken des Siegels sind durchsichtig", b.ecken.every((a) => a <= 20), b.ecken.join("/"));
|
||||
pruefe("Mitte des Siegels ist deckend", b.mitte >= 250, String(b.mitte));
|
||||
/* Und die Anzahl gehört in die Bedingung, nicht in den Meldetext: ein
|
||||
leeres Bild hätte alle drei Prüfungen oben bestanden. Ein Kreis im
|
||||
Quadrat deckt pi/4, hier mit 6 % Rand rund 70 %. */
|
||||
pruefe("Es ist auch wirklich ein rundes Siegel drauf (60-80 % Fläche)",
|
||||
b.anteil > 0.6 && b.anteil < 0.8, `${(b.anteil * 100).toFixed(1)} %`);
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
3 — Code, Shoplink, Konditionen
|
||||
=================================================================== */
|
||||
console.log("\n=== Code und Konditionen ===");
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({});
|
||||
const z = await p.evaluate(() => {
|
||||
const knopf = document.getElementById("rc-code");
|
||||
const link = document.querySelector('.rc-partner a.btn-primary');
|
||||
return {
|
||||
wert: document.querySelector(".rc-code-wert")?.textContent.trim(),
|
||||
daten: knopf?.getAttribute("data-code"),
|
||||
href: link?.getAttribute("href"),
|
||||
ziel: link?.getAttribute("target"),
|
||||
rel: link?.getAttribute("rel"),
|
||||
fakten: [...document.querySelectorAll(".rc-fakten li")].map((l) => l.textContent.trim()),
|
||||
skript: [...document.querySelectorAll("script:not([src])")].map((s) => s.textContent).join("\n"),
|
||||
};
|
||||
});
|
||||
|
||||
pruefe(`Angezeigter Code lautet genau „${CODE}“`, z.wert === CODE, z.wert);
|
||||
/* Kopiert wird data-code, angezeigt wird der Text. Beide müssen
|
||||
dasselbe sagen — sonst kopiert man etwas anderes, als man liest. */
|
||||
pruefe("Angezeigter Code und kopierter Code sind identisch", z.daten === z.wert, `${z.daten} / ${z.wert}`);
|
||||
pruefe(`Shoplink zeigt genau auf ${SHOP}`, z.href === SHOP, z.href);
|
||||
pruefe("Shoplink öffnet in neuem Tab und ohne Rückreferenz",
|
||||
z.ziel === "_blank" && (z.rel || "").includes("noopener"), `${z.ziel} / ${z.rel}`);
|
||||
pruefe("Drei Konditionen stehen da", z.fakten.length === 3, z.fakten.length + "");
|
||||
pruefe("Rabatthöhe genannt (10 %)", z.fakten.some((f) => /10\s?%/.test(f)), z.fakten[0]);
|
||||
pruefe(`Ablaufdatum genannt (${BIS_TEXT})`, z.fakten.some((f) => f.includes(BIS_TEXT)), z.fakten[1]);
|
||||
pruefe("Sale-Ausnahme genannt", z.fakten.some((f) => /reduziert/i.test(f)), z.fakten[2]);
|
||||
|
||||
/* Der Text sagt ein Datum, das Skript rechnet mit einem. Zwei Stellen,
|
||||
eine Wahrheit — hier wird sie verglichen. */
|
||||
const imSkript = (z.skript.match(/var ABLAUF = "([\d-]+)"/) || [])[1];
|
||||
pruefe(`Skript rechnet mit demselben Datum wie der Text`,
|
||||
imSkript === BIS, `Skript ${imSkript}, Text ${BIS_TEXT}`);
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
4 — Kopieren, mit Gegenprobe
|
||||
=================================================================== */
|
||||
console.log("\n=== Kopieren ===");
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({});
|
||||
/* Vorher: die Erfolgsmeldung darf NICHT stehen. Ohne diese Zeile
|
||||
würde eine dauerhaft sichtbare Meldung als Erfolg durchgehen. */
|
||||
const vorher = await p.evaluate(() => !document.getElementById("rc-echo-ok").hidden);
|
||||
pruefe("Vor dem Klick steht keine Erfolgsmeldung", vorher === false, String(vorher));
|
||||
|
||||
await p.click("#rc-code");
|
||||
await p.waitForTimeout(400);
|
||||
const nachher = await p.evaluate(async () => ({
|
||||
meldung: !document.getElementById("rc-echo-ok").hidden,
|
||||
fehler: !document.getElementById("rc-echo-fehler").hidden,
|
||||
marke: document.getElementById("rc-code").classList.contains("ist-kopiert"),
|
||||
zwischenablage: await navigator.clipboard.readText(),
|
||||
}));
|
||||
pruefe(`In der Zwischenablage steht „${CODE}“`, nachher.zwischenablage === CODE, nachher.zwischenablage);
|
||||
pruefe("Erfolgsmeldung erscheint", nachher.meldung === true && nachher.fehler === false,
|
||||
`ok=${nachher.meldung} fehler=${nachher.fehler}`);
|
||||
pruefe("Knopf zeigt den kopierten Zustand", nachher.marke === true, String(nachher.marke));
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
5 — Ablauf, in beide Richtungen (gestellte Uhr)
|
||||
=================================================================== */
|
||||
console.log("\n=== Ablaufschaltung ===");
|
||||
for (const [wann, zeit, sollAbgelaufen] of [
|
||||
["am letzten gültigen Abend", new Date(`${BIS}T21:30:00`), false],
|
||||
["am Morgen danach", new Date("2026-10-01T09:00:00"), true],
|
||||
]) {
|
||||
const { ctx, p } = await seiteOeffnen({ zeit });
|
||||
const z = await p.evaluate(() => ({
|
||||
markiert: document.getElementById("rc-partner").classList.contains("ist-abgelaufen"),
|
||||
hinweis: !document.getElementById("rc-abgelaufen").hidden,
|
||||
gesperrt: document.getElementById("rc-code").disabled,
|
||||
}));
|
||||
pruefe(`${wann}: Code gilt ${sollAbgelaufen ? "nicht mehr" : "noch"}`,
|
||||
z.markiert === sollAbgelaufen && z.hinweis === sollAbgelaufen && z.gesperrt === sollAbgelaufen,
|
||||
`markiert=${z.markiert} hinweis=${z.hinweis} gesperrt=${z.gesperrt}`);
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
6 — Alle fünf Sprachen
|
||||
=================================================================== */
|
||||
console.log("\n=== Sprachen ===");
|
||||
{
|
||||
const erwartet = {
|
||||
de: ["Dein Code", "Tippen zum Kopieren"],
|
||||
"de-CH": ["Din Code", "Tippe zum Kopiere"],
|
||||
en: ["Your code", "Tap to copy"],
|
||||
fr: ["Ton code", "Touche pour copier"],
|
||||
pt: ["O teu código", "Toca para copiar"],
|
||||
};
|
||||
for (const [sp, [kopf, fuss]] of Object.entries(erwartet)) {
|
||||
const { ctx, p } = await seiteOeffnen({ sprache: sp });
|
||||
const z = await p.evaluate(() => ({
|
||||
kopf: document.querySelector(".rc-code-kopf")?.textContent.trim(),
|
||||
fuss: document.querySelector(".rc-code-fuss")?.textContent.trim(),
|
||||
/* Ein nicht übersetzter Schlüssel erscheint wörtlich als "rc_..." —
|
||||
das ist der sichtbare Rest, nach dem gesucht wird. */
|
||||
reste: document.getElementById("rc-inhalt").innerText.match(/rc_[a-z_]+/g) || [],
|
||||
entitaeten: document.querySelector(".rc-medaillon img")?.getAttribute("alt") || "",
|
||||
}));
|
||||
pruefe(`[${sp}] Beschriftungen übersetzt`, z.kopf === kopf && z.fuss === fuss, `${z.kopf} / ${z.fuss}`);
|
||||
pruefe(`[${sp}] keine unübersetzten Schlüssel sichtbar`, z.reste.length === 0, z.reste.join(","));
|
||||
/* Die Falle aus pruef-texte.mjs, hier für ein Attribut: Wörterbuch
|
||||
-> setAttribute kodiert NICHT, "&" stünde wörtlich im Alt-Text
|
||||
und würde so vorgelesen. */
|
||||
pruefe(`[${sp}] Alt-Text ohne Kodierungsrest`, !/&(amp|lt|gt|quot|#39);/.test(z.entitaeten), z.entitaeten);
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
7 — Handy, Bewegungsarmut, Bildschirmfotos
|
||||
=================================================================== */
|
||||
console.log("\n=== Handy und Bewegungsarmut ===");
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({ viewport: { width: 390, height: 844 } });
|
||||
const z = await p.evaluate(() => {
|
||||
const k = document.getElementById("rc-partner").getBoundingClientRect();
|
||||
return {
|
||||
quer: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
kartenbreite: Math.round(k.width),
|
||||
knopfhoehe: Math.round(document.getElementById("rc-code").getBoundingClientRect().height),
|
||||
};
|
||||
});
|
||||
pruefe("Handy 390 px: nichts läuft seitlich über", z.quer <= 0, `${z.quer} px Überhang`);
|
||||
pruefe("Handy 390 px: Kopierknopf ist gross genug (>= 44 px)", z.knopfhoehe >= 44, `${z.knopfhoehe} px`);
|
||||
await p.screenshot({ path: "pruef-rabattcodes-handy.png", fullPage: true });
|
||||
await ctx.close();
|
||||
}
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({ reducedMotion: "reduce" });
|
||||
const laufen = await p.evaluate(() => {
|
||||
const teile = [".rc-medaillon", ".rc-glanz", ".rc-schliff", ".rc-code-wert"];
|
||||
return teile.filter((s) => {
|
||||
const el = document.querySelector(s);
|
||||
return el && getComputedStyle(el).animationName !== "none";
|
||||
});
|
||||
});
|
||||
pruefe("Bei „weniger Bewegung“ steht alles still", laufen.length === 0, laufen.join(","));
|
||||
await ctx.close();
|
||||
}
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({});
|
||||
await p.waitForTimeout(500);
|
||||
await p.locator("#rc-partner").screenshot({ path: "pruef-rabattcodes-karte.png" });
|
||||
await ctx.close();
|
||||
}
|
||||
{
|
||||
const { ctx, p } = await seiteOeffnen({ abo: false });
|
||||
await p.screenshot({ path: "pruef-rabattcodes-gesperrt.png", fullPage: true });
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
srv.close();
|
||||
|
||||
/* ===================================================================
|
||||
Ergebnis — mit der ANZAHL in der Bedingung.
|
||||
|
||||
Ein grüner Lauf mit weniger Prüfungen als vorher ist kein grüner Lauf,
|
||||
sondern ein stiller Aussetzer (Erkenntnis 28.08.2026). Die Zahl unten
|
||||
wird beim Erweitern mit angehoben.
|
||||
=================================================================== */
|
||||
const MINDESTENS = 34;
|
||||
const fehl = ergebnisse.filter((e) => !e.ok);
|
||||
console.log(`\n${ergebnisse.length} Prüfungen, ${fehl.length} fehlgeschlagen.`);
|
||||
console.log("Bildschirmfotos: pruef-rabattcodes-karte.png, -handy.png, -gesperrt.png");
|
||||
if (ergebnisse.length < MINDESTENS) {
|
||||
console.error(`FEHLER: nur ${ergebnisse.length} von mindestens ${MINDESTENS} Prüfungen gelaufen — `
|
||||
+ `es ist etwas stillschweigend ausgefallen.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (fehl.length) {
|
||||
console.error("\nFehlgeschlagen:\n" + fehl.map((e) => " - " + e.name).join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Alles in Ordnung.");
|
||||
+436
-50
@@ -25,50 +25,11 @@
|
||||
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260828e" />
|
||||
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260828e" />
|
||||
<style>
|
||||
/* Leere-Zustand-Kachel im "Gutschein"-Look: gestrichelter Rand wie ein
|
||||
ausgeschnittener Coupon, damit auch OHNE echte Codes schon klar ist,
|
||||
wie hier später einzelne Rabattcode-Karten reinkommen. */
|
||||
.code-platzhalter {
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.code-siegel {
|
||||
width: 96px; height: 96px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin: 0 auto 1.4rem;
|
||||
font-size: 2.6rem;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 32% 28%, rgba(52,211,153,.35), rgba(11,13,16,.9) 70%);
|
||||
border: 1.5px solid rgba(52,211,153,.45);
|
||||
box-shadow: 0 0 0 6px rgba(52,211,153,.06), 0 10px 30px rgba(0,0,0,.5);
|
||||
animation: code-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes code-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 6px rgba(52,211,153,.06), 0 10px 30px rgba(0,0,0,.5); }
|
||||
50% { box-shadow: 0 0 0 10px rgba(52,211,153,.12), 0 10px 34px rgba(0,0,0,.55); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .code-siegel { animation: none; } }
|
||||
.code-karte-vorschau {
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
margin: 1.8rem auto 0;
|
||||
padding: 1.2rem 1.4rem;
|
||||
border: 1.5px dashed rgba(52,211,153,.4);
|
||||
border-radius: 14px;
|
||||
background: rgba(52,211,153,.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.code-karte-vorschau .marke { font-weight: 700; }
|
||||
.code-karte-vorschau .prozent {
|
||||
font-weight: 800;
|
||||
color: #5eeaa7;
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Gesperrt-Zustand: an das Supporter-Abo gekoppelt (Nutzer-Feedback
|
||||
03.08.2026), gleiche Prüfung wie die Karte auf links.html. Direkter
|
||||
@@ -85,6 +46,343 @@
|
||||
border: 1.5px solid rgba(212,175,55,.45);
|
||||
box-shadow: 0 0 0 6px rgba(212,175,55,.06), 0 10px 30px rgba(0,0,0,.5);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER ERSTE ECHTE PARTNERCODE (08.09.2026)
|
||||
|
||||
Bis heute war das hier eine Platzhalterkachel ("Codes folgen in
|
||||
Kürze") mit einem erfundenen Beispielcode. Jetzt steht der erste
|
||||
echte drauf: DOGI10 für Van's DIY & Bastelbedarf.
|
||||
|
||||
⚠️ DIE KONDITIONEN SIND ABGELESEN, NICHT GERATEN.
|
||||
|
||||
Quelle der Wahrheit ist der Shop, nicht diese Seite:
|
||||
/home/vandiy/vans-diy-bastelbedarf/src/content/partnercodes.json
|
||||
{ "code": "DOGI10", "prozent": 10, "bis": "2026-09-30",
|
||||
"aktiv": true, "giltAufSale": false }
|
||||
Ausgewertet wird sie an zwei Stellen mit derselben Regel
|
||||
(aktiv !== false UND jetzt <= bis 23:59:59): src/scripts/cart.ts für
|
||||
den Warenkorb, server/lib/preis-berechnen.js für den Endpreis.
|
||||
|
||||
Der naheliegende Weg wäre gewesen, "–10 %" einfach aus dem Namen
|
||||
"DOGI10" zu schließen. Das hätte hier zufällig gestimmt und wäre
|
||||
trotzdem falsch gewesen: Dass der Code NICHT auf bereits reduzierte
|
||||
Artikel gilt (giltAufSale: false) und dass er am 30.09.2026 ausläuft,
|
||||
steht im Namen nicht drin. Eine falsche Zusage auf einer
|
||||
Verkaufsseite kostet VanVan die Diskussion an der Kasse.
|
||||
|
||||
Abgeschrieben ist abgeschrieben: Wer die Konditionen ändert, ändert
|
||||
sie im Shop UND hier — die Zeile unten rechnet nichts aus, sie sagt
|
||||
nur an. Damit das nicht schleichend auseinanderläuft, kennt das
|
||||
Skript am Seitenende das Ablaufdatum und schaltet die Karte von
|
||||
selbst auf "abgelaufen", statt weiter etwas zu versprechen.
|
||||
===================================================================== */
|
||||
.rc-partner {
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 2.4rem 1.6rem 2rem;
|
||||
border-color: rgba(94, 234, 167, .3);
|
||||
}
|
||||
|
||||
/* Eckband wie bei den Feature-Kacheln auf links.html — hier aber ohne
|
||||
Rotation und in Silber statt Bunt, damit es dem Siegel nicht die
|
||||
Aufmerksamkeit wegnimmt. */
|
||||
.rc-band {
|
||||
position: absolute;
|
||||
top: 1rem; right: 1rem;
|
||||
font-size: .64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: .09em;
|
||||
text-transform: uppercase;
|
||||
padding: .36rem .72rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(8, 10, 14, .6);
|
||||
border: 1px solid rgba(255, 255, 255, .24);
|
||||
color: #dfe6ef;
|
||||
}
|
||||
|
||||
/* ---- Das Medaillon --------------------------------------------------
|
||||
Warum für EIN Logo so viel Aufwand: Es ist das einzige Bild auf der
|
||||
Seite und das Gesicht des Partners. Der Weg zu "besonders" führt hier
|
||||
NICHT über mehr Farbe — das Siegel ist schwarz-weiß, jede Einfärbung
|
||||
würde eine fremde Marke umlackieren. Er führt über mehr MATERIAL: aus
|
||||
dem runden Siegel wird eine geprägte Münze, aus sechs Lagen:
|
||||
|
||||
1. Aura weicher Lichthof dahinter, atmet sehr langsam
|
||||
2. Rändel die geriffelte Münzkante (repeating-conic-gradient),
|
||||
72 Zähne. Sie steht STILL — eine sich drehende
|
||||
Riffelung flimmert bei 148 px, und genau diese grelle
|
||||
Optik soll hier nicht sein (Grundregel: augenschonend)
|
||||
3. Glanz stattdessen wandert EIN Lichtpunkt in 22 s einmal um
|
||||
die Kante. Das ist die Bewegung, die eine Münze im
|
||||
Licht macht, und sie ist ruhig genug fürs Auge
|
||||
4. Siegel das freigestellte Logo (partner-vandiy.webp)
|
||||
5. Schliff Prägekante nach innen (oben Licht, unten Schatten) und
|
||||
ein langsamer Lichtstreif über die Fläche
|
||||
6. Ablage elliptischer Schatten darunter, damit die Münze auf der
|
||||
Karte LIEGT statt zu schweben
|
||||
|
||||
Alles davon ist reines CSS: kein zweites Bild, kein Skript, keine
|
||||
Abhängigkeit. Und alles davon steht still, sobald der Rechner
|
||||
"weniger Bewegung" meldet (Block ganz unten). */
|
||||
.rc-medaillon {
|
||||
position: relative;
|
||||
width: clamp(116px, 34vw, 148px);
|
||||
aspect-ratio: 1 / 1;
|
||||
margin: .2rem auto 1.5rem;
|
||||
border-radius: 50%;
|
||||
transition: transform .4s cubic-bezier(.22, .9, .3, 1);
|
||||
}
|
||||
.rc-medaillon > * {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 1 — Aura. Silber (die Farbe des Siegels) mit einem Hauch Mint (die
|
||||
Farbe, in der diese Seite über Codes spricht). */
|
||||
.rc-medaillon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -28%;
|
||||
z-index: 0;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 50% 45%,
|
||||
rgba(226, 234, 244, .30), rgba(94, 234, 167, .11) 45%, transparent 70%);
|
||||
filter: blur(12px);
|
||||
animation: rc-aura-atmen 9s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 6 — Ablageschatten. Steht bewusst VOR der Aura im Quelltext, damit
|
||||
die Reihenfolge im Kopf der Lage entspricht, nicht der Nummer. */
|
||||
.rc-medaillon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 14%; right: 14%;
|
||||
bottom: -8%;
|
||||
height: 13%;
|
||||
z-index: 0;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 0, 0, .6), transparent 72%);
|
||||
filter: blur(4px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 2 — Die geriffelte Münzkante. Der Ring entsteht aus zwei Teilen:
|
||||
border-radius beschneidet den Kegelverlauf auf den Kreis, die Maske
|
||||
stanzt die Mitte heraus. Das Siegel (Lage 4) deckt anschließend
|
||||
alles bis zu seinem eigenen Rand ab — sichtbar bleibt genau der
|
||||
Streifen, der außen übersteht. */
|
||||
.rc-raendel {
|
||||
inset: -10%;
|
||||
z-index: 1;
|
||||
background: repeating-conic-gradient(from 0deg,
|
||||
rgba(228, 234, 243, .92) 0deg 2.5deg,
|
||||
rgba(70, 80, 94, .8) 2.5deg 5deg);
|
||||
-webkit-mask: radial-gradient(closest-side, transparent 0 82%, #000 86%);
|
||||
mask: radial-gradient(closest-side, transparent 0 82%, #000 86%);
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
/* 3 — Der wandernde Lichtpunkt auf derselben Kante. */
|
||||
.rc-glanz {
|
||||
inset: -10%;
|
||||
z-index: 2;
|
||||
background: conic-gradient(from 0deg,
|
||||
rgba(255, 255, 255, 0) 0deg,
|
||||
rgba(255, 255, 255, 0) 248deg,
|
||||
rgba(255, 255, 255, .72) 322deg,
|
||||
rgba(255, 255, 255, 0) 360deg);
|
||||
-webkit-mask: radial-gradient(closest-side, transparent 0 82%, #000 86%);
|
||||
mask: radial-gradient(closest-side, transparent 0 82%, #000 86%);
|
||||
animation: rc-glanz-dreht 22s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 4 — Das Siegel selbst. */
|
||||
.rc-medaillon img {
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, .55));
|
||||
}
|
||||
|
||||
/* 5 — Prägekante und Lichtstreif. Der Streifen benutzt dieselbe
|
||||
Bewegung wie die Marken-Kacheln auf links.html (nav-cta-shimmer aus
|
||||
main.css), nur deutlich langsamer. */
|
||||
.rc-schliff {
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(118deg, transparent 36%, rgba(255, 255, 255, .28) 49%, transparent 62%);
|
||||
background-size: 260% 100%;
|
||||
box-shadow:
|
||||
inset 0 2px 4px rgba(255, 255, 255, .3),
|
||||
inset 0 -8px 16px rgba(0, 0, 0, .4),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, .1);
|
||||
animation: nav-cta-shimmer 6.5s linear infinite;
|
||||
}
|
||||
|
||||
/* Beim Zeigen kippt die Münze leicht ins Licht. Ausgelöst wird das an
|
||||
der ganzen KARTE, nicht am Medaillon: Es ist kein Knopf, und ein
|
||||
Effekt, der erst kommt, wenn man ein Bild genau trifft, findet
|
||||
niemand. */
|
||||
.rc-partner:hover .rc-medaillon {
|
||||
transform: perspective(680px) rotateX(9deg) rotateY(-7deg) scale(1.045);
|
||||
}
|
||||
|
||||
@keyframes rc-aura-atmen {
|
||||
0%, 100% { opacity: .75; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.05); }
|
||||
}
|
||||
@keyframes rc-glanz-dreht { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ---- Name, Beschreibung ------------------------------------------- */
|
||||
.rc-marke {
|
||||
font-size: clamp(1.35rem, 5vw, 1.7rem);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
.rc-marke small {
|
||||
display: block;
|
||||
font-size: .58em;
|
||||
font-weight: 700;
|
||||
letter-spacing: .05em;
|
||||
color: var(--text-muted);
|
||||
margin-top: .25rem;
|
||||
}
|
||||
.rc-beschreibung {
|
||||
margin: .7rem auto 0;
|
||||
max-width: 40ch;
|
||||
font-size: .97rem;
|
||||
}
|
||||
|
||||
/* ---- Der Code selbst ----------------------------------------------
|
||||
Ein Knopf, kein Textfeld: Die einzige Handlung hier ist "kopieren",
|
||||
und die gehört auf etwas Drückbares — mit Tastatur erreichbar, ohne
|
||||
dass jemand erst markieren muss. Der gestrichelte Rand ist der
|
||||
Gutschein-Look, den diese Seite vorher schon in der Vorschaukachel
|
||||
hatte; er bleibt, das Erfundene daran verschwindet. */
|
||||
.rc-code {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
margin: 1.7rem auto 0;
|
||||
padding: .9rem 1.1rem 1rem;
|
||||
border: 1.5px dashed rgba(94, 234, 167, .45);
|
||||
border-radius: 14px;
|
||||
background: rgba(52, 211, 153, .06);
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: border-color .2s ease, background-color .2s ease, transform .12s ease;
|
||||
}
|
||||
.rc-code:hover {
|
||||
border-color: rgba(94, 234, 167, .8);
|
||||
background: rgba(52, 211, 153, .1);
|
||||
}
|
||||
.rc-code:active { transform: scale(.99); }
|
||||
.rc-code.ist-kopiert {
|
||||
border-style: solid;
|
||||
border-color: rgba(94, 234, 167, .9);
|
||||
background: rgba(52, 211, 153, .16);
|
||||
}
|
||||
.rc-code-kopf {
|
||||
display: block;
|
||||
font-size: .7rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: .16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.rc-code-wert {
|
||||
display: block;
|
||||
margin: .32rem 0 .4rem;
|
||||
font-size: clamp(1.7rem, 7.5vw, 2.15rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: .16em;
|
||||
text-indent: .16em; /* gleicht den Sperrsatz hinter dem letzten Zeichen aus */
|
||||
background: linear-gradient(100deg,
|
||||
#7fe3b4 0%, #ffffff 26%, #d9fff1 44%, #86e9bd 62%, #ffffff 80%, #7fe3b4 100%);
|
||||
background-size: 260% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: btn-silver-text-shine 5.5s linear infinite; /* aus main.css */
|
||||
}
|
||||
.rc-code-fuss {
|
||||
display: block;
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
/* Rückmeldung steht AUSSERHALB des Knopfes und wird nicht per Skript
|
||||
beschriftet, sondern nur ein- und ausgeblendet. Sonst wäre der Text
|
||||
nach einem Sprachwechsel deutsch geblieben — data-i18n greift nur an
|
||||
Elementen, die im HTML stehen. Die feste Höhe verhindert, dass die
|
||||
Karte beim Kopieren springt. */
|
||||
.rc-code-echo {
|
||||
min-height: 1.4em;
|
||||
margin: .5rem 0 0;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
color: #8ff0c0;
|
||||
}
|
||||
.rc-code-echo .fehler { color: #ffc9a3; }
|
||||
|
||||
/* ---- Konditionen --------------------------------------------------- */
|
||||
.rc-fakten {
|
||||
list-style: none;
|
||||
margin: 1.2rem 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
font-size: .9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.rc-fakten li { display: flex; gap: .5rem; align-items: baseline; }
|
||||
.rc-fakten b { color: #8ff0c0; font-weight: 800; }
|
||||
|
||||
/* ---- Abgelaufen ---------------------------------------------------- */
|
||||
.rc-abgelaufen {
|
||||
max-width: 380px;
|
||||
margin: 1.2rem auto 0;
|
||||
padding: .7rem 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid rgba(212, 175, 55, .4);
|
||||
background: rgba(212, 175, 55, .08);
|
||||
color: #ffe9a8;
|
||||
font-size: .88rem;
|
||||
}
|
||||
.rc-abgelaufen[hidden] { display: none; }
|
||||
.rc-partner.ist-abgelaufen .rc-code-wert {
|
||||
text-decoration: line-through;
|
||||
opacity: .5;
|
||||
animation: none;
|
||||
}
|
||||
.rc-partner.ist-abgelaufen .rc-code { cursor: not-allowed; opacity: .75; }
|
||||
|
||||
.rc-mehr {
|
||||
max-width: 620px;
|
||||
margin: 1.4rem auto 0;
|
||||
text-align: center;
|
||||
font-size: .9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.rc-medaillon,
|
||||
.rc-medaillon::before,
|
||||
.rc-glanz,
|
||||
.rc-schliff,
|
||||
.rc-code-wert { animation: none; transition: none; }
|
||||
.rc-partner:hover .rc-medaillon { transform: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-links.jpg');--page-bg-mobile:url('/assets/img/bg-links-mobile.jpg');">
|
||||
@@ -103,25 +401,54 @@
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<!-- Nur für Supporter sichtbar (siehe Skript unten) -->
|
||||
<div class="card code-platzhalter" id="rc-inhalt" hidden>
|
||||
<div class="code-siegel" aria-hidden="true">🏷️</div>
|
||||
<h2 data-i18n="rc_titel">Codes folgen in Kürze</h2>
|
||||
<p data-i18n="rc_text">Als Creator bekommt DogFather eigene Rabattcodes bei ausgewählten Partnern — die gibt er direkt an Team Dogi weiter. Sobald die ersten Kooperationen live sind, stehen die Codes genau hier.</p>
|
||||
<div class="code-karte-vorschau" aria-hidden="true">
|
||||
<span class="marke" data-i18n="rc_beispiel_marke">Partner-Marke</span>
|
||||
<span class="prozent" data-i18n="rc_beispiel_code">CODE „DOGFATHER" · –10 %</span>
|
||||
<div id="rc-inhalt" hidden>
|
||||
<article class="card rc-partner" id="rc-partner">
|
||||
<span class="rc-band" data-i18n="rc_band">Partner Nr. 1</span>
|
||||
|
||||
<div class="rc-medaillon">
|
||||
<span class="rc-raendel" aria-hidden="true"></span>
|
||||
<span class="rc-glanz" aria-hidden="true"></span>
|
||||
<img src="/assets/img/partner-vandiy.webp" width="384" height="384"
|
||||
alt="Logo von Van’s DIY & Bastelbedarf"
|
||||
data-i18n-attr="alt:rc_logo_alt" loading="lazy" decoding="async" />
|
||||
<span class="rc-schliff" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class="btn-row" style="justify-content:center;margin-top:1.8rem;">
|
||||
<a class="btn btn-primary" href="kontakt.html" data-i18n="rc_btn_kontakt">Auf dem Laufenden bleiben</a>
|
||||
|
||||
<h2 class="rc-marke">VAN’S DIY<small>& Bastelbedarf</small></h2>
|
||||
<p class="rc-beschreibung" data-i18n="rc_beschreibung">Häkelzubehör, Bastelbedarf, DIY-Plushies und Modeschmuck — VanVans eigener Shop.</p>
|
||||
|
||||
<button type="button" class="rc-code" id="rc-code" data-code="DOGI10">
|
||||
<span class="rc-code-kopf" data-i18n="rc_code_kopf">Dein Code</span>
|
||||
<span class="rc-code-wert">DOGI10</span>
|
||||
<span class="rc-code-fuss" data-i18n="rc_code_fuss">Tippen zum Kopieren</span>
|
||||
</button>
|
||||
<p class="rc-code-echo" role="status" aria-live="polite">
|
||||
<span id="rc-echo-ok" data-i18n="rc_code_kopiert" hidden>Code kopiert ✓</span>
|
||||
<span class="fehler" id="rc-echo-fehler" data-i18n="rc_code_fehler" hidden>Kopieren ging nicht — bitte von Hand abtippen.</span>
|
||||
</p>
|
||||
|
||||
<ul class="rc-fakten">
|
||||
<li><span aria-hidden="true">💸</span><span data-i18n="rc_fakt_rabatt"><b>10 %</b> Rabatt auf deinen Einkauf</span></li>
|
||||
<li><span aria-hidden="true">📅</span><span data-i18n="rc_fakt_bis">Gültig bis 30.09.2026</span></li>
|
||||
<li><span aria-hidden="true">ℹ️</span><span data-i18n="rc_fakt_sale">Nicht auf bereits reduzierte Artikel</span></li>
|
||||
</ul>
|
||||
|
||||
<p class="rc-abgelaufen" id="rc-abgelaufen" hidden data-i18n="rc_abgelaufen">Dieser Code ist ausgelaufen. Dogi klärt gerade die Verlängerung — schau bald wieder rein.</p>
|
||||
|
||||
<div class="btn-row" style="justify-content:center;margin-top:1.6rem;">
|
||||
<a class="btn btn-primary" href="https://vans-diy-bastelbedarf.com/" target="_blank" rel="noopener" data-i18n="rc_btn_shop">Zum Shop von Van’s DIY ↗</a>
|
||||
<a class="btn btn-outline" href="links.html" data-i18n="rc_btn_zurueck">← Zurück zu allen Links</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="rc-mehr" data-i18n="rc_mehr">Weitere Codes folgen, sobald die nächsten Kooperationen stehen.</p>
|
||||
</div>
|
||||
|
||||
<!-- Nur für Nicht-Supporter sichtbar (siehe Skript unten) -->
|
||||
<div class="card code-platzhalter" id="rc-gesperrt" hidden>
|
||||
<div class="code-gesperrt-siegel" aria-hidden="true">🔒</div>
|
||||
<h2 data-i18n="rc_gesperrt_titel">Nur für Dogfather-Supporter</h2>
|
||||
<p data-i18n="rc_gesperrt_text">Rabattcodes sind ein exklusiver Vorteil der DogiCrew 🩵. Werde jetzt Teil davon, um sie freizuschalten, sobald die ersten Kooperationen live sind.</p>
|
||||
<p data-i18n="rc_gesperrt_text">Rabattcodes sind ein exklusiver Vorteil der DogiCrew 🩵. Der erste Partnercode ist schon drin — werde jetzt Teil davon, um ihn freizuschalten.</p>
|
||||
<div class="btn-row" style="justify-content:center;margin-top:1.8rem;">
|
||||
<a class="btn btn-primary" href="abonnieren.html" data-i18n="rc_btn_abo">Jetzt Supporter werden 👑</a>
|
||||
<a class="btn btn-outline" href="links.html" data-i18n="rc_btn_zurueck">← Zurück zu allen Links</a>
|
||||
@@ -132,10 +459,27 @@
|
||||
</main>
|
||||
|
||||
<div id="site-footer"></div>
|
||||
<script src="assets/js/i18n-rabattcodes.js?v=20260828e"></script>
|
||||
<script src="assets/js/i18n-rabattcodes.js?v=20260908a"></script>
|
||||
<script src="assets/js/main.js?v=20260828e"></script>
|
||||
<script>
|
||||
(function () {
|
||||
/* Ablaufdatum von DOGI10 — dieselbe Angabe wie "bis" in
|
||||
src/content/partnercodes.json im Shop, und dieselbe Auslegung:
|
||||
einschließlich dieses Tages bis 23:59:59 (server/lib/preis-
|
||||
berechnen.js, istRabattAktiv).
|
||||
|
||||
WARUM DAS HIER ÜBERHAUPT STEHT: Ein fest eingetipptes Datum in einem
|
||||
Satz ist eine Zeitbombe — es stimmt, bis der Kalender es überholt,
|
||||
und ab dann verspricht die Seite etwas, das der Shop schon ablehnt.
|
||||
Deshalb steht das Datum genau EINMAL als Zahl da und nicht nur als
|
||||
Text, damit die Seite selbst merken kann, wann sie stillhalten muss.
|
||||
|
||||
Gerechnet wird in der Zeitzone des Besuchers, der Shop rechnet in
|
||||
seiner. In den wenigen Stunden Unterschied kann die Seite den Code
|
||||
bereits als abgelaufen zeigen, während er noch gilt — das ist die
|
||||
harmlose Richtung des Fehlers. Andersherum wäre sie es nicht. */
|
||||
var ABLAUF = "2026-09-30";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var status = localStorage.getItem("supporterSubStatus");
|
||||
var hatAbo = ["active", "pending", "suspended", "cancelled"].indexOf(status) !== -1;
|
||||
@@ -143,6 +487,48 @@
|
||||
var gesperrt = document.getElementById("rc-gesperrt");
|
||||
if (inhalt) inhalt.hidden = !hatAbo;
|
||||
if (gesperrt) gesperrt.hidden = hatAbo;
|
||||
|
||||
var karte = document.getElementById("rc-partner");
|
||||
var knopf = document.getElementById("rc-code");
|
||||
var ok = document.getElementById("rc-echo-ok");
|
||||
var fehler = document.getElementById("rc-echo-fehler");
|
||||
var abgelaufen = new Date() > new Date(ABLAUF + "T23:59:59");
|
||||
|
||||
if (abgelaufen) {
|
||||
if (karte) karte.classList.add("ist-abgelaufen");
|
||||
var hinweis = document.getElementById("rc-abgelaufen");
|
||||
if (hinweis) hinweis.hidden = false;
|
||||
if (knopf) knopf.disabled = true;
|
||||
}
|
||||
|
||||
if (!knopf) return;
|
||||
var uhr = null;
|
||||
knopf.addEventListener("click", function () {
|
||||
if (abgelaufen) return;
|
||||
var zeigen = function (welches) {
|
||||
if (ok) ok.hidden = welches !== "ok";
|
||||
if (fehler) fehler.hidden = welches !== "fehler";
|
||||
knopf.classList.toggle("ist-kopiert", welches === "ok");
|
||||
clearTimeout(uhr);
|
||||
uhr = setTimeout(function () {
|
||||
if (ok) ok.hidden = true;
|
||||
if (fehler) fehler.hidden = true;
|
||||
knopf.classList.remove("ist-kopiert");
|
||||
}, 2600);
|
||||
};
|
||||
/* Ohne HTTPS gibt es navigator.clipboard nicht, und auch mit kann
|
||||
das Versprechen abgelehnt werden. Beide Fälle bekommen dieselbe
|
||||
ehrliche Antwort statt einer stillen Nicht-Reaktion — der Code
|
||||
steht ja sichtbar da und lässt sich abtippen. */
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(knopf.getAttribute("data-code") || "").then(
|
||||
function () { zeigen("ok"); },
|
||||
function () { zeigen("fehler"); }
|
||||
);
|
||||
} else {
|
||||
zeigen("fehler");
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<url><loc>https://dogfather-universe.com/medien-casper.html</loc><lastmod>2026-08-01</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/links.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/abonnieren.html</loc><lastmod>2026-08-03</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/rabattcodes.html</loc><lastmod>2026-08-03</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/rabattcodes.html</loc><lastmod>2026-09-08</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/manager.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/werte.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
<url><loc>https://dogfather-universe.com/team-scouts.html</loc><lastmod>2026-07-30</lastmod></url>
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/* ===================================================================
|
||||
Stellt die Siegel der Rabattcode-Partner frei.
|
||||
|
||||
ANLASS (08.09.2026). Auf rabattcodes.html steht ab jetzt der erste
|
||||
echte Partnercode (DOGI10 fuer Van's DIY & Bastelbedarf). Filipe hat
|
||||
das Logo als Bildschirmfoto aus TikTok geliefert: ein rundes Siegel
|
||||
auf schwarzem Grund, 1080x1047, JPEG.
|
||||
|
||||
WARUM DAS SO NICHT AUF DIE SEITE KANN. Der Grund ist NICHT dieselbe
|
||||
Farbe wie die Karte -- rgb(1,1,1) gegen die Kartenflaeche. Ungestellt
|
||||
liegt das Siegel also in einem sichtbaren schwarzen KASTEN mitten in
|
||||
einer runden Fassung. Genau der Fehler, der im Workspace schon einmal
|
||||
passiert ist (siehe marken-freistellen.mjs) -- dort mit einer Datei,
|
||||
deren Zahlen tadellos aussahen.
|
||||
|
||||
WARUM EIN EIGENES WERKZEUG UND NICHT marken-freistellen.mjs. Jenes
|
||||
liefert nach workspace/assets/img/ und traegt zwei Sonderdurchgaenge,
|
||||
die hier beide falsch waeren: `kopfTrennen` (schneidet unter dem Kinn
|
||||
ab) und `nurFarbmotiv` (behaelt nur Buntes). Das Siegel ist rund,
|
||||
vollstaendig gewollt und vollstaendig GRAU -- `nurFarbmotiv` haette
|
||||
ein leeres Bild ergeben, `kopfTrennen` die untere Haelfte gekappt.
|
||||
Gemeinsam ist nur die Flutfuellung; die steht hier deshalb noch
|
||||
einmal, dafuer ohne jede Verzweigung.
|
||||
|
||||
WARUM FLUTFUELLUNG UND KEINE KREISMASKE. Ein Kreis waere hier sogar
|
||||
ausrechenbar (Mitte 539,5/526,5, Radius 495 -- nachgemessen). Er
|
||||
waere aber an genau dieses eine Bild gebunden: Der naechste Partner
|
||||
liefert sein Logo in einer anderen Groesse, vielleicht nicht mittig,
|
||||
vielleicht gar nicht rund. Die Fuellung laeuft vom Bildrand nach
|
||||
innen und bleibt an der ersten Kontur stehen -- sie misst die Form,
|
||||
statt sie vorauszusetzen. Ein neuer Partner ist damit ein Eintrag in
|
||||
AUFTRAEGE und sonst nichts.
|
||||
|
||||
DIE QUELLEN LIEGEN MIT IM REPO (assets/img/marke/quelle-*.jpg), nicht
|
||||
nur in Filipes Bilderordner. Ein Werkzeug, dessen Eingangsdatei auf
|
||||
einem fremden Rechner liegt, laesst sich genau einmal ausfuehren --
|
||||
danach ist es Dekoration.
|
||||
|
||||
WEBP STATT PNG. Gemessen an derselben Vorlage, 384 px Kante:
|
||||
PNG (RGBA) 222 KB, PNG als Graustufen+Alpha 83 KB, WebP 0.94 rund
|
||||
45 KB -- bei einem Bild, das auf der Seite hoechstens 148 px breit
|
||||
erscheint. WebP steckt im Repo bereits in <img> (workspace/index.html,
|
||||
die Buehnenbilder) und in CSS (cryo-*.webp), ist also nichts Neues.
|
||||
|
||||
Aufrufen mit: node tools/partner-siegel-freistellen.mjs
|
||||
=================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const AUFTRAEGE = [
|
||||
{
|
||||
name: "Van's DIY & Bastelbedarf",
|
||||
quelle: join(WURZEL, "assets", "img", "marke", "quelle-vandiy.jpg"),
|
||||
ziel: join(WURZEL, "assets", "img", "partner-vandiy.webp"),
|
||||
},
|
||||
];
|
||||
|
||||
/* 384 px Kante. Das Medaillon auf rabattcodes.html misst hoechstens
|
||||
148 CSS-px (Desktop) bzw. 116 (Handy) -- 384 deckt damit auch ein
|
||||
Handy mit dreifacher Pixeldichte noch scharf ab, ohne dass die Datei
|
||||
in die Naehe der 120-KB-Schwelle aus pruef-bildgroessen.mjs kommt. */
|
||||
const KANTE = 384;
|
||||
|
||||
for (const a of AUFTRAEGE) {
|
||||
if (!existsSync(a.quelle)) {
|
||||
console.error(`Quelle fehlt: ${a.quelle}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const seite = await (await browser.newContext()).newPage();
|
||||
|
||||
const arbeiten = async (auftrag) => await seite.evaluate(async ({ quelle, KANTE }) => {
|
||||
const bild = new Image();
|
||||
bild.src = quelle;
|
||||
await bild.decode();
|
||||
|
||||
const b = bild.naturalWidth, h = bild.naturalHeight;
|
||||
const leinwand = document.createElement("canvas");
|
||||
leinwand.width = b; leinwand.height = h;
|
||||
const stift = leinwand.getContext("2d", { willReadFrequently: true });
|
||||
stift.drawImage(bild, 0, 0);
|
||||
const daten = stift.getImageData(0, 0, b, h);
|
||||
const p = daten.data;
|
||||
|
||||
/* GRUNDTON AUS DEM HAEUFIGSTEN RANDTON, nicht aus den vier Ecken.
|
||||
Uebernommen aus logo-freistellen.mjs, wo die Eckenmittelung an
|
||||
abgerundeten Ecken gescheitert ist. Der Weg ueber den ganzen Rand
|
||||
ueberlebt Rahmen, runde Ecken und einzelne Ausreisser. */
|
||||
const haufen = new Map();
|
||||
const merke = (x, y) => {
|
||||
const i = (y * b + x) * 4;
|
||||
if (p[i + 3] < 200) return;
|
||||
const s = `${p[i] >> 3},${p[i + 1] >> 3},${p[i + 2] >> 3}`;
|
||||
const e = haufen.get(s) || { n: 0, r: 0, g: 0, bl: 0 };
|
||||
e.n++; e.r += p[i]; e.g += p[i + 1]; e.bl += p[i + 2];
|
||||
haufen.set(s, e);
|
||||
};
|
||||
for (let x = 0; x < b; x++) { merke(x, 0); merke(x, h - 1); }
|
||||
for (let y = 0; y < h; y++) { merke(0, y); merke(b - 1, y); }
|
||||
if (!haufen.size) return { fehler: "Der ganze Rand ist durchsichtig." };
|
||||
|
||||
const groesster = [...haufen.values()].sort((a, c) => c.n - a.n)[0];
|
||||
const randPunkte = [...haufen.values()].reduce((s, e) => s + e.n, 0);
|
||||
const randAnteil = groesster.n / randPunkte;
|
||||
if (randAnteil < 0.5) {
|
||||
return { fehler: `Der Rand ist nicht einfarbig genug `
|
||||
+ `(haeufigster Ton nur ${Math.round(randAnteil * 100)} % des Randes).` };
|
||||
}
|
||||
const grund = [
|
||||
Math.round(groesster.r / groesster.n),
|
||||
Math.round(groesster.g / groesster.n),
|
||||
Math.round(groesster.bl / groesster.n),
|
||||
];
|
||||
const abstand = (i) => Math.sqrt(
|
||||
(p[i] - grund[0]) ** 2 + (p[i + 1] - grund[1]) ** 2 + (p[i + 2] - grund[2]) ** 2);
|
||||
|
||||
/* NAH bis hierher gilt ein Punkt als Hintergrund
|
||||
WEIT ab hier gehoert er zum Motiv; dazwischen wird weich abgestuft.
|
||||
Dieselben Werte wie bei den Marken: Auch diese Quelle ist ein JPEG,
|
||||
und um den weissen Ring liegt ein Saum aus Kompressionsartefakten,
|
||||
der weder Grund noch Motiv ist. Wer ihn stehen laesst, bekommt einen
|
||||
schmutzigen Rand -- auf einer dunklen Seite faellt das als grauer
|
||||
Hof um das Siegel auf. */
|
||||
const NAH = 58, WEIT = 118;
|
||||
|
||||
const besucht = new Uint8Array(b * h);
|
||||
const liste = [];
|
||||
for (let x = 0; x < b; x++) { liste.push(x, x + (h - 1) * b); }
|
||||
for (let y = 0; y < h; y++) { liste.push(y * b, b - 1 + y * b); }
|
||||
|
||||
let entfernt = 0, weich = 0;
|
||||
while (liste.length) {
|
||||
const feld = liste.pop();
|
||||
if (besucht[feld]) continue;
|
||||
besucht[feld] = 1;
|
||||
const i = feld * 4;
|
||||
const a = abstand(i);
|
||||
if (a > WEIT) continue; // Motivkante erreicht, hier ist Schluss
|
||||
if (a <= NAH) { p[i + 3] = 0; entfernt++; }
|
||||
else { p[i + 3] = Math.round(255 * ((a - NAH) / (WEIT - NAH))); weich++; }
|
||||
const x = feld % b, y = (feld - x) / b;
|
||||
if (x > 0) liste.push(feld - 1);
|
||||
if (x < b - 1) liste.push(feld + 1);
|
||||
if (y > 0) liste.push(feld - b);
|
||||
if (y < h - 1) liste.push(feld + b);
|
||||
}
|
||||
|
||||
/* ---- Wo steht das Motiv? ---------------------------------------- */
|
||||
let x0 = b, y0 = h, x1 = -1, y1 = -1, sichtbar = 0;
|
||||
for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) {
|
||||
if (p[(y * b + x) * 4 + 3] > 40) {
|
||||
sichtbar++;
|
||||
if (x < x0) x0 = x; if (x > x1) x1 = x;
|
||||
if (y < y0) y0 = y; if (y > y1) y1 = y;
|
||||
}
|
||||
}
|
||||
if (x1 < 0) return { fehler: "Nach dem Freistellen ist nichts uebrig." };
|
||||
|
||||
stift.putImageData(daten, 0, 0);
|
||||
|
||||
/* ---- Quadratisch und zentriert ausgeben --------------------------
|
||||
Das Medaillon in der Seite ist ein Kreis. Ein nicht quadratisch
|
||||
ausgegebenes Bild sitzt darin aus der Mitte gerueckt -- und bei
|
||||
einem runden Siegel sieht man einen Versatz von wenigen Punkten
|
||||
sofort, weil zwei Kreise nebeneinander liegen. 6 % Luft, damit die
|
||||
weiche Kante nicht am Bildrand abgeschnitten wird. */
|
||||
const mb = x1 - x0 + 1, mh = y1 - y0 + 1;
|
||||
const seiteQ = Math.round(Math.max(mb, mh) * 1.06);
|
||||
const mx = x0 + mb / 2, my = y0 + mh / 2;
|
||||
|
||||
const ziel = document.createElement("canvas");
|
||||
ziel.width = KANTE; ziel.height = KANTE;
|
||||
const zs = ziel.getContext("2d");
|
||||
zs.imageSmoothingQuality = "high";
|
||||
zs.drawImage(leinwand, mx - seiteQ / 2, my - seiteQ / 2, seiteQ, seiteQ, 0, 0, KANTE, KANTE);
|
||||
|
||||
const zp = zs.getImageData(0, 0, KANTE, KANTE).data;
|
||||
let deckend = 0, halb = 0;
|
||||
for (let i = 3; i < zp.length; i += 4) {
|
||||
if (zp[i] > 200) deckend++; else if (zp[i] > 10) halb++;
|
||||
}
|
||||
const eckenAlpha = [[0, 0], [KANTE - 1, 0], [0, KANTE - 1], [KANTE - 1, KANTE - 1]]
|
||||
.map(([x, y]) => zp[(y * KANTE + x) * 4 + 3]);
|
||||
const mitteAlpha = zp[((KANTE >> 1) * KANTE + (KANTE >> 1)) * 4 + 3];
|
||||
|
||||
return {
|
||||
webp: ziel.toDataURL("image/webp", 0.94),
|
||||
grund, randAnteil: Math.round(randAnteil * 100),
|
||||
entfernt, weich, quelle: `${b}x${h}`, rahmen: `${mb}x${mh}`,
|
||||
seitenverhaeltnis: mb / mh,
|
||||
deckend, halb, eckenAlpha, mitteAlpha,
|
||||
anteil: deckend / (KANTE * KANTE),
|
||||
};
|
||||
}, { quelle: "data:image/jpeg;base64," + readFileSync(auftrag.quelle).toString("base64"), KANTE });
|
||||
|
||||
let fehlt = 0;
|
||||
for (const auftrag of AUFTRAEGE) {
|
||||
console.log(`\n=== ${auftrag.name} ===`);
|
||||
const e = await arbeiten(auftrag);
|
||||
if (e.fehler) { console.error(` Abgebrochen: ${e.fehler}`); fehlt++; continue; }
|
||||
|
||||
console.log(` Quelle: ${e.quelle}, Grundton rgb(${e.grund.join(",")}) `
|
||||
+ `auf ${e.randAnteil} % des Randes`);
|
||||
console.log(` Freigestellt: ${e.entfernt} Punkte ganz, ${e.weich} weich`);
|
||||
console.log(` Motivrahmen: ${e.rahmen} (Seitenverhaeltnis ${e.seitenverhaeltnis.toFixed(3)})`);
|
||||
console.log(` Ergebnis: ${e.deckend} deckende Punkte (${(e.anteil * 100).toFixed(1)} %), `
|
||||
+ `${e.halb} weiche Kante, Ecken-Alpha ${e.eckenAlpha.join("/")}, Mitte ${e.mitteAlpha}`);
|
||||
|
||||
/* VIER BEDINGUNGEN, und alle vier muessen halten. Die Anzahl steht in
|
||||
der Bedingung, nicht nur im Meldetext -- eine Pruefung, die immer
|
||||
bestaetigt, bestaetigt nichts. */
|
||||
|
||||
/* Ein Kreis fuellt pi/4 seines Quadrats, hier zusaetzlich geteilt
|
||||
durch die 6 % Luft: 0,785 / 1,06^2 = 0,70. Der Rahmen 0,55..0,85
|
||||
laesst Siegel mit Ecken oder mit Rand zu und faengt trotzdem beide
|
||||
Ausrutscher: Fuellung hat das Motiv gefressen / es wurde nichts
|
||||
entfernt. */
|
||||
if (e.anteil < 0.55 || e.anteil > 0.85) {
|
||||
console.error(` FEHLER: ${(e.anteil * 100).toFixed(1)} % Flaeche uebrig, erwartet 55-85 % `
|
||||
+ `(ein Kreis mit 6 % Luft ergibt rund 70 %). Nichts gespeichert.`); fehlt++; continue;
|
||||
}
|
||||
/* Ecken durchsichtig -- genau daran ist die alte Workspace-Datei
|
||||
gescheitert, und man sieht es einer Zahl wie "60,8 % deckend" nicht
|
||||
an. */
|
||||
if (e.eckenAlpha.some((a) => a > 20)) {
|
||||
console.error(` FEHLER: eine Ecke ist noch deckend (${e.eckenAlpha.join("/")}) -- `
|
||||
+ `der Kasten steht noch. Nichts gespeichert.`); fehlt++; continue;
|
||||
}
|
||||
/* ... und die Mitte NICHT. Ohne diese Gegenprobe waere ein Bild, aus
|
||||
dem die Fuellung den Kern herausgeloest hat, durch die Eckenpruefung
|
||||
glatt durchgelaufen. */
|
||||
if (e.mitteAlpha < 250) {
|
||||
console.error(` FEHLER: die Mitte ist durchsichtig (Alpha ${e.mitteAlpha}) -- die Fuellung `
|
||||
+ `ist ins Motiv gelaufen. Nichts gespeichert.`); fehlt++; continue;
|
||||
}
|
||||
/* Rund heisst quadratischer Rahmen. Ein schiefes Verhaeltnis bedeutet,
|
||||
dass die Fuellung an einer Seite weitergelaufen ist als an der
|
||||
anderen -- im Medaillon saehe man das als aus der Mitte geruecktes
|
||||
Siegel. */
|
||||
if (Math.abs(e.seitenverhaeltnis - 1) > 0.04) {
|
||||
console.error(` FEHLER: Motivrahmen ${e.rahmen} ist nicht rund `
|
||||
+ `(Verhaeltnis ${e.seitenverhaeltnis.toFixed(3)}). Nichts gespeichert.`); fehlt++; continue;
|
||||
}
|
||||
|
||||
writeFileSync(auftrag.ziel, Buffer.from(e.webp.split(",")[1], "base64"));
|
||||
const kb = (readFileSync(auftrag.ziel).length / 1024).toFixed(1);
|
||||
console.log(` Gespeichert: ${auftrag.ziel.replace(WURZEL, ".")} (${KANTE}x${KANTE}, ${kb} KB)`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
if (fehlt) { console.error(`\n${fehlt} von ${AUFTRAEGE.length} nicht gespeichert.`); process.exit(1); }
|
||||
console.log(`\n${AUFTRAEGE.length} Partnersiegel freigestellt.`);
|
||||
Reference in New Issue
Block a user