This commit is contained in:
Lars Nolden
2026-08-13 23:12:07 +02:00
parent e9c94f74f8
commit ef90873efc
26 changed files with 260 additions and 134 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

@@ -1,13 +1,6 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import buildingAvif from "@/assets/gebos/building.avif" import { AssetRender, renders } from "@/components/gebos/site-assets"
import buildingWebp from "@/assets/gebos/building.webp"
import consumptionRing from "@/assets/gebos/consumption-ring.svg"
import smokeAlarmAvif from "@/assets/gebos/smoke-alarm.avif"
import smokeAlarmWebp from "@/assets/gebos/smoke-alarm.webp"
import tealBars from "@/assets/gebos/teal-bars.svg"
import uviBadge from "@/assetAs/gebos/uvi-badge.svg"
import { AssetRender, renders } from ""
/** /**
* Homepage hero composition from the GebOS visual asset kit: * Homepage hero composition from the GebOS visual asset kit:
@@ -26,41 +19,15 @@ function HeroArt({ className }: { className?: string }) {
aria-label="GebOS Produktvisualisierung" aria-label="GebOS Produktvisualisierung"
role="img" role="img"
> >
<img
src={tealBars}
alt=""
className="absolute top-[12%] -right-[5%] z-0 w-[46%]"
/>
<AssetRender <AssetRender
render={renders.barChart} render={renders.barChart}
alt="" alt=""
className="absolute top-[52%] left-[10%] w-[16%] scale-200" className="absolute bottom-[0%] right-[10%] w-[16%] scale-200 z-10"
/> />
<picture> <AssetRender
<source srcSet={buildingAvif} type="image/avif" /> render={renders.buildingAlpha}
<img className="absolute top-[2%] left-[2%] z-1 h-[90%] w-[96%]"
src={buildingWebp} imgClassName="h-full w-full object-contain"
alt="Modernes Mehrfamilienhaus"
className="absolute top-[2%] left-[2%] z-1 h-[90%] w-[96%] object-contain mix-blend-multiply"
/>
</picture>
<picture>
<source srcSet={smokeAlarmAvif} type="image/avif" />
<img
src={smokeAlarmWebp}
alt="Rauchwarnmelder"
className="absolute top-[4%] -left-[2%] z-3 w-[22%] rounded-full mix-blend-multiply"
/>
</picture>
<img
src={uviBadge}
alt=""
className="absolute bottom-[7%] left-[2%] z-4 w-[15%]"
/>
<img
src={consumptionRing}
alt=""
className="absolute -bottom-[2%] -right-[2%] z-5 w-[46%]"
/> />
</div> </div>
) )
@@ -34,13 +34,25 @@ import glassBadge2Avif from "@/assets/gebos/glass-badge-2.avif"
import glassBadge2Webp from "@/assets/gebos/glass-badge-2.webp" import glassBadge2Webp from "@/assets/gebos/glass-badge-2.webp"
import glassBadge3Avif from "@/assets/gebos/glass-badge-3.avif" import glassBadge3Avif from "@/assets/gebos/glass-badge-3.avif"
import glassBadge3Webp from "@/assets/gebos/glass-badge-3.webp" import glassBadge3Webp from "@/assets/gebos/glass-badge-3.webp"
import glassBadge4Avif from "@/assets/gebos/glass-badge-4.avif"
import glassBadge4Webp from "@/assets/gebos/glass-badge-4.webp"
import glassBadge5Avif from "@/assets/gebos/glass-badge-5.avif"
import glassBadge5Webp from "@/assets/gebos/glass-badge-5.webp"
import glassBadge6Avif from "@/assets/gebos/glass-badge-6.avif"
import glassBadge6Webp from "@/assets/gebos/glass-badge-6.webp"
import glassCubes from "@/assets/gebos/glass-cubes.svg" import glassCubes from "@/assets/gebos/glass-cubes.svg"
import glassCubesStackAvif from "@/assets/gebos/glass-cubes-stack.avif" import glassCubesStackAvif from "@/assets/gebos/glass-cubes-stack.avif"
import glassCubesStackWebp from "@/assets/gebos/glass-cubes-stack.webp" import glassCubesStackWebp from "@/assets/gebos/glass-cubes-stack.webp"
import glassDonutShadowAvif from "@/assets/gebos/glass-donut-shadow.avif"
import glassDonutShadowWebp from "@/assets/gebos/glass-donut-shadow.webp"
import glassDonutAvif from "@/assets/gebos/glass-donut.avif"
import glassDonutWebp from "@/assets/gebos/glass-donut.webp"
import heatMeterAvif from "@/assets/gebos/heat-meter.avif" import heatMeterAvif from "@/assets/gebos/heat-meter.avif"
import heatMeterWebp from "@/assets/gebos/heat-meter.webp" import heatMeterWebp from "@/assets/gebos/heat-meter.webp"
import houseSmallIsoAvif from "@/assets/gebos/house-small-isometric.avif" import houseSmallIsoAvif from "@/assets/gebos/house-small-isometric.avif"
import houseSmallIsoWebp from "@/assets/gebos/house-small-isometric.webp" import houseSmallIsoWebp from "@/assets/gebos/house-small-isometric.webp"
import housesSmallIsoAvif from "@/assets/gebos/houses-small-isometric.avif"
import housesSmallIsoWebp from "@/assets/gebos/houses-small-isometric.webp"
import iconBilling from "@/assets/gebos/icon-billing.svg" import iconBilling from "@/assets/gebos/icon-billing.svg"
import iconPortal from "@/assets/gebos/icon-portal.svg" import iconPortal from "@/assets/gebos/icon-portal.svg"
import iconRadio from "@/assets/gebos/icon-radio.svg" import iconRadio from "@/assets/gebos/icon-radio.svg"
@@ -61,8 +73,12 @@ import shippingBoxClosedAvif from "@/assets/gebos/shipping-box-closed.avif"
import shippingBoxClosedWebp from "@/assets/gebos/shipping-box-closed.webp" import shippingBoxClosedWebp from "@/assets/gebos/shipping-box-closed.webp"
import shippingBoxMetersAvif from "@/assets/gebos/shipping-box-meters.avif" import shippingBoxMetersAvif from "@/assets/gebos/shipping-box-meters.avif"
import shippingBoxMetersWebp from "@/assets/gebos/shipping-box-meters.webp" import shippingBoxMetersWebp from "@/assets/gebos/shipping-box-meters.webp"
import smokeAlarmAlphaAvif from "@/assets/gebos/smoke-alarm-alpha.avif"
import smokeAlarmAlphaWebp from "@/assets/gebos/smoke-alarm-alpha.webp"
import smokeAlarmAvif from "@/assets/gebos/smoke-alarm.avif" import smokeAlarmAvif from "@/assets/gebos/smoke-alarm.avif"
import smokeAlarmWebp from "@/assets/gebos/smoke-alarm.webp" import smokeAlarmWebp from "@/assets/gebos/smoke-alarm.webp"
import sprinterAvif from "@/assets/gebos/sprinter.avif"
import sprinterWebp from "@/assets/gebos/sprinter.webp"
import tealBars from "@/assets/gebos/teal-bars.svg" import tealBars from "@/assets/gebos/teal-bars.svg"
import technicianPipeAvif from "@/assets/gebos/technician-pipe.avif" import technicianPipeAvif from "@/assets/gebos/technician-pipe.avif"
import technicianPipeWebp from "@/assets/gebos/technician-pipe.webp" import technicianPipeWebp from "@/assets/gebos/technician-pipe.webp"
@@ -165,6 +181,13 @@ const renders = {
alt: "Modernes Mehrfamilienhaus", alt: "Modernes Mehrfamilienhaus",
matte: "alpha", matte: "alpha",
}, },
/** same subject as `smokeAlarm`, but with genuine alpha safe on any surface */
smokeAlarmAlpha: {
avif: smokeAlarmAlphaAvif,
webp: smokeAlarmAlphaWebp,
alt: "Rauchwarnmelder",
matte: "alpha",
},
/** isometric corner view of the same building type, genuine alpha */ /** isometric corner view of the same building type, genuine alpha */
buildingIso: { buildingIso: {
avif: buildingIsoAvif, avif: buildingIsoAvif,
@@ -179,6 +202,13 @@ const renders = {
alt: "Einfamilienhaus, isometrisch", alt: "Einfamilienhaus, isometrisch",
matte: "alpha", matte: "alpha",
}, },
/** cluster of small apartment blocks, isometric, genuine alpha */
housesSmallIso: {
avif: housesSmallIsoAvif,
webp: housesSmallIsoWebp,
alt: "Mehrere Mehrfamilienhäuser, isometrisch",
matte: "alpha",
},
/** full hero scene: building, gateway, consumption bars & ring on a glass platform */ /** full hero scene: building, gateway, consumption bars & ring on a glass platform */
fullHeroSection: { fullHeroSection: {
avif: fullHeroSectionAvif, avif: fullHeroSectionAvif,
@@ -211,6 +241,18 @@ const renders = {
alt: "", alt: "",
matte: "alpha", matte: "alpha",
}, },
glassDonut: {
avif: glassDonutAvif,
webp: glassDonutWebp,
alt: "",
matte: "alpha",
},
glassDonutShadow: {
avif: glassDonutShadowAvif,
webp: glassDonutShadowWebp,
alt: "",
matte: "alpha",
},
/** glossy numbered badge "1" process-step numeral, genuine alpha */ /** glossy numbered badge "1" process-step numeral, genuine alpha */
glassBadge1: { glassBadge1: {
avif: glassBadge1Avif, avif: glassBadge1Avif,
@@ -232,6 +274,27 @@ const renders = {
alt: "", alt: "",
matte: "alpha", matte: "alpha",
}, },
/** glossy numbered badge "4" process-step numeral, genuine alpha */
glassBadge4: {
avif: glassBadge4Avif,
webp: glassBadge4Webp,
alt: "",
matte: "alpha",
},
/** glossy numbered badge "5" process-step numeral, genuine alpha */
glassBadge5: {
avif: glassBadge5Avif,
webp: glassBadge5Webp,
alt: "",
matte: "alpha",
},
/** glossy numbered badge "6" process-step numeral, genuine alpha */
glassBadge6: {
avif: glassBadge6Avif,
webp: glassBadge6Webp,
alt: "",
matte: "alpha",
},
cubesRound: { cubesRound: {
avif: cubesRoundAvif, avif: cubesRoundAvif,
webp: cubesRoundWebp, webp: cubesRoundWebp,
@@ -262,6 +325,12 @@ const renders = {
alt: "Fußbodenheizungsrohre", alt: "Fußbodenheizungsrohre",
matte: "alpha", matte: "alpha",
}, },
sprinter: {
avif: sprinterAvif,
webp: sprinterWebp,
alt: "Servicefahrzeug",
matte: "alpha",
},
workerTablet: { workerTablet: {
avif: workerTabletAvif, avif: workerTabletAvif,
webp: workerTabletWebp, webp: workerTabletWebp,
@@ -31,14 +31,16 @@ type NavItem = {
/** Source of truth for nav order desktop pill, dropdown and mobile sheet all read it. */ /** Source of truth for nav order desktop pill, dropdown and mobile sheet all read it. */
const NAV_ITEMS: NavItem[] = [ const NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Start" },
{ to: "/loesungen", label: "Lösungen" }, { to: "/loesungen", label: "Lösungen" },
{ to: "/so-funktionierts", label: "So funktionierts" }, { to: "/so-funktionierts", label: "So funktionierts" },
{ to: "/fuer-wen", label: "Für wen?", menu: AUDIENCES }, { to: "/fuer-wen", label: "Für wen?", menu: AUDIENCES },
{ to: "/konfigurator", label: "Konfigurator" }, { to: "/konfigurator", label: "Konfigurator" },
{ to: "/unternehmen", label: "Unternehmen" },
{ to: "/kontakt", label: "Kontakt" },
] ]
/** "/" is a prefix of every route, so only the root may match exactly. */
const isExact = (to: string) => to === "/"
/** /**
* Only colour changes between states: the highlight itself is one shared pill * Only colour changes between states: the highlight itself is one shared pill
* that slides, so a weight change here would reflow the row it has to land on. * that slides, so a weight change here would reflow the row it has to land on.
@@ -147,9 +149,9 @@ function AudienceMenu({ item, active }: { item: NavItem; active: boolean }) {
function DesktopNav() { function DesktopNav() {
const { pathname } = useLocation() const { pathname } = useLocation()
/** mirrors NavLink's default (non-`end`) matching, but for the whole row at once */ /** mirrors each NavLink's own matching, but for the whole row at once */
const activeKey = NAV_ITEMS.find((item) => const activeKey = NAV_ITEMS.find((item) =>
matchPath({ path: item.to, end: false }, pathname) matchPath({ path: item.to, end: isExact(item.to) }, pathname)
)?.to )?.to
const { navRef, pill } = useActivePill(activeKey) const { navRef, pill } = useActivePill(activeKey)
@@ -173,6 +175,7 @@ function DesktopNav() {
<NavLink <NavLink
key={item.to} key={item.to}
to={item.to} to={item.to}
end={isExact(item.to)}
data-nav={item.to} data-nav={item.to}
className={navItemClass(item.to === activeKey)} className={navItemClass(item.to === activeKey)}
> >
@@ -6,9 +6,9 @@ import { cn } from "@/lib/utils"
/** /**
* On/off switch. The lit track is the same teal glass as a primary button, at * On/off switch. The lit track is the same teal glass as a primary button, at
* pill scale (`inset-shadow-glass-pill`); off it is a recessed navy pane. The * pill scale (`inset-shadow-glass-pill`); underneath sits a recessed navy pane.
* thumb travels on the site's spring easing and carries the checkbox tick, so * The thumb travels on the site's spring easing and carries the checkbox tick,
* the state reads without relying on colour alone. * so the state reads without relying on colour alone.
*/ */
function Switch({ function Switch({
className, className,
@@ -18,19 +18,28 @@ function Switch({
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"group peer inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-colors outline-none", "group peer relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full p-0.5 outline-none",
"inset-shadow-xs bg-navy/20 transition-colors duration-300 motion-reduce:transition-none",
"data-[state=unchecked]:hover:bg-navy/30 data-[state=checked]:bg-brand-600",
"focus-visible:ring-[3px] focus-visible:ring-ring/40", "focus-visible:ring-[3px] focus-visible:ring-ring/40",
"disabled:cursor-not-allowed disabled:opacity-50", "disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=unchecked]:inset-shadow-xs data-[state=unchecked]:bg-navy/20 data-[state=unchecked]:hover:bg-navy/30",
"data-[state=checked]:glass-teal data-[state=checked]:inset-shadow-glass-pill data-[state=checked]:shadow-glass data-[state=checked]:bg-brand-600 data-[state=checked]:hover:glass-lit",
className className
)} )}
{...props} {...props}
> >
{/*
* The teal glass rides its own layer: gradients cannot interpolate from
* `none`, so switching it on the track itself would snap. Fading the
* layer carries the gradient, rim and cast bloom across together.
*/}
<span
aria-hidden="true"
className="glass-teal inset-shadow-glass-pill shadow-glass pointer-events-none absolute inset-0 rounded-full opacity-0 transition-opacity duration-300 group-hover:glass-lit group-data-[state=checked]:opacity-100 motion-reduce:transition-none"
/>
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className={cn(
"pointer-events-none flex size-6 items-center justify-center rounded-full bg-card ring-1 ring-navy/10", "pointer-events-none relative flex size-6 items-center justify-center rounded-full bg-card ring-1 ring-navy/10",
"shadow-[0_1px_2px_rgb(19_41_60/0.2)]", "shadow-[0_1px_2px_rgb(19_41_60/0.2)]",
"transition-transform duration-300 ease-[cubic-bezier(0.34,1.16,0.42,1)] motion-reduce:transition-none", "transition-transform duration-300 ease-[cubic-bezier(0.34,1.16,0.42,1)] motion-reduce:transition-none",
"data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-5" "data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-5"
@@ -279,7 +279,7 @@ export default function HauseigentuemerPage() {
} }
action={ action={
<Button asChild size="lg"> <Button asChild size="lg">
<Link to="/konfigurator">Preis berechnen</Link> <Link to="/konfigurator?installation=0">Preis berechnen</Link>
</Button> </Button>
} }
/> />
@@ -296,7 +296,7 @@ export default function HauseigentuemerPage() {
visual={<FullServiceArt />} visual={<FullServiceArt />}
action={ action={
<Button asChild size="lg" variant="outline"> <Button asChild size="lg" variant="outline">
<Link to="/kontakt">Komplettlösung anfragen</Link> <Link to="/konfigurator?installation=1">Preis berechnen</Link>
</Button> </Button>
} }
/> />
+101 -8
View File
@@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import { useSearchParams } from "react-router-dom"
import { Ban, Check, CircleCheck, Euro, Plus, X } from "lucide-react" import { Ban, Check, CircleCheck, Euro, Plus, X } from "lucide-react"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -40,9 +41,13 @@ const HEAT_OPTIONS: { value: Heat; label: string }[] = [
{ value: "unbekannt", label: "Weiß ich nicht" }, { value: "unbekannt", label: "Weiß ich nicht" },
] ]
const INITIAL_BUILDINGS: Building[] = [ const INITIAL_BUILDING: Building = {
{ id: 1, adresse: "", heat: "heizkoerper", wohnungen: 12, zimmer: 3 }, id: 1,
] adresse: "",
heat: "heizkoerper",
wohnungen: 12,
zimmer: 3,
}
/** Grundpreise je Wohnung / Monat Selbstmontage, wie in der Klarpreis-Liste. */ /** Grundpreise je Wohnung / Monat Selbstmontage, wie in der Klarpreis-Liste. */
const UVI_MONTHLY = 10.5 const UVI_MONTHLY = 10.5
@@ -81,6 +86,74 @@ function settle(value: number | "", min: number) {
return value === "" || value < min ? min : value 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 */ /** glossy numerals for the three configurator steps */
type Step = 1 | 2 | 3 type Step = 1 | 2 | 3
@@ -254,12 +327,32 @@ function PriceRow({
} }
export default function ConfiguratorPage() { export default function ConfiguratorPage() {
const [uvi, setUvi] = React.useState(true) const [searchParams] = useSearchParams()
const [rwm, setRwm] = React.useState(true) const preset = React.useMemo(() => readPreset(searchParams), [searchParams])
const [buildings, setBuildings] = React.useState<Building[]>(INITIAL_BUILDINGS)
const [install, setInstall] = React.useState(true) 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 [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>) => { const updateBuilding = (id: number, patch: Partial<Building>) => {
setBuildings((prev) => prev.map((b) => (b.id === id ? { ...b, ...patch } : b))) setBuildings((prev) => prev.map((b) => (b.id === id ? { ...b, ...patch } : b)))
+48 -57
View File
@@ -1,19 +1,12 @@
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { import { ArrowRight, Calculator } from "lucide-react"
ArrowRight,
Building2,
Calculator,
HardHat,
House,
Network,
} from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Container, Section, SectionHeading } from "@/components/gebos/section" import { Container, Section, SectionHeading } from "@/components/gebos/section"
import { PageHero } from "@/components/gebos/page-hero" import { PageHero } from "@/components/gebos/page-hero"
import { HeroArt } from "@/components/gebos/hero-art" 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 { ProcessSteps, NumberedList } from "@/components/gebos/process-steps"
import { Checklist } from "@/components/gebos/checklist" import { Checklist } from "@/components/gebos/checklist"
import { CtaBanner } from "@/components/gebos/cta-banner" import { CtaBanner } from "@/components/gebos/cta-banner"
@@ -27,34 +20,34 @@ import {
const features = [ const features = [
{ {
media: <img src={icons.uvi} alt="" className="size-14" loading="lazy" />, media: <img src={icons.uvi} alt="" className="size-14" loading="lazy" />,
title: "UVI", title: "Unterjährighe Verbrauchsinformation (UVI)",
sub: "Monatliche Verbrauchsinformation", // sub: "Monatliche Verbrauchsinformation",
}, },
{ {
media: ( media: (
<img src={icons.billing} alt="" className="size-14" loading="lazy" /> <img src={icons.billing} alt="" className="size-14" loading="lazy" />
), ),
title: "Heizkosten\u00adabrechnung", title: "Heizkosten\u00adabrechnung",
sub: "Jährlich korrekt abrechnen", // sub: "Jährlich korrekt abrechnen",
}, },
{ {
media: ( media: (
<img src={icons.smokeAlarm} alt="" className="size-14" loading="lazy" /> <img src={icons.smokeAlarm} alt="" className="size-14" loading="lazy" />
), ),
title: "Rauchwarnmelder", title: "Rauchwarnmelder",
sub: "Sicher überwacht, zentral verwaltet", // sub: "Sicher überwacht, zentral verwaltet",
}, },
{ {
media: <img src={icons.radio} alt="" className="size-14" loading="lazy" />, media: <img src={icons.radio} alt="" className="size-14" loading="lazy" />,
title: "Fernauslesbare Messtechnik", title: "Fernauslesbare Messtechnik",
sub: "Automatisch erfasst, sicher übertragen", // sub: "Automatisch erfasst, sicher übertragen",
}, },
{ {
media: ( media: (
<img src={icons.portal} alt="" className="size-14" loading="lazy" /> <img src={icons.portal} alt="" className="size-14" loading="lazy" />
), ),
title: "Portale & Verwaltung", 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 = [ const lessEffort = [
{ {
vector: vectors.packageBox,
title: "Vorkonfigurierte Hardware", title: "Vorkonfigurierte Hardware",
text: "Geräte werden passend zum jeweiligen Gebäude und zur vorgesehenen Nutzung vorbereitet.", text: "Geräte werden passend zum jeweiligen Gebäude und zur vorgesehenen Nutzung vorbereitet.",
}, },
{ {
vector: vectors.checkOrb,
title: "Keine aufwendige Einrichtung auf der Baustelle", title: "Keine aufwendige Einrichtung auf der Baustelle",
text: "Der Installateur kann sich auf die fachgerechte Montage konzentrieren.", text: "Der Installateur kann sich auf die fachgerechte Montage konzentrieren.",
}, },
{ {
vector: vectors.checkOrb,
title: "Bestehende Fachbetriebe nutzen", title: "Bestehende Fachbetriebe nutzen",
text: "Kein spezieller GebOS-Partner erforderlich eigene Hausmeister, Heizungsbauer oder bestehende Installationspartner können weiterhin eingesetzt werden.", text: "Kein spezieller GebOS-Partner erforderlich eigene Hausmeister, Heizungsbauer oder bestehende Installationspartner können weiterhin eingesetzt werden.",
}, },
{ {
vector: vectors.checkOrb,
title: "Weniger doppelte Dateneingabe", title: "Weniger doppelte Dateneingabe",
text: "Informationen aus Bestellung und Konfiguration werden für den späteren Betrieb weiterverwendet.", text: "Informationen aus Bestellung und Konfiguration werden für den späteren Betrieb weiterverwendet.",
}, },
@@ -203,7 +192,14 @@ const systemTasks = [
const audiences = [ 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", name: "Hauseigentümer",
headline: "Messdienstleistungen für Ihr Mehrfamilienhaus", headline: "Messdienstleistungen für Ihr Mehrfamilienhaus",
text: "Installieren Sie vorkonfigurierte Technik selbst beziehungsweise mit Ihrem bestehenden Fachbetrieb oder überlassen Sie GebOS die komplette Umsetzung.", 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", 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", name: "Hausverwaltungen",
headline: "Viele Gebäude. Ein einfacher Rollout.", 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.", 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", 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", name: "Messdienstleister",
headline: "Ihre Messdienstleistung. Unsere Infrastruktur.", 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.", 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", 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", name: "Installateure & Heizungsbauer",
headline: "Bestellen. Montieren. Fertig.", headline: "Bestellen. Montieren. Fertig.",
text: "Setzen Sie GebOS bei Ihren bestehenden Kunden ein, ohne zunächst ein Partnerprogramm durchlaufen zu müssen.", 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. <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={ actions={
<> <>
<Button asChild size="lg"> <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="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"> <div className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 lg:grid-cols-5">
{features.map((feature) => ( {features.map((feature) => (
<FeatureTile key={feature.sub} {...feature} /> <FeatureTile key={feature.title} {...feature} />
))} ))}
</div> </div>
</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." lead="Von der Bestellung bis zum laufenden Betrieb GebOS verbindet Bestellung, Konfiguration und Installation miteinander."
/> />
<ProcessSteps steps={processSteps} className="mt-12" /> <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>
<Section> <Section>
@@ -372,12 +381,6 @@ export default function HomePage() {
{lessEffort.map((item) => ( {lessEffort.map((item) => (
<Card key={item.title} className="p-6 gap-0"> <Card key={item.title} className="p-6 gap-0">
<CardContent className="flex h-full flex-col items-start gap-3 p-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"> <h3 className="text-base font-bold text-navy">
{item.title} {item.title}
</h3> </h3>
@@ -460,16 +463,15 @@ export default function HomePage() {
</Section> </Section>
<Section> <Section>
<div className="grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
<div>
<SectionHeading <SectionHeading
eyebrow="Preis" eyebrow="Preis"
title="Was kostet GebOS?" title="Was kostet GebOS?"
lead="Mit dem GebOS-Konfigurator erhalten Sie bereits mit wenigen Angaben eine erste Preisindikation." lead="Mit dem GebOS-Konfigurator erhalten Sie schnell einen ersten Preis und können sofort bestellen."
/> />
<div className="mt-10 grid items-start gap-10 lg:grid-cols-2 lg:gap-14"> <p className="mt-10 text-sm leading-relaxed text-muted-foreground">
<div> Für einen ersten Preis brauchen wir Grundlegende Informationen:
<p className="text-sm leading-relaxed text-muted-foreground">
Dafür werden zunächst nur grundlegende Informationen benötigt,
beispielsweise:
</p> </p>
<Checklist <Checklist
className="mt-4" className="mt-4"
@@ -485,15 +487,6 @@ export default function HomePage() {
Auf Basis dieser Angaben schätzt GebOS die voraussichtlich Auf Basis dieser Angaben schätzt GebOS die voraussichtlich
benötigte Geräteanzahl und berechnet einen ungefähren Preis. benötigte Geräteanzahl und berechnet einen ungefähren Preis.
</p> </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"> <Button asChild size="lg" className="mt-6">
<Link to="/konfigurator">Preis berechnen</Link> <Link to="/konfigurator">Preis berechnen</Link>
</Button> </Button>
@@ -536,9 +529,7 @@ export default function HomePage() {
<Card key={audience.to} className="p-6 gap-0 sm:p-7"> <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"> <CardContent className="flex h-full flex-col items-start gap-3 p-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<IconBadge variant="soft" shape="squircle" size="lg"> {audience.media}
{audience.icon}
</IconBadge>
<span className="text-xs font-bold tracking-wide text-primary uppercase"> <span className="text-xs font-bold tracking-wide text-primary uppercase">
{audience.name} {audience.name}
</span> </span>
@@ -117,9 +117,6 @@ export default function HowItWorksPage() {
<Card className="mt-10 gap-0 p-8 sm:mt-12 sm:p-10"> <Card className="mt-10 gap-0 p-8 sm:mt-12 sm:p-10">
<ProcessSteps steps={steps} /> <ProcessSteps steps={steps} />
</Card> </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> </Section>
</Container> </Container>
</div> </div>
+2 -11
View File
@@ -6,11 +6,7 @@ import { Card } from "@/components/ui/card"
import { CtaBanner } from "@/components/gebos/cta-banner" import { CtaBanner } from "@/components/gebos/cta-banner"
import { Container, Section, SectionHeading } from "@/components/gebos/section" import { Container, Section, SectionHeading } from "@/components/gebos/section"
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb" import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
import { import { AssetRender, renders } from "@/components/gebos/site-assets"
AssetRender,
renders,
vectors,
} from "@/components/gebos/site-assets"
const solutions = [ const solutions = [
{ {
@@ -21,11 +17,6 @@ const solutions = [
illustration: ( illustration: (
<div className="relative"> <div className="relative">
<AssetRender render={renders.gateway} imgClassName="w-28" /> <AssetRender render={renders.gateway} imgClassName="w-28" />
<img
src={vectors.glassCubes}
alt=""
className="absolute -top-3 -right-8 w-14"
/>
</div> </div>
), ),
}, },
@@ -34,7 +25,7 @@ const solutions = [
sub: "Verbrauchsdaten korrekt abrechnen", sub: "Verbrauchsdaten korrekt abrechnen",
description: description:
"Verbrauchsdaten strukturiert erfassen und für die jährliche Abrechnung nutzen.", "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", title: "Rauchwarnmelder",
+6
View File
@@ -0,0 +1,6 @@
It is important that we highlight the self serve model.
Meaning a customer can enter and register hist building and then righ away order.
For example on the configuration page it would be cool to have an indicator saying like "Von Gebäude-erfassung zur fertigen Bestellung in nur 15 Minuten".
Also this self serve model should be explained more prominently on the home page and haseigentuemer page.
Also to capture the european market as early as possible, we should build in translation for all european languages and english.