This commit is contained in:
Lars Nolden
2026-08-13 23:12:07 +02:00
parent 2f09345ad3
commit a5d773d9f3
26 changed files with 260 additions and 134 deletions
@@ -279,7 +279,7 @@ export default function HauseigentuemerPage() {
}
action={
<Button asChild size="lg">
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator?installation=0">Preis berechnen</Link>
</Button>
}
/>
@@ -296,7 +296,7 @@ export default function HauseigentuemerPage() {
visual={<FullServiceArt />}
action={
<Button asChild size="lg" variant="outline">
<Link to="/kontakt">Komplettlösung anfragen</Link>
<Link to="/konfigurator?installation=1">Preis berechnen</Link>
</Button>
}
/>
+101 -8
View File
@@ -1,4 +1,5 @@
import * as React from "react"
import { useSearchParams } from "react-router-dom"
import { Ban, Check, CircleCheck, Euro, Plus, X } from "lucide-react"
import { Badge } from "@/components/ui/badge"
@@ -40,9 +41,13 @@ const HEAT_OPTIONS: { value: Heat; label: string }[] = [
{ value: "unbekannt", label: "Weiß ich nicht" },
]
const INITIAL_BUILDINGS: Building[] = [
{ id: 1, adresse: "", heat: "heizkoerper", wohnungen: 12, zimmer: 3 },
]
const INITIAL_BUILDING: Building = {
id: 1,
adresse: "",
heat: "heizkoerper",
wohnungen: 12,
zimmer: 3,
}
/** Grundpreise je Wohnung / Monat Selbstmontage, wie in der Klarpreis-Liste. */
const UVI_MONTHLY = 10.5
@@ -81,6 +86,74 @@ function settle(value: number | "", min: number) {
return value === "" || value < min ? min : value
}
/**
* Deep-link preset. Pages elsewhere on the site link into the configurator with
* parts of the form already answered, e.g. `/konfigurator?installation=0` for
* the self-install route. Supported params: `installation`, `uvi`, `rwm`,
* `adresse`, `heizung`, `wohnungen`, `zimmer`. Missing or unreadable values
* keep the page defaults; numbers below the field minimum settle at it, just
* as they do when typed into the form.
*/
type Preset = {
uvi?: boolean
rwm?: boolean
install?: boolean
/** Seeds the single building the configurator starts with. */
building: Partial<Building>
}
/** A bare flag (`?installation`) counts as "on" so short links stay readable. */
function presetFlag(raw: string | null): boolean | undefined {
if (raw === null) return undefined
switch (raw.trim().toLowerCase()) {
case "":
case "1":
case "true":
case "ja":
return true
case "0":
case "false":
case "nein":
return false
default:
return undefined
}
}
function presetNumber(
raw: string | null,
parse: (raw: string) => number | ""
): number | undefined {
if (raw === null) return undefined
const value = parse(raw)
return value === "" ? undefined : settle(value, 1)
}
function presetHeat(raw: string | null): Heat | undefined {
return HEAT_OPTIONS.some((opt) => opt.value === raw)
? (raw as Heat)
: undefined
}
function readPreset(params: URLSearchParams): Preset {
const building: Partial<Building> = {}
const adresse = params.get("adresse")
if (adresse !== null) building.adresse = adresse
const heat = presetHeat(params.get("heizung"))
if (heat !== undefined) building.heat = heat
const wohnungen = presetNumber(params.get("wohnungen"), parseCount)
if (wohnungen !== undefined) building.wohnungen = wohnungen
const zimmer = presetNumber(params.get("zimmer"), parseAmount)
if (zimmer !== undefined) building.zimmer = zimmer
return {
uvi: presetFlag(params.get("uvi")),
rwm: presetFlag(params.get("rwm")),
install: presetFlag(params.get("installation")),
building,
}
}
/** glossy numerals for the three configurator steps */
type Step = 1 | 2 | 3
@@ -254,12 +327,32 @@ function PriceRow({
}
export default function ConfiguratorPage() {
const [uvi, setUvi] = React.useState(true)
const [rwm, setRwm] = React.useState(true)
const [buildings, setBuildings] = React.useState<Building[]>(INITIAL_BUILDINGS)
const [install, setInstall] = React.useState(true)
const [searchParams] = useSearchParams()
const preset = React.useMemo(() => readPreset(searchParams), [searchParams])
const [uvi, setUvi] = React.useState(preset.uvi ?? true)
const [rwm, setRwm] = React.useState(preset.rwm ?? true)
const [buildings, setBuildings] = React.useState<Building[]>(() => [
{ ...INITIAL_BUILDING, ...preset.building },
])
const [install, setInstall] = React.useState(preset.install ?? true)
const [email, setEmail] = React.useState("")
const nextId = React.useRef(INITIAL_BUILDINGS.length + 1)
const nextId = React.useRef(INITIAL_BUILDING.id + 1)
/*
* Following a second preset link while the page stays mounted re-seeds the
* form; the mount-time preset is already applied by the initialisers above.
*/
const appliedPreset = React.useRef(preset)
React.useEffect(() => {
if (appliedPreset.current === preset) return
appliedPreset.current = preset
setUvi(preset.uvi ?? true)
setRwm(preset.rwm ?? true)
setInstall(preset.install ?? true)
setBuildings([{ ...INITIAL_BUILDING, ...preset.building }])
nextId.current = INITIAL_BUILDING.id + 1
}, [preset])
const updateBuilding = (id: number, patch: Partial<Building>) => {
setBuildings((prev) => prev.map((b) => (b.id === id ? { ...b, ...patch } : b)))
+51 -60
View File
@@ -1,19 +1,12 @@
import { Link } from "react-router-dom"
import {
ArrowRight,
Building2,
Calculator,
HardHat,
House,
Network,
} from "lucide-react"
import { ArrowRight, Calculator } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Container, Section, SectionHeading } from "@/components/gebos/section"
import { PageHero } from "@/components/gebos/page-hero"
import { HeroArt } from "@/components/gebos/hero-art"
import { FeatureTile, IconBadge } from "@/components/gebos/icon-tile"
import { FeatureTile } from "@/components/gebos/icon-tile"
import { ProcessSteps, NumberedList } from "@/components/gebos/process-steps"
import { Checklist } from "@/components/gebos/checklist"
import { CtaBanner } from "@/components/gebos/cta-banner"
@@ -27,34 +20,34 @@ import {
const features = [
{
media: <img src={icons.uvi} alt="" className="size-14" loading="lazy" />,
title: "UVI",
sub: "Monatliche Verbrauchsinformation",
title: "Unterjährighe Verbrauchsinformation (UVI)",
// sub: "Monatliche Verbrauchsinformation",
},
{
media: (
<img src={icons.billing} alt="" className="size-14" loading="lazy" />
),
title: "Heizkosten\u00adabrechnung",
sub: "Jährlich korrekt abrechnen",
// sub: "Jährlich korrekt abrechnen",
},
{
media: (
<img src={icons.smokeAlarm} alt="" className="size-14" loading="lazy" />
),
title: "Rauchwarnmelder",
sub: "Sicher überwacht, zentral verwaltet",
// sub: "Sicher überwacht, zentral verwaltet",
},
{
media: <img src={icons.radio} alt="" className="size-14" loading="lazy" />,
title: "Fernauslesbare Messtechnik",
sub: "Automatisch erfasst, sicher übertragen",
// sub: "Automatisch erfasst, sicher übertragen",
},
{
media: (
<img src={icons.portal} alt="" className="size-14" loading="lazy" />
),
title: "Portale & Verwaltung",
sub: "Alle Daten und Geräte im Blick",
// sub: "Alle Daten und Geräte im Blick",
},
]
@@ -153,22 +146,18 @@ const processSteps = [
const lessEffort = [
{
vector: vectors.packageBox,
title: "Vorkonfigurierte Hardware",
text: "Geräte werden passend zum jeweiligen Gebäude und zur vorgesehenen Nutzung vorbereitet.",
},
{
vector: vectors.checkOrb,
title: "Keine aufwendige Einrichtung auf der Baustelle",
text: "Der Installateur kann sich auf die fachgerechte Montage konzentrieren.",
},
{
vector: vectors.checkOrb,
title: "Bestehende Fachbetriebe nutzen",
text: "Kein spezieller GebOS-Partner erforderlich eigene Hausmeister, Heizungsbauer oder bestehende Installationspartner können weiterhin eingesetzt werden.",
},
{
vector: vectors.checkOrb,
title: "Weniger doppelte Dateneingabe",
text: "Informationen aus Bestellung und Konfiguration werden für den späteren Betrieb weiterverwendet.",
},
@@ -203,7 +192,14 @@ const systemTasks = [
const audiences = [
{
icon: <House />,
media: (
<AssetRender
render={renders.houseSmallIso}
alt=""
className="flex size-14 shrink-0 items-center justify-center"
imgClassName="w-14"
/>
),
name: "Hauseigentümer",
headline: "Messdienstleistungen für Ihr Mehrfamilienhaus",
text: "Installieren Sie vorkonfigurierte Technik selbst beziehungsweise mit Ihrem bestehenden Fachbetrieb oder überlassen Sie GebOS die komplette Umsetzung.",
@@ -216,7 +212,14 @@ const audiences = [
to: "/fuer-wen/hauseigentuemer",
},
{
icon: <Building2 />,
media: (
<AssetRender
render={renders.housesSmallIso}
alt=""
className="flex size-14 shrink-0 items-center justify-center"
imgClassName="w-14"
/>
),
name: "Hausverwaltungen",
headline: "Viele Gebäude. Ein einfacher Rollout.",
text: "Erfassen Sie Ihre Objekte einmal, erhalten Sie vorkonfigurierte Hardware und nutzen Sie bestehende Mitarbeiter oder Installationspartner für die Montage.",
@@ -229,7 +232,14 @@ const audiences = [
to: "/fuer-wen/hausverwaltungen",
},
{
icon: <Network />,
media: (
<AssetRender
render={renders.sprinter}
alt=""
className="flex size-14 shrink-0 items-center justify-center"
imgClassName="w-14"
/>
),
name: "Messdienstleister",
headline: "Ihre Messdienstleistung. Unsere Infrastruktur.",
text: "Nutzen Sie GebOS als technische Grundlage für Ihre eigenen Dienstleistungen mit Hardware, Plattform, Gerätemanagement und optionalem White Label.",
@@ -238,7 +248,14 @@ const audiences = [
to: "/fuer-wen/messdienstleister",
},
{
icon: <HardHat />,
media: (
<AssetRender
render={renders.workerTablet}
alt=""
className="flex size-14 shrink-0 items-center justify-center"
imgClassName="w-14"
/>
),
name: "Installateure & Heizungsbauer",
headline: "Bestellen. Montieren. Fertig.",
text: "Setzen Sie GebOS bei Ihren bestehenden Kunden ein, ohne zunächst ein Partnerprogramm durchlaufen zu müssen.",
@@ -294,7 +311,7 @@ export default function HomePage() {
<span className="text-primary">einfach</span> gemacht.
</>
}
lead="Unterjährige Verbrauchsinformation (UVI), Heizkostenabrechnung und Rauchwarnmelder von der vorkonfigurierten Hardware bis zum laufenden Betrieb."
lead="Unterjährige Verbrauchsinformation (UVI), Heizkostenabrechnung und Rauchwarnmelder mit vorkonfigurierter Hardware."
actions={
<>
<Button asChild size="lg">
@@ -313,7 +330,7 @@ export default function HomePage() {
<div className="rounded-2xl border border-border bg-card p-6 shadow-card sm:p-8">
<div className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 lg:grid-cols-5">
{features.map((feature) => (
<FeatureTile key={feature.sub} {...feature} />
<FeatureTile key={feature.title} {...feature} />
))}
</div>
</div>
@@ -351,14 +368,6 @@ export default function HomePage() {
lead="Von der Bestellung bis zum laufenden Betrieb GebOS verbindet Bestellung, Konfiguration und Installation miteinander."
/>
<ProcessSteps steps={processSteps} className="mt-12" />
<p className="mt-10 text-center text-base font-semibold italic text-navy">
Bestellung, Konfiguration und Installation greifen ineinander.
</p>
<div className="mt-6 flex justify-center">
<Button asChild size="lg">
<Link to="/konfigurator">Preis für mein Gebäude berechnen</Link>
</Button>
</div>
</Section>
<Section>
@@ -372,12 +381,6 @@ export default function HomePage() {
{lessEffort.map((item) => (
<Card key={item.title} className="p-6 gap-0">
<CardContent className="flex h-full flex-col items-start gap-3 p-0">
<img
src={item.vector}
alt=""
className="size-12"
loading="lazy"
/>
<h3 className="text-base font-bold text-navy">
{item.title}
</h3>
@@ -460,16 +463,15 @@ export default function HomePage() {
</Section>
<Section>
<SectionHeading
eyebrow="Preis"
title="Was kostet GebOS?"
lead="Mit dem GebOS-Konfigurator erhalten Sie bereits mit wenigen Angaben eine erste Preisindikation."
/>
<div className="mt-10 grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
<div className="grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
<div>
<p className="text-sm leading-relaxed text-muted-foreground">
Dafür werden zunächst nur grundlegende Informationen benötigt,
beispielsweise:
<SectionHeading
eyebrow="Preis"
title="Was kostet GebOS?"
lead="Mit dem GebOS-Konfigurator erhalten Sie schnell einen ersten Preis und können sofort bestellen."
/>
<p className="mt-10 text-sm leading-relaxed text-muted-foreground">
Für einen ersten Preis brauchen wir Grundlegende Informationen:
</p>
<Checklist
className="mt-4"
@@ -485,15 +487,6 @@ export default function HomePage() {
Auf Basis dieser Angaben schätzt GebOS die voraussichtlich
benötigte Geräteanzahl und berechnet einen ungefähren Preis.
</p>
<h3 className="mt-8 text-lg font-extrabold tracking-tight text-navy">
Erst schätzen. Dann genau konfigurieren.
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
Der Schnellkonfigurator arbeitet bewusst mit Annahmen. Für
einen genauen Preis können Sie die Konfiguration anschließend
speichern und Ihre Gebäude detailliert erfassen. Die bereits
eingegebenen Daten werden übernommen.
</p>
<Button asChild size="lg" className="mt-6">
<Link to="/konfigurator">Preis berechnen</Link>
</Button>
@@ -536,9 +529,7 @@ export default function HomePage() {
<Card key={audience.to} className="p-6 gap-0 sm:p-7">
<CardContent className="flex h-full flex-col items-start gap-3 p-0">
<div className="flex items-center gap-3">
<IconBadge variant="soft" shape="squircle" size="lg">
{audience.icon}
</IconBadge>
{audience.media}
<span className="text-xs font-bold tracking-wide text-primary uppercase">
{audience.name}
</span>
@@ -117,9 +117,6 @@ export default function HowItWorksPage() {
<Card className="mt-10 gap-0 p-8 sm:mt-12 sm:p-10">
<ProcessSteps steps={steps} />
</Card>
<p className="mx-auto mt-10 max-w-xl text-center text-sm font-medium text-muted-foreground italic">
Bestellung, Konfiguration und Installation greifen ineinander.
</p>
</Section>
</Container>
</div>
+2 -11
View File
@@ -6,11 +6,7 @@ import { Card } from "@/components/ui/card"
import { CtaBanner } from "@/components/gebos/cta-banner"
import { Container, Section, SectionHeading } from "@/components/gebos/section"
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
import {
AssetRender,
renders,
vectors,
} from "@/components/gebos/site-assets"
import { AssetRender, renders } from "@/components/gebos/site-assets"
const solutions = [
{
@@ -21,11 +17,6 @@ const solutions = [
illustration: (
<div className="relative">
<AssetRender render={renders.gateway} imgClassName="w-28" />
<img
src={vectors.glassCubes}
alt=""
className="absolute -top-3 -right-8 w-14"
/>
</div>
),
},
@@ -34,7 +25,7 @@ const solutions = [
sub: "Verbrauchsdaten korrekt abrechnen",
description:
"Verbrauchsdaten strukturiert erfassen und für die jährliche Abrechnung nutzen.",
illustration: <img src={vectors.consumptionRing} alt="" className="w-28" />,
illustration: <AssetRender render={renders.glassDonutShadow} alt="" className="w-28" />,
},
{
title: "Rauchwarnmelder",