save
@@ -13,7 +13,7 @@ import HomePage from "@/pages/home"
|
||||
import HowItWorksPage from "@/pages/how-it-works"
|
||||
import KlarpreisPage from "@/pages/klarpreis"
|
||||
import { SimplePage } from "@/pages/placeholder"
|
||||
import SolutionsPage from "@/pages/solutions"
|
||||
import ProductsPage from "@/pages/products"
|
||||
|
||||
function ScrollToTop() {
|
||||
const { pathname } = useLocation()
|
||||
@@ -35,7 +35,7 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/so-funktionierts" element={<HowItWorksPage />} />
|
||||
<Route path="/loesungen" element={<SolutionsPage />} />
|
||||
<Route path="/produkte" element={<ProductsPage />} />
|
||||
<Route path="/fuer-wen" element={<ForWhomPage />} />
|
||||
<Route
|
||||
path="/fuer-wen/hauseigentuemer"
|
||||
@@ -55,6 +55,32 @@ export default function App() {
|
||||
/>
|
||||
<Route path="/konfigurator" element={<ConfiguratorPage />} />
|
||||
<Route path="/klarpreis" element={<KlarpreisPage />} />
|
||||
<Route
|
||||
path="/wissen"
|
||||
element={
|
||||
<SimplePage
|
||||
breadcrumb={[
|
||||
{ label: "Startseite", to: "/" },
|
||||
{ label: "Wissen" },
|
||||
]}
|
||||
title="Wissen"
|
||||
lead="Hintergründe zu Messtechnik, Abrechnung und gesetzlichen Vorgaben rund um Wohngebäude."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/technik-sicherheit"
|
||||
element={
|
||||
<SimplePage
|
||||
breadcrumb={[
|
||||
{ label: "Startseite", to: "/" },
|
||||
{ label: "Technik & Sicherheit" },
|
||||
]}
|
||||
title="Technik & Sicherheit"
|
||||
lead="Messtechnik, Datenübertragung, Hosting und Zugriffsrechte – wie GebOS den laufenden Messbetrieb technisch und organisatorisch absichert."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/unternehmen"
|
||||
element={
|
||||
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 131 KiB |
@@ -0,0 +1,84 @@
|
||||
import * as React from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
|
||||
type FaqItem = {
|
||||
question: React.ReactNode
|
||||
answer: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle disc: hairline teal plus that fills in and turns into an × once its
|
||||
* item opens. Driven by the trigger's own `group`, so hovering the answer body
|
||||
* below leaves the disc alone.
|
||||
*/
|
||||
function FaqIndicator() {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-brand-100 bg-card text-primary shadow-pill transition-[transform,color,background-color,border-color] duration-200 group-hover:border-brand-200 group-hover:bg-accent group-data-[state=open]:rotate-45 group-data-[state=open]:border-transparent group-data-[state=open]:bg-primary group-data-[state=open]:text-primary-foreground"
|
||||
>
|
||||
<Plus className="size-4" strokeWidth={2.5} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ list: one white card per question, plus disc as the toggle, and the open
|
||||
* card tinted brand-50 behind a mint hairline so the answer reads as its own
|
||||
* panel. Single-open and collapsible — the block only ever moves one card's
|
||||
* worth of height, and every answer can be closed again.
|
||||
*
|
||||
* Sized for a reading column (centred `max-w-3xl`) instead of the full 6xl
|
||||
* container; override the width via `className`.
|
||||
*/
|
||||
function FaqAccordion({
|
||||
items,
|
||||
defaultOpen = 0,
|
||||
className,
|
||||
}: {
|
||||
items: FaqItem[]
|
||||
/** index of the item open on first paint; `null` starts fully collapsed */
|
||||
defaultOpen?: number | null
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={defaultOpen == null ? undefined : `faq-${defaultOpen}`}
|
||||
className={cn("mx-auto flex w-full max-w-3xl flex-col gap-3", className)}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`faq-${i}`}
|
||||
/* the all-round `border` supersedes the primitive's divider width,
|
||||
which leaves `last:border-b-0` to strip the final card's bottom
|
||||
edge — `last:border-b` puts it back. */
|
||||
className="rounded-2xl border border-border/70 bg-card px-5 shadow-card transition-colors duration-200 last:border-b hover:border-brand-200 data-[state=open]:border-brand-200 data-[state=open]:bg-brand-50/50 sm:px-6"
|
||||
>
|
||||
<AccordionTrigger
|
||||
className="group items-center gap-5 rounded-xl py-5 text-base leading-snug font-bold text-navy"
|
||||
indicator={<FaqIndicator />}
|
||||
>
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-6 leading-relaxed text-pretty sm:pr-13">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
export { FaqAccordion }
|
||||
export type { FaqItem }
|
||||
@@ -20,7 +20,7 @@ function HeroArt({ className }: { className?: string }) {
|
||||
role="img"
|
||||
>
|
||||
<AssetRender
|
||||
render={renders.barChart}
|
||||
render={renders.cubesRound}
|
||||
alt=""
|
||||
className="absolute bottom-[0%] right-[10%] w-[16%] scale-200 z-10"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type InfoStripItem = {
|
||||
/** self-contained visual (asset-kit render / icon) */
|
||||
media: React.ReactNode
|
||||
eyebrow: React.ReactNode
|
||||
title: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Wide white strip directly under a hero: hairline-divided columns (2 up on
|
||||
* mobile, 3 from `sm`, 5 from `lg`), each a glass render over a teal eyebrow
|
||||
* and a navy title. Dividers are drawn per column for the current layout so
|
||||
* they stay inset from the card's padding.
|
||||
*/
|
||||
function InfoStrip({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: InfoStripItem[]
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-2 rounded-3xl border border-border bg-card p-2 shadow-card sm:grid-cols-3 sm:p-3 lg:grid-cols-5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"flex flex-col items-center px-3 py-4 text-center sm:px-4",
|
||||
/* dividers for the 2-up layout … */
|
||||
i % 2 !== 0 ? "border-l border-border" : "border-l-0",
|
||||
i >= 2 ? "border-t border-border" : "border-t-0",
|
||||
/* … the 3-up layout … */
|
||||
i % 3 !== 0 ? "sm:border-l" : "sm:border-l-0",
|
||||
i >= 3 ? "sm:border-t" : "sm:border-t-0",
|
||||
/* … and the 5-up layout */
|
||||
i % 5 !== 0 ? "lg:border-l" : "lg:border-l-0",
|
||||
"lg:border-t-0"
|
||||
)}
|
||||
>
|
||||
<span className="flex h-14 items-center justify-center">
|
||||
{item.media}
|
||||
</span>
|
||||
<p className="mt-3 text-[0.625rem] font-bold tracking-[0.14em] text-primary uppercase">
|
||||
{item.eyebrow}
|
||||
</p>
|
||||
<div className="mt-1.5 text-xs leading-snug font-semibold text-balance text-navy/75 sm:text-[0.8125rem]">
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { InfoStrip }
|
||||
export type { InfoStripItem }
|
||||
@@ -115,10 +115,24 @@ function PortalGaugeIcon(props: React.ComponentProps<"svg">) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Germany's silhouette: the metering data never leaves the country. Traced
|
||||
* from the border's waypoints, projected equirectangular at 51° N so the
|
||||
* proportions hold, then thinned to the points that survive at icon size.
|
||||
*/
|
||||
function GermanyIcon(props: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<LineIcon {...props}>
|
||||
<path d="M8.62 1.7 10.34 2.23 11.5 3.6 13.21 3.33 12.74 4.59 13.73 4.72 17.03 2.93 18.42 4.67 18.24 7.23 19.67 12.07 19.38 12.84 14.98 14.47 17.06 18.96 16.04 20.06 16.32 21.55 14.98 21.34 11.75 22.3 9.76 21.39 7.22 21.61 7.24 19.68 8 17.86 5.98 17.41 4.77 15.06 4.33 12.33 4.72 10.2 5.95 9.26 5.73 8.33 6.32 8.06 6.57 6.46 6.23 6.19 9.09 4.77 9.34 3.12Z" />
|
||||
</LineIcon>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
CloudAutomationIcon,
|
||||
CrossedToolsIcon,
|
||||
EuroCircleIcon,
|
||||
GermanyIcon,
|
||||
LineIcon,
|
||||
PortalGaugeIcon,
|
||||
PreconfiguredIcon,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { AssetRender, renders } from "@/components/gebos/site-assets"
|
||||
|
||||
/**
|
||||
* Filled warning triangle. lucide only ships the stroked outline, and at badge
|
||||
* size the fill is what carries the signal. The mark is knocked out of the fill
|
||||
* (`evenodd`, one path) so the plate underneath shows through instead of being
|
||||
* painted over — the glyph stays correct on any backing colour.
|
||||
*/
|
||||
function WarningGlyph({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
className={cn("shrink-0", className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M10.26 4.02a2 2 0 0 1 3.48 0l8.15 14.26a2 2 0 0 1-1.74 3.02H3.85a2 2 0 0 1-1.74-3.02L10.26 4.02Z
|
||||
M12 8.6a1.02 1.02 0 0 0-1.02 1.02v4.16a1.02 1.02 0 0 0 2.04 0V9.62A1.02 1.02 0 0 0 12 8.6Z
|
||||
M12 16.05a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Glass disc holding a white plate with the notice glyph: the same optics as
|
||||
* the glass buttons, sized off the disc so it scales as one object.
|
||||
*/
|
||||
function NoticeSigil({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"glass-veil inset-shadow-glass shadow-glass flex size-24 shrink-0 items-center justify-center rounded-full bg-white/25 backdrop-blur-sm sm:size-32 lg:size-36",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="bg-card shadow-card flex size-[65%] items-center justify-center rounded-full">
|
||||
<span className="text-brand-600 flex size-[54%] items-center justify-center">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notice card for a regulatory deadline: glass sigil left, headline and body
|
||||
* centre, cited source underneath, brand art and the follow-up link right.
|
||||
* The teal wash bleeds in behind the sigil and dissolves before the copy.
|
||||
*/
|
||||
function NoticeBanner({
|
||||
title,
|
||||
children,
|
||||
source,
|
||||
action,
|
||||
visual,
|
||||
icon,
|
||||
className,
|
||||
}: {
|
||||
title: React.ReactNode
|
||||
/** the notice body */
|
||||
children: React.ReactNode
|
||||
source?: {
|
||||
/** what is being cited, e.g. "§ 5 HeizkostenV" */
|
||||
cite: React.ReactNode
|
||||
href: string
|
||||
/** link text; defaults to `href` without its scheme */
|
||||
label?: React.ReactNode
|
||||
}
|
||||
/** follow-up affordance, rendered under the visual */
|
||||
action?: React.ReactNode
|
||||
visual?: React.ReactNode
|
||||
/** overrides the warning glyph in the sigil */
|
||||
icon?: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"border-border bg-card shadow-card relative isolate overflow-hidden rounded-3xl border",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="bg-notice-wash pointer-events-none absolute inset-y-0 left-0 -z-10 w-2/5 opacity-70 sm:opacity-100"
|
||||
/>
|
||||
<div className="flex flex-col gap-6 p-6 sm:flex-row sm:flex-wrap sm:items-center sm:gap-8 sm:p-8 lg:flex-nowrap lg:gap-10 lg:p-10">
|
||||
<NoticeSigil>
|
||||
{icon ?? <WarningGlyph className="size-full" />}
|
||||
</NoticeSigil>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-navy text-2xl font-extrabold tracking-tight text-balance sm:text-3xl">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-navy/85 mt-3 max-w-2xl text-base leading-relaxed text-pretty sm:text-lg">
|
||||
{children}
|
||||
</p>
|
||||
{source ? (
|
||||
<p className="text-muted-foreground mt-5 text-xs sm:text-[0.8125rem]">
|
||||
Quelle: {source.cite} –{" "}
|
||||
<a
|
||||
href={source.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-700 hover:text-brand-800 decoration-brand-300 hover:decoration-brand-600 font-medium break-words underline-offset-2 transition-colors hover:underline"
|
||||
>
|
||||
{source.label ?? source.href.replace(/^https?:\/\/(www\.)?/, "")}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{visual || action ? (
|
||||
<div className="flex w-full shrink-0 flex-col items-start gap-4 sm:items-end lg:w-[30%]">
|
||||
{visual}
|
||||
{action}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The notice's brand art: glass bars with the consumption donut settling in
|
||||
* front of them. Decorative — the copy next to it carries the meaning.
|
||||
*/
|
||||
function NoticeArt({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn("relative aspect-[1.2] w-full", className)}>
|
||||
<AssetRender
|
||||
render={renders.barChart}
|
||||
alt=""
|
||||
className="absolute top-0 left-[4%] w-[64%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassDonutShadow}
|
||||
alt=""
|
||||
className="absolute right-0 bottom-0 w-[62%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { NoticeArt, NoticeBanner }
|
||||
@@ -4,7 +4,7 @@ import { cn } from "@/lib/utils"
|
||||
import { Container } from "@/components/gebos/section"
|
||||
|
||||
/**
|
||||
* Split hero: copy left (breadcrumb/eyebrow, H1, lead, CTAs, extra),
|
||||
* Split hero: copy left (breadcrumb/eyebrow, H1, lead, CTAs, note, extra),
|
||||
* visual right, soft radial glow behind.
|
||||
*/
|
||||
function PageHero({
|
||||
@@ -12,6 +12,7 @@ function PageHero({
|
||||
title,
|
||||
lead,
|
||||
actions,
|
||||
note,
|
||||
extra,
|
||||
visual,
|
||||
className,
|
||||
@@ -20,6 +21,8 @@ function PageHero({
|
||||
title: React.ReactNode
|
||||
lead?: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
/** microcopy tied to the actions (e.g. "Wenige Angaben · …") */
|
||||
note?: React.ReactNode
|
||||
/** slot below the actions (e.g. hint pill) */
|
||||
extra?: React.ReactNode
|
||||
visual?: React.ReactNode
|
||||
@@ -51,6 +54,9 @@ function PageHero({
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
{note ? (
|
||||
<p className="mt-3 text-xs text-muted-foreground">{note}</p>
|
||||
) : null}
|
||||
{extra ? <div className="mt-6">{extra}</div> : null}
|
||||
</div>
|
||||
{visual ? <div className="relative w-full">{visual}</div> : null}
|
||||
|
||||
@@ -51,10 +51,27 @@ 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 glassGatewayAvif from "@/assets/gebos/glass-gateway.avif"
|
||||
import glassGatewayWebp from "@/assets/gebos/glass-gateway.webp"
|
||||
import glassLaptopAvif from "@/assets/gebos/glass-laptop.avif"
|
||||
import glassLaptopWebp from "@/assets/gebos/glass-laptop.webp"
|
||||
import glassLockAvif from "@/assets/gebos/glass-lock.avif"
|
||||
import glassLockWebp from "@/assets/gebos/glass-lock.webp"
|
||||
import glassMoneyAvif from "@/assets/gebos/glass-money.avif"
|
||||
import glassMoneyWebp from "@/assets/gebos/glass-money.webp"
|
||||
import glassShieldLockAvif from "@/assets/gebos/glass-shield-lock.avif"
|
||||
import glassShieldLockWebp from "@/assets/gebos/glass-shield-lock.webp"
|
||||
import glassSmokeDetectorAvif from "@/assets/gebos/glass-smoke-detector.avif"
|
||||
import glassSmokeDetectorWebp from "@/assets/gebos/glass-smoke-detector.webp"
|
||||
import glassWaterMeterAvif from "@/assets/gebos/glass-water-meter.avif"
|
||||
import glassWaterMeterWebp from "@/assets/gebos/glass-water-meter.webp"
|
||||
import heatMeterAvif from "@/assets/gebos/heat-meter.avif"
|
||||
import heatMeterWebp from "@/assets/gebos/heat-meter.webp"
|
||||
import houseSmallIsoAvif from "@/assets/gebos/house-small-isometric.avif"
|
||||
import houseSmallIsoWebp from "@/assets/gebos/house-small-isometric.webp"
|
||||
import houseWithSprinterMp4 from "@/assets/gebos/house-with-sprinter.mp4"
|
||||
import houseWithSprinterPosterWebp from "@/assets/gebos/house-with-sprinter-poster.webp"
|
||||
import houseWithSprinterWebm from "@/assets/gebos/house-with-sprinter.webm"
|
||||
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"
|
||||
@@ -87,6 +104,8 @@ import tealBars from "@/assets/gebos/teal-bars.svg"
|
||||
import technicianPipeAvif from "@/assets/gebos/technician-pipe.avif"
|
||||
import technicianPipeWebp from "@/assets/gebos/technician-pipe.webp"
|
||||
import uviBadge from "@/assets/gebos/uvi-badge.svg"
|
||||
import uviGlassPaneAvif from "@/assets/gebos/uvi-glass-pane.avif"
|
||||
import uviGlassPaneWebp from "@/assets/gebos/uvi-glass-pane.webp"
|
||||
import wirelessOrb from "@/assets/gebos/wireless-orb.svg"
|
||||
import wordmarkDarkAvif from "@/assets/gebos/wordmark-cubes-dark.avif"
|
||||
import wordmarkDarkWebp from "@/assets/gebos/wordmark-cubes-dark.webp"
|
||||
@@ -257,6 +276,54 @@ const renders = {
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
uviGlassPane: {
|
||||
avif: uviGlassPaneAvif,
|
||||
webp: uviGlassPaneWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassMoney: {
|
||||
avif: glassMoneyAvif,
|
||||
webp: glassMoneyWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassLaptop: {
|
||||
avif: glassLaptopAvif,
|
||||
webp: glassLaptopWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassGateway: {
|
||||
avif: glassGatewayAvif,
|
||||
webp: glassGatewayWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassWaterMeter: {
|
||||
avif: glassWaterMeterAvif,
|
||||
webp: glassWaterMeterWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassSmokeDetector: {
|
||||
avif: glassSmokeDetectorAvif,
|
||||
webp: glassSmokeDetectorWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassLock: {
|
||||
avif: glassLockAvif,
|
||||
webp: glassLockWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassShieldLock: {
|
||||
avif: glassShieldLockAvif,
|
||||
webp: glassShieldLockWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "1" – process-step numeral, genuine alpha */
|
||||
glassBadge1: {
|
||||
avif: glassBadge1Avif,
|
||||
@@ -499,4 +566,66 @@ function GlassNumber({
|
||||
)
|
||||
}
|
||||
|
||||
export { AssetRender, GlassNumber, icons, renders, vectors }
|
||||
export interface VideoAsset {
|
||||
mp4: string
|
||||
webm: string
|
||||
/** static frame shown before playback starts / while loading */
|
||||
poster: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
/** Looping background/product clips (opaque compositions, not matte assets). */
|
||||
const videos = {
|
||||
houseWithSprinter: {
|
||||
mp4: houseWithSprinterMp4,
|
||||
webm: houseWithSprinterWebm,
|
||||
poster: houseWithSprinterPosterWebp,
|
||||
alt: "Servicefahrzeug fährt zum Gebäude vor",
|
||||
},
|
||||
} satisfies Record<string, VideoAsset>
|
||||
|
||||
/**
|
||||
* Renders a kit video as WebM→MP4 <video>, muted/looping/autoplaying by
|
||||
* default for ambient hero use. Falls back to the poster frame if the
|
||||
* browser can't play either source.
|
||||
*/
|
||||
function VideoRender({
|
||||
video,
|
||||
className,
|
||||
autoPlay = true,
|
||||
loop = true,
|
||||
muted = true,
|
||||
playsInline = true,
|
||||
}: {
|
||||
video: VideoAsset
|
||||
className?: string
|
||||
autoPlay?: boolean
|
||||
loop?: boolean
|
||||
muted?: boolean
|
||||
playsInline?: boolean
|
||||
}) {
|
||||
return (
|
||||
<video
|
||||
className={cn("object-contain", className)}
|
||||
poster={video.poster}
|
||||
autoPlay={autoPlay}
|
||||
loop={loop}
|
||||
muted={muted}
|
||||
playsInline={playsInline}
|
||||
aria-label={video.alt}
|
||||
>
|
||||
<source src={video.webm} type="video/webm" />
|
||||
<source src={video.mp4} type="video/mp4" />
|
||||
</video>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AssetRender,
|
||||
GlassNumber,
|
||||
icons,
|
||||
renders,
|
||||
VideoRender,
|
||||
vectors,
|
||||
videos,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const COLUMNS: { title: string; links: { to: string; label: string }[] }[] = [
|
||||
{
|
||||
title: "Produkt",
|
||||
links: [
|
||||
{ to: "/loesungen", label: "Lösungen" },
|
||||
{ to: "/produkte", label: "Produkte" },
|
||||
{ to: "/so-funktionierts", label: "So funktioniert’s" },
|
||||
{ to: "/konfigurator", label: "Konfigurator" },
|
||||
{ to: "/klarpreis", label: "GebOS Klarpreis" },
|
||||
|
||||
@@ -32,9 +32,10 @@ type NavItem = {
|
||||
/** Source of truth for nav order – desktop pill, dropdown and mobile sheet all read it. */
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ to: "/", label: "Start" },
|
||||
{ to: "/loesungen", label: "Lösungen" },
|
||||
{ to: "/produkte", label: "Produkte" },
|
||||
{ to: "/so-funktionierts", label: "So funktioniert’s" },
|
||||
{ to: "/fuer-wen", label: "Für wen?", menu: AUDIENCES },
|
||||
{ to: "/wissen", label: "Wissen" },
|
||||
{ to: "/konfigurator", label: "Konfigurator" },
|
||||
]
|
||||
|
||||
@@ -222,6 +223,9 @@ function MobileMenu() {
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/klarpreis">Klarpreis</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="https://app.gebos.online">Login</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
@@ -242,8 +246,11 @@ function SiteHeader() {
|
||||
<DesktopNav />
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button asChild size="sm" className="hidden sm:inline-flex">
|
||||
{/* <Button asChild size="sm" className="hidden sm:inline-flex">
|
||||
<Link to="/konfigurator">Preis berechnen</Link>
|
||||
</Button> */}
|
||||
<Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex">
|
||||
<a href="https://app.gebos.online">Login</a>
|
||||
</Button>
|
||||
<MobileMenu />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { AssetRender, renders } from "@/components/gebos/site-assets"
|
||||
|
||||
type TrustPanelItem = {
|
||||
icon: React.ReactNode
|
||||
label: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust panel: a pale plate carrying the headline and the technical properties
|
||||
* as white pills (teal line icon beside a short label). Three pills across from
|
||||
* `lg`, so five items settle into 3 + 2 as in the design, with the security
|
||||
* render alongside and the follow-up link on the baseline underneath.
|
||||
*/
|
||||
function TrustPanel({
|
||||
title,
|
||||
items,
|
||||
visual,
|
||||
action,
|
||||
className,
|
||||
}: {
|
||||
title: React.ReactNode
|
||||
items: TrustPanelItem[]
|
||||
visual?: React.ReactNode
|
||||
/** follow-up affordance, right-aligned under the pills */
|
||||
action?: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border shadow-card rounded-3xl border bg-gradient-to-br from-white via-white to-secondary p-6 sm:p-8 lg:p-10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-8 lg:flex-row lg:items-center lg:gap-10">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-navy text-2xl font-extrabold tracking-tight text-balance sm:text-[1.75rem]">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-card shadow-card flex items-center gap-4 rounded-2xl px-5 py-4"
|
||||
>
|
||||
<span className="text-brand-600 flex shrink-0 [&_svg]:size-8">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="text-navy/80 text-[0.9375rem] leading-snug font-semibold text-balance">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{visual ? <div className="shrink-0 self-center">{visual}</div> : null}
|
||||
</div>
|
||||
{action ? <div className="mt-8 flex justify-end">{action}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel's security render: the glass shield with the lock inside it.
|
||||
* Decorative — the pills next to it carry the meaning.
|
||||
*/
|
||||
function TrustShieldArt({ className }: { className?: string }) {
|
||||
return (
|
||||
<AssetRender
|
||||
render={renders.glassShieldLock}
|
||||
alt=""
|
||||
className={cn("mx-auto block w-40 sm:w-44", className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { TrustPanel, TrustShieldArt }
|
||||
export type { TrustPanelItem }
|
||||
@@ -26,8 +26,12 @@ function AccordionItem({
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
indicator,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger> & {
|
||||
/** replaces the default chevron (e.g. the FAQ's teal plus disc) */
|
||||
indicator?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
@@ -39,7 +43,9 @@ function AccordionTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
{indicator ?? (
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
)}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/gebos/comparison-table"
|
||||
import { Section, SectionHeading } from "@/components/gebos/section"
|
||||
import { CtaBanner } from "@/components/gebos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/gebos/faq-accordion"
|
||||
import { StepRail } from "@/components/gebos/process-steps"
|
||||
import {
|
||||
CloudAutomationIcon,
|
||||
@@ -194,6 +195,34 @@ const VARIANT_ROWS: ComparisonRow[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const FAQS: FaqItem[] = [
|
||||
{
|
||||
question: "Kann ich die Geräte wirklich selbst installieren?",
|
||||
answer:
|
||||
"Ja. Die Technik kommt vorkonfiguriert und objektbezogen bei Ihnen an. Montiert wird von Ihnen, Ihrem Handwerker oder – wenn Sie die Komplettlösung wählen – von GebOS. Eine aufwendige Softwarekonfiguration vor Ort ist in keinem Fall erforderlich.",
|
||||
},
|
||||
{
|
||||
question: "Was kostet GebOS für mein Gebäude?",
|
||||
answer:
|
||||
"Der Konfigurator gibt Ihnen mit wenigen Angaben zu Gebäude, Wohnungen und gewünschten Leistungen eine erste Preisindikation. Den genauen Preis erhalten Sie, sobald die tatsächlichen Gebäudedaten ergänzt sind.",
|
||||
},
|
||||
{
|
||||
question: "Wer übernimmt Ablesung, Übertragung und Auswertung?",
|
||||
answer:
|
||||
"Nach der Montage läuft der Betrieb automatisch: GebOS erfasst die Verbrauchsdaten, überträgt sie und bereitet sie für die gebuchten Leistungen wie UVI und Heizkostenabrechnung auf.",
|
||||
},
|
||||
{
|
||||
question: "Was sehe ich im GebOS Portal?",
|
||||
answer:
|
||||
"Ihre Gebäude, Wohnungen und Geräte an einer Stelle – inklusive Status der Geräte und der laufenden Verbrauchswerte. Bewohner erhalten ihren eigenen Zugang zum Mieterportal.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich später weitere Gebäude oder Geräte ergänzen?",
|
||||
answer:
|
||||
"Ja. Weitere Objekte werden nach demselben standardisierten Prozess ergänzt. Bereits erfasste Gebäudedaten werden weiterverwendet und müssen nicht erneut eingegeben werden.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function HauseigentuemerPage() {
|
||||
return (
|
||||
<AudiencePage
|
||||
@@ -334,6 +363,15 @@ export default function HauseigentuemerPage() {
|
||||
rows={VARIANT_ROWS}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Was Hauseigentümer am häufigsten fragen"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={FAQS} />
|
||||
</Section>
|
||||
</AudiencePage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Section, SectionHeading } from "@/components/gebos/section"
|
||||
import { CtaBanner } from "@/components/gebos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/gebos/faq-accordion"
|
||||
import { IconBadge } from "@/components/gebos/icon-tile"
|
||||
import { NumberedList } from "@/components/gebos/process-steps"
|
||||
import {
|
||||
@@ -130,6 +131,34 @@ const whyGebos = [
|
||||
},
|
||||
]
|
||||
|
||||
const faqs: FaqItem[] = [
|
||||
{
|
||||
question: "Wie viel Aufwand entsteht für unser Team?",
|
||||
answer:
|
||||
"Die Gebäude- und Wohnungsdaten werden bei der Auftragserstellung einmal erfasst und anschließend weiterverwendet. Es entfällt die aufwendige Gerätekonfiguration und die manuelle Anlage jedes einzelnen Gerätes.",
|
||||
},
|
||||
{
|
||||
question: "Können unsere bestehenden Monteure installieren?",
|
||||
answer:
|
||||
"Ja. Eigene Hausmeister, technische Mitarbeiter oder bestehende Installationspartner können die Montage übernehmen. Eine spezielle GebOS-Zertifizierung ist dafür nicht erforderlich.",
|
||||
},
|
||||
{
|
||||
question: "Sehen wir, wo der Rollout gerade steht?",
|
||||
answer:
|
||||
"Ja. Vom angelegten Auftrag über vorbereitete und versendete Hardware bis zu aktiven Geräten und eingeladenen Bewohnern lässt sich der Status je Gebäude nachvollziehen – inklusive Hinweis auf Objekte mit Handlungsbedarf.",
|
||||
},
|
||||
{
|
||||
question: "Lassen sich viele Objekte parallel ausrollen?",
|
||||
answer:
|
||||
"Der Prozess ist auf Portfolios ausgelegt: Standardisierte Abläufe und objektbezogen vorbereitete Hardware ermöglichen die parallele Umsetzung vieler Gebäude.",
|
||||
},
|
||||
{
|
||||
question: "Müssen wir mit dem gesamten Bestand starten?",
|
||||
answer:
|
||||
"Nein. Sie können mit einzelnen Objekten beginnen und weitere Gebäude schrittweise nach demselben standardisierten Prozess ergänzen.",
|
||||
},
|
||||
]
|
||||
|
||||
/** Portfolio/rollout hero per asset kit 06: building, teal bars, consumption ring. */
|
||||
function PortfolioHeroArt() {
|
||||
return (
|
||||
@@ -343,6 +372,15 @@ export default function HausverwaltungenPage() {
|
||||
lead="GebOS soll nicht verlangen, dass eine Hausverwaltung zunächst eine eigene Messdienst-Infrastruktur aufbaut. Neue Gebäude können schrittweise ergänzt und nach demselben standardisierten Prozess ausgerollt werden."
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Was Hausverwaltungen am häufigsten fragen"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={faqs} />
|
||||
</Section>
|
||||
</AudiencePage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Checklist } from "@/components/gebos/checklist"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/gebos/faq-accordion"
|
||||
import { IconBadge } from "@/components/gebos/icon-tile"
|
||||
import { ProcessSteps } from "@/components/gebos/process-steps"
|
||||
import { Section, SectionHeading } from "@/components/gebos/section"
|
||||
@@ -61,6 +62,34 @@ const SIE_UEBERNEHMEN = [
|
||||
"Abschluss der Installation",
|
||||
]
|
||||
|
||||
const FAQS: FaqItem[] = [
|
||||
{
|
||||
question: "Muss ich ein Partnerprogramm abschließen?",
|
||||
answer:
|
||||
"Nein. Für die normale Installation von GebOS gibt es keine verpflichtende Partnerschaft, keine Mindestabnahme und keine verpflichtende Vertriebsschulung. Sie installieren, wenn ein Kunde es benötigt.",
|
||||
},
|
||||
{
|
||||
question: "Brauche ich eine GebOS-Zertifizierung?",
|
||||
answer:
|
||||
"Nein. Vorausgesetzt wird Ihr Handwerk: die fachgerechte Montage der Messtechnik. Die digitale Einrichtung und der laufende Betrieb liegen bei GebOS.",
|
||||
},
|
||||
{
|
||||
question: "Was ist bei der Lieferung schon vorbereitet?",
|
||||
answer:
|
||||
"Je nach Auftrag sind Gebäudestruktur, Nutzungseinheiten, Gerätezuordnung, Hardware, Kommunikationsinfrastruktur und Softwarestruktur bereits angelegt. Die Geräte kommen objektbezogen und installationsfertig an.",
|
||||
},
|
||||
{
|
||||
question: "Muss ich vor Ort Software konfigurieren?",
|
||||
answer:
|
||||
"Nein. Auf der Baustelle bleibt im Wesentlichen die Montage. Eine komplizierte Softwareeinrichtung am Gebäude ist nicht vorgesehen.",
|
||||
},
|
||||
{
|
||||
question: "Bleibt der Kunde mein Kunde?",
|
||||
answer:
|
||||
"Ja. Der Fachbetrieb bleibt der Fachbetrieb des Kunden. GebOS stellt das Produkt und die digitale Infrastruktur bereit – die Kundenbeziehung bleibt bei Ihnen.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function InstallateurePage() {
|
||||
return (
|
||||
<AudiencePage
|
||||
@@ -309,7 +338,7 @@ export default function InstallateurePage() {
|
||||
</Section>
|
||||
|
||||
{/* 6. Und wenn Sie regelmäßig GebOS installieren? */}
|
||||
<Section className="pt-0 pb-10 sm:pt-0 sm:pb-12 lg:pt-0 lg:pb-14">
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h2 className="text-2xl font-extrabold tracking-tight text-balance text-navy sm:text-3xl">
|
||||
Und wenn Sie regelmäßig GebOS installieren?
|
||||
@@ -322,6 +351,16 @@ export default function InstallateurePage() {
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 7. Häufige Fragen */}
|
||||
<Section className="pt-0 pb-10 sm:pt-0 sm:pb-12 lg:pt-0 lg:pb-14">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Was Fachbetriebe am häufigsten fragen"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={FAQS} />
|
||||
</Section>
|
||||
</AudiencePage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Section, SectionHeading } from "@/components/gebos/section"
|
||||
import { Checklist } from "@/components/gebos/checklist"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/gebos/faq-accordion"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
@@ -79,6 +80,34 @@ const businessComponents = [
|
||||
|
||||
const stackFlow = ["Ihre Kunden", "Ihr Messdienst", "GebOS Plattform"]
|
||||
|
||||
const faqs: FaqItem[] = [
|
||||
{
|
||||
question: "Können wir GebOS unter unserer eigenen Marke anbieten?",
|
||||
answer:
|
||||
"Ja. GebOS ist als technische Grundlage für Dienstleistungen unter Ihrer Marke vorgesehen – mit eigenem Branding, mandantenfähiger Verwaltung und Endkundenportalen in Ihrem Auftritt.",
|
||||
},
|
||||
{
|
||||
question: "Behalten wir die Kundenbeziehung?",
|
||||
answer:
|
||||
"Ja. Sie betreuen Ihre Kunden und verkaufen Ihre Dienstleistungen. GebOS stellt Hardware, Datenerfassung, Software und Betrieb im Hintergrund bereit.",
|
||||
},
|
||||
{
|
||||
question: "Wie integrieren wir GebOS in unsere Systeme?",
|
||||
answer:
|
||||
"Über offene Schnittstellen: APIs und Datenexporte für Verbrauchsdaten, Geräte- und Objektstrukturen. Damit lässt sich GebOS an bestehende Abrechnungs- und Verwaltungssysteme anbinden.",
|
||||
},
|
||||
{
|
||||
question: "Wer betreibt die Plattform technisch?",
|
||||
answer:
|
||||
"GebOS – einschließlich Kommunikationsinfrastruktur, Gateways, Datenerfassung, Geräteverwaltung und Monitoring. Sie müssen dafür keine eigene Hardware- und Softwareentwicklung aufbauen.",
|
||||
},
|
||||
{
|
||||
question: "Wie werden die Konditionen festgelegt?",
|
||||
answer:
|
||||
"Individuell beziehungsweise mengenabhängig – mit anderen Geschäftsmodellen, Mengenstaffeln und gegebenenfalls White-Label-Konditionen als im öffentlichen Preiskonfigurator. Die konkreten Konditionen werden nicht öffentlich dargestellt.",
|
||||
},
|
||||
]
|
||||
|
||||
/** Platform hero per asset kit 07: laptop dashboard, glass cubes, cloud support. */
|
||||
function PlatformHeroArt() {
|
||||
return (
|
||||
@@ -266,6 +295,15 @@ export default function MessdienstleisterPage() {
|
||||
lead="GebOS soll insbesondere Messdienstleistern ermöglichen, neue digitale Dienstleistungen anzubieten, ohne dafür eine vollständige eigene Hardware- und Softwareentwicklung aufbauen zu müssen. Der Messdienstleister behält dabei seine Kundenbeziehung und sein eigenes Geschäftsmodell."
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Was Messdienstleister am häufigsten fragen"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={faqs} />
|
||||
</Section>
|
||||
</AudiencePage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,71 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowRight, Calculator } from "lucide-react"
|
||||
import {
|
||||
ArrowRight,
|
||||
Calculator,
|
||||
ChevronRight,
|
||||
Lock,
|
||||
Puzzle,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} 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 } from "@/components/gebos/icon-tile"
|
||||
import { InfoStrip } from "@/components/gebos/info-strip"
|
||||
import { ProcessSteps, NumberedList } from "@/components/gebos/process-steps"
|
||||
import { Checklist } from "@/components/gebos/checklist"
|
||||
import { CtaBanner } from "@/components/gebos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/gebos/faq-accordion"
|
||||
import { NoticeArt, NoticeBanner } from "@/components/gebos/notice-banner"
|
||||
import { GermanyIcon } from "@/components/gebos/line-icons"
|
||||
import { TrustPanel, TrustShieldArt } from "@/components/gebos/trust-panel"
|
||||
import {
|
||||
AssetRender,
|
||||
icons,
|
||||
renders,
|
||||
vectors,
|
||||
type RenderAsset,
|
||||
} from "@/components/gebos/site-assets"
|
||||
|
||||
const features = [
|
||||
function StripIcon({ render }: { render: RenderAsset }) {
|
||||
return (
|
||||
<AssetRender
|
||||
render={render}
|
||||
alt=""
|
||||
className="flex size-14 items-center justify-center"
|
||||
imgClassName="w-14"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const infoStripItems = [
|
||||
{
|
||||
media: <img src={icons.uvi} alt="" className="size-14" loading="lazy" />,
|
||||
title: "Unterjährighe Verbrauchsinformation (UVI)",
|
||||
// sub: "Monatliche Verbrauchsinformation",
|
||||
media: <StripIcon render={renders.uviGlassPane} />,
|
||||
eyebrow: "UVI",
|
||||
title: "Monatliche UVI",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<img src={icons.billing} alt="" className="size-14" loading="lazy" />
|
||||
),
|
||||
media: <StripIcon render={renders.glassMoney} />,
|
||||
eyebrow: "Abrechnung",
|
||||
title: "Heizkosten\u00adabrechnung",
|
||||
// sub: "Jährlich korrekt abrechnen",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<img src={icons.smokeAlarm} alt="" className="size-14" loading="lazy" />
|
||||
),
|
||||
media: <StripIcon render={renders.glassSmokeDetector} />,
|
||||
eyebrow: "Sicherheit",
|
||||
title: "Rauchwarnmelder",
|
||||
// sub: "Sicher überwacht, zentral verwaltet",
|
||||
},
|
||||
{
|
||||
media: <img src={icons.radio} alt="" className="size-14" loading="lazy" />,
|
||||
title: "Fernauslesbare Messtechnik",
|
||||
// sub: "Automatisch erfasst, sicher übertragen",
|
||||
media: <StripIcon render={renders.glassGateway} />,
|
||||
eyebrow: "Technik",
|
||||
title: "Fernablesbare Messtechnik",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<img src={icons.portal} alt="" className="size-14" loading="lazy" />
|
||||
),
|
||||
title: "Portale & Verwaltung",
|
||||
// sub: "Alle Daten und Geräte im Blick",
|
||||
media: <StripIcon render={renders.glassLaptop} />,
|
||||
eyebrow: "Software",
|
||||
title: "Portal & Verwaltung",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -105,11 +123,6 @@ function StepMedia({ src }: { src: string }) {
|
||||
}
|
||||
|
||||
const processSteps = [
|
||||
{
|
||||
media: <StepMedia src={vectors.uviBadge} />,
|
||||
title: "Gebäude erfassen",
|
||||
description: "Daten zum Gebäude und den Wohnungen erfassen.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender
|
||||
@@ -118,13 +131,14 @@ const processSteps = [
|
||||
imgClassName="w-16"
|
||||
/>
|
||||
),
|
||||
title: "Gerätebedarf bestimmen",
|
||||
description: "GebOS ermittelt die benötigte Ausstattung automatisch.",
|
||||
title: "Gebäude anlegen & bestellen",
|
||||
description:
|
||||
"In nur 30 Minuten konfigurieren und bestellen.",
|
||||
},
|
||||
{
|
||||
media: <StepMedia src={vectors.packageBox} />,
|
||||
title: "Vorkonfiguriert liefern",
|
||||
description: "Hardware wird für Ihr Objekt vorbereitet geliefert.",
|
||||
description: "Geräte werden für Ihr Objekt vorbereitet geliefert.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
@@ -137,9 +151,20 @@ const processSteps = [
|
||||
title: "Geräte montieren",
|
||||
description: "Montage vor Ort – ohne aufwendige Softwarekonfiguration.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender
|
||||
render={renders.laptopDashboard}
|
||||
className="flex size-16 items-center justify-center"
|
||||
imgClassName="w-16"
|
||||
/>
|
||||
),
|
||||
title: "Bewohner einladen",
|
||||
description: "Laden Sie die Bewohner in das GebOS Portal ein.",
|
||||
},
|
||||
{
|
||||
media: <StepMedia src={vectors.wirelessOrb} />,
|
||||
title: "Automatisch betreiben",
|
||||
title: "Automatischer Betrieb",
|
||||
description: "GebOS übernimmt Daten, Services und laufenden Betrieb.",
|
||||
},
|
||||
]
|
||||
@@ -300,6 +325,58 @@ const priceFlow = [
|
||||
{ title: "Genauer Preis" },
|
||||
]
|
||||
|
||||
const faqs: FaqItem[] = [
|
||||
{
|
||||
question: "Muss ich GebOS selbst installieren?",
|
||||
answer:
|
||||
"Nein. Je nach Ausstattung können geeignete Arbeiten selbst bzw. durch einen vorhandenen Fachbetrieb durchgeführt werden. Alternativ kann die Installation als Komplettleistung beauftragt werden.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Kann mein bestehender Heizungs- oder Sanitärbetrieb die Geräte montieren?",
|
||||
answer:
|
||||
"In vielen Fällen kann ein vorhandener geeigneter Fachbetrieb eingesetzt werden. Welche Arbeiten erforderlich sind, hängt von der jeweiligen Messtechnik und Einbausituation ab.",
|
||||
},
|
||||
{
|
||||
question: "Muss vor Ort jedes Gerät einzeln in GebOS eingerichtet werden?",
|
||||
answer:
|
||||
"Die Geräte werden soweit vorgesehen bereits vor dem Versand dem Gebäude und den geplanten Einheiten zugeordnet. Dadurch reduziert sich die Softwareeinrichtung vor Ort deutlich.",
|
||||
},
|
||||
{
|
||||
question: "Wie genau ist die erste Preisschätzung?",
|
||||
answer:
|
||||
"Die erste Schätzung basiert auf wenigen Gebäudedaten und Annahmen zur benötigten Ausstattung. Die vollständige Gebäudekonfiguration bestimmt anschließend die konkreten Geräte und den finalen Preis.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich nur UVI buchen?",
|
||||
answer:
|
||||
"Ja. UVI und Rauchwarnmelder sind einzeln oder gemeinsam wählbar. Das UVI-Paket umfasst die dafür benötigte Messtechnik und die zugehörigen digitalen Dienstleistungen; die Heizkostenabrechnung baut auf denselben erfassten Verbrauchsdaten auf. Rauchwarnmelder können auch später ergänzt werden – die bereits erfasste Gebäudestruktur wird dabei weiterverwendet.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich bestehende Messtechnik weiterverwenden?",
|
||||
answer:
|
||||
"Das hängt von den vorhandenen Geräten ab: Voraussetzung ist, dass sie fernauslesbar sind und auf geeigneten Standards basieren – dann lassen sich unterschiedliche Zähler- und Gerätetypen einbinden. Vorhandene Messtechnik wird deshalb bei der vollständigen Konfiguration je Gebäude und Einheit erfasst und geprüft; nicht fernablesbare Verbrauchserfassung muss ohnehin bis zum 31.12.2026 nachgerüstet oder ersetzt werden. Was übernommen werden kann, wirkt sich auf die Geräteliste und damit auf den finalen Preis aus.",
|
||||
},
|
||||
{
|
||||
question: "Was passiert bei einem Nutzerwechsel?",
|
||||
answer:
|
||||
"Der Wechsel wird für die betroffene Einheit im GebOS Portal hinterlegt. Weil die Geräte fernauslesbar sind, werden die Werte zum Stichtag aus der laufenden Datenerfassung übernommen – ein Ablesetermin vor Ort ist dafür in der Regel nicht erforderlich. Die Verbräuche werden anschließend den jeweiligen Nutzungszeiträumen zugeordnet, der neue Bewohner erhält seinen Portalzugang, der bisherige verliert ihn. An der Messtechnik selbst ändert sich nichts.",
|
||||
},
|
||||
{
|
||||
question: "Funktioniert GebOS auch für mehrere Gebäude?",
|
||||
answer:
|
||||
"Ja. Gebäude werden einzeln strukturiert, können aber gemeinsam innerhalb eines Portfolios verwaltet werden.",
|
||||
},
|
||||
]
|
||||
|
||||
const trustProperties = [
|
||||
{ icon: <GermanyIcon />, label: "Hosting in Deutschland" },
|
||||
{ icon: <ShieldCheck />, label: "AES-geschützte Messdaten" },
|
||||
{ icon: <Lock />, label: "TLS-verschlüsselte Übertragung" },
|
||||
{ icon: <Users />, label: "Rollenbasierte Zugriffsrechte" },
|
||||
{ icon: <Puzzle />, label: "Interoperable Messtechnik" },
|
||||
]
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
@@ -322,19 +399,40 @@ export default function HomePage() {
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
note="Wenige Angaben · unverbindliche Kostenschätzung"
|
||||
visual={<HeroArt />}
|
||||
/>
|
||||
|
||||
<Container>
|
||||
<Section className="pt-0">
|
||||
<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.title} {...feature} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<InfoStrip
|
||||
items={infoStripItems}
|
||||
className="-mt-6 mb-12 sm:-mt-8 sm:mb-14 lg:-mt-10"
|
||||
/>
|
||||
|
||||
<NoticeBanner
|
||||
title="Nachrüstung bis 31.12.2026 erforderlich"
|
||||
source={{
|
||||
cite: "§ 5 HeizkostenV",
|
||||
href: "https://www.gesetze-im-internet.de/heizkostenv/__5.html",
|
||||
}}
|
||||
visual={<NoticeArt className="max-lg:hidden" />}
|
||||
action={
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="text-brand-800 hover:text-navy decoration-brand-500 hover:decoration-brand-800 h-auto p-0 text-[15px] font-bold underline decoration-2 underline-offset-[6px]"
|
||||
>
|
||||
<Link to="/wissen">
|
||||
Mehr erfahren
|
||||
<ChevronRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Bestehende nicht fernablesbare Verbrauchserfassung muss grundsätzlich
|
||||
bis zum 31.12.2026 nachgerüstet oder ersetzt werden. GebOS verbindet
|
||||
die neue Messtechnik direkt mit UVI und Abrechnung.
|
||||
</NoticeBanner>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
@@ -418,7 +516,7 @@ export default function HomePage() {
|
||||
variant="link"
|
||||
className="h-auto shrink-0 self-start p-0 sm:self-center"
|
||||
>
|
||||
<Link to="/loesungen">
|
||||
<Link to="/produkte">
|
||||
Mehr erfahren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
@@ -547,6 +645,32 @@ export default function HomePage() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<TrustPanel
|
||||
title="Technik, Datenschutz & Sicherheit"
|
||||
items={trustProperties}
|
||||
visual={<TrustShieldArt />}
|
||||
action={
|
||||
<Button asChild variant="link" className="h-auto p-0 font-bold">
|
||||
<Link to="/technik-sicherheit">
|
||||
Mehr zu Technik & Sicherheit
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zu GebOS"
|
||||
lead="Von Installation und Ausstattung bis zu Preis und laufendem Betrieb."
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={faqs} />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<CtaBanner
|
||||
icon={<Calculator />}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { CtaBanner } from "@/components/gebos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/gebos/faq-accordion"
|
||||
import { ProcessSteps } from "@/components/gebos/process-steps"
|
||||
import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
|
||||
@@ -25,22 +26,13 @@ const steps = [
|
||||
{
|
||||
media: (
|
||||
<StepMedia>
|
||||
<img src={vectors.uviBadge} alt="" className="w-16" />
|
||||
<AssetRender render={renders.building} imgClassName="w-12" />
|
||||
<img src={vectors.uviBadge} alt="" className="w-9" />
|
||||
</StepMedia>
|
||||
),
|
||||
title: "Gebäude erfassen",
|
||||
title: "Gebäude anlegen und bestellen",
|
||||
description:
|
||||
"Bei der Konfiguration werden Gebäude, Wohnungen und die benötigten Leistungen erfasst.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<StepMedia>
|
||||
<AssetRender render={renders.building} imgClassName="w-16" />
|
||||
</StepMedia>
|
||||
),
|
||||
title: "Gerätebedarf bestimmen",
|
||||
description:
|
||||
"Auf Basis der Gebäudedaten wird ermittelt, welche Messtechnik, Rauchwarnmelder und weiteren Komponenten benötigt werden.",
|
||||
"Bei der Konfiguration werden Gebäude, Wohnungen und die benötigten Leistungen erfasst. Auf Basis dieser Daten wird automatisch ermittelt, welche Messtechnik, Rauchwarnmelder und weiteren Komponenten benötigt werden.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
@@ -70,13 +62,22 @@ const steps = [
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<StepMedia>
|
||||
<AssetRender render={renders.laptopDashboard} imgClassName="w-16" />
|
||||
</StepMedia>
|
||||
),
|
||||
title: "Bewohner einladen",
|
||||
description: "Laden Sie die Bewohner in das GebOS Portal ein.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<StepMedia>
|
||||
<img src={vectors.wirelessOrb} alt="" className="w-16" />
|
||||
</StepMedia>
|
||||
),
|
||||
title: "Automatisch betreiben",
|
||||
title: "Automatischer Betrieb",
|
||||
description:
|
||||
"Nach der Installation übernimmt GebOS die laufende Datenerfassung und die gebuchten digitalen Dienstleistungen.",
|
||||
},
|
||||
@@ -96,6 +97,34 @@ const journey = [
|
||||
"automatischer laufender Betrieb",
|
||||
]
|
||||
|
||||
const faqs: FaqItem[] = [
|
||||
{
|
||||
question: "Wo fange ich an?",
|
||||
answer:
|
||||
"Im Konfigurator. Dort erfassen Sie Gebäude, Wohnungen und die benötigten Leistungen und erhalten eine erste Preisindikation. Aus denselben Daten wird anschließend der Auftrag aufgebaut.",
|
||||
},
|
||||
{
|
||||
question: "Wie verbindlich ist der Preis aus dem Konfigurator?",
|
||||
answer:
|
||||
"Er ist eine Preisschätzung auf Basis Ihrer Angaben. Den genauen Preis erhalten Sie, sobald die tatsächlichen Gebäudedaten ergänzt und geprüft sind – erst danach wird der Auftrag erteilt.",
|
||||
},
|
||||
{
|
||||
question: "Muss ich meine Gebäudedaten mehrfach erfassen?",
|
||||
answer:
|
||||
"Nein. Die einmal eingegebenen Gebäudedaten werden durch den gesamten Prozess weiterverwendet: von der Preisschätzung über die Gerätevorbereitung bis in den laufenden Betrieb.",
|
||||
},
|
||||
{
|
||||
question: "Wer montiert die Geräte?",
|
||||
answer:
|
||||
"Das entscheiden Sie: Sie selbst, Ihr Fachbetrieb oder GebOS als Komplettlösung. Die Hardware kommt objektbezogen vorkonfiguriert an, eine aufwendige Softwarekonfiguration vor Ort entfällt.",
|
||||
},
|
||||
{
|
||||
question: "Was passiert nach der Installation?",
|
||||
answer:
|
||||
"GebOS übernimmt die laufende Datenerfassung und die gebuchten digitalen Dienstleistungen. Bewohner werden ins Portal eingeladen, der Betrieb läuft danach automatisch.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function HowItWorksPage() {
|
||||
return (
|
||||
<>
|
||||
@@ -159,6 +188,17 @@ export default function HowItWorksPage() {
|
||||
</div>
|
||||
</Section>
|
||||
</Container>
|
||||
<Container>
|
||||
<Section className="pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zum Ablauf"
|
||||
lead="Von der ersten Preisindikation bis zum laufenden Betrieb – die häufigsten Fragen zum GebOS-Prozess."
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={faqs} />
|
||||
</Section>
|
||||
</Container>
|
||||
<Container>
|
||||
<Section className="pt-0">
|
||||
<CtaBanner
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
|
||||
import { AssetRender, renders } from "@/components/gebos/site-assets"
|
||||
|
||||
const solutions = [
|
||||
const products = [
|
||||
{
|
||||
title: "UVI",
|
||||
sub: "Monatliche Verbrauchsinformation",
|
||||
@@ -36,7 +36,7 @@ const solutions = [
|
||||
},
|
||||
]
|
||||
|
||||
export default function SolutionsPage() {
|
||||
export default function ProductsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-hero-glow">
|
||||
@@ -46,15 +46,15 @@ export default function SolutionsPage() {
|
||||
className="mb-4"
|
||||
items={[
|
||||
{ label: "Startseite", to: "/" },
|
||||
{ label: "Lösungen" },
|
||||
{ label: "Produkte" },
|
||||
]}
|
||||
/>
|
||||
<SectionHeading
|
||||
title="Unsere Lösungen"
|
||||
lead="Hier finden sie eine Übersicht unserer Produkt Lösungen."
|
||||
title="Unsere Produkte"
|
||||
lead="Hier finden Sie eine Übersicht unserer Produkte für Messtechnik, Abrechnung und Verwaltung."
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{solutions.map((s) => (
|
||||
{products.map((s) => (
|
||||
<Card key={s.title} className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-xl font-extrabold tracking-tight text-navy">
|
||||
{s.title}
|
||||
@@ -71,7 +71,7 @@ export default function SolutionsPage() {
|
||||
className="mt-3 self-start px-0"
|
||||
asChild
|
||||
>
|
||||
<Link to="/loesungen">
|
||||
<Link to="/produkte">
|
||||
Mehr erfahren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
@@ -95,7 +95,7 @@ export default function SolutionsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="link" className="shrink-0 px-0 sm:px-4" asChild>
|
||||
<Link to="/loesungen">
|
||||
<Link to="/produkte">
|
||||
Mehr erfahren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
@@ -193,6 +193,32 @@
|
||||
);
|
||||
}
|
||||
|
||||
/* teal wash bleeding in from the left edge of a notice card: brand-300 at the
|
||||
top turning into brand-800 at the base, masked out towards the middle so it
|
||||
dissolves into the card surface, and thinned again over the last third of the
|
||||
drop so small cards keep their footer copy readable. Hand-written, so the
|
||||
stops interpolate in srgb like the tokens. */
|
||||
@utility bg-notice-wash {
|
||||
background-image: linear-gradient(
|
||||
176deg,
|
||||
var(--brand-300) 0%,
|
||||
var(--brand-400) 16%,
|
||||
var(--brand-500) 46%,
|
||||
var(--brand-700) 82%,
|
||||
var(--brand-800) 100%
|
||||
);
|
||||
mask-image:
|
||||
linear-gradient(
|
||||
92deg,
|
||||
#000 0%,
|
||||
rgb(0 0 0 / 0.74) 26%,
|
||||
rgb(0 0 0 / 0.24) 60%,
|
||||
transparent 92%
|
||||
),
|
||||
linear-gradient(180deg, #000 0%, #000 64%, rgb(0 0 0 / 0.5) 100%);
|
||||
mask-composite: intersect;
|
||||
}
|
||||
|
||||
/* dotted connector line used by process steppers */
|
||||
@utility border-dotted-brand {
|
||||
border-color: var(--brand-300);
|
||||
|
||||