Neue öffentliche Routen unter src/app/(marketing): Übersicht /funktionen, sechs Detailseiten (planung, baustelle, zeiterfassung, berichte, abrechnung, lotse), /sicherheit und /preise, gemeinsames Gerüst mit Kopf- und Fußzeile. scripts/marketing-shots.ts nimmt die Bilder direkt aus der laufenden App auf (Playwright über den installierten Chrome, Session wie im Smoke-Test, ohne Passworteingabe); 14 Aufnahmen in public/marketing ersetzen die gezeichneten Vorschauen. Plantafel: „Vorschläge" hängt jetzt als Fußzeile in der Auftragskarte statt frei zwischen zwei Karten zu stehen (neue Prop footer an OrderCard, Schlüssel unplanned.suggestionsFor). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
5.1 KiB
TypeScript
103 lines
5.1 KiB
TypeScript
/**
|
|
* Screenshots der laufenden App für die Produktseite (`public/marketing/*.png`).
|
|
*
|
|
* Meldet sich OHNE Passworteingabe an: die Session wird wie in `scripts/smoke-auth.ts` gebaut
|
|
* (finalizeIdentityLogin + next-auth/jwt) und als Cookie in den Browser gesetzt. Gezeigt werden
|
|
* ausschließlich Demo-Daten des Mandanten `demo`.
|
|
*
|
|
* npm run dev # Dev-Server muss laufen
|
|
* npx tsx scripts/marketing-shots.ts # BASE=http://localhost:3000
|
|
*/
|
|
import "dotenv/config";
|
|
import { mkdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { chromium, type BrowserContext } from "playwright-core";
|
|
import { encode } from "next-auth/jwt";
|
|
import { prisma } from "../src/server/db";
|
|
import { finalizeIdentityLogin } from "../src/server/auth";
|
|
|
|
const BASE = process.env.BASE ?? "http://localhost:3000";
|
|
const COOKIE = "authjs.session-token";
|
|
const OUT = path.join(process.cwd(), "public", "marketing");
|
|
|
|
async function sessionCookie(email: string, tenant: string) {
|
|
const identity = await prisma.identity.findUniqueOrThrow({ where: { email } });
|
|
const user = await finalizeIdentityLogin(identity.id, tenant);
|
|
if (!user) throw new Error(`keine Mitgliedschaft für ${email}`);
|
|
const token = {
|
|
sub: user.id, name: user.name, email: user.email, userId: user.id, identityId: user.identityId,
|
|
tenantId: user.tenantId, tenantSlug: user.tenantSlug, activeMembershipId: user.activeMembershipId,
|
|
memberships: user.memberships, roles: user.roles, permissions: user.permissions,
|
|
isPlatformAdmin: user.isPlatformAdmin, mfaEnrolled: user.mfaEnrolled,
|
|
};
|
|
return encode({ token, secret: process.env.AUTH_SECRET!, salt: COOKIE, maxAge: 60 * 60 });
|
|
}
|
|
|
|
type Shot = { name: string; path: string; wait?: string; click?: string; fullPage?: boolean };
|
|
|
|
async function shoot(ctx: BrowserContext, shots: Shot[], label: string) {
|
|
const page = await ctx.newPage();
|
|
for (const s of shots) {
|
|
await page.goto(`${BASE}${s.path}`, { waitUntil: "domcontentloaded", timeout: 120_000 });
|
|
if (s.wait) await page.waitForSelector(s.wait, { timeout: 60_000 }).catch(() => undefined);
|
|
if (s.click) await page.click(s.click, { timeout: 15_000 }).catch(() => undefined);
|
|
// Dev-Overlay von Next.js gehört nicht ins Produktbild
|
|
await page.addStyleTag({ content: "nextjs-portal, [data-nextjs-toast], #__next-build-watcher { display: none !important; }" });
|
|
await page.waitForTimeout(1200);
|
|
await page.screenshot({ path: path.join(OUT, `${s.name}.png`), fullPage: s.fullPage ?? false });
|
|
console.log(`✔ ${label}: ${s.name}.png (${s.path})`);
|
|
}
|
|
await page.close();
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(OUT, { recursive: true });
|
|
const tenant = await prisma.tenant.findUniqueOrThrow({ where: { slug: "demo" }, select: { id: true } });
|
|
const order = await prisma.workOrder.findFirstOrThrow({
|
|
where: { tenantId: tenant.id, status: "in_progress", deletedAt: null },
|
|
select: { id: true },
|
|
});
|
|
const browser = await chromium.launch({ channel: "chrome", headless: true });
|
|
const value = await sessionCookie("admin@demo.example", "demo");
|
|
const techValue = await sessionCookie("monteur@demo.example", "demo");
|
|
const cookie = { name: COOKIE, value, domain: "localhost", path: "/", httpOnly: true, secure: false, sameSite: "Lax" as const };
|
|
|
|
const desktop = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, locale: "de-DE" });
|
|
await desktop.addCookies([cookie]);
|
|
await shoot(desktop, [
|
|
{ name: "plantafel", path: "/planning", wait: "text=Plantafel" },
|
|
{ name: "live-lage", path: "/planning/live", wait: "text=Live-Lage", click: "text=Kacheln" },
|
|
{ name: "auftrag", path: `/work-orders/${order.id}`, wait: "text=Status" },
|
|
{ name: "abrechnung", path: "/billing", wait: "text=Abrechnung" },
|
|
{ name: "dashboard", path: "/dashboard", wait: "text=Dashboard" },
|
|
{ name: "auftraege", path: "/work-orders", wait: "text=Aufträge" },
|
|
{ name: "berichte", path: "/reports", wait: "text=Berichte" },
|
|
{ name: "zeitfreigabe", path: "/work-orders/time-approvals", wait: "text=Zeiten" },
|
|
{ name: "audit", path: "/settings/audit", wait: "text=Audit" },
|
|
{ name: "teams", path: "/teams", wait: "text=Teams" },
|
|
], "Desktop");
|
|
await desktop.close();
|
|
|
|
const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 3, locale: "de-DE", isMobile: true, hasTouch: true });
|
|
await mobile.addCookies([{ ...cookie, value: techValue }]);
|
|
await shoot(mobile, [
|
|
{ name: "mobil-heute", path: "/m", wait: "text=Heute" },
|
|
{ name: "mobil-auftrag", path: `/m/orders/${order.id}`, wait: "text=Auftrag" },
|
|
{ name: "mobil-lotse", path: "/m/lotse", wait: "text=Lotse" },
|
|
{ name: "mobil-zeiten", path: "/m/time", wait: "text=Meine Zeiten" },
|
|
], "Mobil");
|
|
await mobile.close();
|
|
|
|
await browser.close();
|
|
console.log(`\nFertig — Dateien in public/marketing/`);
|
|
}
|
|
|
|
main()
|
|
.then(() => prisma.$disconnect())
|
|
.then(() => process.exit(0))
|
|
.catch(async (err) => {
|
|
console.error(err instanceof Error ? err.message : err);
|
|
await prisma.$disconnect().catch(() => undefined);
|
|
process.exit(1);
|
|
});
|