Bedienungsanleitung mit Bildern
docs/anleitung.md fuehrt vom ersten Code bis zur Auswertung von Rueckmeldungen: statisch gegen dynamisch, Gestaltung, Meine Codes, die fuenf Typen mit eigener Seite, Verwaltung, Konto und haeufige Fragen. Die Bilder erzeugt docs/screenshots.mjs. Von Hand aufgenommene Bilder veralten beim ersten Umbau — so sind sie in einer Minute neu erzeugt, samt Beispieldaten, die das Skript hinterher wieder entfernt.
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 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<</Type/Catalog/Pages 2 0 R>>endobj\n" +
|
||||
"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
|
||||
"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]>>endobj\n" +
|
||||
"trailer<</Root 1 0 R>>\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}.`);
|
||||
Reference in New Issue
Block a user