Files
craftvia/src/app/(marketing)/funktionen/[slug]/page.tsx
T
msolarczekandClaude Opus 5 4815a957b0 Produktseite ausgebaut: Unterseiten und Bildschirmfotos aus der App
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>
2026-09-17 13:35:04 +02:00

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 />
</>
);
}