save
Deploy Static Site / deploy (push) Successful in 10m3s

This commit is contained in:
Lars Nolden
2026-08-14 13:51:36 +02:00
parent b6ed4118d1
commit e217d1a97f
9 changed files with 553 additions and 40 deletions
@@ -0,0 +1,169 @@
import type * as React from "react"
import { Link } from "react-router-dom"
import { ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Card, CardContent } from "@/components/ui/card"
import { IconBadge } from "@/components/gebos/icon-tile"
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
import { PageHero } from "@/components/gebos/page-hero"
import { Container, Section } from "@/components/gebos/section"
/** The paragraph of the HeizkostenV a property is cited against. */
type LegalSource = {
cite: string
href: string
}
/** One technical property: line icon, title, body, optional legal citation. */
type Topic = {
icon: React.ReactNode
title: string
body: string
source?: LegalSource
}
const HEIZKOSTENV = "https://www.gesetze-im-internet.de/heizkostenv"
/** Cited source line, pinned to the card's baseline so rows stay aligned. */
function LegalSourceNote({ source }: { source: LegalSource }) {
return (
<p className="mt-auto pt-4 text-xs text-muted-foreground">
Quelle:{" "}
<a
href={source.href}
target="_blank"
rel="noreferrer"
className="font-medium text-brand-700 underline-offset-2 transition-colors hover:text-brand-800 hover:underline"
>
{source.cite}
</a>
</p>
)
}
/** Property grid: line icon over a navy title, body copy, optional citation. */
function TopicCards({
items,
className,
}: {
items: Topic[]
className?: string
}) {
return (
<div className={cn("mt-10 grid gap-5", className)}>
{items.map((item) => (
<Card key={item.title}>
<CardContent className="flex flex-1 flex-col">
<IconBadge variant="outline" shape="squircle" size="lg">
{item.icon}
</IconBadge>
<h3 className="mt-5 text-base font-bold text-balance text-navy">
{item.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-pretty text-muted-foreground">
{item.body}
</p>
{item.source ? <LegalSourceNote source={item.source} /> : null}
</CardContent>
</Card>
))}
</div>
)
}
/**
* Shared shell for the /wissen articles: split PageHero with the
* Startseite → Wissen → article breadcrumb, then the article's own sections.
*/
function WissenArticle({
breadcrumb,
eyebrow,
title,
tagline,
lead,
visual,
children,
}: {
/** label of the current page, appended to Startseite → Wissen */
breadcrumb: string
eyebrow?: React.ReactNode
title: React.ReactNode
/** short bold subline under the H1 (e.g. "Für den Messbetrieb gebaut.") */
tagline?: React.ReactNode
lead?: React.ReactNode
visual?: React.ReactNode
children?: React.ReactNode
}) {
return (
<>
<PageHero
breadcrumb={
<PageBreadcrumb
items={[
{ label: "Startseite", to: "/" },
{ label: "Wissen", to: "/wissen" },
{ label: breadcrumb },
]}
/>
}
eyebrow={eyebrow}
title={title}
lead={
<>
{tagline ? (
<span className="mb-2 block text-lg font-bold text-navy sm:text-xl">
{tagline}
</span>
) : null}
{lead}
</>
}
visual={visual}
/>
<Container>{children}</Container>
</>
)
}
/** Baseline link to the sibling article, closing an article out. */
function WissenRelated({
to,
title,
summary,
}: {
to: string
title: string
summary: string
}) {
return (
<Section className="pt-0 sm:pt-0 lg:pt-0">
<Link
to={to}
className="group flex items-center gap-6 rounded-3xl border border-border bg-card p-6 shadow-card transition-colors hover:border-brand-200 sm:p-8"
>
<div className="min-w-0 flex-1">
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
Weiterlesen
</p>
<h2 className="mt-2 text-xl font-extrabold tracking-tight text-balance text-navy">
{title}
</h2>
<p className="mt-2 text-sm leading-relaxed text-pretty text-muted-foreground">
{summary}
</p>
</div>
<IconBadge
variant="outline"
size="lg"
className="transition-transform group-hover:translate-x-0.5"
>
<ArrowRight />
</IconBadge>
</Link>
</Section>
)
}
export { HEIZKOSTENV, TopicCards, WissenArticle, WissenRelated }
export type { LegalSource, Topic }