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>
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { notFound } from "next/navigation";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { CtaBand, PageHero, PointList, ShotFrame } from "@/components/marketing/blocks";
|
|
import { FEATURE_PAGES, FEATURE_SLUGS, isFeatureSlug } from "@/lib/marketing/pages";
|
|
|
|
/** Detailseite eines Moduls — Inhalt aus `marketing.pages.<slug>`, Bilder aus public/marketing. */
|
|
|
|
type Props = { params: Promise<{ slug: string }> };
|
|
|
|
export function generateStaticParams() {
|
|
return FEATURE_SLUGS.map((slug) => ({ slug }));
|
|
}
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
if (!isFeatureSlug(slug)) return {};
|
|
const t = await getTranslations("marketing");
|
|
return { title: t(`pages.${slug}.title`), description: t(`pages.${slug}.lead`) };
|
|
}
|
|
|
|
export default async function FeatureDetailPage({ params }: Props) {
|
|
const { slug } = await params;
|
|
if (!isFeatureSlug(slug)) notFound();
|
|
const page = FEATURE_PAGES[slug];
|
|
const t = await getTranslations("marketing");
|
|
const points = Array.from({ length: page.points }, (_, i) => t(`pages.${slug}.p${i + 1}`));
|
|
const cards = Array.from({ length: page.cards }, (_, i) => ({
|
|
title: t(`pages.${slug}.c${i + 1}title`),
|
|
text: t(`pages.${slug}.c${i + 1}text`),
|
|
}));
|
|
const [first, second] = page.shots;
|
|
|
|
return (
|
|
<>
|
|
<PageHero eyebrow={t("nav.features")} title={t(`pages.${slug}.title`)} lead={t(`pages.${slug}.lead`)} />
|
|
|
|
<section aria-label={t(`pages.${slug}.title`)} className="mx-auto max-w-6xl px-4 pb-12 sm:px-6">
|
|
<div className={`grid gap-8 ${first.kind === "mobile" ? "lg:grid-cols-[1fr_0.6fr]" : "lg:grid-cols-[0.9fr_1.1fr]"} lg:items-center`}>
|
|
<PointList points={points} />
|
|
<ShotFrame shot={first} caption={t(`pages.${slug}.shot1`)} priority />
|
|
</div>
|
|
</section>
|
|
|
|
<section aria-labelledby="detail-cards" className="border-y bg-card">
|
|
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
|
<h2 id="detail-cards" className="sr-only">
|
|
{t(`pages.${slug}.title`)}
|
|
</h2>
|
|
<ul className="grid gap-4 md:grid-cols-3">
|
|
{cards.map((c) => (
|
|
<li key={c.title} className="rounded-xl border bg-background p-5">
|
|
<h3 className="font-heading text-[16px] font-semibold">{c.title}</h3>
|
|
<p className="mt-1.5 text-[14px] leading-relaxed text-muted-foreground">{c.text}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
{second && (
|
|
<section aria-label={t(`pages.${slug}.shot2`)} className="mx-auto max-w-5xl px-4 py-12 sm:px-6">
|
|
<ShotFrame shot={second} caption={t(`pages.${slug}.shot2`)} />
|
|
</section>
|
|
)}
|
|
|
|
<CtaBand />
|
|
</>
|
|
);
|
|
}
|