Öffentliche Startseite: Produktseite unter /

Aufbau: Hero mit Plantafel-Vorschau, Leitgedanken, 12 Funktionen, vertiefte Abschnitte zu
Planung, Baustelle und Lotse, Ablauf, Gewerke, Sicherheit, Testphase, häufige Fragen und
Fußzeile. Texte auf Deutsch und Englisch, Vorschauen als Markup statt Screenshots.
Angemeldete Nutzer leitet die Route weiterhin auf Dashboard bzw. mobile Ansicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 11:40:19 +02:00
co-authored by Claude Opus 5
parent a4a61dc31f
commit 31343d329a
6 changed files with 1021 additions and 3 deletions
+357
View File
@@ -0,0 +1,357 @@
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import {
ArrowRight,
Bot,
CalendarRange,
Check,
CheckCircle2,
ClipboardList,
Download,
EyeOff,
FileInput,
FileSignature,
FolderOpen,
KeyRound,
Layers,
MapPin,
Package,
Receipt,
ScrollText,
Siren,
Timer,
UsersRound,
WifiOff,
type LucideIcon,
} from "lucide-react";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import { BRAND } from "@/lib/brand";
import { BoardMockup, FieldMockup, LotseMockup } from "./mockups";
/**
* Öffentliche Startseite (Produktseite). Wird von `/` gerendert, solange keine Session besteht;
* angemeldete Nutzer leitet die Route auf Dashboard bzw. mobile Ansicht. Alle Texte kommen aus
* `messages/<locale>/marketing.json` — keine Screenshots, die Vorschauen sind Markup (mockups.tsx).
*/
const FEATURES: { key: string; icon: LucideIcon }[] = [
{ key: "orders", icon: ClipboardList },
{ key: "board", icon: CalendarRange },
{ key: "live", icon: MapPin },
{ key: "time", icon: Timer },
{ key: "offline", icon: WifiOff },
{ key: "reports", icon: FileSignature },
{ key: "material", icon: Package },
{ key: "billing", icon: Receipt },
{ key: "emergency", icon: Siren },
{ key: "lotse", icon: Bot },
{ key: "documents", icon: FolderOpen },
{ key: "imports", icon: FileInput },
];
const SECURITY: { key: string; icon: LucideIcon }[] = [
{ key: "item1", icon: Layers },
{ key: "item2", icon: UsersRound },
{ key: "item3", icon: KeyRound },
{ key: "item4", icon: ScrollText },
{ key: "item5", icon: Download },
{ key: "item6", icon: EyeOff },
];
const NAV = ["features", "planning", "mobile", "security", "pricing", "faq"] as const;
const FAQ = ["q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8"] as const;
const INDUSTRIES = ["shk", "electric", "cooling", "lifts", "windows", "service"] as const;
const ctaCls =
"inline-flex min-h-12 items-center justify-center gap-2 rounded-lg bg-cta px-5 text-[15px] font-semibold text-cta-foreground hover:brightness-95 focus-visible:outline-2 focus-visible:outline-offset-2";
const outlineCls =
"inline-flex min-h-12 items-center justify-center gap-2 rounded-lg border px-5 text-[15px] font-semibold hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2";
export async function MarketingHome({ contactEmail }: { contactEmail?: string }) {
const t = await getTranslations("marketing");
return (
<div className="flex flex-1 flex-col">
<header className="sticky top-0 z-20 border-b bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-6xl items-center gap-4 px-4 py-3 sm:px-6">
<Link href="/" className="flex min-h-11 items-center" aria-label={BRAND.name}>
<CraftviaLogo variant="horizontal" height={30} />
</Link>
<nav aria-label={t("nav.label")} className="ml-auto hidden lg:block">
<ul className="flex items-center gap-1">
{NAV.map((key) => (
<li key={key}>
<a href={`#${key}`} className="inline-flex min-h-11 items-center rounded-md px-3 text-[14px] font-medium text-muted-foreground hover:text-foreground">
{t(`nav.${key}`)}
</a>
</li>
))}
</ul>
</nav>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Link href="/login" className="inline-flex min-h-11 items-center px-2 text-[14px] font-semibold text-[var(--ui-primary)]">
{t("nav.login")}
</Link>
<Link href="/testen" className="inline-flex min-h-11 items-center rounded-lg bg-cta px-4 text-[14px] font-semibold text-cta-foreground hover:brightness-95">
{t("nav.cta")}
</Link>
</div>
</div>
</header>
<main className="flex-1">
{/* Hero */}
<section className="border-b bg-card">
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-12 sm:px-6 lg:grid-cols-[1.1fr_0.9fr] lg:items-center lg:py-20">
<div>
<p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{t("hero.eyebrow")}</p>
<h1 className="mt-3 font-heading text-[34px] leading-[1.1] font-bold text-[var(--ui-primary)] sm:text-[46px]">{t("hero.title")}</h1>
<p className="mt-4 max-w-xl text-[16.5px] leading-relaxed text-muted-foreground">{t("hero.subtitle")}</p>
<div className="mt-7 flex flex-wrap gap-3">
<Link href="/testen" className={ctaCls}>
{t("hero.ctaPrimary")}
<ArrowRight className="size-4" aria-hidden />
</Link>
<a href="#features" className={outlineCls}>
{t("hero.ctaSecondary")}
</a>
</div>
<ul className="mt-6 grid gap-2">
{(["bullet1", "bullet2", "bullet3"] as const).map((k) => (
<li key={k} className="flex items-start gap-2 text-[14.5px]">
<CheckCircle2 className="mt-0.5 size-[18px] shrink-0 text-[var(--ok)]" aria-hidden />
{t(`hero.${k}`)}
</li>
))}
</ul>
</div>
<BoardMockup />
</div>
</section>
{/* Leitgedanken */}
<section aria-labelledby="pillars" className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<h2 id="pillars" className="font-heading text-[26px] font-semibold">{t("pillars.title")}</h2>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{(["one", "two", "three"] as const).map((k) => (
<article key={k} className="shadow-card rounded-xl border bg-card p-5">
<h3 className="font-heading text-[17px] font-semibold">{t(`pillars.${k}.title`)}</h3>
<p className="mt-2 text-[14.5px] leading-relaxed text-muted-foreground">{t(`pillars.${k}.text`)}</p>
</article>
))}
</div>
</section>
{/* Funktionen */}
<section id="features" aria-labelledby="features-title" className="scroll-mt-20 border-y bg-card">
<div className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<h2 id="features-title" className="font-heading text-[26px] font-semibold">{t("features.title")}</h2>
<p className="mt-2 max-w-2xl text-[15px] text-muted-foreground">{t("features.sub")}</p>
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map(({ key, icon: Icon }) => (
<li key={key} className="rounded-xl border bg-background p-5">
<span className="flex size-10 items-center justify-center rounded-lg bg-[var(--ui-primary-soft)] text-[var(--ui-primary)]">
<Icon className="size-5" aria-hidden />
</span>
<h3 className="mt-3 font-heading text-[16px] font-semibold">{t(`features.${key}.title`)}</h3>
<p className="mt-1.5 text-[14px] leading-relaxed text-muted-foreground">{t(`features.${key}.text`)}</p>
</li>
))}
</ul>
</div>
</section>
{/* Planung */}
<section id="planning" aria-labelledby="planning-title" className="scroll-mt-20">
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-14 sm:px-6 lg:grid-cols-2 lg:items-center">
<div>
<p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{t("planning.eyebrow")}</p>
<h2 id="planning-title" className="mt-2 font-heading text-[26px] font-semibold">{t("planning.title")}</h2>
<p className="mt-3 text-[15px] leading-relaxed text-muted-foreground">{t("planning.text")}</p>
<ul className="mt-5 grid gap-2.5">
{(["point1", "point2", "point3", "point4"] as const).map((k) => (
<li key={k} className="flex items-start gap-2 text-[14.5px]">
<Check className="mt-0.5 size-[18px] shrink-0 text-[var(--ui-primary)]" aria-hidden />
{t(`planning.${k}`)}
</li>
))}
</ul>
</div>
<BoardMockup />
</div>
</section>
{/* Baustelle */}
<section id="mobile" aria-labelledby="mobile-title" className="scroll-mt-20 border-y bg-card">
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-14 sm:px-6 lg:grid-cols-[0.9fr_1.1fr] lg:items-center">
<div>
<p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{t("mobile.eyebrow")}</p>
<h2 id="mobile-title" className="mt-2 font-heading text-[26px] font-semibold">{t("mobile.title")}</h2>
<p className="mt-3 text-[15px] leading-relaxed text-muted-foreground">{t("mobile.text")}</p>
<p className="mt-4 flex items-start gap-2 rounded-lg border border-[var(--info)] bg-background px-3 py-2 text-[14px]">
<WifiOff className="mt-0.5 size-[18px] shrink-0 text-[var(--info)]" aria-hidden />
{t("mobile.offlineNote")}
</p>
</div>
<FieldMockup />
</div>
</section>
{/* Lotse */}
<section aria-labelledby="lotse-title">
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-14 sm:px-6 lg:grid-cols-2 lg:items-center">
<div>
<p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{t("lotse.eyebrow")}</p>
<h2 id="lotse-title" className="mt-2 font-heading text-[26px] font-semibold">{t("lotse.title")}</h2>
<p className="mt-3 text-[15px] leading-relaxed text-muted-foreground">{t("lotse.text")}</p>
<p className="mt-4 text-[13.5px] text-muted-foreground">{t("lotse.note")}</p>
</div>
<LotseMockup />
</div>
</section>
{/* Ablauf */}
<section aria-labelledby="steps-title" className="border-y bg-card">
<div className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<h2 id="steps-title" className="font-heading text-[26px] font-semibold">{t("steps.title")}</h2>
<ol className="mt-6 grid gap-4 md:grid-cols-3">
{(["one", "two", "three"] as const).map((k, i) => (
<li key={k} className="rounded-xl border bg-background p-5">
<span className="flex size-9 items-center justify-center rounded-full bg-[var(--ui-primary)] text-[15px] font-bold text-white tabular-nums">{i + 1}</span>
<h3 className="mt-3 font-heading text-[16px] font-semibold">{t(`steps.${k}.title`)}</h3>
<p className="mt-1.5 text-[14px] leading-relaxed text-muted-foreground">{t(`steps.${k}.text`)}</p>
</li>
))}
</ol>
</div>
</section>
{/* Gewerke */}
<section aria-labelledby="industries-title" className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<h2 id="industries-title" className="font-heading text-[26px] font-semibold">{t("industries.title")}</h2>
<p className="mt-2 text-[15px] text-muted-foreground">{t("industries.sub")}</p>
<ul className="mt-6 flex flex-wrap gap-2.5">
{INDUSTRIES.map((key) => (
<li key={key} className="rounded-full border bg-card px-4 py-2 text-[14px] font-medium">
{t(`industries.${key}`)}
</li>
))}
</ul>
</section>
{/* Sicherheit */}
<section id="security" aria-labelledby="security-title" className="scroll-mt-20 border-y bg-card">
<div className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{t("security.eyebrow")}</p>
<h2 id="security-title" className="mt-2 font-heading text-[26px] font-semibold">{t("security.title")}</h2>
<p className="mt-3 max-w-3xl text-[15px] leading-relaxed text-muted-foreground">{t("security.text")}</p>
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{SECURITY.map(({ key, icon: Icon }) => (
<li key={key} className="rounded-xl border bg-background p-5">
<span className="flex size-10 items-center justify-center rounded-lg bg-[var(--ui-primary-soft)] text-[var(--ui-primary)]">
<Icon className="size-5" aria-hidden />
</span>
<h3 className="mt-3 font-heading text-[16px] font-semibold">{t(`security.${key}.title`)}</h3>
<p className="mt-1.5 text-[14px] leading-relaxed text-muted-foreground">{t(`security.${key}.text`)}</p>
</li>
))}
</ul>
</div>
</section>
{/* Testphase */}
<section id="pricing" aria-labelledby="trial-title" className="scroll-mt-20">
<div className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<div className="shadow-card grid gap-8 rounded-2xl border bg-card p-6 sm:p-9 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
<div>
<p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{t("trial.eyebrow")}</p>
<h2 id="trial-title" className="mt-2 font-heading text-[26px] font-semibold">{t("trial.title")}</h2>
<p className="mt-3 text-[15px] leading-relaxed text-muted-foreground">{t("trial.text")}</p>
<ul className="mt-5 grid gap-2.5">
{(["point1", "point2", "point3"] as const).map((k) => (
<li key={k} className="flex items-start gap-2 text-[14.5px]">
<CheckCircle2 className="mt-0.5 size-[18px] shrink-0 text-[var(--ok)]" aria-hidden />
{t(`trial.${k}`)}
</li>
))}
</ul>
</div>
<div className="grid gap-3">
<Link href="/testen" className={ctaCls}>
{t("trial.cta")}
<ArrowRight className="size-4" aria-hidden />
</Link>
{contactEmail && (
<a href={`mailto:${contactEmail}`} className={outlineCls}>
{t("trial.secondary")}
</a>
)}
<p className="text-[13px] leading-relaxed text-muted-foreground">{t("trial.note")}</p>
</div>
</div>
</div>
</section>
{/* Fragen */}
<section id="faq" aria-labelledby="faq-title" className="scroll-mt-20 border-y bg-card">
<div className="mx-auto max-w-4xl px-4 py-14 sm:px-6">
<h2 id="faq-title" className="font-heading text-[26px] font-semibold">{t("faq.title")}</h2>
<div className="mt-6 divide-y rounded-xl border bg-background">
{FAQ.map((key) => (
<details key={key} className="group px-4">
<summary className="flex min-h-14 cursor-pointer list-none items-center justify-between gap-3 text-[15px] font-semibold">
{t(`faq.${key}.q`)}
<span aria-hidden className="text-[var(--ui-primary)] transition-transform group-open:rotate-45">+</span>
</summary>
<p className="pb-4 text-[14.5px] leading-relaxed text-muted-foreground">{t(`faq.${key}.a`)}</p>
</details>
))}
</div>
</div>
</section>
{/* Abschluss */}
<section aria-labelledby="closing-title" className="mx-auto max-w-6xl px-4 py-16 text-center sm:px-6">
<h2 id="closing-title" className="font-heading text-[28px] font-semibold text-[var(--ui-primary)]">{t("closing.title")}</h2>
<p className="mx-auto mt-3 max-w-xl text-[15.5px] text-muted-foreground">{t("closing.text")}</p>
<div className="mt-6 flex flex-wrap justify-center gap-3">
<Link href="/testen" className={ctaCls}>
{t("closing.cta")}
<ArrowRight className="size-4" aria-hidden />
</Link>
<Link href="/login" className={outlineCls}>
{t("closing.login")}
</Link>
</div>
</section>
</main>
<footer className="border-t bg-card">
<div className="mx-auto grid max-w-6xl gap-8 px-4 py-10 sm:px-6 md:grid-cols-[1.4fr_1fr_1fr]">
<div>
<CraftviaLogo variant="horizontal" height={28} />
<p className="mt-3 max-w-xs text-[13.5px] text-muted-foreground">{t("footer.tagline")}</p>
</div>
<nav aria-labelledby="footer-product">
<h2 id="footer-product" className="text-[13px] font-semibold tracking-wide uppercase">{t("footer.product")}</h2>
<ul className="mt-2 grid">
<li><a href="#features" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.features")}</a></li>
<li><Link href="/testen" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.trial")}</Link></li>
<li><Link href="/login" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.login")}</Link></li>
</ul>
</nav>
<nav aria-labelledby="footer-legal">
<h2 id="footer-legal" className="text-[13px] font-semibold tracking-wide uppercase">{t("footer.company")}</h2>
<ul className="mt-2 grid">
<li><Link href="/testen/nutzungsbedingungen" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.terms")}</Link></li>
<li><Link href="/testen/datenschutz" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.privacy")}</Link></li>
</ul>
</nav>
</div>
<p className="border-t px-4 py-4 text-center text-[12.5px] text-muted-foreground sm:px-6">
© {new Date().getFullYear()} {BRAND.name} · {BRAND.tagline} · {t("footer.rights")}
</p>
</footer>
</div>
);
}
+105
View File
@@ -0,0 +1,105 @@
import { Check, MapPin, PenLine, Truck, Wrench } from "lucide-react";
import { getTranslations } from "next-intl/server";
/**
* Abstrakte Produktvorschauen für die Startseite — bewusst als Markup statt Screenshot:
* bleibt scharf, mehrsprachig, ohne Bildassets und ohne Daten aus einem echten Mandanten.
*/
const BAR = "h-2 rounded-full";
export async function BoardMockup() {
const t = await getTranslations("marketing.hero.mockup");
const rows = [
{ crew: t("crewNorth"), load: t("load"), percent: 94, jobs: [t("job1"), t("job2")], tone: "var(--ok)" },
{ crew: t("crewSouth"), load: t("conflict"), percent: 113, jobs: [t("job3")], tone: "var(--warn)" },
];
return (
<figure aria-label={t("label")} className="shadow-card rounded-2xl border bg-card p-4">
<figcaption className="mb-3 flex items-center justify-between">
<span className="font-heading text-[15px] font-semibold">{t("board")}</span>
<span className="rounded-full border px-2 py-0.5 text-[11px] font-semibold text-muted-foreground">{t("today")}</span>
</figcaption>
<div className="space-y-3">
{rows.map((row) => (
<div key={row.crew} className="rounded-xl border p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-[13px] font-semibold">{row.crew}</span>
<span className="text-[11px] font-semibold" style={{ color: row.tone }}>
{row.load}
</span>
</div>
<div className="mt-2 h-2 w-full rounded-full bg-muted" role="presentation">
<div className={BAR} style={{ width: `${Math.min(row.percent, 100)}%`, background: row.tone }} />
</div>
<ul className="mt-2 flex flex-wrap gap-1.5">
{row.jobs.map((job) => (
<li key={job} className="rounded-md border bg-background px-2 py-1 text-[11.5px]">
{job}
</li>
))}
</ul>
</div>
))}
</div>
<p className="mt-3 flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
<MapPin className="size-3.5" aria-hidden />
{t("live")}
</p>
</figure>
);
}
export async function FieldMockup() {
const t = await getTranslations("marketing.mobile");
const steps = [
{ icon: Truck, ...{ title: t("step1.title"), text: t("step1.text") } },
{ icon: Wrench, ...{ title: t("step2.title"), text: t("step2.text") } },
{ icon: PenLine, ...{ title: t("step3.title"), text: t("step3.text") } },
{ icon: Check, ...{ title: t("step4.title"), text: t("step4.text") } },
];
return (
<ol className="grid gap-3 sm:grid-cols-2">
{steps.map((step, i) => {
const Icon = step.icon;
return (
<li key={step.title} className="shadow-card rounded-xl border bg-card p-4">
<span className="flex size-9 items-center justify-center rounded-lg bg-[var(--ui-primary-soft)] text-[var(--ui-primary)]">
<Icon className="size-5" aria-hidden />
</span>
<p className="mt-3 text-[14.5px] font-semibold">
<span className="text-muted-foreground tabular-nums">{i + 1}. </span>
{step.title}
</p>
<p className="mt-1 text-[13.5px] text-muted-foreground">{step.text}</p>
</li>
);
})}
</ol>
);
}
export async function LotseMockup() {
const t = await getTranslations("marketing.lotse");
return (
<div className="shadow-card space-y-3 rounded-2xl border bg-card p-4">
<p className="ml-auto max-w-[85%] rounded-2xl rounded-br-sm bg-[var(--ui-primary)] px-3 py-2 text-[13.5px] text-white">{t("chatUser")}</p>
<p className="max-w-[90%] rounded-2xl rounded-bl-sm border bg-background px-3 py-2 text-[13.5px]">{t("chatAssistant")}</p>
<div className="rounded-xl border border-l-4 border-l-[var(--ui-accent)] bg-background p-3">
<p className="text-[12px] font-semibold text-muted-foreground uppercase">{t("cardTitle")}</p>
<ul className="mt-1.5 space-y-1 text-[13.5px]">
{[t("cardLine1"), t("cardLine2"), t("cardLine3")].map((line) => (
<li key={line} className="flex items-start gap-2">
<Check className="mt-0.5 size-4 shrink-0 text-[var(--ok)]" aria-hidden />
{line}
</li>
))}
</ul>
<div className="mt-3 flex flex-wrap gap-2">
<span className="rounded-lg bg-cta px-3 py-1.5 text-[13px] font-semibold text-cta-foreground">{t("cardConfirm")}</span>
<span className="rounded-lg border px-3 py-1.5 text-[13px] font-semibold">{t("cardDiscard")}</span>
</div>
</div>
</div>
);
}