This commit is contained in:
Lars Nolden
2026-08-13 18:47:55 +02:00
parent 2c1a8ae9b5
commit 33690b30a3
30 changed files with 1070 additions and 186 deletions
@@ -3,6 +3,7 @@ import * as React from "react"
import { Container, Section, SectionHeading } from "@/components/gebos/section"
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
import { PageHero } from "@/components/gebos/page-hero"
import { FeatureBand } from "@/components/gebos/feature-band"
import { FeatureTile } from "@/components/gebos/icon-tile"
type AudienceFeature = {
@@ -34,6 +35,7 @@ function AudiencePage({
actions,
visual,
features,
featureLayout = "tiles",
children,
closing,
}: {
@@ -45,6 +47,11 @@ function AudiencePage({
actions?: React.ReactNode
visual?: React.ReactNode
features: AudienceFeature[]
/**
* `tiles` — bare icon-over-label tiles on the page background.
* `band` — one divided white card (larger line icons, two-line subs).
*/
featureLayout?: "tiles" | "band"
/** additional full-width spec sections between feature row and closing */
children?: React.ReactNode
closing?: React.ReactNode
@@ -69,17 +76,21 @@ function AudiencePage({
/>
<Container>
<Section className="pt-2 sm:pt-4 lg:pt-6">
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-4">
{features.map((feature, i) => (
<FeatureTile
key={i}
icon={feature.icon}
media={feature.media}
title={feature.title}
sub={feature.sub}
/>
))}
</div>
{featureLayout === "band" ? (
<FeatureBand items={features} />
) : (
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-4">
{features.map((feature, i) => (
<FeatureTile
key={i}
icon={feature.icon}
media={feature.media}
title={feature.title}
sub={feature.sub}
/>
))}
</div>
)}
</Section>
{children}
{closing}
@@ -1,19 +1,27 @@
import { Link } from "react-router-dom"
import { Euro, Handshake, ReceiptText, ShieldCheck, Wrench } from "lucide-react"
import { Clock, Euro, HardHat, Network } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
BenefitGrid,
type BenefitGridItem,
} from "@/components/gebos/benefit-grid"
import {
ComparisonTable,
type ComparisonColumn,
type ComparisonRow,
} from "@/components/gebos/comparison-table"
import { Section, SectionHeading } from "@/components/gebos/section"
import { CtaBanner } from "@/components/gebos/cta-banner"
import { NumberedList } from "@/components/gebos/process-steps"
import { StepRail } from "@/components/gebos/process-steps"
import {
CloudAutomationIcon,
CrossedToolsIcon,
EuroCircleIcon,
PortalGaugeIcon,
PreconfiguredIcon,
} from "@/components/gebos/line-icons"
import { OptionCard } from "@/components/gebos/option-card"
import { AssetRender, renders } from "@/components/gebos/site-assets"
import { AudiencePage } from "@/pages/audiences/audience-page"
@@ -31,7 +39,7 @@ function HomeownerArt() {
imgClassName="w-full"
loading="eager"
/>
<AssetRender
{/* <AssetRender
render={renders.smokeAlarm}
className="absolute top-[8%] left-[2%] w-[26%]"
imgClassName="w-full"
@@ -50,69 +58,140 @@ function HomeownerArt() {
render={renders.cubesRound}
alt=""
className="absolute top-[50%] right-[30%] w-[16%] scale-200 z-30"
/> */}
</div>
)
}
/**
* Page-local scene for the full-service route: the fitter drilling the gateway
* onto the drywall panel. 4:3 frame to match the shipping-box render next door.
* The render is a self-contained scene whose subject occupies the middle 79%
* of its 4:5 canvas, so it is scaled past the frame and dropped below it: the
* subject then fills the frame's height while the surrounding matte — invisible
* on the card — is what overflows.
*/
function FullServiceArt() {
return (
<div className="relative w-full aspect-[4/3]">
<AssetRender
render={renders.workerDrillingGateway2}
className="absolute inset-x-0 -bottom-[15%] mx-auto w-[74%]"
/>
</div>
)
}
/**
* Portal laptop for the benefit grid's centre cell. Measured on the master,
* the laptop occupies 11.088.5 % of the canvas height and 7.783.9 % of its
* width, i.e. 77.4 % of each with clear air around it. Fitting the *canvas*
* to the cell would therefore leave the laptop floating small in the middle,
* so the canvas is inflated past the cell by exactly that air (11.04 % /
* 0.7744 above, 11.52 % / 0.7744 below): the laptop then fills the cell's
* height, flush with the flanking cards' floor and a grid gap clear of the
* row above — it never crosses into a card. `object-contain` keeps that true
* whatever height the row settles at; the wider right inset cancels the
* laptop's own leftward bias inside the canvas so it reads centred.
*/
function PortalLaptopArt() {
return (
<AssetRender
render={renders.laptopDashboard}
className="mx-auto block w-full max-w-md lg:absolute lg:-top-[14.3%] lg:-right-[12.3%] lg:-bottom-[14.9%] lg:-left-[3%] lg:mx-0 lg:w-auto lg:max-w-none"
imgClassName="h-full w-full object-contain"
/>
)
}
const SELF_INSTALL_STEPS = [
{
media: (
<AssetRender render={renders.houseSmallIso} imgClassName="w-28" />
),
title: "Gebäude angeben",
description:
"Sie teilen uns die grundlegenden Informationen zum Gebäude und den Wohnungen mit.",
"Daten zu Ihrem Gebäude und den Wohneinheiten in wenigen Schritten erfassen.",
},
{
media: (
<>
<AssetRender render={renders.heatMeter} imgClassName="w-24" />
<AssetRender render={renders.smokeAlarm} imgClassName="w-20" />
</>
),
title: "Passende Ausstattung zusammenstellen",
description:
"Die benötigten Zähler, Kommunikationskomponenten und gegebenenfalls Rauchwarnmelder werden zusammengestellt.",
description: "Wir empfehlen die benötigte Ausstattung Sie wählen aus.",
},
{
media: (
<AssetRender render={renders.shippingBoxClosed} imgClassName="w-36" />
),
title: "Vorkonfiguriert erhalten",
description:
"Die Geräte werden bereits für Ihr Gebäude vorbereitet geliefert.",
description: "Ihre Technik kommt vorkonfiguriert und einsatzbereit an.",
},
{
media: <AssetRender render={renders.drillTool} imgClassName="w-32" />,
title: "Montieren",
description:
"Sie selbst, Ihr Hausmeister oder Ihr Fachbetrieb installiert die Geräte.",
"Sie montieren mit Ihrem Handwerker oder selbst flexibel und unkompliziert.",
},
{
media: (
<AssetRender render={renders.refreshLoopCloud} imgClassName="w-28" />
),
title: "GebOS übernimmt",
description:
"Nach der Inbetriebnahme laufen Datenerfassung und die gebuchten Dienstleistungen über GebOS.",
"Wir übernehmen Ablesung, Übertragung und Auswertung automatisch.",
},
]
const WHY_GEBOS = [
const WHY_GEBOS: BenefitGridItem[] = [
{
icon: <EuroCircleIcon />,
title: "Kosten kontrollieren",
text: "Entscheiden Sie selbst, welche Arbeiten Sie übernehmen und welche Sie abgeben möchten.",
},
{
icon: <Network strokeWidth={1.6} />,
title: "Keine unnötige technische Komplexität",
text: "Die Hardware wird für das jeweilige Gebäude vorbereitet.",
},
{
icon: <HardHat strokeWidth={1.6} />,
title: "Bestehenden Handwerker nutzen",
text: "Sie benötigen keinen speziellen GebOS-Installateur.",
},
{
icon: <PortalGaugeIcon />,
title: "Alles an einem Ort",
text: "UVI, Abrechnung, Geräte und weitere Gebäudedienstleistungen werden zentral verwaltet.",
},
{
icon: <Clock strokeWidth={1.6} />,
title: "Weniger laufender Aufwand",
text: "Nach der Einrichtung werden Verbrauchsdaten automatisch erfasst und verarbeitet.",
},
]
const VARIANT_ROWS = [
["Planung", "gemeinsam mit GebOS", "GebOS"],
["Hardware", "GebOS", "GebOS"],
["Installation", "Kunde / eigener Fachbetrieb", "organisiert durch GebOS"],
["Softwareeinrichtung", "vorbereitet durch GebOS", "GebOS"],
["Laufender Betrieb", "GebOS", "GebOS"],
["Kosten", "möglichst niedrig", "höhere Planungssicherheit und Komfort"],
const VARIANT_COLUMNS: ComparisonColumn[] = [
{ title: "Selbst installieren", highlight: true },
{ title: "Komplettlösung" },
]
const VARIANT_ROWS: ComparisonRow[] = [
{ label: "Planung", cells: ["gemeinsam mit GebOS", "GebOS"] },
{ label: "Hardware", cells: ["GebOS", "GebOS"] },
{
label: "Installation",
cells: ["Kunde / eigener Fachbetrieb", "organisiert durch GebOS"],
},
{ label: "Softwareeinrichtung", cells: ["vorbereitet durch GebOS", "GebOS"] },
{ label: "Laufender Betrieb", cells: ["GebOS", "GebOS"] },
{
label: "Kosten",
cells: ["möglichst niedrig", "höhere Planungssicherheit und Komfort"],
},
]
export default function HauseigentuemerPage() {
@@ -131,26 +210,27 @@ export default function HauseigentuemerPage() {
</Button>
}
visual={<HomeownerArt />}
featureLayout="band"
features={[
{
icon: <Wrench />,
title: "Selbst installieren",
sub: "Kosten sparen, flexibel bleiben",
icon: <PreconfiguredIcon />,
title: "Vorkonfigurierte Technik",
sub: "Einsatzbereit, perfekt aufeinander abgestimmt.",
},
{
icon: <ShieldCheck />,
title: "Komplettlösung",
sub: "Wir kümmern uns um alles",
icon: <CrossedToolsIcon />,
title: "Flexibel installieren",
sub: "Selbst übernehmen oder vom Profi montieren lassen.",
},
{
icon: <ReceiptText />,
title: "Transparente Preise",
sub: "Keine versteckten Kosten",
icon: <CloudAutomationIcon />,
title: "Automatischer Betrieb",
sub: "GebOS übernimmt Ablesung, Übertragung und Auswertung.",
},
{
icon: <Handshake />,
title: "Bestehende Partner",
sub: "Ihre Handwerker bleiben",
icon: <PortalGaugeIcon />,
title: "Alles zentral verwaltet",
sub: "Alle Daten, Geräte und Gebäude im Blick.",
},
]}
closing={
@@ -181,51 +261,45 @@ export default function HauseigentuemerPage() {
title="Zwei Wege zu GebOS"
/>
<div className="mt-10 grid gap-6 md:grid-cols-2">
<Card className="relative gap-0 overflow-hidden p-7">
<AssetRender
render={renders.drillTool}
className="pointer-events-none absolute -top-2 -right-3 w-28 opacity-90"
/>
<h3 className="pr-24 text-lg font-extrabold tracking-tight text-navy">
Selbst installieren
</h3>
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
Sie möchten Kosten niedrig halten und können die Installation
selbst oder über einen vertrauten Heizungsbauer, Hausmeister
oder anderen Fachbetrieb organisieren? GebOS liefert die
benötigte Technik passend zu Ihrem Gebäude vorkonfiguriert.
</p>
<p className="mt-3 text-sm font-bold leading-relaxed text-navy">
Sie organisieren die Montage. GebOS übernimmt den digitalen
Betrieb.
</p>
<div className="mt-6">
<Button asChild size="sm">
<OptionCard
title="Selbst installieren"
lead="Sie erhalten vorkonfigurierte Technik und installieren mit Ihrem Handwerker oder in Eigenregie."
takeaway={
<>
Sie organisieren die Montage.
<br />
GebOS übernimmt den digitalen Betrieb.
</>
}
visual={
<AssetRender
render={renders.shippingBoxMeters}
className="block w-full"
/>
}
action={
<Button asChild size="lg">
<Link to="/konfigurator">Preis berechnen</Link>
</Button>
</div>
</Card>
<Card className="relative gap-0 overflow-hidden p-7">
<AssetRender
render={renders.shippingBoxMeters}
className="pointer-events-none absolute -top-1 -right-4 w-32"
/>
<h3 className="pr-28 text-lg font-extrabold tracking-tight text-navy">
Komplett installieren lassen
</h3>
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
Sie möchten sich möglichst wenig mit Planung, Beschaffung und
Installation beschäftigen? GebOS organisiert die komplette
Umsetzung für Sie. Von der Ermittlung der benötigten Geräte
über die Installation bis zum laufenden Betrieb erhalten Sie
eine durchgängige Lösung.
</p>
<div className="mt-6">
<Button asChild size="sm" variant="outline">
}
/>
<OptionCard
title="Komplett installieren lassen"
lead="Lehnen Sie sich zurück. GebOS übernimmt Planung, Installation und Einrichtung inklusive Service."
takeaway={
<>
Ein Ansprechpartner. Alles aus einer Hand.
<br />
Komplettlösung mit voller Betreuung.
</>
}
visual={<FullServiceArt />}
action={
<Button asChild size="lg" variant="outline">
<Link to="/kontakt">Komplettlösung anfragen</Link>
</Button>
</div>
</Card>
}
/>
</div>
</Section>
@@ -234,7 +308,7 @@ export default function HauseigentuemerPage() {
eyebrow="Schritt für Schritt"
title="So funktioniert die Selbstinstallation"
/>
<NumberedList steps={SELF_INSTALL_STEPS} className="mt-10 max-w-2xl" />
<StepRail steps={SELF_INSTALL_STEPS} className="mt-10" />
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
@@ -242,16 +316,11 @@ export default function HauseigentuemerPage() {
eyebrow="Ihre Vorteile"
title="Warum GebOS für Hauseigentümer?"
/>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{WHY_GEBOS.map((item) => (
<Card key={item.title} className="gap-0 p-6">
<h3 className="text-sm font-bold text-navy">{item.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{item.text}
</p>
</Card>
))}
</div>
<BenefitGrid
className="mt-10"
items={WHY_GEBOS}
visual={<PortalLaptopArt />}
/>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
@@ -259,32 +328,11 @@ export default function HauseigentuemerPage() {
eyebrow="Im Vergleich"
title="Welche Variante passt zu mir?"
/>
<Card className="mt-10 gap-0 overflow-hidden p-2 sm:p-4">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[30%]" />
<TableHead>Selbst installieren</TableHead>
<TableHead>Komplettlösung</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{VARIANT_ROWS.map(([label, self, full]) => (
<TableRow key={label}>
<TableCell className="font-bold text-navy">
{label}
</TableCell>
<TableCell className="text-muted-foreground">
{self}
</TableCell>
<TableCell className="text-muted-foreground">
{full}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<ComparisonTable
className="mt-10"
columns={VARIANT_COLUMNS}
rows={VARIANT_ROWS}
/>
</Section>
</AudiencePage>
)