/** * Erzeugt die Bildschirmfotos für docs/anleitung.md. * * Warum als Skript und nicht von Hand: Bilder in einer Anleitung veralten * beim ersten Umbau. So lassen sie sich in einer Minute neu erzeugen. * * Voraussetzungen: * - eine **leere** Instanz (die Beispieldaten würden sonst mit fremden * Codes im Bild landen), erreichbar unter BASE * - AUTH_ENABLED=true, damit Kopfzeile und Verwaltung so aussehen wie im * Regelbetrieb * - Playwright: npm i -D playwright && npx playwright install chromium * * Aufruf: * node docs/screenshots.mjs http://localhost:3000 */ import { mkdir } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { chromium } from "playwright"; const BASE = (process.argv[2] ?? "http://localhost:3000").replace(/\/+$/, ""); const ZIEL = join(dirname(fileURLToPath(import.meta.url)), "bilder"); await mkdir(ZIEL, { recursive: true }); const ADMIN = { email: "demo@example.com", password: "nur-fuer-die-bilder-1" }; const DESIGN = { fgColor: "#312E81", bgColor: "#FFFFFF", size: 512, ecLevel: "M", margin: 4 }; /* ------------------------------------------------------------- Anmelden */ let cookie = null; async function api(pfad, init = {}) { const headers = new Headers(init.headers ?? {}); if (init.json !== undefined) headers.set("Content-Type", "application/json"); if (cookie) headers.set("Cookie", cookie); const response = await fetch(BASE + pfad, { ...init, headers, body: init.json !== undefined ? JSON.stringify(init.json) : init.body, redirect: "manual", }); const gesetzt = response.headers.get("set-cookie"); if (gesetzt) cookie = gesetzt.split(";")[0]; let body = null; try { body = await response.json(); } catch { /* leerer Rumpf */ } return { status: response.status, body }; } const zustand = await api("/api/auth"); if (zustand.body?.authEnabled) { const aktion = zustand.body.setupOpen ? "setup" : "login"; const angemeldet = await api("/api/auth", { method: "POST", json: { action: aktion, ...ADMIN, name: "Demo Betreiber" }, }); if (angemeldet.status >= 400) { console.error("Anmeldung fehlgeschlagen:", angemeldet.body); process.exit(1); } } /* --------------------------------------------------------- Beispieldaten */ const angelegt = []; async function code(type, data, label) { const erstellt = await api("/api/codes", { method: "POST", json: { type, data, design: DESIGN, label }, }); if (erstellt.status !== 201) { console.error(`Beispielcode „${label}“ ging nicht:`, erstellt.body); process.exit(1); } angelegt.push(erstellt.body.code); return erstellt.body.code; } await code("website", { url: "https://example.com/nordwind" }, "Projekt Nordwind"); await code("website", { url: "https://example.com/messe" }, "Messestand Halle 3"); const links = await code( "links", { title: "Projekt Nordwind", description: "Alle Unterlagen auf einen Blick.", links: [ { label: "Preisliste als PDF", url: "https://example.com/preise.pdf" }, { label: "Anfahrt zum Werk", url: "https://example.com/anfahrt" }, { label: "Ansprechpartner", url: "https://example.com/kontakt" }, ], }, "Nordwind Linkseite", ); const social = await code( "social", { title: "Bäckerei Sonnenschein", description: "Folge uns für Neues aus der Backstube.", profiles: [ { network: "instagram", url: "@sonnenschein" }, { network: "facebook", url: "sonnenschein.backt" }, { network: "youtube", url: "@sonnenschein" }, { network: "website", url: "https://sonnenschein.example.com" }, ], }, "Sonnenschein Profile", ); /* --- Eine Datei zum Vorzeigen: klein, aber gültig ------------------------ */ async function hochladen(kind, name, inhalt, typ) { const form = new FormData(); form.set("kind", kind); form.set("file", new File([inhalt], name, { type: typ })); const antwort = await api("/api/uploads", { method: "POST", body: form }); if (antwort.status !== 201) { console.error(`Upload „${name}“ ging nicht:`, antwort.body); process.exit(1); } return antwort.body.upload; } const pdfDatei = await hochladen( "pdf", "preisliste-2026.pdf", Buffer.from( "%PDF-1.4\n1 0 obj<>endobj\n" + "2 0 obj<>endobj\n" + "3 0 obj<>endobj\n" + "trailer<>\n%%EOF\n", "latin1", ), "application/pdf", ); const pdf = await code( "pdf", { fileId: pdfDatei.id, filename: pdfDatei.filename, size: pdfDatei.size, title: "Preisliste 2026", description: "Gültig ab Januar, alle Preise ohne Mehrwertsteuer.", }, "Preisliste", ); const feedback = await code( "feedback", { title: "Wie war dein Besuch?", question: "Wie zufrieden bist du?", scale: "stars", allowComment: true, commentLabel: "Möchtest du uns noch etwas sagen?", thankYou: "Danke, das hilft uns weiter!", }, "Gastraum", ); // Ein paar Rückmeldungen, damit die Auswertung nicht leer ist. for (const [note, text] of [ [5, "Sehr freundlich, gerne wieder."], [4, ""], [5, "Der Kuchen war hervorragend."], [3, "Etwas lange Wartezeit."], ]) { await api("/api/feedback", { method: "POST", json: { codeId: feedback.id, rating: note, comment: text }, }); } /* ------------------------------------------------------------ Aufnehmen */ const browser = await chromium.launch(); const desktop = await browser.newContext({ viewport: { width: 1200, height: 820 }, deviceScaleFactor: 1, locale: "de-DE", }); if (cookie) { const [name, ...rest] = cookie.split("="); await desktop.addCookies([{ name, value: rest.join("="), url: BASE }]); } const handy = await browser.newContext({ viewport: { width: 390, height: 800 }, deviceScaleFactor: 1, locale: "de-DE", }); async function schuss(context, pfad, datei, { ganzeSeite = true, vorher } = {}) { const page = await context.newPage(); await page.goto(BASE + pfad, { waitUntil: "networkidle" }); if (vorher) await vorher(page); await page.waitForTimeout(300); await page.screenshot({ path: join(ZIEL, datei), fullPage: ganzeSeite }); await page.close(); console.log(" ✓", datei); } console.log("Bilder werden erzeugt:"); await schuss(desktop, "/", "01-startseite.png"); await schuss(desktop, "/erstellen/website", "02-website-formular.png", { vorher: async (page) => { await page.locator("#website-url").fill("https://example.com/nordwind"); await page.waitForTimeout(900); }, }); await schuss(desktop, "/erstellen/website", "03-dynamisch.png", { vorher: async (page) => { await page.locator("#website-url").fill("https://example.com/nordwind"); await page.locator('button[data-mode="dynamic"]').click(); await page.waitForTimeout(900); }, }); await schuss(desktop, "/meine-codes", "04-meine-codes-liste.png"); await schuss(desktop, "/meine-codes", "05-meine-codes-karten.png", { vorher: async (page) => { await page.locator('button[data-view="cards"]').click(); await page.waitForTimeout(600); }, }); await schuss(desktop, "/meine-codes", "06-ziel-bearbeiten.png", { ganzeSeite: false, vorher: async (page) => { await page.locator('button[data-view="list"]').click(); await page.waitForTimeout(400); // Bewusst die letzte Zeile: ein Website-Code, dessen Dialog ganz ins Bild passt. await page.locator("tbody tr").last().getByRole("button", { name: /bearbeiten/ }).click(); await page.getByRole("heading", { name: "Ziel bearbeiten" }).waitFor(); }, }); await schuss(handy, `/s/${links.slug}`, "07-landing-links.png"); await schuss(handy, `/s/${social.slug}`, "08-landing-social.png"); await schuss(handy, `/s/${pdf.slug}`, "09-landing-pdf.png"); await schuss(handy, `/s/${feedback.slug}`, "09b-landing-feedback.png"); await schuss(desktop, `/meine-codes/${feedback.id}/feedback`, "10-feedback-auswertung.png"); await schuss(desktop, "/verwaltung", "11-verwaltung.png"); await schuss(desktop, "/verwaltung/typen", "12-verwaltung-typen.png"); await schuss(desktop, "/verwaltung/gruppen", "13-verwaltung-gruppen.png"); await schuss(desktop, "/verwaltung/domains", "14-verwaltung-domains.png"); await schuss(desktop, "/konto", "15-konto.png"); await browser.close(); /* ------------------------------------------------------------ Aufräumen */ for (const eintrag of angelegt) { await api(`/api/codes/${eintrag.id}`, { method: "DELETE" }); } console.log(`\nFertig — ${angelegt.length} Beispielcodes wieder entfernt.`); console.log(`Die Bilder liegen in ${ZIEL}.`);