docs: bebilderte Anleitung, API-Dokumentation und Screenshots
docs/ANLEITUNG.md führt einmal durch die ganze Anwendung — von der ersten Anmeldung über das tägliche Abhaken bis zu Universen, Statistik und Verwaltung, mit 18 Aufnahmen aus der laufenden Instanz. Die Bilder entstehen mit npm run screenshots: aufgenommen mit doppelter Pixeldichte für scharfe Schrift, abgelegt als WebP in halber Kantenlänge — 13 MB werden so zu 0,6 MB.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import sharp from "sharp";
|
||||
import { readdir, unlink, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Verkleinert die aufgenommenen Screenshots für das Repository.
|
||||
* Die Aufnahme läuft mit doppelter Pixeldichte (scharfe Schrift), abgelegt
|
||||
* wird die halbe Kantenlänge als WebP — das spart rund 90 Prozent.
|
||||
*/
|
||||
const DIR = "docs/screenshots";
|
||||
const files = (await readdir(DIR)).filter((name) => name.endsWith(".png"));
|
||||
|
||||
let before = 0;
|
||||
let after = 0;
|
||||
|
||||
for (const name of files) {
|
||||
const source = join(DIR, name);
|
||||
before += (await stat(source)).size;
|
||||
|
||||
const image = sharp(source);
|
||||
const meta = await image.metadata();
|
||||
const target = join(DIR, name.replace(/\.png$/, ".webp"));
|
||||
|
||||
await image
|
||||
.resize({ width: Math.round((meta.width ?? 1400) / 2) })
|
||||
.webp({ quality: 82, effort: 5 })
|
||||
.toFile(target);
|
||||
|
||||
after += (await stat(target)).size;
|
||||
await unlink(source);
|
||||
}
|
||||
|
||||
const mb = (bytes) => (bytes / 1024 / 1024).toFixed(1);
|
||||
console.log(`${files.length} Bilder: ${mb(before)} MB -> ${mb(after)} MB`);
|
||||
@@ -0,0 +1,99 @@
|
||||
import { chromium } from "playwright";
|
||||
import fs from "node:fs";
|
||||
|
||||
const OUT = "docs/screenshots";
|
||||
const BASE = "http://localhost:3011";
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const errs = [];
|
||||
|
||||
async function session(width, height, theme) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width, height },
|
||||
deviceScaleFactor: 2,
|
||||
colorScheme: theme === "light" ? "light" : "dark",
|
||||
locale: "de-DE",
|
||||
timezoneId: "Europe/Berlin",
|
||||
});
|
||||
await ctx.addInitScript((mode) => localStorage.setItem("flimmer-theme", mode), theme);
|
||||
// Der Entwicklungs-Indikator von Next hat in einer Anleitung nichts verloren.
|
||||
await ctx.addStyleTag?.({ content: "nextjs-portal { display: none !important; }" });
|
||||
const page = await ctx.newPage();
|
||||
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }).catch(() => {});
|
||||
page.on("pageerror", (e) => errs.push(String(e).slice(0, 160)));
|
||||
return { ctx, page };
|
||||
}
|
||||
|
||||
async function login(page) {
|
||||
await page.goto(`${BASE}/anmelden`);
|
||||
await page.fill("#email", "demo@flimmer.local");
|
||||
await page.fill("#password", "flimmer-demo-2026");
|
||||
await Promise.all([page.waitForURL(`${BASE}/`), page.click("button[type=submit]")]);
|
||||
}
|
||||
|
||||
async function shot(page, name, url, opts = {}) {
|
||||
await page.goto(`${BASE}${url}`, { waitUntil: "networkidle" });
|
||||
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }).catch(() => {});
|
||||
await page.waitForTimeout(opts.wait ?? 900);
|
||||
if (opts.before) await opts.before(page);
|
||||
await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: Boolean(opts.full) });
|
||||
console.log(" ", name);
|
||||
}
|
||||
|
||||
// --- Anmeldung (ohne Sitzung) ---
|
||||
{
|
||||
const { ctx, page } = await session(1280, 800, "dark");
|
||||
await shot(page, "01-anmeldung", "/anmelden");
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
// --- Angemeldet, dunkel ---
|
||||
{
|
||||
const { ctx, page } = await session(1400, 900, "dark");
|
||||
await login(page);
|
||||
|
||||
await shot(page, "02-uebersicht", "/", { full: true });
|
||||
await shot(page, "03-suche", "/suche?q=sternenpfad");
|
||||
await shot(page, "05-bibliothek", "/bibliothek");
|
||||
await shot(page, "09-kalender", "/kalender");
|
||||
await shot(page, "10-statistik", "/statistik", { full: true });
|
||||
await shot(page, "11-einstellungen", "/einstellungen");
|
||||
await shot(page, "12-benachrichtigungen", "/einstellungen/benachrichtigungen");
|
||||
await shot(page, "13-api", "/einstellungen/api");
|
||||
await shot(page, "14-daten", "/einstellungen/daten");
|
||||
await shot(page, "15-verwaltung", "/verwaltung", { full: true });
|
||||
await shot(page, "16-papierkorb", "/papierkorb");
|
||||
|
||||
// Detailseite und Universum brauchen IDs aus der Bibliothek
|
||||
await page.goto(`${BASE}/bibliothek?q=nordlicht`, { waitUntil: "networkidle" });
|
||||
const titleHref = await page.locator("a[href^='/titel/']").first().getAttribute("href");
|
||||
if (titleHref) await shot(page, "04-serie", titleHref, { full: true });
|
||||
|
||||
await page.goto(`${BASE}/universen`, { waitUntil: "networkidle" });
|
||||
await shot(page, "06-universen", "/universen");
|
||||
const uniHref = await page.locator("a[href^='/universen/']").first().getAttribute("href");
|
||||
if (uniHref) await shot(page, "07-universum", uniHref, { full: true });
|
||||
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
// --- Hell ---
|
||||
{
|
||||
const { ctx, page } = await session(1400, 900, "light");
|
||||
await login(page);
|
||||
await shot(page, "08-hell", "/bibliothek");
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
// --- Mobil ---
|
||||
{
|
||||
const { ctx, page } = await session(430, 900, "dark");
|
||||
await login(page);
|
||||
await shot(page, "17-mobil-uebersicht", "/");
|
||||
await shot(page, "18-mobil-serie", "/bibliothek");
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
console.log("Fehler:", errs.length ? [...new Set(errs)].slice(0, 5) : "keine");
|
||||
await browser.close();
|
||||
Reference in New Issue
Block a user