save
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/styles/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GebOS – Messdienstleistungen einfach gemacht</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="GebOS Component Library – shadcn-basierte Komponenten für die GebOS Landing Pages."
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "gebos-component-library",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource-variable/inter": "^5.2.5",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.539.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.8.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tw-animate-css": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@types/node": "^24.2.0",
|
||||||
|
"@types/react": "^19.1.9",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.1.1"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"esbuild@0.28.2": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
import { SiteFooter } from "@/components/gebos/site-footer"
|
||||||
|
import { SiteHeader } from "@/components/gebos/site-header"
|
||||||
|
import HauseigentuemerPage from "@/pages/audiences/hauseigentuemer"
|
||||||
|
import HausverwaltungenPage from "@/pages/audiences/hausverwaltungen"
|
||||||
|
import InstallateurePage from "@/pages/audiences/installateure"
|
||||||
|
import MessdienstleisterPage from "@/pages/audiences/messdienstleister"
|
||||||
|
import ConfiguratorPage from "@/pages/configurator"
|
||||||
|
import ForWhomPage from "@/pages/for-whom"
|
||||||
|
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"
|
||||||
|
|
||||||
|
function ScrollToTop() {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<ScrollToTop />
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<SiteHeader />
|
||||||
|
<main className="flex-1">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/so-funktionierts" element={<HowItWorksPage />} />
|
||||||
|
<Route path="/loesungen" element={<SolutionsPage />} />
|
||||||
|
<Route path="/fuer-wen" element={<ForWhomPage />} />
|
||||||
|
<Route
|
||||||
|
path="/fuer-wen/hauseigentuemer"
|
||||||
|
element={<HauseigentuemerPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fuer-wen/hausverwaltungen"
|
||||||
|
element={<HausverwaltungenPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fuer-wen/messdienstleister"
|
||||||
|
element={<MessdienstleisterPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fuer-wen/installateure"
|
||||||
|
element={<InstallateurePage />}
|
||||||
|
/>
|
||||||
|
<Route path="/konfigurator" element={<ConfiguratorPage />} />
|
||||||
|
<Route path="/klarpreis" element={<KlarpreisPage />} />
|
||||||
|
<Route
|
||||||
|
path="/unternehmen"
|
||||||
|
element={
|
||||||
|
<SimplePage
|
||||||
|
breadcrumb={[
|
||||||
|
{ label: "Startseite", to: "/" },
|
||||||
|
{ label: "Unternehmen" },
|
||||||
|
]}
|
||||||
|
title="Unternehmen"
|
||||||
|
lead="GebOS verbindet Messtechnik, Datenerfassung und Software zu einem durchgängigen System für Wohngebäude."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/kontakt"
|
||||||
|
element={
|
||||||
|
<SimplePage
|
||||||
|
breadcrumb={[
|
||||||
|
{ label: "Startseite", to: "/" },
|
||||||
|
{ label: "Kontakt" },
|
||||||
|
]}
|
||||||
|
title="Kontakt"
|
||||||
|
lead="Sprechen Sie mit uns über Ihr Gebäude, Ihr Portfolio oder Ihre Plattform-Anforderungen."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<SimplePage
|
||||||
|
title="Seite nicht gefunden"
|
||||||
|
lead="Die angeforderte Seite existiert nicht."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
<SiteFooter />
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 520" role="img" aria-label="Teal approval check badge">
|
||||||
|
<defs><radialGradient id="o" cx="32%" cy="24%"><stop stop-color="#36e4d4"/><stop offset=".65" stop-color="#009b9a"/><stop offset="1" stop-color="#006175"/></radialGradient><filter id="sh" x="-40%" y="-40%" width="180%" height="190%"><feDropShadow dx="0" dy="22" stdDeviation="22" flood-color="#005a6c" flood-opacity=".24"/></filter></defs>
|
||||||
|
<ellipse cx="260" cy="423" rx="150" ry="30" fill="#007284" opacity=".15" filter="url(#sh)"/>
|
||||||
|
<circle cx="260" cy="238" r="162" fill="url(#o)" stroke="#9afff2" stroke-opacity=".55" stroke-width="5" filter="url(#sh)"/>
|
||||||
|
<circle cx="260" cy="238" r="126" fill="none" stroke="#fff" stroke-opacity=".16" stroke-width="4"/>
|
||||||
|
<path d="m172 238 56 58 122-132" fill="none" stroke="#fff" stroke-width="34" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 908 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 620" role="img" aria-label="Cloud platform and support headphones">
|
||||||
|
<defs><linearGradient id="c" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#fff"/><stop offset="1" stop-color="#d9e8eb"/></linearGradient><linearGradient id="t" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#37e1d2"/><stop offset="1" stop-color="#00798b"/></linearGradient><filter id="sh" x="-40%" y="-40%" width="190%" height="200%"><feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#075465" flood-opacity=".18"/></filter></defs>
|
||||||
|
<g filter="url(#sh)"><path d="M117 290c0-62 49-112 110-115 27-66 88-105 157-105 90 0 164 68 174 155 58 2 104 48 104 106 0 59-48 107-107 107H219c-57 0-102-46-102-103 0-16 3-31 10-45Z" fill="url(#c)" stroke="#e3eef0" stroke-width="5"/><path d="m356 331 66-66m-10 0h10v10" fill="none" stroke="url(#t)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/></g>
|
||||||
|
<g transform="translate(451 304)" filter="url(#sh)" fill="none" stroke="url(#t)" stroke-width="28" stroke-linecap="round"><path d="M20 112v-25c0-79 59-137 132-137s132 58 132 137v25"/><path d="M20 108v76c0 25 20 45 45 45h24v-121H65c-25 0-45 20-45 45ZM284 108v76c0 25-20 45-45 45h-24v-121h24c25 0 45 20 45 45Z" fill="#e8fbf8" stroke-width="18"/><path d="M216 229c-8 32-33 45-73 45" stroke-width="14"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 560" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">GebOS segmented consumption ring</title>
|
||||||
|
<desc id="desc">A substantial eight-part white, teal and translucent cyan ring in three-quarter perspective.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="whiteTop" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#ffffff"/><stop offset=".58" stop-color="#f1f5f6"/><stop offset="1" stop-color="#d9e2e5"/></linearGradient>
|
||||||
|
<linearGradient id="whiteSide" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#d8e1e4"/><stop offset="1" stop-color="#aebdc2"/></linearGradient>
|
||||||
|
<linearGradient id="aquaTop" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#78f3e6"/><stop offset=".52" stop-color="#23cfc4"/><stop offset="1" stop-color="#00a5a6"/></linearGradient>
|
||||||
|
<linearGradient id="aquaGlass" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#b9fff7" stop-opacity=".76"/><stop offset=".52" stop-color="#41dcd1" stop-opacity=".68"/><stop offset="1" stop-color="#059ca1" stop-opacity=".78"/></linearGradient>
|
||||||
|
<linearGradient id="tealTop" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#18c8c0"/><stop offset=".52" stop-color="#008f96"/><stop offset="1" stop-color="#006579"/></linearGradient>
|
||||||
|
<linearGradient id="deepTop" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#009198"/><stop offset=".5" stop-color="#006d7d"/><stop offset="1" stop-color="#00485e"/></linearGradient>
|
||||||
|
<linearGradient id="aquaSide" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#00a3a4"/><stop offset="1" stop-color="#006774"/></linearGradient>
|
||||||
|
<linearGradient id="tealSide" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#007681"/><stop offset="1" stop-color="#003d52"/></linearGradient>
|
||||||
|
<filter id="shadow" x="-35%" y="-110%" width="170%" height="320%"><feGaussianBlur stdDeviation="24"/></filter>
|
||||||
|
<filter id="lift" x="-25%" y="-60%" width="150%" height="230%"><feDropShadow dx="0" dy="16" stdDeviation="12" flood-color="#073f4f" flood-opacity=".2"/></filter>
|
||||||
|
</defs>
|
||||||
|
<ellipse cx="450" cy="430" rx="285" ry="55" fill="#0b4856" opacity=".16" filter="url(#shadow)"/>
|
||||||
|
<g transform="translate(450 250) scale(1 .58) translate(0 58) rotate(-18)" fill="none" stroke-width="132" stroke-linecap="butt">
|
||||||
|
<circle r="216" stroke="url(#whiteSide)" stroke-dasharray="168 1189" stroke-dashoffset="0"/>
|
||||||
|
<circle r="216" stroke="url(#whiteSide)" stroke-dasharray="168 1189" stroke-dashoffset="-170"/>
|
||||||
|
<circle r="216" stroke="url(#tealSide)" stroke-dasharray="168 1189" stroke-dashoffset="-340"/>
|
||||||
|
<circle r="216" stroke="url(#aquaSide)" stroke-dasharray="168 1189" stroke-dashoffset="-510"/>
|
||||||
|
<circle r="216" stroke="url(#aquaSide)" stroke-dasharray="168 1189" stroke-dashoffset="-680"/>
|
||||||
|
<circle r="216" stroke="url(#tealSide)" stroke-dasharray="168 1189" stroke-dashoffset="-850"/>
|
||||||
|
<circle r="216" stroke="url(#tealSide)" stroke-dasharray="168 1189" stroke-dashoffset="-1020"/>
|
||||||
|
<circle r="216" stroke="url(#whiteSide)" stroke-dasharray="168 1189" stroke-dashoffset="-1190"/>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(450 250) scale(1 .58) rotate(-18)" fill="none" stroke-width="132" stroke-linecap="butt" filter="url(#lift)">
|
||||||
|
<circle r="216" stroke="url(#whiteTop)" stroke-dasharray="168 1189" stroke-dashoffset="0"/>
|
||||||
|
<circle r="216" stroke="url(#whiteTop)" stroke-dasharray="168 1189" stroke-dashoffset="-170"/>
|
||||||
|
<circle r="216" stroke="url(#deepTop)" stroke-dasharray="168 1189" stroke-dashoffset="-340"/>
|
||||||
|
<circle r="216" stroke="url(#aquaTop)" stroke-dasharray="168 1189" stroke-dashoffset="-510"/>
|
||||||
|
<circle r="216" stroke="url(#aquaGlass)" stroke-dasharray="168 1189" stroke-dashoffset="-680"/>
|
||||||
|
<circle r="216" stroke="url(#tealTop)" stroke-dasharray="168 1189" stroke-dashoffset="-850"/>
|
||||||
|
<circle r="216" stroke="url(#deepTop)" stroke-dasharray="168 1189" stroke-dashoffset="-1020"/>
|
||||||
|
<circle r="216" stroke="url(#whiteTop)" stroke-dasharray="168 1189" stroke-dashoffset="-1190"/>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(450 250) scale(1 .58)" fill="none">
|
||||||
|
<ellipse rx="150" ry="150" stroke="#ffffff" stroke-opacity=".72" stroke-width="8"/>
|
||||||
|
<ellipse rx="283" ry="283" stroke="#dffffb" stroke-opacity=".34" stroke-width="6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 620" role="img" aria-label="Glass euro price orb">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="g" cx="30%" cy="20%"><stop stop-color="#dffffb" stop-opacity=".78"/><stop offset=".45" stop-color="#4cded4" stop-opacity=".46"/><stop offset="1" stop-color="#007487" stop-opacity=".7"/></radialGradient>
|
||||||
|
<linearGradient id="r" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#8ffff2"/><stop offset=".5" stop-color="#00b4ad"/><stop offset="1" stop-color="#006276"/></linearGradient>
|
||||||
|
<filter id="gl" x="-50%" y="-50%" width="200%" height="220%"><feGaussianBlur stdDeviation="18" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
||||||
|
<filter id="sh" x="-50%" y="-50%" width="200%" height="220%"><feDropShadow dx="0" dy="24" stdDeviation="20" flood-color="#005b6b" flood-opacity=".28"/></filter>
|
||||||
|
</defs>
|
||||||
|
<ellipse cx="310" cy="500" rx="215" ry="66" fill="none" stroke="url(#r)" stroke-width="18" opacity=".8" filter="url(#gl)"/>
|
||||||
|
<ellipse cx="310" cy="500" rx="145" ry="38" fill="#35e1d3" opacity=".26" filter="url(#gl)"/>
|
||||||
|
<circle cx="310" cy="283" r="190" fill="url(#g)" stroke="#b9fff8" stroke-opacity=".78" stroke-width="5" filter="url(#sh)"/>
|
||||||
|
<circle cx="270" cy="216" r="80" fill="#fff" opacity=".15"/>
|
||||||
|
<text x="310" y="360" text-anchor="middle" font-family="Arial,sans-serif" font-weight="700" font-size="220" fill="#fff">€</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 600" role="img" aria-label="Translucent teal data cubes">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="f" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#34e5d5" stop-opacity=".78"/><stop offset="1" stop-color="#008c99" stop-opacity=".62"/></linearGradient>
|
||||||
|
<linearGradient id="s" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#008b96" stop-opacity=".72"/><stop offset="1" stop-color="#005267" stop-opacity=".82"/></linearGradient>
|
||||||
|
<linearGradient id="t" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#b9fff7" stop-opacity=".82"/><stop offset="1" stop-color="#30ded1" stop-opacity=".6"/></linearGradient>
|
||||||
|
<filter id="sh" x="-40%" y="-40%" width="190%" height="200%"><feDropShadow dx="0" dy="18" stdDeviation="17" flood-color="#006b77" flood-opacity=".18"/></filter>
|
||||||
|
</defs>
|
||||||
|
<ellipse cx="360" cy="522" rx="270" ry="36" fill="#006678" opacity=".12" filter="url(#sh)"/>
|
||||||
|
<g filter="url(#sh)" stroke="#8afff2" stroke-opacity=".45" stroke-width="3">
|
||||||
|
<g transform="translate(75 235)"><path d="m0 60 90 26v104L0 164Z" fill="url(#f)"/><path d="m90 86 52-38v104l-52 38Z" fill="url(#s)"/><path d="M0 60 52 22l90 26-52 38Z" fill="url(#t)"/></g>
|
||||||
|
<g transform="translate(220 105)"><path d="m0 60 90 26v104L0 164Z" fill="url(#f)"/><path d="m90 86 52-38v104l-52 38Z" fill="url(#s)"/><path d="M0 60 52 22l90 26-52 38Z" fill="url(#t)"/></g>
|
||||||
|
<g transform="translate(220 315)"><path d="m0 60 90 26v104L0 164Z" fill="url(#f)"/><path d="m90 86 52-38v104l-52 38Z" fill="url(#s)"/><path d="M0 60 52 22l90 26-52 38Z" fill="url(#t)"/></g>
|
||||||
|
<g transform="translate(365 210)"><path d="m0 60 90 26v104L0 164Z" fill="url(#f)"/><path d="m90 86 52-38v104l-52 38Z" fill="url(#s)"/><path d="M0 60 52 22l90 26-52 38Z" fill="url(#t)"/></g>
|
||||||
|
<g transform="translate(510 315)"><path d="m0 60 90 26v104L0 164Z" fill="url(#f)"/><path d="m90 86 52-38v104l-52 38Z" fill="url(#s)"/><path d="M0 60 52 22l90 26-52 38Z" fill="url(#t)"/></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><circle cx="48" cy="48" r="42" fill="#f2f8f9"/><path d="M30 23h28l10 10v38H30Z" fill="none" stroke="#008e96" stroke-width="4" stroke-linejoin="round"/><path d="M58 23v12h12M39 44h19M39 53h13" fill="none" stroke="#008e96" stroke-width="4" stroke-linecap="round"/><circle cx="61" cy="62" r="12" fill="#fff" stroke="#008e96" stroke-width="4"/><path d="m56 62 4 4 7-9" fill="none" stroke="#008e96" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 528 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><circle cx="48" cy="48" r="42" fill="#f2f8f9"/><rect x="22" y="26" width="52" height="38" rx="6" fill="none" stroke="#008e96" stroke-width="4"/><path d="M38 72h20M48 64v8M31 52l8-9 7 6 10-13 9 8" fill="none" stroke="#008e96" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><circle cx="48" cy="48" r="42" fill="#f2f8f9"/><rect x="25" y="24" width="34" height="49" rx="8" fill="none" stroke="#008e96" stroke-width="4"/><circle cx="42" cy="61" r="4" fill="#008e96"/><path d="M65 35c8 7 8 19 0 26M72 28c13 12 13 28 0 40" fill="none" stroke="#008e96" stroke-width="4" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 381 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><circle cx="48" cy="48" r="42" fill="#f2f8f9"/><path d="M25 46c0-15 10-25 23-25s23 10 23 25c0 17-11 29-23 29S25 63 25 46Z" fill="none" stroke="#008e96" stroke-width="4"/><circle cx="48" cy="48" r="8" fill="none" stroke="#008e96" stroke-width="4"/><path d="M34 32 29 27M62 32l5-5M48 26v-7" stroke="#008e96" stroke-width="4" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><circle cx="48" cy="48" r="42" fill="#f2f8f9"/><path d="M31 33 48 23l17 10v28L48 71 31 61Z" fill="none" stroke="#008e96" stroke-width="4" stroke-linejoin="round"/><path d="M42 53V39h12v14M39 58h18" fill="none" stroke="#008e96" stroke-width="4" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 560" role="img" aria-label="Preconfigured device package">
|
||||||
|
<defs><linearGradient id="f" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#fff"/><stop offset="1" stop-color="#dce8ea"/></linearGradient><linearGradient id="s" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#c8dadd"/><stop offset="1" stop-color="#9db4ba"/></linearGradient><linearGradient id="t" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#f7ffff"/><stop offset="1" stop-color="#dff4f2"/></linearGradient><filter id="sh" x="-40%" y="-40%" width="190%" height="200%"><feDropShadow dx="0" dy="20" stdDeviation="18" flood-color="#064d5b" flood-opacity=".18"/></filter></defs>
|
||||||
|
<ellipse cx="310" cy="492" rx="225" ry="35" fill="#006576" opacity=".12" filter="url(#sh)"/>
|
||||||
|
<g filter="url(#sh)" stroke="#b9cbcf" stroke-width="4" stroke-linejoin="round"><path d="m104 188 234 67v221l-234-68Z" fill="url(#f)"/><path d="m338 255 178-129v221L338 476Z" fill="url(#s)"/><path d="M104 188 282 59l234 67-178 129Z" fill="url(#t)"/><path d="m241 99 233 67-55 40-234-67Z" fill="#20cfc3" fill-opacity=".7" stroke="none"/></g>
|
||||||
|
<circle cx="221" cy="323" r="56" fill="#009b9a"/><path d="m192 323 20 21 40-46" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 640" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">GebOS translucent data bars</title>
|
||||||
|
<desc id="desc">Five translucent teal isometric bars of increasing height.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="front" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#19e1d2" stop-opacity=".86"/><stop offset="1" stop-color="#008797" stop-opacity=".72"/></linearGradient>
|
||||||
|
<linearGradient id="side" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#00788b" stop-opacity=".74"/><stop offset="1" stop-color="#004c68" stop-opacity=".84"/></linearGradient>
|
||||||
|
<linearGradient id="top" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#9efff2" stop-opacity=".88"/><stop offset="1" stop-color="#2ce0d4" stop-opacity=".72"/></linearGradient>
|
||||||
|
<filter id="shadow" x="-30%" y="-30%" width="170%" height="180%"><feDropShadow dx="0" dy="18" stdDeviation="17" flood-color="#006875" flood-opacity=".18"/></filter>
|
||||||
|
</defs>
|
||||||
|
<ellipse cx="385" cy="572" rx="310" ry="38" fill="#007789" opacity=".13" filter="url(#shadow)"/>
|
||||||
|
<g filter="url(#shadow)" stroke="#75fff1" stroke-opacity=".34" stroke-width="2">
|
||||||
|
<g><path d="M75 425 159 449 159 551 75 527Z" fill="url(#front)"/><path d="m159 449 43-31v102l-43 31Z" fill="url(#side)"/><path d="m75 425 43-31 84 24-43 31Z" fill="url(#top)"/></g>
|
||||||
|
<g><path d="m195 352 84 24v175l-84-24Z" fill="url(#front)"/><path d="m279 376 43-31v175l-43 31Z" fill="url(#side)"/><path d="m195 352 43-31 84 24-43 31Z" fill="url(#top)"/></g>
|
||||||
|
<g><path d="m315 270 84 24v257l-84-24Z" fill="url(#front)"/><path d="m399 294 43-31v257l-43 31Z" fill="url(#side)"/><path d="m315 270 43-31 84 24-43 31Z" fill="url(#top)"/></g>
|
||||||
|
<g><path d="m435 176 84 24v351l-84-24Z" fill="url(#front)"/><path d="m519 200 43-31v351l-43 31Z" fill="url(#side)"/><path d="m435 176 43-31 84 24-43 31Z" fill="url(#top)"/></g>
|
||||||
|
<g><path d="m555 83 84 24v444l-84-24Z" fill="url(#front)"/><path d="m639 107 43-31v444l-43 31Z" fill="url(#side)"/><path d="m555 83 43-31 84 24-43 31Z" fill="url(#top)"/></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 620" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">GebOS UVI sensor badge</title>
|
||||||
|
<desc id="desc">A white sensor with a teal consumption display and wireless symbol.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="case" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#fff"/><stop offset="1" stop-color="#dce7e9"/></linearGradient>
|
||||||
|
<linearGradient id="screen" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#20d9c9"/><stop offset="1" stop-color="#007b8b"/></linearGradient>
|
||||||
|
<radialGradient id="orb"><stop stop-color="#22dacd"/><stop offset=".78" stop-color="#008796"/><stop offset="1" stop-color="#005568"/></radialGradient>
|
||||||
|
<filter id="s" x="-40%" y="-40%" width="180%" height="200%"><feDropShadow dx="0" dy="22" stdDeviation="20" flood-color="#064b58" flood-opacity=".2"/></filter>
|
||||||
|
</defs>
|
||||||
|
<ellipse cx="245" cy="548" rx="160" ry="30" fill="#005c6f" opacity=".13" filter="url(#s)"/>
|
||||||
|
<g filter="url(#s)">
|
||||||
|
<rect x="98" y="98" width="240" height="400" rx="54" fill="url(#case)" stroke="#d7e4e7" stroke-width="4"/>
|
||||||
|
<rect x="135" y="170" width="166" height="192" rx="34" fill="url(#screen)"/>
|
||||||
|
<path d="M180 273c16-31 31 31 47 0s31 31 47 0" fill="none" stroke="#fff" stroke-width="15" stroke-linecap="round"/>
|
||||||
|
<circle cx="218" cy="423" r="12" fill="#00b5aa"/>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(320 320)" filter="url(#s)">
|
||||||
|
<circle r="108" fill="url(#orb)" stroke="#77fff2" stroke-opacity=".55" stroke-width="3"/>
|
||||||
|
<circle r="72" fill="none" stroke="#fff" stroke-opacity=".17" stroke-width="3"/>
|
||||||
|
<path d="M-15 35V-35M8-52c34 25 34 79 0 104M37-75c58 43 58 107 0 150M-38-24c-14 14-14 34 0 48" fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 520" role="img" aria-label="Wireless operation badge">
|
||||||
|
<defs><radialGradient id="o" cx="30%" cy="20%"><stop stop-color="#37e7d7"/><stop offset=".65" stop-color="#009796"/><stop offset="1" stop-color="#005d72"/></radialGradient><filter id="sh" x="-40%" y="-40%" width="180%" height="190%"><feDropShadow dx="0" dy="22" stdDeviation="22" flood-color="#005a6c" flood-opacity=".24"/></filter></defs>
|
||||||
|
<ellipse cx="260" cy="423" rx="150" ry="30" fill="#007284" opacity=".15" filter="url(#sh)"/><circle cx="260" cy="238" r="162" fill="url(#o)" stroke="#9afff2" stroke-opacity=".55" stroke-width="5" filter="url(#sh)"/>
|
||||||
|
<circle cx="260" cy="277" r="18" fill="#fff"/><path d="M211 231c27-27 71-27 98 0M178 198c46-46 118-46 164 0M147 166c63-63 163-63 226 0" fill="none" stroke="#fff" stroke-width="20" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 883 B |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 87 KiB |
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { CircleCheck } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ChecklistItem({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className={cn("flex items-start gap-2.5 text-sm", className)}>
|
||||||
|
<CircleCheck className="mt-0.5 size-4 shrink-0 text-primary" />
|
||||||
|
<span>{children}</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Checklist({
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
items: React.ReactNode[]
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ul className={cn("flex flex-col gap-2.5", className)}>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<ChecklistItem key={i}>{item}</ChecklistItem>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checklist, ChecklistItem }
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teal gradient call-to-action band:
|
||||||
|
* glassy icon disc · bold white title + muted subline · actions right.
|
||||||
|
*/
|
||||||
|
function CtaBanner({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode
|
||||||
|
title: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
actions: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-5 rounded-2xl bg-brand-gradient p-6 shadow-band sm:flex-row sm:items-center sm:gap-6 sm:p-7",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconBadge variant="glass" size="lg">
|
||||||
|
{icon}
|
||||||
|
</IconBadge>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-lg font-bold tracking-tight text-white sm:text-xl">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{description ? (
|
||||||
|
<p className="mt-1 text-sm leading-relaxed text-white/75">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 flex-wrap items-center gap-3">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CtaBanner }
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
import buildingAvif from "@/assets/gebos/building.avif"
|
||||||
|
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 "@/assets/gebos/uvi-badge.svg"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Homepage hero composition from the GebOS visual asset kit:
|
||||||
|
* building render (white matte → mix-blend-multiply), smoke alarm,
|
||||||
|
* translucent data bars, UVI badge and consumption ring.
|
||||||
|
* Layout mirrors the kit's demo.html so proportions can be tuned
|
||||||
|
* without rerendering.
|
||||||
|
*/
|
||||||
|
function HeroArt({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative mx-auto aspect-[1.28] w-full max-w-[680px] lg:mr-0 lg:ml-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
aria-label="GebOS Produktvisualisierung"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={tealBars}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[12%] -right-[5%] z-0 w-[46%]"
|
||||||
|
/>
|
||||||
|
<picture>
|
||||||
|
<source srcSet={buildingAvif} type="image/avif" />
|
||||||
|
<img
|
||||||
|
src={buildingWebp}
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HeroArt }
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { CircleCheck, Wifi } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
import {
|
||||||
|
BuildingIllustration,
|
||||||
|
DonutIllustration,
|
||||||
|
} from "@/components/gebos/illustrations"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floating status chip ("Inspektion OK · 06.05.2024") used on hero visuals.
|
||||||
|
*/
|
||||||
|
function StatusChip({
|
||||||
|
title,
|
||||||
|
sub,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
title: React.ReactNode
|
||||||
|
sub?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2.5 rounded-xl border border-border/60 bg-card/95 px-3.5 py-2.5 shadow-card-lg backdrop-blur",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconBadge variant="solid" size="sm">
|
||||||
|
<CircleCheck />
|
||||||
|
</IconBadge>
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div className="text-xs font-bold text-navy">{title}</div>
|
||||||
|
{sub ? (
|
||||||
|
<div className="text-[10px] text-muted-foreground">{sub}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable hero composition standing in for the 3D building renders:
|
||||||
|
* glow stage, building, floating status card, signal chip and donut.
|
||||||
|
* `media` swaps the centre illustration.
|
||||||
|
*/
|
||||||
|
function HeroVisual({
|
||||||
|
media,
|
||||||
|
chipTitle = "Inspektion OK",
|
||||||
|
chipSub = "Letzte Prüfung 06.05.2024",
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
media?: React.ReactNode
|
||||||
|
chipTitle?: React.ReactNode
|
||||||
|
chipSub?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn("relative mx-auto w-full max-w-md", className)}>
|
||||||
|
{/* stage */}
|
||||||
|
<div className="absolute inset-x-6 top-8 bottom-0 rounded-[2.5rem] bg-gradient-to-br from-white via-brand-50 to-brand-100 shadow-card-lg" />
|
||||||
|
<div className="relative px-10 pt-4 pb-8">
|
||||||
|
{media ?? <BuildingIllustration className="mx-auto w-56" />}
|
||||||
|
</div>
|
||||||
|
{/* floating elements */}
|
||||||
|
<StatusChip
|
||||||
|
title={chipTitle}
|
||||||
|
sub={chipSub}
|
||||||
|
className="absolute top-16 -left-2 sm:left-0"
|
||||||
|
/>
|
||||||
|
<IconBadge
|
||||||
|
variant="solid"
|
||||||
|
size="lg"
|
||||||
|
className="absolute bottom-20 -left-1 shadow-band sm:left-4"
|
||||||
|
>
|
||||||
|
<Wifi />
|
||||||
|
</IconBadge>
|
||||||
|
<DonutIllustration className="absolute -right-2 bottom-4 w-24 drop-shadow-lg sm:right-0" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HeroVisual, StatusChip }
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const iconBadgeVariants = cva(
|
||||||
|
"flex shrink-0 items-center justify-center [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
/* soft teal disc */
|
||||||
|
soft: "bg-accent text-brand-700",
|
||||||
|
/* white disc with hairline + teal line icon (feature rows) */
|
||||||
|
outline: "border border-brand-100 bg-card text-primary shadow-pill",
|
||||||
|
solid: "bg-primary text-primary-foreground",
|
||||||
|
navy: "bg-navy text-white",
|
||||||
|
/* translucent white on gradient bands */
|
||||||
|
glass: "bg-white/15 text-white ring-1 ring-white/25",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: "size-9 [&_svg]:size-4",
|
||||||
|
md: "size-11 [&_svg]:size-5",
|
||||||
|
lg: "size-14 [&_svg]:size-6",
|
||||||
|
xl: "size-16 [&_svg]:size-7",
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
circle: "rounded-full",
|
||||||
|
squircle: "rounded-xl",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "soft",
|
||||||
|
size: "md",
|
||||||
|
shape: "circle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function IconBadge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
shape,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof iconBadgeVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(iconBadgeVariants({ variant, size, shape }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon-over-label tile used in the feature rows underneath heroes
|
||||||
|
* (UVI · Heizkostenabrechnung · Rauchwarnmelder · …).
|
||||||
|
*/
|
||||||
|
function FeatureTile({
|
||||||
|
icon,
|
||||||
|
media,
|
||||||
|
title,
|
||||||
|
sub,
|
||||||
|
align = "center",
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
/** self-contained visual (e.g. asset-kit icon) rendered without IconBadge */
|
||||||
|
media?: React.ReactNode
|
||||||
|
title: React.ReactNode
|
||||||
|
sub?: React.ReactNode
|
||||||
|
align?: "center" | "start"
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-2.5",
|
||||||
|
align === "center" ? "items-center text-center" : "items-start",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{media ?? (
|
||||||
|
<IconBadge variant="outline" shape="squircle" size="lg">
|
||||||
|
{icon}
|
||||||
|
</IconBadge>
|
||||||
|
)}
|
||||||
|
<div className="text-sm font-bold text-navy">{title}</div>
|
||||||
|
{sub ? (
|
||||||
|
<p className="-mt-1.5 text-xs leading-snug text-muted-foreground">
|
||||||
|
{sub}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { IconBadge, iconBadgeVariants, FeatureTile }
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight SVG stand-ins for the 3D renders in the designs.
|
||||||
|
* All of them draw exclusively from the chart/brand tokens.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Smoke detector: white disc, vent ring, teal status LED. */
|
||||||
|
function SmokeDetectorIllustration({ className }: { className?: string }) {
|
||||||
|
const vents = Array.from({ length: 12 }, (_, i) => {
|
||||||
|
const a = (i / 12) * Math.PI * 2
|
||||||
|
return (
|
||||||
|
<circle
|
||||||
|
key={i}
|
||||||
|
cx={60 + Math.cos(a) * 30}
|
||||||
|
cy={60 + Math.sin(a) * 30}
|
||||||
|
r="3.2"
|
||||||
|
fill="var(--muted)"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 120 120" aria-hidden="true" className={className}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="sd-body" cx="0.35" cy="0.3" r="0.9">
|
||||||
|
<stop offset="0" stopColor="#ffffff" />
|
||||||
|
<stop offset="0.75" stopColor="#f2f6f7" />
|
||||||
|
<stop offset="1" stopColor="#dde7e9" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="60" cy="64" r="52" fill="rgb(19 41 60 / 0.08)" />
|
||||||
|
<circle cx="60" cy="60" r="52" fill="url(#sd-body)" />
|
||||||
|
<circle
|
||||||
|
cx="60"
|
||||||
|
cy="60"
|
||||||
|
r="41"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgb(19 41 60 / 0.08)"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
{vents}
|
||||||
|
<circle cx="60" cy="60" r="12" fill="#eef3f4" />
|
||||||
|
<circle cx="60" cy="60" r="4" fill="var(--brand-500)" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cluster of translucent isometric sensor cubes (UVI). */
|
||||||
|
function SensorCubesIllustration({ className }: { className?: string }) {
|
||||||
|
const cube = (x: number, y: number, s: number, o: number) => (
|
||||||
|
<g transform={`translate(${x} ${y}) scale(${s})`} opacity={o}>
|
||||||
|
<path d="M0 10 20 0l20 10-20 10Z" fill="var(--brand-300)" />
|
||||||
|
<path d="M0 10v22l20 10V20Z" fill="var(--brand-500)" />
|
||||||
|
<path d="M40 10v22L20 42V20Z" fill="var(--brand-600)" />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 120 120" aria-hidden="true" className={className}>
|
||||||
|
<g opacity="0.9">
|
||||||
|
{cube(12, 12, 1.1, 0.55)}
|
||||||
|
{cube(58, 30, 1.0, 0.8)}
|
||||||
|
{cube(26, 56, 1.3, 1)}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Donut chart in brand tones (portfolio / reporting motif). */
|
||||||
|
function DonutIllustration({ className }: { className?: string }) {
|
||||||
|
/* three segments: 55% teal, 25% light, 20% pale */
|
||||||
|
const C = 2 * Math.PI * 40
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 120 120" aria-hidden="true" className={className}>
|
||||||
|
<g transform="rotate(-90 60 60)">
|
||||||
|
<circle
|
||||||
|
cx="60"
|
||||||
|
cy="60"
|
||||||
|
r="40"
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--chart-3)"
|
||||||
|
strokeWidth="22"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="60"
|
||||||
|
cy="60"
|
||||||
|
r="40"
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--chart-2)"
|
||||||
|
strokeWidth="22"
|
||||||
|
strokeDasharray={`${C * 0.25} ${C}`}
|
||||||
|
strokeDashoffset={-C * 0.55}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="60"
|
||||||
|
cy="60"
|
||||||
|
r="40"
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--chart-1)"
|
||||||
|
strokeWidth="22"
|
||||||
|
strokeDasharray={`${C * 0.55} ${C}`}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stylised apartment building: white slabs, window grid, teal accents. */
|
||||||
|
function BuildingIllustration({ className }: { className?: string }) {
|
||||||
|
const windows: React.ReactNode[] = []
|
||||||
|
for (let row = 0; row < 5; row++) {
|
||||||
|
for (let col = 0; col < 4; col++) {
|
||||||
|
windows.push(
|
||||||
|
<rect
|
||||||
|
key={`${row}-${col}`}
|
||||||
|
x={34 + col * 22}
|
||||||
|
y={36 + row * 26}
|
||||||
|
width="14"
|
||||||
|
height="16"
|
||||||
|
rx="2"
|
||||||
|
fill={row === 1 && col === 2 ? "var(--brand-300)" : "#dbe6ea"}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 160 200" aria-hidden="true" className={className}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bld-face" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stopColor="#ffffff" />
|
||||||
|
<stop offset="1" stopColor="#e9f0f2" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<ellipse cx="80" cy="188" rx="72" ry="10" fill="rgb(19 41 60 / 0.08)" />
|
||||||
|
<rect x="24" y="22" width="112" height="166" rx="8" fill="url(#bld-face)" />
|
||||||
|
<rect x="24" y="22" width="112" height="10" rx="5" fill="#f7fafb" />
|
||||||
|
{windows}
|
||||||
|
{/* balcony bands */}
|
||||||
|
<rect x="28" y="60" width="104" height="3" rx="1.5" fill="#cfdde1" />
|
||||||
|
<rect x="28" y="112" width="104" height="3" rx="1.5" fill="#cfdde1" />
|
||||||
|
{/* entrance */}
|
||||||
|
<rect x="70" y="160" width="20" height="28" rx="3" fill="var(--navy)" opacity="0.85" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
SmokeDetectorIllustration,
|
||||||
|
SensorCubesIllustration,
|
||||||
|
DonutIllustration,
|
||||||
|
BuildingIllustration,
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import mark3dAvif from "@/assets/gebos/brand-mark-3d-96.avif"
|
||||||
|
import mark3dWebp from "@/assets/gebos/brand-mark-3d-96.webp"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GebOS brand mark: isometric cube inside a hexagon, teal gradient stroke,
|
||||||
|
* followed by the two-tone wordmark ("Geb" navy / "OS" teal).
|
||||||
|
*/
|
||||||
|
function LogoMark({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("size-8", className)}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gebos-mark" x1="8" y1="6" x2="42" y2="42">
|
||||||
|
<stop offset="0" stopColor="var(--brand-400)" />
|
||||||
|
<stop offset="0.55" stopColor="var(--brand-600)" />
|
||||||
|
<stop offset="1" stopColor="var(--navy)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{/* top face */}
|
||||||
|
<path
|
||||||
|
d="M24 5.5 40.5 15 24 24.5 7.5 15Z"
|
||||||
|
fill="var(--brand-100)"
|
||||||
|
opacity="0.7"
|
||||||
|
/>
|
||||||
|
{/* hexagon silhouette */}
|
||||||
|
<path
|
||||||
|
d="M24 4.5 41 14.3v19.4L24 43.5 7 33.7V14.3Z"
|
||||||
|
stroke="url(#gebos-mark)"
|
||||||
|
strokeWidth="3.4"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
{/* inner cube edges */}
|
||||||
|
<path
|
||||||
|
d="M7.6 14.7 24 24.2m0 0 16.4-9.5M24 24.2v18.6"
|
||||||
|
stroke="url(#gebos-mark)"
|
||||||
|
strokeWidth="3.4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 3D glass brand mark (rendered asset, genuine alpha). */
|
||||||
|
function LogoMark3d({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<picture>
|
||||||
|
<source srcSet={mark3dAvif} type="image/avif" />
|
||||||
|
<img
|
||||||
|
src={mark3dWebp}
|
||||||
|
alt=""
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
className={cn("size-8 object-contain", className)}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Logo({
|
||||||
|
className,
|
||||||
|
markClassName,
|
||||||
|
inverse = false,
|
||||||
|
mark = "line",
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
markClassName?: string
|
||||||
|
inverse?: boolean
|
||||||
|
/** "line" = flat SVG mark, "3d" = glass render */
|
||||||
|
mark?: "line" | "3d"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span className={cn("inline-flex items-center gap-2", className)}>
|
||||||
|
{mark === "3d" ? (
|
||||||
|
<LogoMark3d className={markClassName} />
|
||||||
|
) : (
|
||||||
|
<LogoMark className={markClassName} />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[1.35rem] leading-none font-extrabold tracking-tight",
|
||||||
|
inverse ? "text-white" : "text-navy"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Geb
|
||||||
|
<span className={inverse ? "text-brand-300" : "text-primary"}>OS</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Logo, LogoMark, LogoMark3d }
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Fragment } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb"
|
||||||
|
|
||||||
|
export interface Crumb {
|
||||||
|
label: string
|
||||||
|
/** omit on the last crumb – it renders as the current page */
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site-wide breadcrumb trail: parent chain → current page.
|
||||||
|
* Top-level pages start at "Startseite", audience pages at "Für wen?".
|
||||||
|
*/
|
||||||
|
function PageBreadcrumb({
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
items: Crumb[]
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList className={className}>
|
||||||
|
{items.map((item, i) => {
|
||||||
|
const last = i === items.length - 1
|
||||||
|
return (
|
||||||
|
<Fragment key={item.label}>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
{last || !item.to ? (
|
||||||
|
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
||||||
|
) : (
|
||||||
|
<BreadcrumbLink asChild>
|
||||||
|
<Link to={item.to}>{item.label}</Link>
|
||||||
|
</BreadcrumbLink>
|
||||||
|
)}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
{last ? null : <BreadcrumbSeparator />}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PageBreadcrumb }
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Container } from "@/components/gebos/section"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split hero: copy left (breadcrumb/eyebrow, H1, lead, CTAs, extra),
|
||||||
|
* visual right, soft radial glow behind.
|
||||||
|
*/
|
||||||
|
function PageHero({
|
||||||
|
breadcrumb,
|
||||||
|
title,
|
||||||
|
lead,
|
||||||
|
actions,
|
||||||
|
extra,
|
||||||
|
visual,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
breadcrumb?: React.ReactNode
|
||||||
|
title: React.ReactNode
|
||||||
|
lead?: React.ReactNode
|
||||||
|
actions?: React.ReactNode
|
||||||
|
/** slot below the actions (e.g. hint pill) */
|
||||||
|
extra?: React.ReactNode
|
||||||
|
visual?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn("bg-hero-glow", className)}>
|
||||||
|
{breadcrumb ? (
|
||||||
|
<Container className="pt-6 lg:pt-8">{breadcrumb}</Container>
|
||||||
|
) : null}
|
||||||
|
<Container
|
||||||
|
className={cn(
|
||||||
|
"grid items-center gap-10 pb-14 sm:pb-16 lg:grid-cols-[1.05fr_0.95fr] lg:gap-6 lg:pb-20",
|
||||||
|
/* breadcrumb already provides the top offset */
|
||||||
|
breadcrumb ? "pt-5" : "pt-14 sm:pt-16 lg:pt-20"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn("max-w-xl", breadcrumb && "lg:self-start")}>
|
||||||
|
<h1 className="text-4xl font-extrabold tracking-tight text-balance text-navy sm:text-5xl">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{lead ? (
|
||||||
|
<p className="mt-4 max-w-lg text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
|
||||||
|
{lead}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{actions ? (
|
||||||
|
<div className="mt-7 flex flex-wrap items-center gap-3">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{extra ? <div className="mt-6">{extra}</div> : null}
|
||||||
|
</div>
|
||||||
|
{visual ? <div className="relative w-full">{visual}</div> : null}
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PageHero }
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Ban, CircleCheck } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Klarpreis pricing card:
|
||||||
|
* title · cadence pill · large price + unit · allocability pill ·
|
||||||
|
* divider · description. Optional media floats top-right.
|
||||||
|
*/
|
||||||
|
function PriceCard({
|
||||||
|
title,
|
||||||
|
cadence,
|
||||||
|
price,
|
||||||
|
unit,
|
||||||
|
allocation,
|
||||||
|
description,
|
||||||
|
media,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
title: React.ReactNode
|
||||||
|
/** "Einmalig" | "Monatlich" pill under the title */
|
||||||
|
cadence: React.ReactNode
|
||||||
|
price: React.ReactNode
|
||||||
|
unit: React.ReactNode
|
||||||
|
/** umlagefähig? tone drives soft-teal vs. soft-red pill */
|
||||||
|
allocation: { label: React.ReactNode; tone: "positive" | "negative" }
|
||||||
|
description: React.ReactNode
|
||||||
|
media?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className={cn("relative gap-0 p-6 sm:p-7", className)}>
|
||||||
|
{media ? (
|
||||||
|
<div className="pointer-events-none absolute top-6 right-6 w-24 sm:w-28">
|
||||||
|
{media}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<h3 className="pr-24 text-xl font-bold tracking-tight text-navy">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<Badge className="mt-2.5">{cadence}</Badge>
|
||||||
|
<div className="mt-5 text-[2.6rem] leading-none font-extrabold tracking-tight text-navy">
|
||||||
|
{price}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1.5 text-sm text-muted-foreground">{unit}</div>
|
||||||
|
<Badge
|
||||||
|
variant={allocation.tone === "positive" ? "default" : "destructive"}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
{allocation.tone === "positive" ? <CircleCheck /> : <Ban />}
|
||||||
|
{allocation.label}
|
||||||
|
</Badge>
|
||||||
|
<Separator className="mt-5 mb-4" />
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PriceCard }
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
|
||||||
|
export interface ProcessStep {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
/** self-contained visual (asset render/vector) shown instead of IconBadge */
|
||||||
|
media?: React.ReactNode
|
||||||
|
title: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numbered process row ("So funktioniert GebOS", steps 1–5):
|
||||||
|
* numbered chips joined by a dotted teal line, device icon, title, copy.
|
||||||
|
*/
|
||||||
|
function ProcessSteps({
|
||||||
|
steps,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
steps: ProcessStep[]
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
className={cn(
|
||||||
|
"relative grid gap-x-6 gap-y-10 sm:grid-cols-2",
|
||||||
|
steps.length >= 5 ? "lg:grid-cols-5" : "lg:grid-cols-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-[10%] top-3.5 hidden border-t-2 border-dotted-brand lg:block"
|
||||||
|
/>
|
||||||
|
{steps.map((step, i) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className="relative flex flex-col items-center gap-3 text-center"
|
||||||
|
>
|
||||||
|
<span className="z-10 flex size-7 items-center justify-center rounded-full bg-card text-xs font-bold text-primary shadow-pill ring-2 ring-brand-300">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
{step.media ?? (
|
||||||
|
<IconBadge variant="outline" shape="squircle" size="xl">
|
||||||
|
{step.icon}
|
||||||
|
</IconBadge>
|
||||||
|
)}
|
||||||
|
<h3 className="text-sm font-bold text-navy">{step.title}</h3>
|
||||||
|
{step.description ? (
|
||||||
|
<p className="-mt-1 max-w-[16rem] text-xs leading-relaxed text-muted-foreground">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical variant used by the Konfigurator overview (steps 1–4).
|
||||||
|
*/
|
||||||
|
function NumberedList({
|
||||||
|
steps,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
steps: { title: React.ReactNode; description?: React.ReactNode }[]
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ol className={cn("relative flex flex-col gap-6", className)}>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute top-2 bottom-2 left-3.5 border-l-2 border-dotted-brand"
|
||||||
|
/>
|
||||||
|
{steps.map((step, i) => (
|
||||||
|
<li key={i} className="relative flex items-start gap-4">
|
||||||
|
<span className="z-10 flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground shadow-pill">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
<div className="pt-0.5">
|
||||||
|
<h3 className="text-sm font-bold text-navy">{step.title}</h3>
|
||||||
|
{step.description ? (
|
||||||
|
<p className="mt-0.5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ProcessSteps, NumberedList }
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Container({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Section({ className, ...props }: React.ComponentProps<"section">) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn("scroll-mt-20 py-14 sm:py-18 lg:py-20", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard section intro: optional teal eyebrow, navy title, muted lead.
|
||||||
|
*/
|
||||||
|
function SectionHeading({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
lead,
|
||||||
|
align = "left",
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
eyebrow?: React.ReactNode
|
||||||
|
title: React.ReactNode
|
||||||
|
lead?: React.ReactNode
|
||||||
|
align?: "left" | "center"
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"max-w-2xl",
|
||||||
|
align === "center" && "mx-auto text-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{eyebrow ? (
|
||||||
|
<p className="mb-2 text-xs font-bold tracking-[0.14em] text-primary uppercase">
|
||||||
|
{eyebrow}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<h2 className="text-3xl font-extrabold tracking-tight text-balance text-navy sm:text-4xl">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{lead ? (
|
||||||
|
<p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
|
||||||
|
{lead}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Container, Section, SectionHeading }
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
import barChartAvif from "@/assets/gebos/bar-chart.avif"
|
||||||
|
import barChartWebp from "@/assets/gebos/bar-chart.webp"
|
||||||
|
import brandCubeAvif from "@/assets/gebos/brand-cube-render.avif"
|
||||||
|
import brandCubeWebp from "@/assets/gebos/brand-cube-render.webp"
|
||||||
|
import brandMark3dAvif from "@/assets/gebos/brand-mark-3d.avif"
|
||||||
|
import brandMark3dWebp from "@/assets/gebos/brand-mark-3d.webp"
|
||||||
|
import brandMarkFlatAvif from "@/assets/gebos/brand-mark-flat.avif"
|
||||||
|
import brandMarkFlatWebp from "@/assets/gebos/brand-mark-flat.webp"
|
||||||
|
import buildingAlphaAvif from "@/assets/gebos/building-alpha.avif"
|
||||||
|
import buildingAlphaWebp from "@/assets/gebos/building-alpha.webp"
|
||||||
|
import buildingIsoAvif from "@/assets/gebos/building-iso.avif"
|
||||||
|
import buildingIsoWebp from "@/assets/gebos/building-iso.webp"
|
||||||
|
import buildingAvif from "@/assets/gebos/building.avif"
|
||||||
|
import buildingWebp from "@/assets/gebos/building.webp"
|
||||||
|
import checkOrb from "@/assets/gebos/check-orb.svg"
|
||||||
|
import cloudSupport from "@/assets/gebos/cloud-support.svg"
|
||||||
|
import consumptionRing from "@/assets/gebos/consumption-ring.svg"
|
||||||
|
import cubesRoundAvif from "@/assets/gebos/cubes-round.avif"
|
||||||
|
import cubesRoundWebp from "@/assets/gebos/cubes-round.webp"
|
||||||
|
import drillToolAvif from "@/assets/gebos/drill-tool.avif"
|
||||||
|
import drillToolWebp from "@/assets/gebos/drill-tool.webp"
|
||||||
|
import euroOrb from "@/assets/gebos/euro-orb.svg"
|
||||||
|
import gatewayAvif from "@/assets/gebos/gateway.avif"
|
||||||
|
import gatewayWebp from "@/assets/gebos/gateway.webp"
|
||||||
|
import glassCubes from "@/assets/gebos/glass-cubes.svg"
|
||||||
|
import glassCubesStackAvif from "@/assets/gebos/glass-cubes-stack.avif"
|
||||||
|
import glassCubesStackWebp from "@/assets/gebos/glass-cubes-stack.webp"
|
||||||
|
import heatMeterAvif from "@/assets/gebos/heat-meter.avif"
|
||||||
|
import heatMeterWebp from "@/assets/gebos/heat-meter.webp"
|
||||||
|
import iconBilling from "@/assets/gebos/icon-billing.svg"
|
||||||
|
import iconPortal from "@/assets/gebos/icon-portal.svg"
|
||||||
|
import iconRadio from "@/assets/gebos/icon-radio.svg"
|
||||||
|
import iconSmokeAlarm from "@/assets/gebos/icon-smoke-alarm.svg"
|
||||||
|
import iconUvi from "@/assets/gebos/icon-uvi.svg"
|
||||||
|
import laptopAvif from "@/assets/gebos/laptop-dashboard.avif"
|
||||||
|
import laptopWebp from "@/assets/gebos/laptop-dashboard.webp"
|
||||||
|
import packageBox from "@/assets/gebos/package-box.svg"
|
||||||
|
import shippingBoxClosedAvif from "@/assets/gebos/shipping-box-closed.avif"
|
||||||
|
import shippingBoxClosedWebp from "@/assets/gebos/shipping-box-closed.webp"
|
||||||
|
import shippingBoxMetersAvif from "@/assets/gebos/shipping-box-meters.avif"
|
||||||
|
import shippingBoxMetersWebp from "@/assets/gebos/shipping-box-meters.webp"
|
||||||
|
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 "@/assets/gebos/uvi-badge.svg"
|
||||||
|
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"
|
||||||
|
import wordmarkGlowAvif from "@/assets/gebos/wordmark-cubes-glow.avif"
|
||||||
|
import wordmarkGlowWebp from "@/assets/gebos/wordmark-cubes-glow.webp"
|
||||||
|
import workerTabletAvif from "@/assets/gebos/worker-tablet.avif"
|
||||||
|
import workerTabletWebp from "@/assets/gebos/worker-tablet.webp"
|
||||||
|
|
||||||
|
export interface RenderAsset {
|
||||||
|
avif: string
|
||||||
|
webp: string
|
||||||
|
alt: string
|
||||||
|
/**
|
||||||
|
* "white" – pure-white matte, needs multiply blending to sit on light surfaces.
|
||||||
|
* "alpha" – genuine transparency, composited as-is.
|
||||||
|
*/
|
||||||
|
matte: "white" | "alpha"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Photographic / 3D renders. Always draw through <AssetRender>, which picks
|
||||||
|
* the right compositing mode for the asset's matte.
|
||||||
|
*/
|
||||||
|
const renders = {
|
||||||
|
/* --- site kit: white-matte masters --------------------------------- */
|
||||||
|
building: {
|
||||||
|
avif: buildingAvif,
|
||||||
|
webp: buildingWebp,
|
||||||
|
alt: "Modernes Mehrfamilienhaus",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
smokeAlarm: {
|
||||||
|
avif: smokeAlarmAvif,
|
||||||
|
webp: smokeAlarmWebp,
|
||||||
|
alt: "Rauchwarnmelder",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
avif: gatewayAvif,
|
||||||
|
webp: gatewayWebp,
|
||||||
|
alt: "GebOS Gateway",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
heatMeter: {
|
||||||
|
avif: heatMeterAvif,
|
||||||
|
webp: heatMeterWebp,
|
||||||
|
alt: "Wärmemengenzähler",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
laptopDashboard: {
|
||||||
|
avif: laptopAvif,
|
||||||
|
webp: laptopWebp,
|
||||||
|
alt: "GebOS Portal auf dem Laptop",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
brandCube: {
|
||||||
|
avif: brandCubeAvif,
|
||||||
|
webp: brandCubeWebp,
|
||||||
|
alt: "",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
shippingBoxMeters: {
|
||||||
|
avif: shippingBoxMetersAvif,
|
||||||
|
webp: shippingBoxMetersWebp,
|
||||||
|
alt: "Objektbezogen vorbereitete Hardware",
|
||||||
|
matte: "white",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* --- transparent renders (free placement, any background) ---------- */
|
||||||
|
/** same subject as `building`, but with genuine alpha – safe on any surface */
|
||||||
|
buildingAlpha: {
|
||||||
|
avif: buildingAlphaAvif,
|
||||||
|
webp: buildingAlphaWebp,
|
||||||
|
alt: "Modernes Mehrfamilienhaus",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
/** isometric corner view of the same building type, genuine alpha */
|
||||||
|
buildingIso: {
|
||||||
|
avif: buildingIsoAvif,
|
||||||
|
webp: buildingIsoWebp,
|
||||||
|
alt: "Modernes Mehrfamilienhaus",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
brandMark3d: {
|
||||||
|
avif: brandMark3dAvif,
|
||||||
|
webp: brandMark3dWebp,
|
||||||
|
alt: "GebOS Markenzeichen",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
brandMarkFlat: {
|
||||||
|
avif: brandMarkFlatAvif,
|
||||||
|
webp: brandMarkFlatWebp,
|
||||||
|
alt: "GebOS Markenzeichen",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
glassCubesStack: {
|
||||||
|
avif: glassCubesStackAvif,
|
||||||
|
webp: glassCubesStackWebp,
|
||||||
|
alt: "",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
cubesRound: {
|
||||||
|
avif: cubesRoundAvif,
|
||||||
|
webp: cubesRoundWebp,
|
||||||
|
alt: "",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
barChart: {
|
||||||
|
avif: barChartAvif,
|
||||||
|
webp: barChartWebp,
|
||||||
|
alt: "",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
drillTool: {
|
||||||
|
avif: drillToolAvif,
|
||||||
|
webp: drillToolWebp,
|
||||||
|
alt: "",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
workerTablet: {
|
||||||
|
avif: workerTabletAvif,
|
||||||
|
webp: workerTabletWebp,
|
||||||
|
alt: "Monteur mit Tablet",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
shippingBoxClosed: {
|
||||||
|
avif: shippingBoxClosedAvif,
|
||||||
|
webp: shippingBoxClosedWebp,
|
||||||
|
alt: "Vorkonfigurierte Lieferung",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
/** baked light glow – use on white/near-white surfaces */
|
||||||
|
wordmarkGlow: {
|
||||||
|
avif: wordmarkGlowAvif,
|
||||||
|
webp: wordmarkGlowWebp,
|
||||||
|
alt: "GebOS",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
/** baked dark glow – use on navy/dark surfaces */
|
||||||
|
wordmarkDark: {
|
||||||
|
avif: wordmarkDarkAvif,
|
||||||
|
webp: wordmarkDarkWebp,
|
||||||
|
alt: "GebOS",
|
||||||
|
matte: "alpha",
|
||||||
|
},
|
||||||
|
} satisfies Record<string, RenderAsset>
|
||||||
|
|
||||||
|
/** Transparent brand vectors (decorative; render as plain <img alt="">). */
|
||||||
|
const vectors = {
|
||||||
|
tealBars,
|
||||||
|
uviBadge,
|
||||||
|
consumptionRing,
|
||||||
|
glassCubes,
|
||||||
|
checkOrb,
|
||||||
|
euroOrb,
|
||||||
|
cloudSupport,
|
||||||
|
packageBox,
|
||||||
|
wirelessOrb,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Feature icons with baked-in pale disc (use in FeatureTile `media`). */
|
||||||
|
const icons = {
|
||||||
|
uvi: iconUvi,
|
||||||
|
billing: iconBilling,
|
||||||
|
smokeAlarm: iconSmokeAlarm,
|
||||||
|
radio: iconRadio,
|
||||||
|
portal: iconPortal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a kit asset as AVIF→WebP <picture>. White-matte masters get
|
||||||
|
* mix-blend-multiply so their matte disappears on light surfaces;
|
||||||
|
* transparent assets composite normally.
|
||||||
|
*/
|
||||||
|
function AssetRender({
|
||||||
|
render,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
imgClassName,
|
||||||
|
loading = "lazy",
|
||||||
|
}: {
|
||||||
|
render: RenderAsset
|
||||||
|
/** override the registry alt (e.g. decorative usage) */
|
||||||
|
alt?: string
|
||||||
|
className?: string
|
||||||
|
imgClassName?: string
|
||||||
|
loading?: "lazy" | "eager"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<picture className={className}>
|
||||||
|
<source srcSet={render.avif} type="image/avif" />
|
||||||
|
<img
|
||||||
|
src={render.webp}
|
||||||
|
alt={alt ?? render.alt}
|
||||||
|
loading={loading}
|
||||||
|
className={cn(
|
||||||
|
"object-contain",
|
||||||
|
render.matte === "white" && "mix-blend-multiply",
|
||||||
|
imgClassName
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AssetRender, icons, renders, vectors }
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
import { Logo } from "@/components/gebos/logo"
|
||||||
|
import { Container } from "@/components/gebos/section"
|
||||||
|
|
||||||
|
const COLUMNS: { title: string; links: { to: string; label: string }[] }[] = [
|
||||||
|
{
|
||||||
|
title: "Produkt",
|
||||||
|
links: [
|
||||||
|
{ to: "/loesungen", label: "Lösungen" },
|
||||||
|
{ to: "/so-funktionierts", label: "So funktioniert’s" },
|
||||||
|
{ to: "/konfigurator", label: "Konfigurator" },
|
||||||
|
{ to: "/klarpreis", label: "GebOS Klarpreis" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Für wen?",
|
||||||
|
links: [
|
||||||
|
{ to: "/fuer-wen", label: "Übersicht" },
|
||||||
|
{ to: "/fuer-wen/hauseigentuemer", label: "Hauseigentümer" },
|
||||||
|
{ to: "/fuer-wen/hausverwaltungen", label: "Hausverwaltungen" },
|
||||||
|
{ to: "/fuer-wen/messdienstleister", label: "Messdienstleister" },
|
||||||
|
{ to: "/fuer-wen/installateure", label: "Installateure & Heizungsbauer" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Unternehmen",
|
||||||
|
links: [
|
||||||
|
{ to: "/unternehmen", label: "Über GebOS" },
|
||||||
|
{ to: "/kontakt", label: "Kontakt" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function SiteFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="mt-20 bg-navy text-white">
|
||||||
|
<Container className="grid gap-10 py-14 sm:grid-cols-2 lg:grid-cols-[1.2fr_1fr_1fr_1fr]">
|
||||||
|
<div>
|
||||||
|
<Logo inverse />
|
||||||
|
<p className="mt-4 max-w-xs text-sm leading-relaxed text-white/60">
|
||||||
|
Messdienstleistungen einfach gemacht – von der vorkonfigurierten
|
||||||
|
Hardware bis zum laufenden Betrieb.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{COLUMNS.map((col) => (
|
||||||
|
<nav key={col.title} aria-label={col.title}>
|
||||||
|
<h3 className="text-xs font-bold tracking-[0.14em] text-brand-300 uppercase">
|
||||||
|
{col.title}
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 flex flex-col gap-2.5">
|
||||||
|
{col.links.map((l) => (
|
||||||
|
<li key={l.to}>
|
||||||
|
<Link
|
||||||
|
to={l.to}
|
||||||
|
className="text-sm text-white/75 transition-colors hover:text-white"
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
<div className="border-t border-white/10">
|
||||||
|
<Container className="flex flex-wrap items-center justify-between gap-3 py-5 text-xs text-white/50">
|
||||||
|
<span>© 2026 GebOS. Alle Rechte vorbehalten.</span>
|
||||||
|
<span>Impressum · Datenschutz · AGB</span>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SiteFooter }
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { ChevronDown, Menu } from "lucide-react"
|
||||||
|
import { Link, NavLink, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { Logo } from "@/components/gebos/logo"
|
||||||
|
import { Container } from "@/components/gebos/section"
|
||||||
|
|
||||||
|
const AUDIENCES = [
|
||||||
|
{ to: "/fuer-wen/hauseigentuemer", label: "Hauseigentümer" },
|
||||||
|
{ to: "/fuer-wen/hausverwaltungen", label: "Hausverwaltungen" },
|
||||||
|
{ to: "/fuer-wen/messdienstleister", label: "Messdienstleister" },
|
||||||
|
{ to: "/fuer-wen/installateure", label: "Installateure & Heizungsbauer" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const NAV_LINKS = [
|
||||||
|
{ to: "/loesungen", label: "Lösungen" },
|
||||||
|
{ to: "/so-funktionierts", label: "So funktioniert’s" },
|
||||||
|
/* "Für wen?" rendered separately as dropdown */
|
||||||
|
{ to: "/konfigurator", label: "Konfigurator" },
|
||||||
|
{ to: "/unternehmen", label: "Unternehmen" },
|
||||||
|
{ to: "/kontakt", label: "Kontakt" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const navItemClass = (active: boolean) =>
|
||||||
|
cn(
|
||||||
|
"rounded-full px-3 py-1.5 text-sm font-medium transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-accent font-semibold text-brand-800 ring-1 ring-brand-200"
|
||||||
|
: "text-foreground/70 hover:text-navy"
|
||||||
|
)
|
||||||
|
|
||||||
|
function ForWhomMenu() {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const active = pathname.startsWith("/fuer-wen")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
className={cn(
|
||||||
|
navItemClass(active),
|
||||||
|
"inline-flex cursor-pointer items-center gap-1 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Für wen?
|
||||||
|
<ChevronDown className="size-3.5 opacity-60" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/fuer-wen">Übersicht</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{AUDIENCES.map((a) => (
|
||||||
|
<DropdownMenuItem key={a.to} asChild>
|
||||||
|
<Link to={a.to}>{a.label}</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileMenu() {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon" className="lg:hidden">
|
||||||
|
<Menu />
|
||||||
|
<span className="sr-only">Menü öffnen</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
|
{NAV_LINKS.slice(0, 2).map((l) => (
|
||||||
|
<DropdownMenuItem key={l.to} asChild>
|
||||||
|
<Link to={l.to}>{l.label}</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>Für wen?</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/fuer-wen">Übersicht</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{AUDIENCES.map((a) => (
|
||||||
|
<DropdownMenuItem key={a.to} asChild>
|
||||||
|
<Link to={a.to}>{a.label}</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{NAV_LINKS.slice(2).map((l) => (
|
||||||
|
<DropdownMenuItem key={l.to} asChild>
|
||||||
|
<Link to={l.to}>{l.label}</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/klarpreis">Klarpreis</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-40 border-b border-border/60 bg-background/85 backdrop-blur">
|
||||||
|
<Container className="flex h-16 items-center justify-between gap-4">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="rounded-lg outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
||||||
|
>
|
||||||
|
<Logo mark="3d" markClassName="size-9" />
|
||||||
|
<span className="sr-only">GebOS Startseite</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="hidden items-center gap-1 lg:flex">
|
||||||
|
{NAV_LINKS.slice(0, 2).map((l) => (
|
||||||
|
<NavLink key={l.to} to={l.to} className={({ isActive }) => navItemClass(isActive)}>
|
||||||
|
{l.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
<ForWhomMenu />
|
||||||
|
{NAV_LINKS.slice(2).map((l) => (
|
||||||
|
<NavLink key={l.to} to={l.to} className={({ isActive }) => navItemClass(isActive)}>
|
||||||
|
{l.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button asChild size="sm" className="hidden sm:inline-flex">
|
||||||
|
<Link to="/konfigurator">Preis berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
<MobileMenu />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SiteHeader }
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b border-border/70 last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-semibold transition-all outline-none hover:text-primary focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4 text-muted-foreground", className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex w-fit shrink-0 items-center justify-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold whitespace-nowrap transition-colors [&>svg]:pointer-events-none [&>svg]:size-3.5",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
/* soft teal pill – "Monatlich", "Grundsätzlich umlagefähig" */
|
||||||
|
default: "border-transparent bg-accent text-accent-foreground",
|
||||||
|
/* solid teal pill */
|
||||||
|
solid: "border-transparent bg-primary text-primary-foreground",
|
||||||
|
/* soft red pill – "Nicht umlagefähig" */
|
||||||
|
destructive: "border-transparent bg-destructive/10 text-destructive",
|
||||||
|
outline: "border-border bg-card text-foreground shadow-pill",
|
||||||
|
muted: "border-transparent bg-muted text-muted-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 text-xs font-semibold tracking-wide break-words text-muted-foreground sm:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="breadcrumb-link"
|
||||||
|
className={cn("transition-colors hover:text-primary", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-semibold text-primary", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">Mehr</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-sm hover:bg-brand-700",
|
||||||
|
outline:
|
||||||
|
"border border-border bg-card text-foreground shadow-pill hover:border-brand-200 hover:bg-brand-50",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-brand-100",
|
||||||
|
ghost: "text-foreground hover:bg-secondary",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
/* white button sitting on the teal gradient band */
|
||||||
|
inverse: "bg-white text-brand-800 shadow-sm hover:bg-brand-50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-5 py-2",
|
||||||
|
sm: "h-9 gap-1.5 px-4",
|
||||||
|
lg: "h-11 px-6 text-[15px]",
|
||||||
|
icon: "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-6 rounded-2xl border border-border/70 bg-card py-6 text-card-foreground shadow-card",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"text-lg leading-snug font-bold tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm leading-relaxed text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn("self-start justify-self-end", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer size-5 shrink-0 cursor-pointer rounded-md border border-input bg-card shadow-pill transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="flex items-center justify-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 8,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border/70 bg-popover p-1.5 text-popover-foreground shadow-card-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer items-center gap-2 rounded-lg py-2 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer items-center gap-2 rounded-lg py-2 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-xs font-medium text-muted-foreground data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-xl border border-border/70 bg-popover p-1.5 text-popover-foreground shadow-card-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full min-w-0 rounded-lg border border-input bg-card px-3.5 py-2 text-sm shadow-pill transition-colors outline-none selection:bg-brand-200 selection:text-navy file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30",
|
||||||
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-semibold select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"flex w-fit cursor-pointer items-center justify-between gap-2 rounded-lg border border-input bg-card px-3.5 py-2 text-sm font-medium whitespace-nowrap shadow-pill transition-colors outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-10 data-[size=sm]:h-9 data-[placeholder]:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 text-muted-foreground" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border/70 bg-popover text-popover-foreground shadow-card-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1.5",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-xs font-medium text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-2 pr-8 pl-3 text-sm font-medium outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4 text-primary" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"border-b border-border/70 transition-colors hover:bg-brand-50/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"h-11 px-3 text-left align-middle text-sm font-bold whitespace-nowrap text-navy [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-3 align-middle text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn("flex flex-col gap-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn("flex flex-wrap items-center gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audience-switcher style triggers: white cards that fill teal when active
|
||||||
|
* (see "Für wen ist GebOS?" – Hauseigentümer / Hausverwaltungen / …).
|
||||||
|
*/
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-border/80 bg-card px-4 py-2.5 text-sm font-semibold whitespace-nowrap text-foreground shadow-pill transition-all outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
"hover:border-brand-200 hover:bg-brand-50",
|
||||||
|
"data-[state=active]:border-brand-700 data-[state=active]:bg-brand-700 data-[state=active]:text-white data-[state=active]:shadow-band",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from "react"
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
|
||||||
|
import "@fontsource-variable/inter"
|
||||||
|
import "@/styles/globals.css"
|
||||||
|
|
||||||
|
import App from "@/App"
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
|
||||||
|
import { PageHero } from "@/components/gebos/page-hero"
|
||||||
|
import { FeatureTile } from "@/components/gebos/icon-tile"
|
||||||
|
|
||||||
|
type AudienceFeature = {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
/** self-contained visual (asset-kit icon/vector) rendered without IconBadge */
|
||||||
|
media?: React.ReactNode
|
||||||
|
title: React.ReactNode
|
||||||
|
sub?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function AudienceBreadcrumb({ current }: { current: string }) {
|
||||||
|
return (
|
||||||
|
<PageBreadcrumb
|
||||||
|
items={[{ label: "Für wen?", to: "/fuer-wen" }, { label: current }]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared audience page template (tiles 5–8):
|
||||||
|
* split PageHero with breadcrumb, feature tile row, closing slot
|
||||||
|
* (teal CtaBanner or slim repeat-CTA section).
|
||||||
|
*/
|
||||||
|
function AudiencePage({
|
||||||
|
breadcrumb,
|
||||||
|
title,
|
||||||
|
tagline,
|
||||||
|
lead,
|
||||||
|
actions,
|
||||||
|
visual,
|
||||||
|
features,
|
||||||
|
children,
|
||||||
|
closing,
|
||||||
|
}: {
|
||||||
|
breadcrumb: string
|
||||||
|
title: React.ReactNode
|
||||||
|
/** short bold subline under the H1 (e.g. "Bestellen. Montieren. Fertig.") */
|
||||||
|
tagline?: React.ReactNode
|
||||||
|
lead?: React.ReactNode
|
||||||
|
actions?: React.ReactNode
|
||||||
|
visual?: React.ReactNode
|
||||||
|
features: AudienceFeature[]
|
||||||
|
/** additional full-width spec sections between feature row and closing */
|
||||||
|
children?: React.ReactNode
|
||||||
|
closing?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
breadcrumb={<AudienceBreadcrumb current={breadcrumb} />}
|
||||||
|
title={title}
|
||||||
|
lead={
|
||||||
|
<>
|
||||||
|
{tagline ? (
|
||||||
|
<span className="mb-2 block text-lg font-bold text-navy sm:text-xl">
|
||||||
|
{tagline}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{lead}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={actions}
|
||||||
|
visual={visual}
|
||||||
|
/>
|
||||||
|
<Container>
|
||||||
|
<Section className="pt-2 sm:pt-4 lg:pt-6">
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-4">
|
||||||
|
{features.map((feature, i) => (
|
||||||
|
<FeatureTile
|
||||||
|
key={i}
|
||||||
|
icon={feature.icon}
|
||||||
|
media={feature.media}
|
||||||
|
title={feature.title}
|
||||||
|
sub={feature.sub}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
{children}
|
||||||
|
{closing}
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slim closing section repeating the hero CTAs
|
||||||
|
* (tiles 7 and 8 close without a gradient band).
|
||||||
|
*/
|
||||||
|
function AudienceClosing({
|
||||||
|
title,
|
||||||
|
lead,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
title: React.ReactNode
|
||||||
|
lead?: React.ReactNode
|
||||||
|
actions: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading align="center" title={title} lead={lead} />
|
||||||
|
<div className="mt-7 flex flex-wrap items-center justify-center gap-3">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AudiencePage, AudienceClosing }
|
||||||
|
export type { AudienceFeature }
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Euro, Handshake, ReceiptText, ShieldCheck, Wrench } from "lucide-react"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
import { Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { CtaBanner } from "@/components/gebos/cta-banner"
|
||||||
|
import { NumberedList } from "@/components/gebos/process-steps"
|
||||||
|
import { AssetRender, renders } from "@/components/gebos/site-assets"
|
||||||
|
import { AudiencePage } from "@/pages/audiences/audience-page"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page-local hero composition (05-homeowners kit):
|
||||||
|
* building render on the right, smoke alarm top-left,
|
||||||
|
* detached check orb as accent between them.
|
||||||
|
*/
|
||||||
|
function HomeownerArt() {
|
||||||
|
return (
|
||||||
|
<div className="relative mx-auto aspect-[1.28] w-full max-w-[560px]">
|
||||||
|
<AssetRender
|
||||||
|
render={renders.buildingIso}
|
||||||
|
className="absolute top-[4%] right-0 w-[74%] z-50"
|
||||||
|
imgClassName="w-full"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.smokeAlarm}
|
||||||
|
className="absolute top-[8%] left-[2%] w-[26%]"
|
||||||
|
imgClassName="w-full"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.barChart}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[52%] left-[10%] w-[16%] scale-200"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.barChart}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[40%] right-[0%] w-[16%] scale-200 z-30"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.cubesRound}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[50%] right-[30%] w-[16%] scale-200 z-30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SELF_INSTALL_STEPS = [
|
||||||
|
{
|
||||||
|
title: "Gebäude angeben",
|
||||||
|
description:
|
||||||
|
"Sie teilen uns die grundlegenden Informationen zum Gebäude und den Wohnungen mit.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Passende Ausstattung zusammenstellen",
|
||||||
|
description:
|
||||||
|
"Die benötigten Zähler, Kommunikationskomponenten und gegebenenfalls Rauchwarnmelder werden zusammengestellt.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Vorkonfiguriert erhalten",
|
||||||
|
description:
|
||||||
|
"Die Geräte werden bereits für Ihr Gebäude vorbereitet geliefert.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Montieren",
|
||||||
|
description:
|
||||||
|
"Sie selbst, Ihr Hausmeister oder Ihr Fachbetrieb installiert die Geräte.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "GebOS übernimmt",
|
||||||
|
description:
|
||||||
|
"Nach der Inbetriebnahme laufen Datenerfassung und die gebuchten Dienstleistungen über GebOS.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const WHY_GEBOS = [
|
||||||
|
{
|
||||||
|
title: "Kosten kontrollieren",
|
||||||
|
text: "Entscheiden Sie selbst, welche Arbeiten Sie übernehmen und welche Sie abgeben möchten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Keine unnötige technische Komplexität",
|
||||||
|
text: "Die Hardware wird für das jeweilige Gebäude vorbereitet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bestehenden Handwerker nutzen",
|
||||||
|
text: "Sie benötigen keinen speziellen GebOS-Installateur.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Alles an einem Ort",
|
||||||
|
text: "UVI, Abrechnung, Geräte und weitere Gebäudedienstleistungen werden zentral verwaltet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Weniger laufender Aufwand",
|
||||||
|
text: "Nach der Einrichtung werden Verbrauchsdaten automatisch erfasst und verarbeitet.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const VARIANT_ROWS = [
|
||||||
|
["Planung", "gemeinsam mit GebOS", "GebOS"],
|
||||||
|
["Hardware", "GebOS", "GebOS"],
|
||||||
|
["Installation", "Kunde / eigener Fachbetrieb", "organisiert durch GebOS"],
|
||||||
|
["Softwareeinrichtung", "vorbereitet durch GebOS", "GebOS"],
|
||||||
|
["Laufender Betrieb", "GebOS", "GebOS"],
|
||||||
|
["Kosten", "möglichst niedrig", "höhere Planungssicherheit und Komfort"],
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function HauseigentuemerPage() {
|
||||||
|
return (
|
||||||
|
<AudiencePage
|
||||||
|
breadcrumb="Hauseigentümer"
|
||||||
|
title={
|
||||||
|
<span className="block text-[2.25rem] text-pretty sm:text-[2.75rem]">
|
||||||
|
Messdienstleistungen für Ihr Mehrfamilienhaus. Einfach organisiert.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
lead="UVI, Heizkostenabrechnung und Rauchwarnmelder mit vorkonfigurierter Technik – selbst installieren oder als Komplettlösung beauftragen."
|
||||||
|
actions={
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/konfigurator">Preis für mein Gebäude berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
visual={<HomeownerArt />}
|
||||||
|
features={[
|
||||||
|
{
|
||||||
|
icon: <Wrench />,
|
||||||
|
title: "Selbst installieren",
|
||||||
|
sub: "Kosten sparen, flexibel bleiben",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ShieldCheck />,
|
||||||
|
title: "Komplettlösung",
|
||||||
|
sub: "Wir kümmern uns um alles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ReceiptText />,
|
||||||
|
title: "Transparente Preise",
|
||||||
|
sub: "Keine versteckten Kosten",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Handshake />,
|
||||||
|
title: "Bestehende Partner",
|
||||||
|
sub: "Ihre Handwerker bleiben",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
closing={
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<CtaBanner
|
||||||
|
icon={<Euro />}
|
||||||
|
title="Welche Lösung passt zu Ihrem Gebäude?"
|
||||||
|
description="Mit wenigen Angaben zur ersten Preisindikation – oder direkt die Komplettlösung anfragen."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild variant="inverse" size="lg">
|
||||||
|
<Link to="/konfigurator">
|
||||||
|
Preis für mein Gebäude berechnen
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="inverse" size="lg">
|
||||||
|
<Link to="/kontakt">Komplettlösung anfragen</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ihre Entscheidung"
|
||||||
|
title="Zwei Wege zu GebOS"
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid gap-6 md:grid-cols-2">
|
||||||
|
<Card className="relative gap-0 overflow-hidden p-7">
|
||||||
|
<AssetRender
|
||||||
|
render={renders.drillTool}
|
||||||
|
className="pointer-events-none absolute -top-2 -right-3 w-28 opacity-90"
|
||||||
|
/>
|
||||||
|
<h3 className="pr-24 text-lg font-extrabold tracking-tight text-navy">
|
||||||
|
Selbst installieren
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||||
|
Sie möchten Kosten niedrig halten und können die Installation
|
||||||
|
selbst oder über einen vertrauten Heizungsbauer, Hausmeister
|
||||||
|
oder anderen Fachbetrieb organisieren? GebOS liefert die
|
||||||
|
benötigte Technik passend zu Ihrem Gebäude vorkonfiguriert.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm font-bold leading-relaxed text-navy">
|
||||||
|
Sie organisieren die Montage. GebOS übernimmt den digitalen
|
||||||
|
Betrieb.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button asChild size="sm">
|
||||||
|
<Link to="/konfigurator">Preis berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="relative gap-0 overflow-hidden p-7">
|
||||||
|
<AssetRender
|
||||||
|
render={renders.shippingBoxMeters}
|
||||||
|
className="pointer-events-none absolute -top-1 -right-4 w-32"
|
||||||
|
/>
|
||||||
|
<h3 className="pr-28 text-lg font-extrabold tracking-tight text-navy">
|
||||||
|
Komplett installieren lassen
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||||
|
Sie möchten sich möglichst wenig mit Planung, Beschaffung und
|
||||||
|
Installation beschäftigen? GebOS organisiert die komplette
|
||||||
|
Umsetzung für Sie. Von der Ermittlung der benötigten Geräte
|
||||||
|
über die Installation bis zum laufenden Betrieb erhalten Sie
|
||||||
|
eine durchgängige Lösung.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button asChild size="sm" variant="outline">
|
||||||
|
<Link to="/kontakt">Komplettlösung anfragen</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Schritt für Schritt"
|
||||||
|
title="So funktioniert die Selbstinstallation"
|
||||||
|
/>
|
||||||
|
<NumberedList steps={SELF_INSTALL_STEPS} className="mt-10 max-w-2xl" />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ihre Vorteile"
|
||||||
|
title="Warum GebOS für Hauseigentümer?"
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{WHY_GEBOS.map((item) => (
|
||||||
|
<Card key={item.title} className="gap-0 p-6">
|
||||||
|
<h3 className="text-sm font-bold text-navy">{item.title}</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Im Vergleich"
|
||||||
|
title="Welche Variante passt zu mir?"
|
||||||
|
/>
|
||||||
|
<Card className="mt-10 gap-0 overflow-hidden p-2 sm:p-4">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[30%]" />
|
||||||
|
<TableHead>Selbst installieren</TableHead>
|
||||||
|
<TableHead>Komplettlösung</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{VARIANT_ROWS.map(([label, self, full]) => (
|
||||||
|
<TableRow key={label}>
|
||||||
|
<TableCell className="font-bold text-navy">
|
||||||
|
{label}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">
|
||||||
|
{self}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">
|
||||||
|
{full}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
</AudiencePage>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import {
|
||||||
|
Ban,
|
||||||
|
Euro,
|
||||||
|
LayoutDashboard,
|
||||||
|
Package,
|
||||||
|
Timer,
|
||||||
|
TrendingUp,
|
||||||
|
Unlock,
|
||||||
|
X,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
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 { IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
import { NumberedList } from "@/components/gebos/process-steps"
|
||||||
|
import {
|
||||||
|
AssetRender,
|
||||||
|
renders,
|
||||||
|
vectors,
|
||||||
|
} from "@/components/gebos/site-assets"
|
||||||
|
import { AudiencePage } from "@/pages/audiences/audience-page"
|
||||||
|
|
||||||
|
const deviceExamples = [
|
||||||
|
"Wärmemengenzähler",
|
||||||
|
"Warmwasserzähler",
|
||||||
|
"Kaltwasserzähler",
|
||||||
|
"Kommunikationsinfrastruktur",
|
||||||
|
"Rauchwarnmelder",
|
||||||
|
]
|
||||||
|
|
||||||
|
const rolloutSteps = [
|
||||||
|
{
|
||||||
|
title: "Portfolio und Objekte erfassen",
|
||||||
|
description:
|
||||||
|
"Bei der Auftragserstellung werden die betreffenden Gebäude, Wohnungen und die vorhandene beziehungsweise benötigte Ausstattung erfasst.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Gerätebedarf bestimmen",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Für jedes Objekt wird festgelegt, welche Geräte benötigt werden.
|
||||||
|
Beispielsweise:
|
||||||
|
<span className="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
{deviceExamples.map((device) => (
|
||||||
|
<Badge key={device} variant="muted">
|
||||||
|
{device}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "GebOS bereitet den Rollout vor",
|
||||||
|
description:
|
||||||
|
"Gebäude und Wohnungen werden im System angelegt und die Hardware wird objektbezogen vorbereitet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Installationsfertig liefern",
|
||||||
|
description:
|
||||||
|
"Die Geräte werden passend zum jeweiligen Gebäude beziehungsweise zur jeweiligen Nutzungseinheit ausgeliefert.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bestehende Monteure installieren",
|
||||||
|
description:
|
||||||
|
"Eigene Mitarbeiter, Hausmeister oder bestehende Installationspartner können die Montage übernehmen. Eine umfangreiche Softwarekonfiguration vor Ort ist nicht erforderlich.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bewohner einladen",
|
||||||
|
description:
|
||||||
|
"Nach der Einrichtung müssen im Wesentlichen noch die relevanten Bewohnerdaten ergänzt beziehungsweise Einladungen zum Mieterportal versendet werden.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const noEffortItems = [
|
||||||
|
{ icon: <Ban />, title: "Keine aufwendige Gerätekonfiguration." },
|
||||||
|
{ icon: <X />, title: "Keine manuelle Anlage jedes einzelnen Gerätes." },
|
||||||
|
{
|
||||||
|
icon: <Unlock />,
|
||||||
|
title: "Keine spezielle GebOS-Zertifizierung für den Installateur.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const portfolioStats = [
|
||||||
|
{ value: "48", label: "Gebäude" },
|
||||||
|
{ value: "384", label: "Wohnungen" },
|
||||||
|
{ value: "1.746", label: "Geräte" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusInfos = [
|
||||||
|
"Auftrag angelegt",
|
||||||
|
"Hardware vorbereitet",
|
||||||
|
"Versendet",
|
||||||
|
"Installation begonnen",
|
||||||
|
"Installation vollständig",
|
||||||
|
"Geräte aktiv",
|
||||||
|
"Bewohner eingeladen",
|
||||||
|
"Handlungsbedarf",
|
||||||
|
]
|
||||||
|
|
||||||
|
const whyGebos = [
|
||||||
|
{
|
||||||
|
title: "Schneller Rollout",
|
||||||
|
sub: "Standardisierte Abläufe ermöglichen die parallele Umsetzung vieler Objekte.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Weniger Administration",
|
||||||
|
sub: "Gebäudedaten werden möglichst nur einmal erfasst und anschließend weiterverwendet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Vorkonfigurierte Hardware",
|
||||||
|
sub: "Geräte werden bereits passend zu den jeweiligen Objekten vorbereitet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bestehende Installationspartner",
|
||||||
|
sub: "Eigene Hausmeister, technische Mitarbeiter oder externe Fachbetriebe können weiter eingesetzt werden.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Zentrale Portfolioübersicht",
|
||||||
|
sub: "Gebäude, Wohnungen und Geräte lassen sich gemeinsam betrachten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ein Ansprechpartner",
|
||||||
|
sub: "Technik und laufende digitale Dienstleistungen greifen ineinander.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Portfolio/rollout hero per asset kit 06: building, teal bars, consumption ring. */
|
||||||
|
function PortfolioHeroArt() {
|
||||||
|
return (
|
||||||
|
<div className="relative aspect-[1.28] w-full">
|
||||||
|
<img
|
||||||
|
src={vectors.tealBars}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[22%] right-0 w-[44%]"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.building}
|
||||||
|
className="absolute inset-x-[8%] top-[4%]"
|
||||||
|
imgClassName="w-full"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={vectors.consumptionRing}
|
||||||
|
alt=""
|
||||||
|
className="absolute bottom-[3%] left-[2%] w-[28%]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HausverwaltungenPage() {
|
||||||
|
return (
|
||||||
|
<AudiencePage
|
||||||
|
breadcrumb="Hausverwaltungen"
|
||||||
|
title="Für Hausverwaltungen"
|
||||||
|
tagline="Skalieren Sie Ihre Bestände einfach."
|
||||||
|
lead="Rollout über viele Gebäude mit minimalem Aufwand – von der Planung bis zum laufenden Betrieb."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/konfigurator">Portfolio kalkulieren</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="outline">
|
||||||
|
<Link to="/kontakt">Rollout besprechen</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
visual={<PortfolioHeroArt />}
|
||||||
|
features={[
|
||||||
|
{
|
||||||
|
icon: <Package />,
|
||||||
|
title: "Vorkonfigurierte Hardware",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Timer />,
|
||||||
|
title: "Wenig Aufwand vor Ort",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LayoutDashboard />,
|
||||||
|
title: "Zentrale Übersicht",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <TrendingUp />,
|
||||||
|
title: "Skalierbar für wachsende Portfolios",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
closing={
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<CtaBanner
|
||||||
|
icon={<Euro />}
|
||||||
|
title="Wie sieht Ihr Portfolio aus?"
|
||||||
|
description="Erhalten Sie zunächst mit wenigen Angaben eine Preisindikation für mehrere Gebäude – oder sprechen Sie mit uns über vorhandene Messtechnik und den geplanten Rollout."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild variant="inverse" size="lg">
|
||||||
|
<Link to="/konfigurator">Portfolio kalkulieren</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="inverse" size="lg">
|
||||||
|
<Link to="/kontakt">Rollout besprechen</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Skalierung"
|
||||||
|
title="Weniger Projektaufwand pro Gebäude"
|
||||||
|
lead="Bei einem einzelnen Gebäude sind manuelle Arbeitsschritte überschaubar. Bei dutzenden Gebäuden werden sie zum Problem. GebOS reduziert genau diese wiederkehrenden Aufgaben."
|
||||||
|
/>
|
||||||
|
<Card className="mx-auto mt-8 max-w-xl border-l-4 border-l-primary">
|
||||||
|
<CardContent className="py-6 text-center">
|
||||||
|
<p className="text-lg font-bold text-balance text-navy">
|
||||||
|
Objekte einmal erfassen. Geräte vorbereitet erhalten. Montieren.
|
||||||
|
Betreiben.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ablauf"
|
||||||
|
title="Bestellung und Installation greifen ineinander"
|
||||||
|
/>
|
||||||
|
<NumberedList className="mt-10 max-w-2xl" steps={rolloutSteps} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="Der Monteur montiert. GebOS kümmert sich um den Rest."
|
||||||
|
lead="Die Installation soll nicht davon abhängen, dass vor Ort ein speziell geschulter GebOS-Techniker verfügbar ist."
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid gap-6 sm:grid-cols-3">
|
||||||
|
{noEffortItems.map((item, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardContent className="flex flex-col items-center gap-4 py-6 text-center">
|
||||||
|
<IconBadge variant="soft" shape="squircle" size="lg">
|
||||||
|
{item.icon}
|
||||||
|
</IconBadge>
|
||||||
|
<p className="text-sm font-bold text-balance text-navy">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-8 text-center text-base text-muted-foreground">
|
||||||
|
Damit können bestehende Installationsstrukturen weiter genutzt
|
||||||
|
werden.
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Übersicht"
|
||||||
|
title="Rollout zentral überblicken"
|
||||||
|
lead="Bei größeren Portfolios reicht es nicht, Geräte lediglich zu installieren. Die Verwaltung muss erkennen können, wo der Rollout steht und wo Handlungsbedarf besteht."
|
||||||
|
/>
|
||||||
|
<Card className="mx-auto mt-10 max-w-3xl shadow-card-lg">
|
||||||
|
<CardContent className="py-6">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-6">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
|
||||||
|
Beispiel
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-1 text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
Portfolio
|
||||||
|
</h3>
|
||||||
|
<div className="mt-5 flex flex-wrap gap-x-10 gap-y-4">
|
||||||
|
{portfolioStats.map((stat) => (
|
||||||
|
<div key={stat.label}>
|
||||||
|
<div className="text-3xl font-extrabold tracking-tight text-navy">
|
||||||
|
{stat.value}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
|
{stat.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-2">
|
||||||
|
<Badge>43 Gebäude aktiv</Badge>
|
||||||
|
<Badge variant="muted">3 in Installation</Badge>
|
||||||
|
<Badge variant="destructive">2 mit Handlungsbedarf</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={vectors.consumptionRing}
|
||||||
|
alt=""
|
||||||
|
className="w-36 shrink-0 max-sm:hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Separator className="my-6" />
|
||||||
|
<p className="text-xs font-bold text-muted-foreground">
|
||||||
|
Mögliche Statusinformationen
|
||||||
|
</p>
|
||||||
|
<div className="mt-2.5 flex flex-wrap gap-1.5">
|
||||||
|
{statusInfos.map((status) => (
|
||||||
|
<Badge key={status} variant="outline">
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="Warum GebOS für Hausverwaltungen?"
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{whyGebos.map((item) => (
|
||||||
|
<Card key={item.title}>
|
||||||
|
<CardContent className="py-5">
|
||||||
|
<h3 className="text-sm font-bold text-navy">{item.title}</h3>
|
||||||
|
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{item.sub}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="Vom kleinen Bestand bis zum wachsenden Portfolio"
|
||||||
|
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>
|
||||||
|
</AudiencePage>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import {
|
||||||
|
CircleCheck,
|
||||||
|
GraduationCap,
|
||||||
|
Package,
|
||||||
|
PackageX,
|
||||||
|
Unlock,
|
||||||
|
Users,
|
||||||
|
Wrench,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
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 { IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
import { ProcessSteps } from "@/components/gebos/process-steps"
|
||||||
|
import { Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { AssetRender, renders, vectors } from "@/components/gebos/site-assets"
|
||||||
|
import { AudiencePage, AudienceClosing } from "@/pages/audiences/audience-page"
|
||||||
|
|
||||||
|
const KUNDEN_BEDARFE = [
|
||||||
|
"UVI",
|
||||||
|
"Heizkostenabrechnung",
|
||||||
|
"fernauslesbare Messtechnik",
|
||||||
|
"Rauchwarnmelder",
|
||||||
|
]
|
||||||
|
|
||||||
|
const KEINE_HUERDEN = [
|
||||||
|
{
|
||||||
|
icon: <Unlock />,
|
||||||
|
title: "Keine verpflichtende Partnerschaft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PackageX />,
|
||||||
|
title: "Keine Mindestabnahme",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <GraduationCap />,
|
||||||
|
title: "Keine verpflichtende GebOS-Vertriebsschulung",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Wrench />,
|
||||||
|
title: "Keine komplizierte Softwareeinrichtung auf der Baustelle",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const GEBOS_BEREITET_VOR = [
|
||||||
|
"Gebäudestruktur",
|
||||||
|
"Wohnungen beziehungsweise Nutzungseinheiten",
|
||||||
|
"Gerätezuordnung",
|
||||||
|
"benötigte Hardware",
|
||||||
|
"Kommunikationsinfrastruktur",
|
||||||
|
"Softwarestruktur",
|
||||||
|
]
|
||||||
|
|
||||||
|
const SIE_UEBERNEHMEN = [
|
||||||
|
"fachgerechte Montage",
|
||||||
|
"gegebenenfalls Austausch vorhandener Geräte",
|
||||||
|
"Einbau der vorgesehenen Messtechnik",
|
||||||
|
"Abschluss der Installation",
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function InstallateurePage() {
|
||||||
|
return (
|
||||||
|
<AudiencePage
|
||||||
|
breadcrumb="Installateure & Heizungsbauer"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Für Installateure &
|
||||||
|
<br />
|
||||||
|
Heizungsbauer
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
tagline="Bestellen. Montieren. Fertig."
|
||||||
|
lead="GebOS für Ihre Kunden installieren – ohne Partnerprogramm, verpflichtende Zertifizierung oder aufwendige Softwarekonfiguration."
|
||||||
|
actions={
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/konfigurator">Gebäude für meinen Kunden konfigurieren</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
visual={
|
||||||
|
<div className="relative aspect-[1.28] w-full">
|
||||||
|
<img
|
||||||
|
src={vectors.checkOrb}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[6%] right-[8%] w-[16%]"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.heatMeter}
|
||||||
|
className="absolute top-[30%] left-[2%] w-[38%]"
|
||||||
|
imgClassName="w-full"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.gateway}
|
||||||
|
className="absolute top-[24%] right-[16%] w-[46%]"
|
||||||
|
imgClassName="w-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={vectors.glassCubes}
|
||||||
|
alt=""
|
||||||
|
className="absolute right-[1%] bottom-[10%] w-[26%]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
features={[
|
||||||
|
{
|
||||||
|
icon: <Unlock />,
|
||||||
|
title: "Keine Partnerpflichten",
|
||||||
|
sub: "Keine Mindestmengen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Package />,
|
||||||
|
title: "Vorkonfigurierte Hardware",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Wrench />,
|
||||||
|
title: "Einfache Installation",
|
||||||
|
sub: "Keine Softwarekonfiguration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Users />,
|
||||||
|
title: "Ihre Kunden",
|
||||||
|
sub: "Bleiben Ihre Kunden",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
closing={
|
||||||
|
<AudienceClosing
|
||||||
|
title="GebOS bei einem Kunden einsetzen?"
|
||||||
|
lead="Stellen Sie mit wenigen Angaben zunächst die ungefähre Ausstattung und den Preis für das Gebäude zusammen."
|
||||||
|
actions={
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/konfigurator">
|
||||||
|
Gebäude für meinen Kunden konfigurieren
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* 1. Ihr Kunde braucht eine Messdienstlösung? */}
|
||||||
|
<Section className="pt-4 sm:pt-6 lg:pt-8">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ausgangslage"
|
||||||
|
title="Ihr Kunde braucht eine Messdienstlösung?"
|
||||||
|
lead="Sie betreuen ein Mehrfamilienhaus und Ihr Kunde benötigt beispielsweise:"
|
||||||
|
/>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-2.5">
|
||||||
|
{KUNDEN_BEDARFE.map((bedarf) => (
|
||||||
|
<Badge
|
||||||
|
key={bedarf}
|
||||||
|
variant="outline"
|
||||||
|
className="px-4 py-1.5 text-sm font-semibold text-navy"
|
||||||
|
>
|
||||||
|
<CircleCheck className="text-primary" />
|
||||||
|
{bedarf}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground">
|
||||||
|
Dann können Sie GebOS einfach für das betreffende Gebäude einsetzen.{" "}
|
||||||
|
<strong className="font-semibold text-navy">
|
||||||
|
Sie müssen dafür nicht zunächst Teil eines Partnerprogramms werden.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 2. So einfach soll die Installation sein */}
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ablauf"
|
||||||
|
title="So einfach soll die Installation sein"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<ProcessSteps
|
||||||
|
className="mt-12"
|
||||||
|
steps={[
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<AssetRender
|
||||||
|
render={renders.building}
|
||||||
|
className="flex h-16 items-center justify-center"
|
||||||
|
imgClassName="h-16 w-auto"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Gebäude erfassen",
|
||||||
|
description:
|
||||||
|
"Die benötigte Ausstattung wird für das konkrete Objekt zusammengestellt.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img
|
||||||
|
src={vectors.packageBox}
|
||||||
|
alt=""
|
||||||
|
className="h-16 w-16 object-contain"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Geräte bestellen",
|
||||||
|
description:
|
||||||
|
"GebOS liefert die zum Gebäude passenden Geräte.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<AssetRender
|
||||||
|
render={renders.gateway}
|
||||||
|
className="flex h-16 items-center justify-center"
|
||||||
|
imgClassName="h-16 w-auto"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Vorkonfiguriert erhalten",
|
||||||
|
description:
|
||||||
|
"Die Hardware wird bereits objektbezogen vorbereitet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<AssetRender
|
||||||
|
render={renders.heatMeter}
|
||||||
|
className="flex h-16 items-center justify-center"
|
||||||
|
imgClassName="h-16 w-auto"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Montieren",
|
||||||
|
description: "Sie übernehmen die fachgerechte Installation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img
|
||||||
|
src={vectors.checkOrb}
|
||||||
|
alt=""
|
||||||
|
className="h-16 w-16 object-contain"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Fertig",
|
||||||
|
description:
|
||||||
|
"Die digitale Einrichtung und der laufende Betrieb werden von GebOS übernommen.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 3. Kein klassisches Partnerprogramm erforderlich */}
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ohne Einstiegshürden"
|
||||||
|
title="Kein klassisches Partnerprogramm erforderlich"
|
||||||
|
lead="Für die normale Installation von GebOS sollen keine unnötigen Einstiegshürden entstehen."
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{KEINE_HUERDEN.map((item, i) => (
|
||||||
|
<Card key={i} className="items-start gap-4 p-6">
|
||||||
|
<IconBadge variant="soft" shape="squircle">
|
||||||
|
{item.icon}
|
||||||
|
</IconBadge>
|
||||||
|
<p className="text-sm font-bold leading-snug text-navy">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mx-auto mt-8 max-w-2xl text-center text-base leading-relaxed text-muted-foreground">
|
||||||
|
<strong className="font-semibold text-navy">
|
||||||
|
Der Fachbetrieb bleibt der Fachbetrieb des Kunden.
|
||||||
|
</strong>{" "}
|
||||||
|
GebOS stellt das Produkt und die digitale Infrastruktur bereit.
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 4. Sie können bei Ihren bestehenden Kunden bleiben */}
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
title="Sie können bei Ihren bestehenden Kunden bleiben"
|
||||||
|
lead={
|
||||||
|
<>
|
||||||
|
GebOS ist besonders interessant, wenn ein Kunde Sie ohnehin mit
|
||||||
|
Arbeiten an Heizungs-, Wasser- oder Gebäudetechnik beauftragt und
|
||||||
|
zusätzlich eine Lösung für Messdienstleistungen benötigt. Sie
|
||||||
|
können die Installation übernehmen, ohne selbst einen
|
||||||
|
vollständigen Messdienst aufbauen zu müssen.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 5. Was GebOS vorbereitet / Was Sie übernehmen */}
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card className="gap-0 p-6 sm:p-8">
|
||||||
|
<h3 className="text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
Was GebOS vorbereitet
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">Je nach Auftrag:</p>
|
||||||
|
<Checklist className="mt-5" items={GEBOS_BEREITET_VOR} />
|
||||||
|
<p className="mt-5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Damit soll vor Ort möglichst wenig zusätzliche digitale Arbeit
|
||||||
|
entstehen.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="gap-0 p-6 sm:p-8">
|
||||||
|
<h3 className="text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
Was Sie übernehmen
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Das Handwerk bleibt bei Ihnen:
|
||||||
|
</p>
|
||||||
|
<Checklist className="mt-5" items={SIE_UEBERNEHMEN} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<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?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
||||||
|
Auch dann soll der Einstieg freiwillig bleiben. Für Fachbetriebe mit
|
||||||
|
größerem Volumen können später zusätzliche Konditionen oder
|
||||||
|
Leistungen angeboten werden. Das normale Installieren von GebOS
|
||||||
|
bleibt davon unabhängig.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</AudiencePage>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ArrowDown, Code2, Layers, ShieldCheck, Tag } from "lucide-react"
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
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 {
|
||||||
|
AssetRender,
|
||||||
|
renders,
|
||||||
|
vectors,
|
||||||
|
} from "@/components/gebos/site-assets"
|
||||||
|
import { AudiencePage, AudienceClosing } from "@/pages/audiences/audience-page"
|
||||||
|
|
||||||
|
const platformRequirements = [
|
||||||
|
"kompatible Messtechnik",
|
||||||
|
"Kommunikationsinfrastruktur",
|
||||||
|
"Gateways",
|
||||||
|
"sichere Datenerfassung",
|
||||||
|
"Geräteverwaltung",
|
||||||
|
"Monitoring",
|
||||||
|
"Verbrauchsdatenverarbeitung",
|
||||||
|
"Nutzerportale",
|
||||||
|
"Schnittstellen",
|
||||||
|
"laufender technischer Betrieb",
|
||||||
|
]
|
||||||
|
|
||||||
|
const whiteLabelItems = [
|
||||||
|
"eigenes Branding",
|
||||||
|
"eigene Kundenbeziehung",
|
||||||
|
"mandantenfähige Verwaltung",
|
||||||
|
"Endkundenportale",
|
||||||
|
"APIs und Datenexporte",
|
||||||
|
"Geräteverwaltung",
|
||||||
|
"technischer Support",
|
||||||
|
]
|
||||||
|
|
||||||
|
const platformCapabilities = [
|
||||||
|
{
|
||||||
|
title: "Gerätekompatibilität",
|
||||||
|
sub: "Einbindung unterschiedlicher Zähler- und Gerätetypen auf Basis geeigneter Standards.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Gateway-Infrastruktur",
|
||||||
|
sub: "Zentrale Erfassung und sichere Übertragung der Gerätedaten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Gerätemanagement",
|
||||||
|
sub: "Verwaltung großer Gerätebestände über Gebäude und Kunden hinweg.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Monitoring",
|
||||||
|
sub: "Probleme und Ausfälle erkennen, bevor daraus manueller Aufwand beim Kunden entsteht.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "APIs und Integrationen",
|
||||||
|
sub: "Anbindung an eigene Systeme und bestehende Geschäftsprozesse.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Mandantenfähigkeit",
|
||||||
|
sub: "Kunden und Bestände getrennt innerhalb einer gemeinsamen Plattform verwalten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "White Label",
|
||||||
|
sub: "GebOS-Infrastruktur unter der Marke des Messdienstleisters einsetzen.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const businessComponents = [
|
||||||
|
"Einkaufspreise für Hardware",
|
||||||
|
"Plattformgebühren",
|
||||||
|
"Mengenstaffeln",
|
||||||
|
"wiederkehrende Gebühren",
|
||||||
|
"White-Label-Konditionen",
|
||||||
|
"Support- und Servicelevel",
|
||||||
|
"mögliche Mindestmengen",
|
||||||
|
]
|
||||||
|
|
||||||
|
const stackFlow = ["Ihre Kunden", "Ihr Messdienst", "GebOS Plattform"]
|
||||||
|
|
||||||
|
/** Platform hero per asset kit 07: laptop dashboard, glass cubes, cloud support. */
|
||||||
|
function PlatformHeroArt() {
|
||||||
|
return (
|
||||||
|
<div className="relative flex aspect-[1.45] w-full items-center">
|
||||||
|
<img
|
||||||
|
src={vectors.cloudSupport}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-0 right-0 w-[26%]"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.laptopDashboard}
|
||||||
|
className="mx-auto w-full"
|
||||||
|
imgClassName="w-full"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={vectors.glassCubes}
|
||||||
|
alt=""
|
||||||
|
className="absolute bottom-[2%] left-0 w-[24%]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MessdienstleisterPage() {
|
||||||
|
return (
|
||||||
|
<AudiencePage
|
||||||
|
breadcrumb="Messdienstleister"
|
||||||
|
title="Für Messdienstleister"
|
||||||
|
tagline="Ihre Services. Unsere Plattform."
|
||||||
|
lead="Nutzen Sie GebOS als technische Grundlage für Ihren eigenen Messdienst – skalierbar und zuverlässig."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/kontakt">Plattform kennenlernen</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="outline">
|
||||||
|
<Link to="/kontakt">Konditionen anfragen</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
visual={<PlatformHeroArt />}
|
||||||
|
features={[
|
||||||
|
{
|
||||||
|
icon: <Tag />,
|
||||||
|
title: "White Label",
|
||||||
|
sub: "Ihr Branding, Ihre Kunden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Code2 />,
|
||||||
|
title: "Offene APIs",
|
||||||
|
sub: "Einfach integrieren",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Layers />,
|
||||||
|
title: "Skalierbare Plattform",
|
||||||
|
sub: "Für jedes Geschäftsmodell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ShieldCheck />,
|
||||||
|
title: "Zuverlässiger Betrieb",
|
||||||
|
sub: "Sicher und performant",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
closing={
|
||||||
|
<AudienceClosing
|
||||||
|
title="Möchten Sie GebOS für Ihren Messdienst einsetzen?"
|
||||||
|
lead="Wir zeigen Ihnen Plattform, technische Möglichkeiten und mögliche Geschäftsmodelle."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/kontakt">Plattform kennenlernen</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="outline">
|
||||||
|
<Link to="/kontakt">Konditionen anfragen</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Infrastruktur"
|
||||||
|
title="Messdienstleistungen anbieten, ohne die gesamte Plattform selbst zu entwickeln"
|
||||||
|
lead="Moderne Messdienstleistungen benötigen mehr als einzelne Zähler. Erforderlich sind unter anderem:"
|
||||||
|
/>
|
||||||
|
<div className="mt-8 grid max-w-3xl gap-x-10 gap-y-2.5 sm:grid-cols-2">
|
||||||
|
<Checklist items={platformRequirements.slice(0, 5)} />
|
||||||
|
<Checklist items={platformRequirements.slice(5)} />
|
||||||
|
</div>
|
||||||
|
<p className="mt-8 text-base font-bold text-navy">
|
||||||
|
GebOS stellt diese Infrastruktur als Plattform bereit.
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="Konzentrieren Sie sich auf Ihre Kunden"
|
||||||
|
lead="Der Messdienstleister betreut seine Kunden und verkauft seine Dienstleistungen. GebOS stellt die technische Infrastruktur im Hintergrund bereit."
|
||||||
|
/>
|
||||||
|
<Card className="mx-auto mt-10 max-w-md">
|
||||||
|
<CardContent className="flex flex-col items-center gap-2.5 py-8">
|
||||||
|
{stackFlow.map((label) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex flex-col items-center gap-2.5"
|
||||||
|
>
|
||||||
|
<div className="rounded-full border border-border bg-card px-6 py-2.5 text-sm font-bold text-navy shadow-pill">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<ArrowDown className="size-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="rounded-full bg-brand-gradient px-6 py-2.5 text-center text-sm font-bold text-white shadow-band">
|
||||||
|
Hardware · Datenerfassung · Software · Betrieb
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Ihre Marke"
|
||||||
|
title="White Label"
|
||||||
|
lead="GebOS kann als technische Grundlage für Dienstleistungen unter Ihrer eigenen Marke eingesetzt werden. Je nach Geschäftsmodell können unter anderem relevant sein:"
|
||||||
|
/>
|
||||||
|
<div className="mt-8 grid max-w-3xl gap-x-10 gap-y-2.5 sm:grid-cols-2">
|
||||||
|
<Checklist items={whiteLabelItems.slice(0, 4)} />
|
||||||
|
<Checklist items={whiteLabelItems.slice(4)} />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Technik"
|
||||||
|
title="Technische Plattform"
|
||||||
|
lead="Für Messdienstleister steht die technische Leistungsfähigkeit deutlich stärker im Vordergrund."
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{platformCapabilities.map((item) => (
|
||||||
|
<Card key={item.title}>
|
||||||
|
<CardContent className="py-5">
|
||||||
|
<h3 className="text-sm font-bold text-navy">{item.title}</h3>
|
||||||
|
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{item.sub}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Konditionen"
|
||||||
|
title="Das Geschäftsmodell muss funktionieren"
|
||||||
|
lead="Eine Plattform für Messdienstleister ist nur interessant, wenn auch der wirtschaftliche Rahmen stimmt. Deshalb werden kommerzielle Konditionen individuell beziehungsweise mengenabhängig vereinbart – mit anderen Geschäftsmodellen, Mengenstaffeln und gegebenenfalls White-Label-Konditionen als im öffentlichen Preiskonfigurator."
|
||||||
|
/>
|
||||||
|
<div className="mx-auto mt-8 max-w-2xl text-center">
|
||||||
|
<p className="text-xs font-bold text-muted-foreground">
|
||||||
|
Mögliche Bestandteile
|
||||||
|
</p>
|
||||||
|
<div className="mt-2.5 flex flex-wrap justify-center gap-1.5">
|
||||||
|
{businessComponents.map((component) => (
|
||||||
|
<Badge key={component} variant="outline">
|
||||||
|
{component}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-6 text-sm text-muted-foreground italic">
|
||||||
|
Die konkreten Konditionen werden nicht öffentlich auf der Website
|
||||||
|
dargestellt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="Gemeinsam skalieren"
|
||||||
|
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>
|
||||||
|
</AudiencePage>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ArrowRight, Euro, Plus, X } from "lucide-react"
|
||||||
|
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { Checklist } from "@/components/gebos/checklist"
|
||||||
|
import { DonutIllustration } from "@/components/gebos/illustrations"
|
||||||
|
import { IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
|
||||||
|
import { icons, vectors } from "@/components/gebos/site-assets"
|
||||||
|
|
||||||
|
type Heat = "heizkoerper" | "fussbodenheizung" | "gemischt" | "unbekannt"
|
||||||
|
|
||||||
|
type Building = {
|
||||||
|
id: number
|
||||||
|
adresse: string
|
||||||
|
heat: Heat
|
||||||
|
wohnungen: number
|
||||||
|
zimmer: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEAT_OPTIONS: { value: Heat; label: string }[] = [
|
||||||
|
{ value: "heizkoerper", label: "Heizkörper" },
|
||||||
|
{ value: "fussbodenheizung", label: "Fußbodenheizung" },
|
||||||
|
{ value: "gemischt", label: "Gemischt" },
|
||||||
|
{ value: "unbekannt", label: "Weiß ich nicht" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const INITIAL_BUILDINGS: Building[] = [
|
||||||
|
{ id: 1, adresse: "", heat: "heizkoerper", wohnungen: 12, zimmer: 3 },
|
||||||
|
{ id: 2, adresse: "", heat: "heizkoerper", wohnungen: 8, zimmer: 3 },
|
||||||
|
{ id: 3, adresse: "", heat: "heizkoerper", wohnungen: 6, zimmer: 3 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const UVI_MONTHLY = 10.5
|
||||||
|
const INSPECTION_MONTHLY = 2.2
|
||||||
|
const RWM_ONE_TIME = 42
|
||||||
|
|
||||||
|
function formatEuro(value: number) {
|
||||||
|
return value.toLocaleString("de-DE", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepCard({
|
||||||
|
step,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
step: number
|
||||||
|
title: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="gap-0 p-6 sm:p-7">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-extrabold text-primary-foreground">
|
||||||
|
{step}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-lg font-extrabold tracking-tight text-navy">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">{children}</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfiguratorPage() {
|
||||||
|
const [uvi, setUvi] = React.useState(true)
|
||||||
|
const [rwm, setRwm] = React.useState(true)
|
||||||
|
const [buildings, setBuildings] = React.useState<Building[]>(INITIAL_BUILDINGS)
|
||||||
|
const [email, setEmail] = React.useState("")
|
||||||
|
const nextId = React.useRef(INITIAL_BUILDINGS.length + 1)
|
||||||
|
|
||||||
|
const updateBuilding = (id: number, patch: Partial<Building>) => {
|
||||||
|
setBuildings((prev) => prev.map((b) => (b.id === id ? { ...b, ...patch } : b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBuilding = () => {
|
||||||
|
setBuildings((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id: nextId.current++, adresse: "", heat: "heizkoerper", wohnungen: 8, zimmer: 3 },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBuilding = (id: number) => {
|
||||||
|
setBuildings((prev) => (prev.length > 1 ? prev.filter((b) => b.id !== id) : prev))
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBuildings = buildings.length
|
||||||
|
const totalUnits = buildings.reduce((sum, b) => sum + (b.wohnungen || 0), 0)
|
||||||
|
const sensors = buildings.reduce(
|
||||||
|
(sum, b) => sum + Math.round((b.wohnungen || 0) * (b.zimmer || 0) * 0.92),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const smokeDetectors = buildings.reduce(
|
||||||
|
(sum, b) => sum + Math.round((b.wohnungen || 0) * ((b.zimmer || 0) + 0.7)),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const monthly =
|
||||||
|
(uvi ? UVI_MONTHLY : 0) + (uvi || rwm ? INSPECTION_MONTHLY : 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Section className="pt-6 sm:pt-7 lg:pt-8">
|
||||||
|
<div className="relative">
|
||||||
|
<PageBreadcrumb
|
||||||
|
className="mb-4"
|
||||||
|
items={[
|
||||||
|
{ label: "Startseite", to: "/" },
|
||||||
|
{ label: "Konfigurator" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SectionHeading
|
||||||
|
title="Konfigurator"
|
||||||
|
lead="In 4 Schritten zur Preisindikation."
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute -top-6 right-0 hidden items-end lg:flex"
|
||||||
|
>
|
||||||
|
<img src={vectors.euroOrb} alt="" className="w-40" />
|
||||||
|
<img
|
||||||
|
src={vectors.consumptionRing}
|
||||||
|
alt=""
|
||||||
|
className="-mb-3 -ml-6 w-44"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_380px]">
|
||||||
|
{/* Left column: steps */}
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<StepCard step={1} title="Leistungen wählen">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<label className="flex cursor-pointer items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={uvi}
|
||||||
|
onCheckedChange={(v) => setUvi(v === true)}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="block text-sm font-bold text-navy">UVI</span>
|
||||||
|
<span className="block text-sm text-muted-foreground">
|
||||||
|
Unterjährige Verbrauchsinformation inklusive Messtechnik
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex cursor-pointer items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={rwm}
|
||||||
|
onCheckedChange={(v) => setRwm(v === true)}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="block text-sm font-bold text-navy">
|
||||||
|
Rauchwarnmelder
|
||||||
|
</span>
|
||||||
|
<span className="block text-sm text-muted-foreground">
|
||||||
|
Rauchwarnmelder inklusive digitaler Dienstleistungen
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</StepCard>
|
||||||
|
|
||||||
|
<StepCard step={2} title="Gebäude angeben">
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{buildings.map((b, i) => (
|
||||||
|
<div
|
||||||
|
key={b.id}
|
||||||
|
className="rounded-xl border border-border bg-brand-50/40 p-4 sm:p-5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-bold text-navy">
|
||||||
|
Gebäude {i + 1}
|
||||||
|
</div>
|
||||||
|
{buildings.length > 1 ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 text-muted-foreground"
|
||||||
|
aria-label={`Gebäude ${i + 1} entfernen`}
|
||||||
|
onClick={() => removeBuilding(b.id)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<div className="flex flex-col gap-1.5 sm:col-span-2">
|
||||||
|
<Label htmlFor={`adresse-${b.id}`}>
|
||||||
|
Adresse{" "}
|
||||||
|
<span className="font-normal text-muted-foreground">
|
||||||
|
(optional)
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`adresse-${b.id}`}
|
||||||
|
placeholder="Venloer Straße 123, 50823 Köln"
|
||||||
|
value={b.adresse}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateBuilding(b.id, { adresse: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5 sm:col-span-2">
|
||||||
|
<Label htmlFor={`heat-${b.id}`}>Wärmeverteilung</Label>
|
||||||
|
<Select
|
||||||
|
value={b.heat}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
updateBuilding(b.id, { heat: v as Heat })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`heat-${b.id}`} className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{HEAT_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label htmlFor={`wohnungen-${b.id}`}>Wohnungen</Label>
|
||||||
|
<Input
|
||||||
|
id={`wohnungen-${b.id}`}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={b.wohnungen}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateBuilding(b.id, {
|
||||||
|
wohnungen: Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(Number(e.target.value) || 0)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label htmlFor={`zimmer-${b.id}`}>Ø Zimmer</Label>
|
||||||
|
<Input
|
||||||
|
id={`zimmer-${b.id}`}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={b.zimmer}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateBuilding(b.id, {
|
||||||
|
zimmer: Math.max(0, Number(e.target.value) || 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="self-start"
|
||||||
|
onClick={addBuilding}
|
||||||
|
>
|
||||||
|
<Plus /> Gebäude hinzufügen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</StepCard>
|
||||||
|
|
||||||
|
<StepCard step={3} title="Preis sehen">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Sofortige erste Preisindikation.
|
||||||
|
</p>
|
||||||
|
<Accordion type="single" collapsible className="mt-3">
|
||||||
|
<AccordionItem value="annahmen" className="border-b-0">
|
||||||
|
<AccordionTrigger className="text-sm font-bold text-navy">
|
||||||
|
Wie wurde dieser Preis berechnet?
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="text-sm text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
Für diese Schätzung gehen wir aufgrund Ihrer Angaben von
|
||||||
|
ungefähr{" "}
|
||||||
|
<strong className="text-navy">
|
||||||
|
{sensors} Heizsensoren und {smokeDetectors}{" "}
|
||||||
|
Rauchwarnmeldern
|
||||||
|
</strong>{" "}
|
||||||
|
aus.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Bei Gebäuden mit durchschnittlich drei Zimmern verwenden
|
||||||
|
wir eine typische Geräteausstattung als
|
||||||
|
Berechnungsgrundlage.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Die endgültige Anzahl wird bei der detaillierten
|
||||||
|
Konfiguration der Wohnungen bestimmt und ersetzt diese
|
||||||
|
Schätzwerte.
|
||||||
|
</p>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</StepCard>
|
||||||
|
|
||||||
|
<StepCard step={4} title="Konfiguration speichern">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label htmlFor="config-email">E-Mail-Adresse</Label>
|
||||||
|
<Input
|
||||||
|
id="config-email"
|
||||||
|
type="email"
|
||||||
|
placeholder="name@unternehmen.de"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" className="self-start">
|
||||||
|
Konfiguration speichern & fortsetzen
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Sie erhalten einen persönlichen Link (gebos.de/konfiguration/…)
|
||||||
|
zu Ihrer Konfiguration – sofort weitermachen, später
|
||||||
|
zurückkehren oder den Link intern weitergeben.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</StepCard>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button type="button">Jetzt Preis berechnen</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link to="/so-funktionierts">So funktioniert der Konfigurator</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column: sticky summary */}
|
||||||
|
<div className="lg:sticky lg:top-24">
|
||||||
|
<Card className="gap-0 p-6 shadow-card-lg sm:p-7">
|
||||||
|
<h3 className="text-base font-extrabold tracking-tight text-navy">
|
||||||
|
Ihre vorläufige Konfiguration
|
||||||
|
</h3>
|
||||||
|
<div className="mt-4 flex flex-col gap-1">
|
||||||
|
<div className="text-2xl font-extrabold tracking-tight text-navy">
|
||||||
|
{totalBuildings}{" "}
|
||||||
|
{totalBuildings === 1 ? "Gebäude" : "Gebäude"}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-extrabold tracking-tight text-navy">
|
||||||
|
{totalUnits} {totalUnits === 1 ? "Wohnung" : "Wohnungen"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{uvi || rwm ? (
|
||||||
|
<>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<p className="text-xs font-bold tracking-[0.14em] text-muted-foreground uppercase">
|
||||||
|
Voraussichtlich benötigt
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-navy">
|
||||||
|
{uvi ? (
|
||||||
|
<li className="font-semibold">
|
||||||
|
ca. {sensors} Heizsensoren
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{rwm ? (
|
||||||
|
<li className="font-semibold">
|
||||||
|
ca. {smokeDetectors} Rauchwarnmelder
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-5 rounded-xl bg-brand-gradient p-4 text-white shadow-band">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-extrabold tracking-tight">
|
||||||
|
ca. {formatEuro(monthly)}{"\u00A0"}€
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-white/80">
|
||||||
|
je Wohnung / Monat
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IconBadge variant="glass" size="lg">
|
||||||
|
<Euro />
|
||||||
|
</IconBadge>
|
||||||
|
</div>
|
||||||
|
{rwm ? (
|
||||||
|
<div className="mt-3 border-t border-white/20 pt-2 text-xs text-white/80">
|
||||||
|
+ {formatEuro(RWM_ONE_TIME)}{"\u00A0"}€ je Wohnung einmalig
|
||||||
|
(RWM-Hardware)
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex items-start gap-4">
|
||||||
|
<DonutIllustration className="w-16 shrink-0" />
|
||||||
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||||
|
Die erste Berechnung ist eine unverbindliche Preisindikation.
|
||||||
|
Der genaue Preis ergibt sich aus der vollständigen
|
||||||
|
Objektkonfiguration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Was ist in der Schätzung enthalten? */}
|
||||||
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
|
<SectionHeading
|
||||||
|
title="Was ist in der Schätzung enthalten?"
|
||||||
|
lead="Je nach gewähltem Paket beispielsweise:"
|
||||||
|
/>
|
||||||
|
<div className="mt-8 grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card
|
||||||
|
className={
|
||||||
|
"gap-0 p-6 transition-opacity sm:p-8" +
|
||||||
|
(uvi ? "" : " opacity-50")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img src={icons.uvi} alt="" className="size-14" />
|
||||||
|
<h3 className="text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
UVI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<Checklist
|
||||||
|
className="mt-5"
|
||||||
|
items={[
|
||||||
|
"geschätzte benötigte Messtechnik",
|
||||||
|
"GebOS Plattform",
|
||||||
|
"Datenerfassung",
|
||||||
|
"Unterjährige Verbrauchsinformation",
|
||||||
|
"laufender digitaler Betrieb",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
className={
|
||||||
|
"gap-0 p-6 transition-opacity sm:p-8" +
|
||||||
|
(rwm ? "" : " opacity-50")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img src={icons.smokeAlarm} alt="" className="size-14" />
|
||||||
|
<h3 className="text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
Rauchwarnmelder
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<Checklist
|
||||||
|
className="mt-5"
|
||||||
|
items={[
|
||||||
|
"geschätzte Anzahl Rauchwarnmelder",
|
||||||
|
"Verwaltung der Geräte",
|
||||||
|
"gebuchte digitale Dienstleistungen",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Stufe 2 – So geht es weiter */}
|
||||||
|
<Section className="pt-0 pb-16 sm:pt-0 sm:pb-20 lg:pt-0 lg:pb-24">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="So geht es weiter"
|
||||||
|
title="Erst schätzen. Dann genau konfigurieren."
|
||||||
|
lead="Der vollständige Konfigurator beginnt nicht wieder bei null. Die bereits erfassten Informationen werden übernommen – und die bisherigen Annahmen schrittweise durch echte Daten ersetzt."
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex flex-col gap-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
|
<Badge variant="muted" className="px-3.5 py-1.5 text-sm">
|
||||||
|
Geschätzt: 72 Heizsensoren
|
||||||
|
</Badge>
|
||||||
|
<ArrowRight className="size-4 text-primary" aria-hidden="true" />
|
||||||
|
<Badge className="px-3.5 py-1.5 text-sm">
|
||||||
|
Konfiguriert: 68 Heizsensoren
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
|
<Badge variant="muted" className="px-3.5 py-1.5 text-sm">
|
||||||
|
Geschätzt: 96 Rauchwarnmelder
|
||||||
|
</Badge>
|
||||||
|
<ArrowRight className="size-4 text-primary" aria-hidden="true" />
|
||||||
|
<Badge className="px-3.5 py-1.5 text-sm">
|
||||||
|
Konfiguriert: 103 Rauchwarnmelder
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="mt-10 max-w-2xl gap-0 p-6 sm:p-8">
|
||||||
|
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
|
||||||
|
Finales Ergebnis
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-2 text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
Ihre GebOS-Konfiguration
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-navy">
|
||||||
|
3 Gebäude · 26 Wohnungen
|
||||||
|
</p>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<dl className="flex flex-col gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<dt className="font-bold text-navy">UVI</dt>
|
||||||
|
<dd className="text-muted-foreground">
|
||||||
|
68 Heizsensoren / Messgeräte, weitere benötigte Messtechnik
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-bold text-navy">Rauchwarnmelder</dt>
|
||||||
|
<dd className="text-muted-foreground">103 Rauchwarnmelder</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-bold text-navy">Infrastruktur</dt>
|
||||||
|
<dd className="text-muted-foreground">
|
||||||
|
technisch ausgelegte Gateway- und Kommunikationsinfrastruktur
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Ihr Preis ist dann keine grobe Hochrechnung auf Basis von
|
||||||
|
Durchschnittswerten mehr, sondern ein Preis auf Grundlage der
|
||||||
|
tatsächlichen Objektkonfiguration.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Button type="button">Angebot anfordern</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<p className="mt-8 max-w-2xl text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Ihre Daten werden im gesamten Prozess weiterverwendet:
|
||||||
|
Preisschätzung → Konfiguration → Bestellung → Gerätevorbereitung →
|
||||||
|
Installation → laufender Betrieb.
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Building2, House, LineChart, Wrench } from "lucide-react"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Checklist } from "@/components/gebos/checklist"
|
||||||
|
import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
|
||||||
|
import {
|
||||||
|
AssetRender,
|
||||||
|
renders,
|
||||||
|
vectors,
|
||||||
|
} from "@/components/gebos/site-assets"
|
||||||
|
|
||||||
|
type AudienceKey =
|
||||||
|
| "hauseigentuemer"
|
||||||
|
| "hausverwaltung"
|
||||||
|
| "messdienstleister"
|
||||||
|
| "installateur"
|
||||||
|
|
||||||
|
const AUDIENCES: Record<
|
||||||
|
AudienceKey,
|
||||||
|
{
|
||||||
|
select: string
|
||||||
|
tab: string
|
||||||
|
tabIcon: React.ReactNode
|
||||||
|
headline: string
|
||||||
|
text: string
|
||||||
|
bullets: string[]
|
||||||
|
cta: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
hauseigentuemer: {
|
||||||
|
select: "Hauseigentümer",
|
||||||
|
tab: "Hauseigentümer",
|
||||||
|
tabIcon: <House />,
|
||||||
|
headline: "Messdienstleistungen für Ihr Mehrfamilienhaus",
|
||||||
|
text: "Installieren Sie vorkonfigurierte Technik selbst beziehungsweise mit Ihrem bestehenden Fachbetrieb – oder überlassen Sie GebOS die komplette Umsetzung.",
|
||||||
|
bullets: [
|
||||||
|
"einfache Installation",
|
||||||
|
"transparente Kosten",
|
||||||
|
"zuverlässiger laufender Betrieb",
|
||||||
|
"eigene Fachbetriebe weiter nutzen",
|
||||||
|
"wahlweise Selbstinstallation oder Komplettlösung",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Hauseigentümer",
|
||||||
|
href: "/fuer-wen/hauseigentuemer",
|
||||||
|
},
|
||||||
|
hausverwaltung: {
|
||||||
|
select: "Hausverwaltung",
|
||||||
|
tab: "Hausverwaltungen",
|
||||||
|
tabIcon: <Building2 />,
|
||||||
|
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.",
|
||||||
|
bullets: [
|
||||||
|
"standardisierter Rollout",
|
||||||
|
"vorkonfigurierte Geräte",
|
||||||
|
"wenig Softwarearbeit vor Ort",
|
||||||
|
"zentrale Portfolioverwaltung",
|
||||||
|
"geringer administrativer Aufwand",
|
||||||
|
"schnelle Erweiterung um weitere Gebäude",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Hausverwaltungen",
|
||||||
|
href: "/fuer-wen/hausverwaltungen",
|
||||||
|
},
|
||||||
|
messdienstleister: {
|
||||||
|
select: "Messdienstleister",
|
||||||
|
tab: "Messdienstleister",
|
||||||
|
tabIcon: <LineChart />,
|
||||||
|
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.",
|
||||||
|
bullets: [
|
||||||
|
"technische Plattform",
|
||||||
|
"Gerätekompatibilität",
|
||||||
|
"Skalierbarkeit",
|
||||||
|
"APIs und Integrationen",
|
||||||
|
"White Label",
|
||||||
|
"individuelles Geschäftsmodell",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Messdienstleister",
|
||||||
|
href: "/fuer-wen/messdienstleister",
|
||||||
|
},
|
||||||
|
installateur: {
|
||||||
|
select: "Installateur & Heizungsbauer",
|
||||||
|
tab: "Installateure",
|
||||||
|
tabIcon: <Wrench />,
|
||||||
|
headline: "Bestellen. Montieren. Fertig.",
|
||||||
|
text: "Setzen Sie GebOS bei Ihren bestehenden Kunden ein, ohne zunächst ein Partnerprogramm durchlaufen zu müssen.",
|
||||||
|
bullets: [
|
||||||
|
"kein verpflichtender Partnerstatus",
|
||||||
|
"keine Mindestabnahme",
|
||||||
|
"vorkonfigurierte Hardware",
|
||||||
|
"keine aufwendige Softwarekonfiguration",
|
||||||
|
"bestehende Kundenbeziehung bleibt erhalten",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Installateure",
|
||||||
|
href: "/fuer-wen/installateure",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUDIENCE_KEYS = Object.keys(AUDIENCES) as AudienceKey[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page-local hero composition (04-for-whom kit):
|
||||||
|
* building portfolio render, teal growth bars behind right,
|
||||||
|
* consumption ring in front at the lower left.
|
||||||
|
*/
|
||||||
|
function ForWhomArt() {
|
||||||
|
return (
|
||||||
|
<div className="relative mx-auto aspect-[1.28] w-full max-w-[560px]">
|
||||||
|
<img
|
||||||
|
src={vectors.tealBars}
|
||||||
|
alt=""
|
||||||
|
className="absolute top-[16%] right-0 w-[38%]"
|
||||||
|
/>
|
||||||
|
<AssetRender
|
||||||
|
render={renders.building}
|
||||||
|
className="absolute inset-x-[6%] top-[6%]"
|
||||||
|
imgClassName="w-full"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={vectors.consumptionRing}
|
||||||
|
alt=""
|
||||||
|
className="absolute bottom-[2%] left-[2%] w-[30%] drop-shadow-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ForWhomPage() {
|
||||||
|
const [audience, setAudience] = React.useState<AudienceKey>("hausverwaltung")
|
||||||
|
const active = AUDIENCES[audience]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-hero-glow">
|
||||||
|
<Container>
|
||||||
|
<Section className="pt-6 sm:pt-7 lg:pt-8">
|
||||||
|
<PageBreadcrumb
|
||||||
|
className="mb-4"
|
||||||
|
items={[
|
||||||
|
{ label: "Startseite", to: "/" },
|
||||||
|
{ label: "Für wen?" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Finden Sie die passende Lösung"
|
||||||
|
title="Für wen ist GebOS?"
|
||||||
|
lead="GebOS wird von unterschiedlichen Kundengruppen eingesetzt. Wählen Sie aus, was am besten zu Ihnen passt."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||||
|
<Label htmlFor="audience-select" className="text-muted-foreground">
|
||||||
|
Ich bin …
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={audience}
|
||||||
|
onValueChange={(value) => setAudience(value as AudienceKey)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="audience-select" className="w-64">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{AUDIENCE_KEYS.map((key) => (
|
||||||
|
<SelectItem key={key} value={key}>
|
||||||
|
{AUDIENCES[key].select}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid items-center gap-10 lg:grid-cols-[1.05fr_0.95fr] lg:gap-6">
|
||||||
|
<Card className="max-w-xl gap-0 p-7">
|
||||||
|
<h2 className="text-xl font-extrabold tracking-tight text-balance text-navy sm:text-2xl">
|
||||||
|
{active.headline}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||||
|
{active.text}
|
||||||
|
</p>
|
||||||
|
<p className="mt-5 text-sm font-bold text-navy">
|
||||||
|
Besonders wichtig für Sie:
|
||||||
|
</p>
|
||||||
|
<Checklist items={active.bullets} className="mt-3" />
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={active.href}>{active.cta}</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<ForWhomArt />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={audience}
|
||||||
|
onValueChange={(value) => setAudience(value as AudienceKey)}
|
||||||
|
className="mt-12"
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
{AUDIENCE_KEYS.map((key) => (
|
||||||
|
<TabsTrigger key={key} value={key}>
|
||||||
|
{AUDIENCES[key].tabIcon}
|
||||||
|
{AUDIENCES[key].tab}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Building2,
|
||||||
|
Calculator,
|
||||||
|
HardHat,
|
||||||
|
House,
|
||||||
|
Network,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { PageHero } from "@/components/gebos/page-hero"
|
||||||
|
import { HeroArt } from "@/components/gebos/hero-art"
|
||||||
|
import { FeatureTile, IconBadge } from "@/components/gebos/icon-tile"
|
||||||
|
import { ProcessSteps, NumberedList } from "@/components/gebos/process-steps"
|
||||||
|
import { Checklist } from "@/components/gebos/checklist"
|
||||||
|
import { CtaBanner } from "@/components/gebos/cta-banner"
|
||||||
|
import {
|
||||||
|
AssetRender,
|
||||||
|
icons,
|
||||||
|
renders,
|
||||||
|
vectors,
|
||||||
|
} from "@/components/gebos/site-assets"
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
media: <img src={icons.uvi} alt="" className="size-14" loading="lazy" />,
|
||||||
|
title: "UVI",
|
||||||
|
sub: "Monatliche Verbrauchsinformation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.billing} alt="" className="size-14" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Heizkosten\u00adabrechnung",
|
||||||
|
sub: "Jährlich korrekt abrechnen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.smokeAlarm} alt="" className="size-14" loading="lazy" />
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<img src={icons.portal} alt="" className="size-14" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Portale & Verwaltung",
|
||||||
|
sub: "Alle Daten und Geräte im Blick",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const operationsScope = [
|
||||||
|
{
|
||||||
|
media: <img src={icons.uvi} alt="" className="size-12" loading="lazy" />,
|
||||||
|
title: "Unterjährige Verbrauchsinformation",
|
||||||
|
text: "Verbrauchsdaten werden automatisch erfasst und die regelmäßigen Verbrauchsinformationen für die Bewohner bereitgestellt.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.billing} alt="" className="size-12" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Heizkostenabrechnung",
|
||||||
|
text: "Die erfassten Verbrauchsdaten bilden die Grundlage für die jährliche Heizkostenabrechnung.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.smokeAlarm} alt="" className="size-12" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Rauchwarnmelder",
|
||||||
|
text: "Rauchwarnmelder können gemeinsam mit der übrigen Gebäudetechnik geplant, installiert und zentral verwaltet werden.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: <img src={icons.radio} alt="" className="size-12" loading="lazy" />,
|
||||||
|
title: "Fernauslesbare Messtechnik",
|
||||||
|
text: "Wärme- und Wasserverbrauch werden über kompatible Geräte automatisch erfasst.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<AssetRender
|
||||||
|
render={renders.gateway}
|
||||||
|
className="flex size-12 items-center justify-center"
|
||||||
|
imgClassName="w-12"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Gateway und Datenübertragung",
|
||||||
|
text: "Die Messdaten werden zentral gesammelt und an GebOS übertragen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.portal} alt="" className="size-12" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Portale und Verwaltung",
|
||||||
|
text: "Gebäude, Wohnungen, Geräte und Verbrauchsdaten werden zentral verwaltet. Bewohner erhalten Zugriff auf die für sie vorgesehenen Informationen.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function StepMedia({ src }: { src: string }) {
|
||||||
|
return (
|
||||||
|
<span className="flex size-16 items-center justify-center">
|
||||||
|
<img src={src} alt="" className="size-16" loading="lazy" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processSteps = [
|
||||||
|
{
|
||||||
|
media: <StepMedia src={vectors.uviBadge} />,
|
||||||
|
title: "Gebäude erfassen",
|
||||||
|
description: "Daten zum Gebäude und den Wohnungen erfassen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<AssetRender
|
||||||
|
render={renders.building}
|
||||||
|
className="flex size-16 items-center justify-center"
|
||||||
|
imgClassName="w-16"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Gerätebedarf bestimmen",
|
||||||
|
description: "GebOS ermittelt die benötigte Ausstattung automatisch.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: <StepMedia src={vectors.packageBox} />,
|
||||||
|
title: "Vorkonfiguriert liefern",
|
||||||
|
description: "Hardware wird für Ihr Objekt vorbereitet geliefert.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<AssetRender
|
||||||
|
render={renders.heatMeter}
|
||||||
|
className="flex size-16 items-center justify-center"
|
||||||
|
imgClassName="w-16"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Geräte montieren",
|
||||||
|
description: "Montage vor Ort – ohne aufwendige Softwarekonfiguration.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: <StepMedia src={vectors.wirelessOrb} />,
|
||||||
|
title: "Automatisch betreiben",
|
||||||
|
description: "GebOS übernimmt Daten, Services und laufenden Betrieb.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const lessEffort = [
|
||||||
|
{
|
||||||
|
vector: vectors.packageBox,
|
||||||
|
title: "Vorkonfigurierte Hardware",
|
||||||
|
text: "Geräte werden passend zum jeweiligen Gebäude und zur vorgesehenen Nutzung vorbereitet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vector: vectors.checkOrb,
|
||||||
|
title: "Keine aufwendige Einrichtung auf der Baustelle",
|
||||||
|
text: "Der Installateur kann sich auf die fachgerechte Montage konzentrieren.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vector: vectors.checkOrb,
|
||||||
|
title: "Bestehende Fachbetriebe nutzen",
|
||||||
|
text: "Kein spezieller GebOS-Partner erforderlich – eigene Hausmeister, Heizungsbauer oder bestehende Installationspartner können weiterhin eingesetzt werden.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vector: vectors.checkOrb,
|
||||||
|
title: "Weniger doppelte Dateneingabe",
|
||||||
|
text: "Informationen aus Bestellung und Konfiguration werden für den späteren Betrieb weiterverwendet.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const systemTasks = [
|
||||||
|
{
|
||||||
|
media: <img src={icons.uvi} alt="" className="size-10" loading="lazy" />,
|
||||||
|
title: "UVI",
|
||||||
|
text: "Regelmäßige Verbrauchsinformation für Bewohner.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.billing} alt="" className="size-10" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Heizkostenabrechnung",
|
||||||
|
text: "Verbrauchsdaten strukturiert erfassen und für die jährliche Abrechnung nutzen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<img src={icons.smokeAlarm} alt="" className="size-10" loading="lazy" />
|
||||||
|
),
|
||||||
|
title: "Rauchwarnmelder",
|
||||||
|
text: "Rauchwarnmelder gemeinsam mit der übrigen Gebäudetechnik planen und verwalten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: <img src={icons.radio} alt="" className="size-10" loading="lazy" />,
|
||||||
|
title: "Messtechnik und Infrastruktur",
|
||||||
|
text: "Fernauslesbare Zähler, Sensoren und Kommunikationsinfrastruktur für den laufenden Betrieb.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const audiences = [
|
||||||
|
{
|
||||||
|
icon: <House />,
|
||||||
|
name: "Hauseigentümer",
|
||||||
|
headline: "Messdienstleistungen für Ihr Mehrfamilienhaus",
|
||||||
|
text: "Installieren Sie vorkonfigurierte Technik selbst beziehungsweise mit Ihrem bestehenden Fachbetrieb – oder überlassen Sie GebOS die komplette Umsetzung.",
|
||||||
|
points: [
|
||||||
|
"einfache Installation",
|
||||||
|
"transparente Kosten",
|
||||||
|
"zuverlässiger laufender Betrieb",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Hauseigentümer",
|
||||||
|
to: "/fuer-wen/hauseigentuemer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Building2 />,
|
||||||
|
name: "Hausverwaltungen",
|
||||||
|
headline: "Viele Gebäude. Ein einfacher Rollout.",
|
||||||
|
text: "Erfassen Sie Ihre Objekte einmal, erhalten Sie vorkonfigurierte Hardware und nutzen Sie bestehende Mitarbeiter oder Installationspartner für die Montage.",
|
||||||
|
points: [
|
||||||
|
"standardisierter Rollout",
|
||||||
|
"vorkonfigurierte Geräte",
|
||||||
|
"zentrale Portfolioverwaltung",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Hausverwaltungen",
|
||||||
|
to: "/fuer-wen/hausverwaltungen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Network />,
|
||||||
|
name: "Messdienstleister",
|
||||||
|
headline: "Ihre Messdienstleistung. Unsere Infrastruktur.",
|
||||||
|
text: "Nutzen Sie GebOS als technische Grundlage für Ihre eigenen Dienstleistungen – mit Hardware, Plattform, Gerätemanagement und optionalem White Label.",
|
||||||
|
points: ["technische Plattform", "Skalierbarkeit", "APIs und White Label"],
|
||||||
|
cta: "GebOS für Messdienstleister",
|
||||||
|
to: "/fuer-wen/messdienstleister",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <HardHat />,
|
||||||
|
name: "Installateure & Heizungsbauer",
|
||||||
|
headline: "Bestellen. Montieren. Fertig.",
|
||||||
|
text: "Setzen Sie GebOS bei Ihren bestehenden Kunden ein, ohne zunächst ein Partnerprogramm durchlaufen zu müssen.",
|
||||||
|
points: [
|
||||||
|
"kein verpflichtender Partnerstatus",
|
||||||
|
"keine Mindestabnahme",
|
||||||
|
"vorkonfigurierte Hardware",
|
||||||
|
],
|
||||||
|
cta: "GebOS für Installateure",
|
||||||
|
to: "/fuer-wen/installateure",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const scaleFlows = [
|
||||||
|
{
|
||||||
|
vector: vectors.tealBars,
|
||||||
|
label: "1 Gebäude",
|
||||||
|
sub: "Ein Eigentümer kann ein Mehrfamilienhaus konfigurieren.",
|
||||||
|
steps: [
|
||||||
|
{ title: "GebOS konfigurieren" },
|
||||||
|
{ title: "Installieren" },
|
||||||
|
{ title: "Betreiben" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vector: vectors.consumptionRing,
|
||||||
|
label: "50 Gebäude",
|
||||||
|
sub: "Eine Hausverwaltung kann denselben Prozess auf viele Gebäude übertragen.",
|
||||||
|
steps: [
|
||||||
|
{ title: "Nach demselben Prozess konfigurieren" },
|
||||||
|
{ title: "Rollout organisieren" },
|
||||||
|
{ title: "Zentral betreiben" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const priceFlow = [
|
||||||
|
{ title: "Wenige Angaben" },
|
||||||
|
{ title: "Erste Preisindikation" },
|
||||||
|
{ title: "Konfiguration speichern" },
|
||||||
|
{ title: "Gebäude vollständig konfigurieren" },
|
||||||
|
{ title: "Genauer Preis" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Messdienstleistungen
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="text-primary">einfach</span> gemacht.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
lead="Unterjährige Verbrauchsinformation (UVI), Heizkostenabrechnung und Rauchwarnmelder – von der vorkonfigurierten Hardware bis zum laufenden Betrieb."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/konfigurator">Preis berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" size="lg">
|
||||||
|
<Link to="/so-funktionierts">So funktioniert GebOS</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
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.sub} {...feature} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Das System"
|
||||||
|
title="Alles, was für den laufenden Messbetrieb benötigt wird"
|
||||||
|
lead="GebOS verbindet Messtechnik, Datenerfassung und Software zu einem durchgängigen System. Statt einzelne Komponenten selbst zusammenzustellen und miteinander zu verbinden, erhalten Sie eine Lösung, bei der Bestellung, Hardware, Konfiguration und laufender Betrieb zusammenspielen."
|
||||||
|
/>
|
||||||
|
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{operationsScope.map((item) => (
|
||||||
|
<Card key={item.title} className="p-6 gap-0">
|
||||||
|
<CardContent className="flex h-full flex-col items-start gap-3 p-0">
|
||||||
|
{item.media}
|
||||||
|
<h3 className="text-base font-bold text-navy">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Der Prozess"
|
||||||
|
title="So funktioniert GebOS"
|
||||||
|
lead="Von der Bestellung bis zum laufenden Betrieb – GebOS verbindet Bestellung, Konfiguration und Installation miteinander."
|
||||||
|
/>
|
||||||
|
<ProcessSteps steps={processSteps} className="mt-12" />
|
||||||
|
<p className="mt-10 text-center text-base font-semibold italic text-navy">
|
||||||
|
„Bestellung, Konfiguration und Installation greifen ineinander.“
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex justify-center">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/konfigurator">Preis für mein Gebäude berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Installation"
|
||||||
|
title="Weniger Aufwand vor Ort"
|
||||||
|
lead="Die Installation eines Messsystems sollte nicht daran scheitern, dass vor Ort komplizierte Software eingerichtet oder jedes Gerät manuell zugeordnet werden muss. GebOS bereitet die digitale Struktur bereits vor der Installation vor."
|
||||||
|
/>
|
||||||
|
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{lessEffort.map((item) => (
|
||||||
|
<Card key={item.title} className="p-6 gap-0">
|
||||||
|
<CardContent className="flex h-full flex-col items-start gap-3 p-0">
|
||||||
|
<img
|
||||||
|
src={item.vector}
|
||||||
|
alt=""
|
||||||
|
className="size-12"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<h3 className="text-base font-bold text-navy">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Leistungen"
|
||||||
|
title="Ein System für mehrere Aufgaben"
|
||||||
|
lead="GebOS bündelt wiederkehrende Mess- und Gebäudedienstleistungen auf einer gemeinsamen technischen Grundlage."
|
||||||
|
/>
|
||||||
|
<Card className="mt-12 divide-y divide-border p-0 gap-0">
|
||||||
|
{systemTasks.map((task) => (
|
||||||
|
<div
|
||||||
|
key={task.title}
|
||||||
|
className="flex flex-col gap-3 p-5 sm:flex-row sm:items-center sm:gap-5 sm:px-7"
|
||||||
|
>
|
||||||
|
{task.media}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-sm font-bold text-navy">{task.title}</h3>
|
||||||
|
<p className="mt-0.5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{task.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="link"
|
||||||
|
className="h-auto shrink-0 self-start p-0 sm:self-center"
|
||||||
|
>
|
||||||
|
<Link to="/loesungen">
|
||||||
|
Mehr erfahren
|
||||||
|
<ArrowRight />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Skalierung"
|
||||||
|
title="Vom einzelnen Mehrfamilienhaus bis zum Portfolio"
|
||||||
|
lead="GebOS ist so aufgebaut, dass derselbe grundlegende Prozess sowohl für ein einzelnes Gebäude als auch für größere Bestände funktioniert. Neue Objekte werden nach einem einheitlichen Schema ergänzt und ausgerollt."
|
||||||
|
/>
|
||||||
|
<Card className="mt-12 p-0 gap-0 overflow-hidden">
|
||||||
|
<div className="grid divide-y divide-border sm:grid-cols-2 sm:divide-x sm:divide-y-0">
|
||||||
|
{scaleFlows.map((flow) => (
|
||||||
|
<div key={flow.label} className="flex flex-col gap-5 p-6 sm:p-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={flow.vector}
|
||||||
|
alt=""
|
||||||
|
className="size-14"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-extrabold tracking-tight text-navy">
|
||||||
|
{flow.label}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||||
|
{flow.sub}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NumberedList steps={flow.steps} className="mt-1" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="Preis"
|
||||||
|
title="Was kostet GebOS?"
|
||||||
|
lead="Mit dem GebOS-Konfigurator erhalten Sie bereits mit wenigen Angaben eine erste Preisindikation."
|
||||||
|
/>
|
||||||
|
<div className="mt-10 grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Dafür werden zunächst nur grundlegende Informationen benötigt,
|
||||||
|
beispielsweise:
|
||||||
|
</p>
|
||||||
|
<Checklist
|
||||||
|
className="mt-4"
|
||||||
|
items={[
|
||||||
|
"gewünschte Leistungen",
|
||||||
|
"Anzahl der Gebäude",
|
||||||
|
"Anzahl der Wohnungen",
|
||||||
|
"Art der Wärmeverteilung",
|
||||||
|
"durchschnittliche Anzahl der Zimmer",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<p className="mt-4 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Auf Basis dieser Angaben schätzt GebOS die voraussichtlich
|
||||||
|
benötigte Geräteanzahl und berechnet einen ungefähren Preis.
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-8 text-lg font-extrabold tracking-tight text-navy">
|
||||||
|
Erst schätzen. Dann genau konfigurieren.
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Der Schnellkonfigurator arbeitet bewusst mit Annahmen. Für
|
||||||
|
einen genauen Preis können Sie die Konfiguration anschließend
|
||||||
|
speichern und Ihre Gebäude detailliert erfassen. Die bereits
|
||||||
|
eingegebenen Daten werden übernommen.
|
||||||
|
</p>
|
||||||
|
<Button asChild size="lg" className="mt-6">
|
||||||
|
<Link to="/konfigurator">Preis berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Card className="p-6 gap-0 sm:p-8">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={vectors.euroOrb}
|
||||||
|
alt=""
|
||||||
|
className="size-14"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<h3 className="text-base font-bold text-navy">
|
||||||
|
In fünf Schritten zum genauen Preis
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<NumberedList steps={priceFlow} className="mt-6" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<p className="mt-5 text-xs leading-relaxed text-muted-foreground italic">
|
||||||
|
Die erste Berechnung ist eine unverbindliche Preisindikation.
|
||||||
|
Die tatsächliche Ausstattung und der genaue Preis ergeben sich
|
||||||
|
aus der vollständigen Objektkonfiguration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Zielgruppen"
|
||||||
|
title="Für wen ist GebOS?"
|
||||||
|
lead="GebOS wird von unterschiedlichen Kundengruppen eingesetzt. Wählen Sie aus, was am besten zu Ihnen passt."
|
||||||
|
/>
|
||||||
|
<div className="mt-12 grid gap-5 sm:grid-cols-2">
|
||||||
|
{audiences.map((audience) => (
|
||||||
|
<Card key={audience.to} className="p-6 gap-0 sm:p-7">
|
||||||
|
<CardContent className="flex h-full flex-col items-start gap-3 p-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<IconBadge variant="soft" shape="squircle" size="lg">
|
||||||
|
{audience.icon}
|
||||||
|
</IconBadge>
|
||||||
|
<span className="text-xs font-bold tracking-wide text-primary uppercase">
|
||||||
|
{audience.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-extrabold tracking-tight text-navy">
|
||||||
|
{audience.headline}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{audience.text}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs font-bold tracking-wide text-navy uppercase">
|
||||||
|
Besonders wichtig für Sie
|
||||||
|
</p>
|
||||||
|
<Checklist items={audience.points} className="mb-2" />
|
||||||
|
<Button asChild variant="outline" className="mt-auto">
|
||||||
|
<Link to={audience.to}>{audience.cta}</Link>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<CtaBanner
|
||||||
|
icon={<Calculator />}
|
||||||
|
title="Messdienstleistungen müssen nicht kompliziert sein."
|
||||||
|
description="Gebäude erfassen, passende Technik erhalten, montieren und anschließend zentral betreiben."
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button asChild variant="inverse" size="lg">
|
||||||
|
<Link to="/konfigurator">Preis berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
className="border-white/40 bg-transparent text-white hover:bg-white/10 hover:text-white"
|
||||||
|
>
|
||||||
|
<Link to="/so-funktionierts">So funktioniert GebOS</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import type { ReactNode } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Calculator, ChevronRight } from "lucide-react"
|
||||||
|
|
||||||
|
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 { ProcessSteps } from "@/components/gebos/process-steps"
|
||||||
|
import { Container, Section, SectionHeading } from "@/components/gebos/section"
|
||||||
|
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
|
||||||
|
import {
|
||||||
|
AssetRender,
|
||||||
|
renders,
|
||||||
|
vectors,
|
||||||
|
} from "@/components/gebos/site-assets"
|
||||||
|
|
||||||
|
function StepMedia({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-16 items-end justify-center gap-1">{children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<StepMedia>
|
||||||
|
<img src={vectors.uviBadge} alt="" className="w-16" />
|
||||||
|
</StepMedia>
|
||||||
|
),
|
||||||
|
title: "Gebäude erfassen",
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<StepMedia>
|
||||||
|
<img src={vectors.packageBox} alt="" className="w-16" />
|
||||||
|
</StepMedia>
|
||||||
|
),
|
||||||
|
title: "Vorkonfiguriert liefern",
|
||||||
|
description:
|
||||||
|
"Gebäude, Wohnungen und Geräte werden bereits vorbereitet. Die Hardware wird objektbezogen und installationsfertig geliefert.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<StepMedia>
|
||||||
|
<AssetRender render={renders.heatMeter} imgClassName="w-12" />
|
||||||
|
<AssetRender render={renders.gateway} imgClassName="w-9" />
|
||||||
|
</StepMedia>
|
||||||
|
),
|
||||||
|
title: "Geräte montieren",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Die Geräte müssen vor Ort im Wesentlichen nur noch fachgerecht montiert
|
||||||
|
werden.{" "}
|
||||||
|
<strong className="font-bold text-navy">
|
||||||
|
Keine aufwendige Softwarekonfiguration bei der Installation.
|
||||||
|
</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
media: (
|
||||||
|
<StepMedia>
|
||||||
|
<img src={vectors.wirelessOrb} alt="" className="w-16" />
|
||||||
|
</StepMedia>
|
||||||
|
),
|
||||||
|
title: "Automatisch betreiben",
|
||||||
|
description:
|
||||||
|
"Nach der Installation übernimmt GebOS die laufende Datenerfassung und die gebuchten digitalen Dienstleistungen.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const journey = [
|
||||||
|
"Besucher versteht GebOS",
|
||||||
|
"identifiziert seinen Anwendungsfall",
|
||||||
|
"berechnet einen ungefähren Preis",
|
||||||
|
"speichert seine Konfiguration",
|
||||||
|
"ergänzt die tatsächlichen Gebäudedaten",
|
||||||
|
"erhält den genauen Preis",
|
||||||
|
"erteilt den Auftrag",
|
||||||
|
"GebOS bereitet Gebäude und Geräte vor",
|
||||||
|
"vorkonfigurierte Hardware wird geliefert",
|
||||||
|
"Montage vor Ort",
|
||||||
|
"automatischer laufender Betrieb",
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function HowItWorksPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-hero-glow">
|
||||||
|
<Container>
|
||||||
|
<Section className="pt-6 pb-10 sm:pt-7 sm:pb-12 lg:pt-8 lg:pb-14">
|
||||||
|
<PageBreadcrumb
|
||||||
|
className="mb-4 justify-center"
|
||||||
|
items={[
|
||||||
|
{ label: "Startseite", to: "/" },
|
||||||
|
{ label: "So funktioniert’s" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="So funktioniert GebOS"
|
||||||
|
lead="Von der Bestellung bis zum laufenden Betrieb. GebOS verbindet Bestellung, Konfiguration und Installation miteinander."
|
||||||
|
/>
|
||||||
|
<Card className="mt-10 gap-0 p-8 sm:mt-12 sm:p-10">
|
||||||
|
<ProcessSteps steps={steps} />
|
||||||
|
</Card>
|
||||||
|
<p className="mx-auto mt-10 max-w-xl text-center text-sm font-medium text-muted-foreground italic">
|
||||||
|
„Bestellung, Konfiguration und Installation greifen ineinander.“
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
<Container>
|
||||||
|
<Section>
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
eyebrow="Gesamtprozess"
|
||||||
|
title="Der Gesamtprozess aus Kundensicht"
|
||||||
|
lead="Website, Konfigurator und operativer Rollout sind keine getrennten Systeme – sie bauen aufeinander auf."
|
||||||
|
/>
|
||||||
|
<div className="mx-auto mt-10 flex max-w-4xl flex-wrap items-center justify-center gap-x-1.5 gap-y-3">
|
||||||
|
{journey.map((station, i) => (
|
||||||
|
<span key={station} className="flex items-center gap-1.5">
|
||||||
|
<Badge variant="outline" className="bg-card">
|
||||||
|
{station}
|
||||||
|
</Badge>
|
||||||
|
{i < journey.length - 1 ? (
|
||||||
|
<ChevronRight
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-4 shrink-0 text-brand-400"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto mt-10 max-w-2xl text-center">
|
||||||
|
<p className="text-base font-bold text-navy sm:text-lg">
|
||||||
|
Der Konfigurator ist der Beginn des
|
||||||
|
GebOS-Bestell- und Rolloutprozesses.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Die einmal eingegebenen Gebäudedaten werden nicht erneut erfasst,
|
||||||
|
sondern durch den gesamten Prozess weiterverwendet.
|
||||||
|
</p>
|
||||||
|
<p className="mt-6 font-mono text-xs tracking-wide text-muted-foreground uppercase">
|
||||||
|
Preisschätzung → Konfiguration → Bestellung → Gerätevorbereitung →
|
||||||
|
Installation → Betrieb
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
<Container>
|
||||||
|
<Section className="pt-0">
|
||||||
|
<CtaBanner
|
||||||
|
icon={<Calculator />}
|
||||||
|
title="Was kostet GebOS für Ihr Gebäude?"
|
||||||
|
description="Mit wenigen Angaben erhalten Sie eine erste Preisindikation."
|
||||||
|
actions={
|
||||||
|
<Button variant="inverse" size="lg" asChild>
|
||||||
|
<Link to="/konfigurator">Preis berechnen</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||