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>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user