Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02033d3d7a | ||
|
|
ef90873efc | ||
|
|
e9c94f74f8 | ||
|
|
43c5b2e7ac | ||
|
|
3dd74e53a0 | ||
|
|
e881799ff1 | ||
|
|
c734b85a8d |
@@ -1,20 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SolenOS – Messdienstleistungen einfach gemacht</title>
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" />
|
||||
<meta
|
||||
name="description"
|
||||
content="SolenOS Component Library – shadcn-basierte Komponenten für die SolenOS Landing Pages."
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 131 KiB |
@@ -1,704 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { ArrowRight, Plus } from "lucide-react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checklist } from "@/components/solenos/checklist"
|
||||
|
||||
/**
|
||||
* One entry of the grid. The same record feeds all three faces a card can
|
||||
* show, so nothing has to be authored twice: teaser in the grid, single line
|
||||
* in the rail, editorial spread once it is open.
|
||||
*/
|
||||
type ExpandingCard = {
|
||||
/** stable identity – React keys and the open-card state both key off it */
|
||||
id: string
|
||||
/** small mark, carried by the teaser and (shrunk) by the rail line */
|
||||
media?: React.ReactNode
|
||||
/** larger render; only the open card has the room for it */
|
||||
visual?: React.ReactNode
|
||||
title: string
|
||||
text: string
|
||||
/** detail bullets, revealed with the open card */
|
||||
points?: string[]
|
||||
/** deep link, rendered as the open card's action */
|
||||
to?: string
|
||||
/** defaults to "Mehr erfahren" */
|
||||
linkLabel?: string
|
||||
}
|
||||
|
||||
/** which side of the container the open card sits on – the rail takes the other */
|
||||
type Side = "left" | "right"
|
||||
|
||||
/**
|
||||
* Where the minimised cards are allowed to gather. `auto` mirrors the pointer –
|
||||
* the picked card keeps its half, so the rail changes sides as you browse.
|
||||
* Pinning it to one side trades that spatial cue for calm: picking out of the
|
||||
* rail then shifts the column by a single slot instead of sending every card
|
||||
* across the container.
|
||||
*/
|
||||
type RailSide = Side | "auto"
|
||||
|
||||
/** the face a card currently shows */
|
||||
type Face = "brief" | "rail" | "open"
|
||||
|
||||
/** matches the `gap-5` of the static section grids this one grows out of */
|
||||
const GRID_GAP = 20
|
||||
/** the rail packs tighter than the grid, so a column of five still reads as one block */
|
||||
const RAIL_GAP = 12
|
||||
const RAIL_MIN_H = 68
|
||||
const RAIL_MAX_H = 108
|
||||
/** container widths, not viewport: the grid has to work in any column */
|
||||
const TWO_COL = 560
|
||||
const THREE_COL = 900
|
||||
/** below this the open card stacks its render under the copy instead of beside it */
|
||||
const SPREAD_MIN = 520
|
||||
/** share of the open card reserved for its render */
|
||||
const VISUAL_SHARE = 0.34
|
||||
/** stand-in row height for the single render before the first measurement lands */
|
||||
const FALLBACK_ROW_H = 232
|
||||
|
||||
type Frame = { x: number; y: number; w: number; h: number }
|
||||
|
||||
type Plan = {
|
||||
/** absolute box of every card, in container coordinates */
|
||||
frames: Frame[]
|
||||
/** height the container has to hold for this state */
|
||||
height: number
|
||||
cols: number
|
||||
/** open card next to a rail – false while everything is a grid, or on one column */
|
||||
split: boolean
|
||||
/** width a teaser face is laid out at */
|
||||
cardW: number
|
||||
/** width an open card gets: the full row on one column, two thirds in the split */
|
||||
openW: number
|
||||
railW: number
|
||||
/** padding of the open face – the layout needs it to turn copy into a card height */
|
||||
pad: number
|
||||
/** width reserved for the open card's render, 0 while the card is too narrow for it */
|
||||
visualW: number
|
||||
/** open card wide enough to set copy beside its render */
|
||||
spread: boolean
|
||||
}
|
||||
|
||||
const EMPTY_PLAN: Plan = {
|
||||
frames: [],
|
||||
height: 0,
|
||||
cols: 1,
|
||||
split: false,
|
||||
cardW: 0,
|
||||
openW: 0,
|
||||
railW: 0,
|
||||
pad: 24,
|
||||
visualW: 0,
|
||||
spread: false,
|
||||
}
|
||||
|
||||
const clamp = (value: number, low: number, high: number) =>
|
||||
Math.min(high, Math.max(low, value))
|
||||
|
||||
const pad2 = (n: number) => String(n).padStart(2, "0")
|
||||
|
||||
/**
|
||||
* The whole layout, as one pure function: every card gets an absolute frame, so
|
||||
* a state change is a transform and a size to interpolate instead of a reflow.
|
||||
* Grid, rail and open card all fall out of the same three numbers – container
|
||||
* width, measured teaser height, measured open-copy height.
|
||||
*/
|
||||
function planLayout({
|
||||
count,
|
||||
width,
|
||||
teaserHeights,
|
||||
copyHeights,
|
||||
active,
|
||||
side,
|
||||
}: {
|
||||
count: number
|
||||
width: number
|
||||
/** natural height of each teaser face, measured at grid card width */
|
||||
teaserHeights: number[]
|
||||
/** natural height of each open-card copy block, measured at open width */
|
||||
copyHeights: number[]
|
||||
active: number
|
||||
side: Side
|
||||
}): Plan {
|
||||
if (count === 0 || width <= 0) return EMPTY_PLAN
|
||||
|
||||
const cols = width >= THREE_COL ? 3 : width >= TWO_COL ? 2 : 1
|
||||
const cardW = (width - (cols - 1) * GRID_GAP) / cols
|
||||
const rows = Math.ceil(count / cols)
|
||||
|
||||
/* only trust the measurement once every card has reported, so a half-mounted
|
||||
grid never bakes a too-short row height into the open card's height */
|
||||
const measured =
|
||||
teaserHeights.length === count && teaserHeights.every((h) => h > 0)
|
||||
? Math.max(...teaserHeights)
|
||||
: 0
|
||||
const rowH = measured || FALLBACK_ROW_H
|
||||
const gridH = rows * rowH + (rows - 1) * GRID_GAP
|
||||
|
||||
const railW = (width - GRID_GAP) / 3
|
||||
const railCount = Math.max(0, count - 1)
|
||||
const split = active >= 0 && cols > 1 && railCount > 0
|
||||
const openW = split ? width - railW - GRID_GAP : width
|
||||
const spread = openW >= SPREAD_MIN
|
||||
const pad = spread ? 32 : 24
|
||||
const visualW = spread ? Math.round(openW * VISUAL_SHARE) : 0
|
||||
const copyH = (i: number) => (copyHeights[i] ?? 0) + 2 * pad
|
||||
|
||||
const layout = { cols, split, cardW, openW, railW, pad, visualW, spread }
|
||||
|
||||
/* no rail: either the plain grid, or a single column where the open card has
|
||||
nowhere to go but down */
|
||||
if (!split) {
|
||||
const frames: Frame[] = []
|
||||
const height = (i: number) =>
|
||||
i === active ? Math.max(copyH(i), rowH) : rowH
|
||||
|
||||
if (cols === 1) {
|
||||
let y = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
frames.push({ x: 0, y, w: cardW, h: height(i) })
|
||||
y += frames[i].h + GRID_GAP
|
||||
}
|
||||
return { ...layout, frames, height: y - GRID_GAP }
|
||||
}
|
||||
|
||||
let bottom = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
const frame = {
|
||||
x: (i % cols) * (cardW + GRID_GAP),
|
||||
y: Math.floor(i / cols) * (rowH + GRID_GAP),
|
||||
w: cardW,
|
||||
h: height(i),
|
||||
}
|
||||
frames.push(frame)
|
||||
bottom = Math.max(bottom, frame.y + frame.h)
|
||||
}
|
||||
return { ...layout, frames, height: Math.max(gridH, bottom) }
|
||||
}
|
||||
|
||||
/* split: the open card keeps the grid's footprint, the rest becomes a column */
|
||||
const railH = clamp(
|
||||
(gridH - (railCount - 1) * RAIL_GAP) / railCount,
|
||||
RAIL_MIN_H,
|
||||
RAIL_MAX_H
|
||||
)
|
||||
const railTotal = railCount * railH + (railCount - 1) * RAIL_GAP
|
||||
const height = Math.max(gridH, railTotal, copyH(active))
|
||||
const openX = side === "left" ? 0 : railW + GRID_GAP
|
||||
const railX = side === "left" ? openW + GRID_GAP : 0
|
||||
const railTop = (height - railTotal) / 2
|
||||
|
||||
const frames: Frame[] = []
|
||||
let slot = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (i === active) {
|
||||
frames.push({ x: openX, y: 0, w: openW, h: height })
|
||||
continue
|
||||
}
|
||||
frames.push({
|
||||
x: railX,
|
||||
y: railTop + slot * (railH + RAIL_GAP),
|
||||
w: railW,
|
||||
h: railH,
|
||||
})
|
||||
slot++
|
||||
}
|
||||
|
||||
return { ...layout, frames, height }
|
||||
}
|
||||
|
||||
type Metrics = { width: number; teaser: number[]; copy: number[] }
|
||||
|
||||
const sameNumbers = (a: number[], b: number[]) =>
|
||||
a.length === b.length && a.every((v, i) => v === b[i])
|
||||
|
||||
const sameMetrics = (a: Metrics, b: Metrics) =>
|
||||
a.width === b.width &&
|
||||
sameNumbers(a.teaser, b.teaser) &&
|
||||
sameNumbers(a.copy, b.copy)
|
||||
|
||||
/**
|
||||
* Reads the container width plus the natural height of the two faces that have
|
||||
* to size themselves from real copy. Both are laid out at the width they will
|
||||
* have in their own state, so a measurement never depends on the state the grid
|
||||
* is in – and no card reflows while it travels.
|
||||
*
|
||||
* Re-reading on every width change keeps that honest: the pass runs in a layout
|
||||
* effect, so the corrected heights are in before the browser paints.
|
||||
*/
|
||||
function useGridMetrics(count: number) {
|
||||
const hostRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const teaserRefs = React.useRef<(HTMLElement | null)[]>([])
|
||||
const copyRefs = React.useRef<(HTMLElement | null)[]>([])
|
||||
const [metrics, setMetrics] = React.useState<Metrics>({
|
||||
width: 0,
|
||||
teaser: [],
|
||||
copy: [],
|
||||
})
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const host = hostRef.current
|
||||
if (!host) return
|
||||
|
||||
const heights = (nodes: (HTMLElement | null)[]) =>
|
||||
nodes
|
||||
.slice(0, count)
|
||||
.map((node) =>
|
||||
node ? Math.ceil(node.getBoundingClientRect().height) : 0
|
||||
)
|
||||
|
||||
const read = () =>
|
||||
setMetrics((prev) => {
|
||||
const next: Metrics = {
|
||||
width: host.clientWidth,
|
||||
teaser: heights(teaserRefs.current),
|
||||
copy: heights(copyRefs.current),
|
||||
}
|
||||
return sameMetrics(prev, next) ? prev : next
|
||||
})
|
||||
|
||||
read()
|
||||
|
||||
const observer = new ResizeObserver(read)
|
||||
observer.observe(host)
|
||||
for (const node of [...teaserRefs.current, ...copyRefs.current]) {
|
||||
if (node) observer.observe(node)
|
||||
}
|
||||
return () => observer.disconnect()
|
||||
/* width is a dep on purpose: a resize changes the width the faces are laid
|
||||
out at, so their heights have to be read again in the same pre-paint pass */
|
||||
}, [count, metrics.width])
|
||||
|
||||
return { hostRef, teaserRefs, copyRefs, metrics }
|
||||
}
|
||||
|
||||
/** frames travel on ease-out quint: off immediately, settling without overshoot */
|
||||
const FRAME_MOTION =
|
||||
"transition-[transform,width,height] duration-[560ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
|
||||
|
||||
const FACE_MOTION =
|
||||
"transition-[opacity,translate,scale] duration-300 ease-out motion-reduce:transition-none"
|
||||
|
||||
/** the leaving face clears out at once, the arriving one lets the box move first */
|
||||
const faceMotion = (visible: boolean) =>
|
||||
cn(
|
||||
FACE_MOTION,
|
||||
visible
|
||||
? "opacity-100 translate-y-0 delay-[140ms]"
|
||||
: "opacity-0 translate-y-1"
|
||||
)
|
||||
|
||||
/** faces are decoration for the button underneath – only their links take a click */
|
||||
const FACE_BASE = "pointer-events-none absolute top-0 left-0 z-20"
|
||||
|
||||
/** grid face: mark, title, full copy – the card as the static section had it */
|
||||
function TeaserFace({
|
||||
card,
|
||||
width,
|
||||
visible,
|
||||
innerRef,
|
||||
}: {
|
||||
card: ExpandingCard
|
||||
width: number
|
||||
visible: boolean
|
||||
innerRef: (node: HTMLElement | null) => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
inert={!visible}
|
||||
aria-hidden={!visible}
|
||||
style={{ width }}
|
||||
className={cn(
|
||||
FACE_BASE,
|
||||
"flex flex-col items-start gap-3 p-6",
|
||||
faceMotion(visible)
|
||||
)}
|
||||
>
|
||||
{card.media}
|
||||
<h3 className="text-base font-bold text-balance text-navy">
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||
{card.text}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** rail face: one line per card, the mark shrunk to keep the row scannable */
|
||||
function RailFace({
|
||||
card,
|
||||
width,
|
||||
visible,
|
||||
}: {
|
||||
card: ExpandingCard
|
||||
width: number
|
||||
visible: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
inert={!visible}
|
||||
aria-hidden={!visible}
|
||||
style={{ width }}
|
||||
className={cn(
|
||||
FACE_BASE,
|
||||
"flex h-full items-center gap-3 pr-13 pl-5",
|
||||
faceMotion(visible)
|
||||
)}
|
||||
>
|
||||
<span className="flex size-9 shrink-0 items-center justify-center [&_img]:size-9 [&_picture]:size-9">
|
||||
{card.media}
|
||||
</span>
|
||||
{/* long compounds ("Verbrauchsinformation") have to be allowed to break,
|
||||
or the clamp never gets to put its ellipsis anywhere */}
|
||||
<h3 className="line-clamp-2 min-w-0 text-[13px] leading-snug font-bold break-words hyphens-auto text-navy">
|
||||
{card.title}
|
||||
</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open face: index, title, the copy in full, the detail bullets and the deep
|
||||
* link. Padding is inline because the layout has to add it to the measured copy
|
||||
* height to know how tall the card must be on a single column.
|
||||
*/
|
||||
function OpenFace({
|
||||
card,
|
||||
index,
|
||||
total,
|
||||
plan,
|
||||
visible,
|
||||
panelId,
|
||||
innerRef,
|
||||
}: {
|
||||
card: ExpandingCard
|
||||
index: number
|
||||
total: number
|
||||
plan: Plan
|
||||
visible: boolean
|
||||
panelId: string
|
||||
innerRef: (node: HTMLElement | null) => void
|
||||
}) {
|
||||
const { openW, pad, visualW, spread } = plan
|
||||
return (
|
||||
<div
|
||||
id={panelId}
|
||||
inert={!visible}
|
||||
aria-hidden={!visible}
|
||||
style={{
|
||||
width: openW,
|
||||
padding: pad,
|
||||
paddingRight: visualW ? visualW + pad : pad,
|
||||
}}
|
||||
className={cn(
|
||||
FACE_BASE,
|
||||
"flex h-full flex-col justify-center",
|
||||
faceMotion(visible)
|
||||
)}
|
||||
>
|
||||
<div ref={innerRef} className="flex max-w-lg flex-col gap-3">
|
||||
<p className="flex items-center gap-3 text-[11px] font-bold tracking-[0.16em] text-primary uppercase">
|
||||
<span className="tabular-nums">{pad2(index + 1)}</span>
|
||||
<span aria-hidden className="h-px w-8 bg-brand-200" />
|
||||
<span className="tabular-nums text-brand-300">{pad2(total)}</span>
|
||||
</p>
|
||||
<h3
|
||||
className={cn(
|
||||
"font-extrabold tracking-tight text-balance text-navy",
|
||||
spread ? "text-2xl" : "text-xl"
|
||||
)}
|
||||
>
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||
{card.text}
|
||||
</p>
|
||||
{card.points?.length ? (
|
||||
<Checklist items={card.points} className="mt-1" />
|
||||
) : null}
|
||||
{card.to ? (
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="pointer-events-auto mt-1 h-auto self-start p-0 font-bold"
|
||||
>
|
||||
<Link to={card.to}>
|
||||
{card.linkLabel ?? "Mehr erfahren"}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{!spread && card.visual ? (
|
||||
<span className="mt-3 flex justify-center">{card.visual}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle disc, borrowed from the FAQ list so both disclosures speak the same
|
||||
* language: hairline teal plus that fills in and turns into an × once its card
|
||||
* opens, and slides to the middle of the row while the card is in the rail.
|
||||
*/
|
||||
function CardToggleMark({ face }: { face: Face }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute z-30 flex items-center justify-center rounded-full border border-brand-100 bg-card text-primary shadow-pill",
|
||||
"transition-all duration-300 motion-reduce:transition-none",
|
||||
face === "rail"
|
||||
? "top-1/2 right-4 size-7 -translate-y-1/2"
|
||||
: "top-5 right-5 size-8",
|
||||
face === "open"
|
||||
? "rotate-45 border-transparent bg-primary text-primary-foreground"
|
||||
: "group-hover:border-brand-200 group-hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<Plus className="size-4" strokeWidth={2.5} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function GridCard({
|
||||
card,
|
||||
index,
|
||||
total,
|
||||
face,
|
||||
frame,
|
||||
plan,
|
||||
delay,
|
||||
animate,
|
||||
panelId,
|
||||
onToggle,
|
||||
teaserRef,
|
||||
copyRef,
|
||||
}: {
|
||||
card: ExpandingCard
|
||||
index: number
|
||||
total: number
|
||||
face: Face
|
||||
frame: Frame | undefined
|
||||
plan: Plan
|
||||
delay: number
|
||||
animate: boolean
|
||||
panelId: string
|
||||
onToggle: () => void
|
||||
teaserRef: (node: HTMLElement | null) => void
|
||||
copyRef: (node: HTMLElement | null) => void
|
||||
}) {
|
||||
const open = face === "open"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0 left-0",
|
||||
animate ? FRAME_MOTION : "transition-none",
|
||||
open ? "z-20" : "z-10"
|
||||
)}
|
||||
style={{
|
||||
transform: `translate3d(${frame?.x ?? 0}px, ${frame?.y ?? 0}px, 0)`,
|
||||
width: frame?.w ?? 0,
|
||||
height: frame?.h ?? 0,
|
||||
transitionDelay: `${delay}ms`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-face={face}
|
||||
className={cn(
|
||||
"group relative size-full overflow-hidden rounded-2xl border bg-card",
|
||||
"transition-[background-color,border-color,box-shadow,translate] duration-300 motion-reduce:transition-none",
|
||||
"border-border/70 shadow-card",
|
||||
"data-[face=brief]:hover:-translate-y-0.5 data-[face=brief]:hover:border-brand-200 data-[face=brief]:hover:shadow-card-lg",
|
||||
"data-[face=rail]:hover:border-brand-200 data-[face=rail]:hover:bg-accent/40",
|
||||
"data-[face=open]:border-brand-200 data-[face=open]:shadow-card-lg"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 bg-gradient-to-br from-brand-50 via-card to-card",
|
||||
FACE_MOTION,
|
||||
open ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{plan.visualW && card.visual ? (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{ width: plan.visualW }}
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-y-0 right-0 z-10 flex items-center justify-center p-6",
|
||||
FACE_MOTION,
|
||||
open ? "scale-100 opacity-100" : "scale-95 opacity-0"
|
||||
)}
|
||||
>
|
||||
{card.visual}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
aria-expanded={open}
|
||||
aria-controls={panelId}
|
||||
aria-label={card.title}
|
||||
className="absolute inset-0 z-10 cursor-pointer rounded-2xl outline-none focus-visible:inset-ring-[3px] focus-visible:inset-ring-ring/45"
|
||||
/>
|
||||
|
||||
<TeaserFace
|
||||
card={card}
|
||||
width={plan.cardW}
|
||||
visible={face === "brief"}
|
||||
innerRef={teaserRef}
|
||||
/>
|
||||
<RailFace card={card} width={plan.railW} visible={face === "rail"} />
|
||||
<OpenFace
|
||||
card={card}
|
||||
index={index}
|
||||
total={total}
|
||||
plan={plan}
|
||||
visible={open}
|
||||
panelId={panelId}
|
||||
innerRef={copyRef}
|
||||
/>
|
||||
<CardToggleMark face={face} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Card grid that unpacks in place. Clicking a card grows it to the full height
|
||||
* of the grid it was part of, while the remaining cards gather into a column of
|
||||
* a third of the width beside it – by default on the side the open card came
|
||||
* *from*, so the card you picked stays where you clicked it and everything else
|
||||
* moves out of its way. Opening a card out of the rail therefore flips the
|
||||
* composition; pass `railSide` to pin the column and keep the travel short.
|
||||
*
|
||||
* Every card is absolutely positioned from measured geometry, which keeps a
|
||||
* state change to a transform plus a size: the faces are pre-laid out at the
|
||||
* width they end up with, so no copy re-wraps mid-flight. Under two columns the
|
||||
* split has no room, and the open card grows in place instead.
|
||||
*/
|
||||
function ExpandingCardGrid({
|
||||
items,
|
||||
railSide = "auto",
|
||||
className,
|
||||
}: {
|
||||
items: ExpandingCard[]
|
||||
/** side the minimised cards stack on; `auto` follows the card you clicked */
|
||||
railSide?: RailSide
|
||||
className?: string
|
||||
}) {
|
||||
const [open, setOpen] = React.useState<{ id: string; side: Side } | null>(
|
||||
null
|
||||
)
|
||||
const { hostRef, teaserRefs, copyRefs, metrics } = useGridMetrics(
|
||||
items.length
|
||||
)
|
||||
const uid = React.useId()
|
||||
|
||||
const active = open ? items.findIndex((item) => item.id === open.id) : -1
|
||||
/* a pinned rail decides the composition outright; the pointer-driven side
|
||||
stays in state either way, so switching back to `auto` resumes from the
|
||||
half the reader last picked instead of jumping */
|
||||
const pinned =
|
||||
railSide === "auto" ? null : railSide === "left" ? "right" : "left"
|
||||
const plan = planLayout({
|
||||
count: items.length,
|
||||
width: metrics.width,
|
||||
teaserHeights: metrics.teaser,
|
||||
copyHeights: metrics.copy,
|
||||
active,
|
||||
side: pinned ?? open?.side ?? "left",
|
||||
})
|
||||
|
||||
/* transitions only after the measured geometry has been painted once, and
|
||||
never for the reflow of a resize – dragging a window edge should track the
|
||||
pointer, not trail half a second behind it */
|
||||
const [ready, setReady] = React.useState(false)
|
||||
React.useEffect(() => setReady(true), [])
|
||||
const painted = React.useRef(0)
|
||||
const resized = painted.current !== metrics.width
|
||||
/* written after the commit, not during the render, so React's double
|
||||
invocation in development cannot swallow the flag */
|
||||
React.useEffect(() => {
|
||||
painted.current = metrics.width
|
||||
})
|
||||
const animate = ready && !resized
|
||||
|
||||
const toggle = (index: number) => {
|
||||
if (index === active) {
|
||||
setOpen(null)
|
||||
return
|
||||
}
|
||||
const frame = plan.frames[index]
|
||||
/* the card keeps its side: whichever half it is sitting in when clicked is
|
||||
the half it opens into, so the rail forms on the other one – unless a
|
||||
pinned `railSide` overrules it above */
|
||||
const onRight = frame
|
||||
? frame.x + frame.w / 2 > metrics.width / 2 + 1
|
||||
: false
|
||||
setOpen({ id: items[index].id, side: onRight ? "right" : "left" })
|
||||
}
|
||||
|
||||
let slot = 0
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={hostRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape" && active >= 0) {
|
||||
event.stopPropagation()
|
||||
setOpen(null)
|
||||
}
|
||||
}}
|
||||
style={{ height: plan.height || undefined }}
|
||||
className={cn(
|
||||
"relative",
|
||||
animate
|
||||
? "transition-[height] duration-[560ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
|
||||
: "transition-none",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{items.map((card, index) => {
|
||||
const face: Face =
|
||||
index === active ? "open" : plan.split ? "rail" : "brief"
|
||||
/* the rail arrives as a cascade; everything else moves at once */
|
||||
const delay = face === "rail" ? slot++ * 26 : 0
|
||||
|
||||
return (
|
||||
<GridCard
|
||||
key={card.id}
|
||||
card={card}
|
||||
index={index}
|
||||
total={items.length}
|
||||
face={face}
|
||||
frame={plan.frames[index]}
|
||||
plan={plan}
|
||||
delay={delay}
|
||||
animate={animate}
|
||||
panelId={`${uid}-${card.id}`}
|
||||
onToggle={() => toggle(index)}
|
||||
teaserRef={(node) => {
|
||||
teaserRefs.current[index] = node
|
||||
}}
|
||||
copyRef={(node) => {
|
||||
copyRefs.current[index] = node
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ExpandingCardGrid }
|
||||
export type { ExpandingCard }
|
||||
@@ -1,84 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
|
||||
type FaqItem = {
|
||||
question: React.ReactNode
|
||||
answer: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle disc: hairline teal plus that fills in and turns into an × once its
|
||||
* item opens. Driven by the trigger's own `group`, so hovering the answer body
|
||||
* below leaves the disc alone.
|
||||
*/
|
||||
function FaqIndicator() {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-brand-100 bg-card text-primary shadow-pill transition-[transform,color,background-color,border-color] duration-200 group-hover:border-brand-200 group-hover:bg-accent group-data-[state=open]:rotate-45 group-data-[state=open]:border-transparent group-data-[state=open]:bg-primary group-data-[state=open]:text-primary-foreground"
|
||||
>
|
||||
<Plus className="size-4" strokeWidth={2.5} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ list: one white card per question, plus disc as the toggle, and the open
|
||||
* card tinted brand-50 behind a mint hairline so the answer reads as its own
|
||||
* panel. Single-open and collapsible — the block only ever moves one card's
|
||||
* worth of height, and every answer can be closed again.
|
||||
*
|
||||
* Sized for a reading column (centred `max-w-3xl`) instead of the full 6xl
|
||||
* container; override the width via `className`.
|
||||
*/
|
||||
function FaqAccordion({
|
||||
items,
|
||||
defaultOpen = 0,
|
||||
className,
|
||||
}: {
|
||||
items: FaqItem[]
|
||||
/** index of the item open on first paint; `null` starts fully collapsed */
|
||||
defaultOpen?: number | null
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={defaultOpen == null ? undefined : `faq-${defaultOpen}`}
|
||||
className={cn("mx-auto flex w-full max-w-3xl flex-col gap-3", className)}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`faq-${i}`}
|
||||
/* the all-round `border` supersedes the primitive's divider width,
|
||||
which leaves `last:border-b-0` to strip the final card's bottom
|
||||
edge — `last:border-b` puts it back. */
|
||||
className="rounded-2xl border border-border/70 bg-card px-5 shadow-card transition-colors duration-200 last:border-b hover:border-brand-200 data-[state=open]:border-brand-200 data-[state=open]:bg-brand-50/50 sm:px-6"
|
||||
>
|
||||
<AccordionTrigger
|
||||
className="group items-center gap-5 rounded-xl py-5 text-base leading-snug font-bold text-navy"
|
||||
indicator={<FaqIndicator />}
|
||||
>
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-6 leading-relaxed text-pretty sm:pr-13">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
export { FaqAccordion }
|
||||
export type { FaqItem }
|
||||
@@ -1,64 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type InfoStripItem = {
|
||||
/** self-contained visual (asset-kit render / icon) */
|
||||
media: React.ReactNode
|
||||
eyebrow: React.ReactNode
|
||||
title: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Wide white strip directly under a hero: hairline-divided columns (2 up on
|
||||
* mobile, 3 from `sm`, 5 from `lg`), each a glass render over a teal eyebrow
|
||||
* and a navy title. Dividers are drawn per column for the current layout so
|
||||
* they stay inset from the card's padding.
|
||||
*/
|
||||
function InfoStrip({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: InfoStripItem[]
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-2 rounded-3xl border border-border bg-card p-2 shadow-card sm:grid-cols-3 sm:p-3 lg:grid-cols-5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"flex flex-col items-center px-3 py-4 text-center sm:px-4",
|
||||
/* dividers for the 2-up layout … */
|
||||
i % 2 !== 0 ? "border-l border-border" : "border-l-0",
|
||||
i >= 2 ? "border-t border-border" : "border-t-0",
|
||||
/* … the 3-up layout … */
|
||||
i % 3 !== 0 ? "sm:border-l" : "sm:border-l-0",
|
||||
i >= 3 ? "sm:border-t" : "sm:border-t-0",
|
||||
/* … and the 5-up layout */
|
||||
i % 5 !== 0 ? "lg:border-l" : "lg:border-l-0",
|
||||
"lg:border-t-0"
|
||||
)}
|
||||
>
|
||||
<span className="flex h-14 items-center justify-center">
|
||||
{item.media}
|
||||
</span>
|
||||
<p className="mt-3 text-[0.625rem] font-bold tracking-[0.14em] text-primary uppercase">
|
||||
{item.eyebrow}
|
||||
</p>
|
||||
<div className="mt-1.5 text-xs leading-snug font-semibold text-balance text-navy/75 sm:text-[0.8125rem]">
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { InfoStrip }
|
||||
export type { InfoStripItem }
|
||||
@@ -1,157 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { AssetRender, renders } from "@/components/solenos/site-assets"
|
||||
|
||||
/**
|
||||
* Filled warning triangle. lucide only ships the stroked outline, and at badge
|
||||
* size the fill is what carries the signal. The mark is knocked out of the fill
|
||||
* (`evenodd`, one path) so the plate underneath shows through instead of being
|
||||
* painted over — the glyph stays correct on any backing colour.
|
||||
*/
|
||||
function WarningGlyph({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
className={cn("shrink-0", className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M10.26 4.02a2 2 0 0 1 3.48 0l8.15 14.26a2 2 0 0 1-1.74 3.02H3.85a2 2 0 0 1-1.74-3.02L10.26 4.02Z
|
||||
M12 8.6a1.02 1.02 0 0 0-1.02 1.02v4.16a1.02 1.02 0 0 0 2.04 0V9.62A1.02 1.02 0 0 0 12 8.6Z
|
||||
M12 16.05a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Glass disc holding a white plate with the notice glyph: the same optics as
|
||||
* the glass buttons, sized off the disc so it scales as one object.
|
||||
*/
|
||||
function NoticeSigil({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"glass-veil inset-shadow-glass shadow-glass flex size-24 shrink-0 items-center justify-center rounded-full bg-white/25 backdrop-blur-sm sm:size-32 lg:size-36",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="bg-card shadow-card flex size-[65%] items-center justify-center rounded-full">
|
||||
<span className="text-brand-600 flex size-[54%] items-center justify-center">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notice card for a regulatory deadline: glass sigil left, headline and body
|
||||
* centre, cited source underneath, brand art and the follow-up link right.
|
||||
* The teal wash bleeds in behind the sigil and dissolves before the copy.
|
||||
*/
|
||||
function NoticeBanner({
|
||||
title,
|
||||
children,
|
||||
source,
|
||||
action,
|
||||
visual,
|
||||
icon,
|
||||
className,
|
||||
}: {
|
||||
title: React.ReactNode
|
||||
/** the notice body */
|
||||
children: React.ReactNode
|
||||
source?: {
|
||||
/** what is being cited, e.g. "§ 5 HeizkostenV" */
|
||||
cite: React.ReactNode
|
||||
href: string
|
||||
/** link text; defaults to `href` without its scheme */
|
||||
label?: React.ReactNode
|
||||
}
|
||||
/** follow-up affordance, rendered under the visual */
|
||||
action?: React.ReactNode
|
||||
visual?: React.ReactNode
|
||||
/** overrides the warning glyph in the sigil */
|
||||
icon?: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"border-border bg-card shadow-card relative isolate overflow-hidden rounded-3xl border",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="bg-notice-wash pointer-events-none absolute inset-y-0 left-0 -z-10 w-2/5 opacity-70 sm:opacity-100"
|
||||
/>
|
||||
<div className="flex flex-col gap-6 p-6 sm:flex-row sm:flex-wrap sm:items-center sm:gap-8 sm:p-8 lg:flex-nowrap lg:gap-10 lg:p-10">
|
||||
<NoticeSigil>
|
||||
{icon ?? <WarningGlyph className="size-full" />}
|
||||
</NoticeSigil>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-navy text-2xl font-extrabold tracking-tight text-balance sm:text-3xl">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-navy/85 mt-3 max-w-2xl text-base leading-relaxed text-pretty sm:text-lg">
|
||||
{children}
|
||||
</p>
|
||||
{source ? (
|
||||
<p className="text-muted-foreground mt-5 text-xs sm:text-[0.8125rem]">
|
||||
Quelle: {source.cite} –{" "}
|
||||
<a
|
||||
href={source.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-700 hover:text-brand-800 decoration-brand-300 hover:decoration-brand-600 font-medium break-words underline-offset-2 transition-colors hover:underline"
|
||||
>
|
||||
{source.label ?? source.href.replace(/^https?:\/\/(www\.)?/, "")}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{visual || action ? (
|
||||
<div className="flex w-full shrink-0 flex-col items-start gap-4 sm:items-end lg:w-[30%]">
|
||||
{visual}
|
||||
{action}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The notice's brand art: glass bars with the consumption donut settling in
|
||||
* front of them. Decorative — the copy next to it carries the meaning.
|
||||
*/
|
||||
function NoticeArt({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn("relative aspect-[1.2] w-full", className)}>
|
||||
<AssetRender
|
||||
render={renders.barChart}
|
||||
alt=""
|
||||
className="absolute top-0 left-[4%] w-[64%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassDonutShadow}
|
||||
alt=""
|
||||
className="absolute right-0 bottom-0 w-[62%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { NoticeArt, NoticeBanner }
|
||||
@@ -1,631 +0,0 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import barChartManyFewAvif from "@/assets/solenos/bar-chart-many-few.avif"
|
||||
import barChartManyFewWebp from "@/assets/solenos/bar-chart-many-few.webp"
|
||||
import barChartManyFullAvif from "@/assets/solenos/bar-chart-many-full.avif"
|
||||
import barChartManyFullWebp from "@/assets/solenos/bar-chart-many-full.webp"
|
||||
import barChartAvif from "@/assets/solenos/bar-chart.avif"
|
||||
import barChartWebp from "@/assets/solenos/bar-chart.webp"
|
||||
import brandCubeAvif from "@/assets/solenos/brand-cube-render.avif"
|
||||
import brandCubeWebp from "@/assets/solenos/brand-cube-render.webp"
|
||||
import brandMark3dAvif from "@/assets/solenos/brand-mark-3d.avif"
|
||||
import brandMark3dWebp from "@/assets/solenos/brand-mark-3d.webp"
|
||||
import brandMarkFlatAvif from "@/assets/solenos/brand-mark-flat.avif"
|
||||
import brandMarkFlatWebp from "@/assets/solenos/brand-mark-flat.webp"
|
||||
import buildingAlphaAvif from "@/assets/solenos/building-alpha.avif"
|
||||
import buildingAlphaWebp from "@/assets/solenos/building-alpha.webp"
|
||||
import buildingIsoAvif from "@/assets/solenos/building-iso.avif"
|
||||
import buildingIsoWebp from "@/assets/solenos/building-iso.webp"
|
||||
import buildingAvif from "@/assets/solenos/building.avif"
|
||||
import buildingWebp from "@/assets/solenos/building.webp"
|
||||
import checkOrb from "@/assets/solenos/check-orb.svg"
|
||||
import cloudSupport from "@/assets/solenos/cloud-support.svg"
|
||||
import consumptionRing from "@/assets/solenos/consumption-ring.svg"
|
||||
import cubesRoundAvif from "@/assets/solenos/cubes-round.avif"
|
||||
import cubesRoundWebp from "@/assets/solenos/cubes-round.webp"
|
||||
import drillToolAvif from "@/assets/solenos/drill-tool.avif"
|
||||
import drillToolWebp from "@/assets/solenos/drill-tool.webp"
|
||||
import euroOrb from "@/assets/solenos/euro-orb.svg"
|
||||
import floorHeatingAvif from "@/assets/solenos/floor-heating.avif"
|
||||
import floorHeatingWebp from "@/assets/solenos/floor-heating.webp"
|
||||
import fullHeroSectionAvif from "@/assets/solenos/full-hero-section.avif"
|
||||
import fullHeroSectionWebp from "@/assets/solenos/full-hero-section.webp"
|
||||
import gatewayAvif from "@/assets/solenos/gateway.avif"
|
||||
import gatewayWebp from "@/assets/solenos/gateway.webp"
|
||||
import glassBadge1Avif from "@/assets/solenos/glass-badge-1.avif"
|
||||
import glassBadge1Webp from "@/assets/solenos/glass-badge-1.webp"
|
||||
import glassBadge2Avif from "@/assets/solenos/glass-badge-2.avif"
|
||||
import glassBadge2Webp from "@/assets/solenos/glass-badge-2.webp"
|
||||
import glassBadge3Avif from "@/assets/solenos/glass-badge-3.avif"
|
||||
import glassBadge3Webp from "@/assets/solenos/glass-badge-3.webp"
|
||||
import glassBadge4Avif from "@/assets/solenos/glass-badge-4.avif"
|
||||
import glassBadge4Webp from "@/assets/solenos/glass-badge-4.webp"
|
||||
import glassBadge5Avif from "@/assets/solenos/glass-badge-5.avif"
|
||||
import glassBadge5Webp from "@/assets/solenos/glass-badge-5.webp"
|
||||
import glassBadge6Avif from "@/assets/solenos/glass-badge-6.avif"
|
||||
import glassBadge6Webp from "@/assets/solenos/glass-badge-6.webp"
|
||||
import glassCubes from "@/assets/solenos/glass-cubes.svg"
|
||||
import glassCubesStackAvif from "@/assets/solenos/glass-cubes-stack.avif"
|
||||
import glassCubesStackWebp from "@/assets/solenos/glass-cubes-stack.webp"
|
||||
import glassDonutShadowAvif from "@/assets/solenos/glass-donut-shadow.avif"
|
||||
import glassDonutShadowWebp from "@/assets/solenos/glass-donut-shadow.webp"
|
||||
import glassDonutAvif from "@/assets/solenos/glass-donut.avif"
|
||||
import glassDonutWebp from "@/assets/solenos/glass-donut.webp"
|
||||
import glassGatewayAvif from "@/assets/solenos/glass-gateway.avif"
|
||||
import glassGatewayWebp from "@/assets/solenos/glass-gateway.webp"
|
||||
import glassLaptopAvif from "@/assets/solenos/glass-laptop.avif"
|
||||
import glassLaptopWebp from "@/assets/solenos/glass-laptop.webp"
|
||||
import glassLockAvif from "@/assets/solenos/glass-lock.avif"
|
||||
import glassLockWebp from "@/assets/solenos/glass-lock.webp"
|
||||
import glassMoneyAvif from "@/assets/solenos/glass-money.avif"
|
||||
import glassMoneyWebp from "@/assets/solenos/glass-money.webp"
|
||||
import glassShieldLockAvif from "@/assets/solenos/glass-shield-lock.avif"
|
||||
import glassShieldLockWebp from "@/assets/solenos/glass-shield-lock.webp"
|
||||
import glassSmokeDetectorAvif from "@/assets/solenos/glass-smoke-detector.avif"
|
||||
import glassSmokeDetectorWebp from "@/assets/solenos/glass-smoke-detector.webp"
|
||||
import glassWaterMeterAvif from "@/assets/solenos/glass-water-meter.avif"
|
||||
import glassWaterMeterWebp from "@/assets/solenos/glass-water-meter.webp"
|
||||
import heatMeterAvif from "@/assets/solenos/heat-meter.avif"
|
||||
import heatMeterWebp from "@/assets/solenos/heat-meter.webp"
|
||||
import houseSmallIsoAvif from "@/assets/solenos/house-small-isometric.avif"
|
||||
import houseSmallIsoWebp from "@/assets/solenos/house-small-isometric.webp"
|
||||
import houseWithSprinterMp4 from "@/assets/solenos/house-with-sprinter.mp4"
|
||||
import houseWithSprinterPosterWebp from "@/assets/solenos/house-with-sprinter-poster.webp"
|
||||
import houseWithSprinterWebm from "@/assets/solenos/house-with-sprinter.webm"
|
||||
import housesSmallIsoAvif from "@/assets/solenos/houses-small-isometric.avif"
|
||||
import housesSmallIsoWebp from "@/assets/solenos/houses-small-isometric.webp"
|
||||
import iconBilling from "@/assets/solenos/icon-billing.svg"
|
||||
import iconPortal from "@/assets/solenos/icon-portal.svg"
|
||||
import iconRadio from "@/assets/solenos/icon-radio.svg"
|
||||
import iconSmokeAlarm from "@/assets/solenos/icon-smoke-alarm.svg"
|
||||
import iconUvi from "@/assets/solenos/icon-uvi.svg"
|
||||
import laptopAvif from "@/assets/solenos/laptop-dashboard.avif"
|
||||
import laptopWebp from "@/assets/solenos/laptop-dashboard.webp"
|
||||
import laptopPortfolioAvif from "@/assets/solenos/laptop-portfolio.avif"
|
||||
import laptopPortfolioWebp from "@/assets/solenos/laptop-portfolio.webp"
|
||||
import packageBox from "@/assets/solenos/package-box.svg"
|
||||
import radiatorAvif from "@/assets/solenos/radiator.avif"
|
||||
import radiatorWebp from "@/assets/solenos/radiator.webp"
|
||||
import refreshLoopCloudAvif from "@/assets/solenos/refresh-loop-cloud.avif"
|
||||
import refreshLoopCloudWebp from "@/assets/solenos/refresh-loop-cloud.webp"
|
||||
import refreshLoopAvif from "@/assets/solenos/refresh-loop.avif"
|
||||
import refreshLoopWebp from "@/assets/solenos/refresh-loop.webp"
|
||||
import shippingBoxClosedAvif from "@/assets/solenos/shipping-box-closed.avif"
|
||||
import shippingBoxClosedWebp from "@/assets/solenos/shipping-box-closed.webp"
|
||||
import shippingBoxMetersAvif from "@/assets/solenos/shipping-box-meters.avif"
|
||||
import shippingBoxMetersWebp from "@/assets/solenos/shipping-box-meters.webp"
|
||||
import smokeAlarmAlphaAvif from "@/assets/solenos/smoke-alarm-alpha.avif"
|
||||
import smokeAlarmAlphaWebp from "@/assets/solenos/smoke-alarm-alpha.webp"
|
||||
import smokeAlarmAvif from "@/assets/solenos/smoke-alarm.avif"
|
||||
import smokeAlarmWebp from "@/assets/solenos/smoke-alarm.webp"
|
||||
import sprinterAvif from "@/assets/solenos/sprinter.avif"
|
||||
import sprinterWebp from "@/assets/solenos/sprinter.webp"
|
||||
import tealBars from "@/assets/solenos/teal-bars.svg"
|
||||
import technicianPipeAvif from "@/assets/solenos/technician-pipe.avif"
|
||||
import technicianPipeWebp from "@/assets/solenos/technician-pipe.webp"
|
||||
import uviBadge from "@/assets/solenos/uvi-badge.svg"
|
||||
import uviGlassPaneAvif from "@/assets/solenos/uvi-glass-pane.avif"
|
||||
import uviGlassPaneWebp from "@/assets/solenos/uvi-glass-pane.webp"
|
||||
import wirelessOrb from "@/assets/solenos/wireless-orb.svg"
|
||||
import wordmarkDarkAvif from "@/assets/solenos/wordmark-cubes-dark.avif"
|
||||
import wordmarkDarkWebp from "@/assets/solenos/wordmark-cubes-dark.webp"
|
||||
import wordmarkGlowAvif from "@/assets/solenos/wordmark-cubes-glow.avif"
|
||||
import wordmarkGlowWebp from "@/assets/solenos/wordmark-cubes-glow.webp"
|
||||
import workerDrillingGateway2Avif from "@/assets/solenos/worker-drilling-gateway-2.avif"
|
||||
import workerDrillingGateway2Webp from "@/assets/solenos/worker-drilling-gateway-2.webp"
|
||||
import workerDrillingGatewayAvif from "@/assets/solenos/worker-drilling-gateway.avif"
|
||||
import workerDrillingGatewayWebp from "@/assets/solenos/worker-drilling-gateway.webp"
|
||||
import workerTabletAvif from "@/assets/solenos/worker-tablet.avif"
|
||||
import workerTabletWebp from "@/assets/solenos/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: "SolenOS Gateway",
|
||||
matte: "white",
|
||||
},
|
||||
heatMeter: {
|
||||
avif: heatMeterAvif,
|
||||
webp: heatMeterWebp,
|
||||
alt: "Wärmemengenzähler",
|
||||
matte: "white",
|
||||
},
|
||||
laptopDashboard: {
|
||||
avif: laptopAvif,
|
||||
webp: laptopWebp,
|
||||
alt: "SolenOS 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",
|
||||
},
|
||||
technicianPipe: {
|
||||
avif: technicianPipeAvif,
|
||||
webp: technicianPipeWebp,
|
||||
alt: "Techniker prüft Rohrleitung mit Messgerät",
|
||||
matte: "white",
|
||||
},
|
||||
workerDrillingGateway: {
|
||||
avif: workerDrillingGatewayAvif,
|
||||
webp: workerDrillingGatewayWebp,
|
||||
alt: "Monteur montiert SolenOS Gateway an der Wand",
|
||||
matte: "white",
|
||||
},
|
||||
workerDrillingGateway2: {
|
||||
avif: workerDrillingGateway2Avif,
|
||||
webp: workerDrillingGateway2Webp,
|
||||
alt: "Monteur montiert SolenOS Gateway an Trockenbauwand",
|
||||
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",
|
||||
},
|
||||
/** same subject as `smokeAlarm`, but with genuine alpha – safe on any surface */
|
||||
smokeAlarmAlpha: {
|
||||
avif: smokeAlarmAlphaAvif,
|
||||
webp: smokeAlarmAlphaWebp,
|
||||
alt: "Rauchwarnmelder",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** isometric corner view of the same building type, genuine alpha */
|
||||
buildingIso: {
|
||||
avif: buildingIsoAvif,
|
||||
webp: buildingIsoWebp,
|
||||
alt: "Modernes Mehrfamilienhaus",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** compact single-family house, isometric, genuine alpha */
|
||||
houseSmallIso: {
|
||||
avif: houseSmallIsoAvif,
|
||||
webp: houseSmallIsoWebp,
|
||||
alt: "Einfamilienhaus, isometrisch",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** cluster of small apartment blocks, isometric, genuine alpha */
|
||||
housesSmallIso: {
|
||||
avif: housesSmallIsoAvif,
|
||||
webp: housesSmallIsoWebp,
|
||||
alt: "Mehrere Mehrfamilienhäuser, isometrisch",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** full hero scene: building, gateway, consumption bars & ring on a glass platform */
|
||||
fullHeroSection: {
|
||||
avif: fullHeroSectionAvif,
|
||||
webp: fullHeroSectionWebp,
|
||||
alt: "SolenOS Systemübersicht: Gebäude, Gateway und Verbrauchsdaten",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** same laptop mockup as `laptopDashboard`, portfolio/room screen, genuine alpha */
|
||||
laptopPortfolio: {
|
||||
avif: laptopPortfolioAvif,
|
||||
webp: laptopPortfolioWebp,
|
||||
alt: "SolenOS Portfolio-Ansicht auf dem Laptop",
|
||||
matte: "alpha",
|
||||
},
|
||||
brandMark3d: {
|
||||
avif: brandMark3dAvif,
|
||||
webp: brandMark3dWebp,
|
||||
alt: "SolenOS Markenzeichen",
|
||||
matte: "alpha",
|
||||
},
|
||||
brandMarkFlat: {
|
||||
avif: brandMarkFlatAvif,
|
||||
webp: brandMarkFlatWebp,
|
||||
alt: "SolenOS Markenzeichen",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassCubesStack: {
|
||||
avif: glassCubesStackAvif,
|
||||
webp: glassCubesStackWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassDonut: {
|
||||
avif: glassDonutAvif,
|
||||
webp: glassDonutWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassDonutShadow: {
|
||||
avif: glassDonutShadowAvif,
|
||||
webp: glassDonutShadowWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
uviGlassPane: {
|
||||
avif: uviGlassPaneAvif,
|
||||
webp: uviGlassPaneWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassMoney: {
|
||||
avif: glassMoneyAvif,
|
||||
webp: glassMoneyWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassLaptop: {
|
||||
avif: glassLaptopAvif,
|
||||
webp: glassLaptopWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassGateway: {
|
||||
avif: glassGatewayAvif,
|
||||
webp: glassGatewayWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassWaterMeter: {
|
||||
avif: glassWaterMeterAvif,
|
||||
webp: glassWaterMeterWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassSmokeDetector: {
|
||||
avif: glassSmokeDetectorAvif,
|
||||
webp: glassSmokeDetectorWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassLock: {
|
||||
avif: glassLockAvif,
|
||||
webp: glassLockWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
glassShieldLock: {
|
||||
avif: glassShieldLockAvif,
|
||||
webp: glassShieldLockWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "1" – process-step numeral, genuine alpha */
|
||||
glassBadge1: {
|
||||
avif: glassBadge1Avif,
|
||||
webp: glassBadge1Webp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "2" – process-step numeral, genuine alpha */
|
||||
glassBadge2: {
|
||||
avif: glassBadge2Avif,
|
||||
webp: glassBadge2Webp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "3" – process-step numeral, genuine alpha */
|
||||
glassBadge3: {
|
||||
avif: glassBadge3Avif,
|
||||
webp: glassBadge3Webp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "4" – process-step numeral, genuine alpha */
|
||||
glassBadge4: {
|
||||
avif: glassBadge4Avif,
|
||||
webp: glassBadge4Webp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "5" – process-step numeral, genuine alpha */
|
||||
glassBadge5: {
|
||||
avif: glassBadge5Avif,
|
||||
webp: glassBadge5Webp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** glossy numbered badge "6" – process-step numeral, genuine alpha */
|
||||
glassBadge6: {
|
||||
avif: glassBadge6Avif,
|
||||
webp: glassBadge6Webp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
cubesRound: {
|
||||
avif: cubesRoundAvif,
|
||||
webp: cubesRoundWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
barChart: {
|
||||
avif: barChartAvif,
|
||||
webp: barChartWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
barChartManyFew: {
|
||||
avif: barChartManyFewAvif,
|
||||
webp: barChartManyFewWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
barChartManyFull: {
|
||||
avif: barChartManyFullAvif,
|
||||
webp: barChartManyFullWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
drillTool: {
|
||||
avif: drillToolAvif,
|
||||
webp: drillToolWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
radiator: {
|
||||
avif: radiatorAvif,
|
||||
webp: radiatorWebp,
|
||||
alt: "Heizkörper",
|
||||
matte: "alpha",
|
||||
},
|
||||
floorHeating: {
|
||||
avif: floorHeatingAvif,
|
||||
webp: floorHeatingWebp,
|
||||
alt: "Fußbodenheizungsrohre",
|
||||
matte: "alpha",
|
||||
},
|
||||
sprinter: {
|
||||
avif: sprinterAvif,
|
||||
webp: sprinterWebp,
|
||||
alt: "Servicefahrzeug",
|
||||
matte: "alpha",
|
||||
},
|
||||
workerTablet: {
|
||||
avif: workerTabletAvif,
|
||||
webp: workerTabletWebp,
|
||||
alt: "Monteur mit Tablet",
|
||||
matte: "alpha",
|
||||
},
|
||||
shippingBoxClosed: {
|
||||
avif: shippingBoxClosedAvif,
|
||||
webp: shippingBoxClosedWebp,
|
||||
alt: "Vorkonfigurierte Lieferung",
|
||||
matte: "alpha",
|
||||
},
|
||||
refreshLoop: {
|
||||
avif: refreshLoopAvif,
|
||||
webp: refreshLoopWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
refreshLoopCloud: {
|
||||
avif: refreshLoopCloudAvif,
|
||||
webp: refreshLoopCloudWebp,
|
||||
alt: "",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** baked light glow – use on white/near-white surfaces */
|
||||
wordmarkGlow: {
|
||||
avif: wordmarkGlowAvif,
|
||||
webp: wordmarkGlowWebp,
|
||||
alt: "SolenOS",
|
||||
matte: "alpha",
|
||||
},
|
||||
/** baked dark glow – use on navy/dark surfaces */
|
||||
wordmarkDark: {
|
||||
avif: wordmarkDarkAvif,
|
||||
webp: wordmarkDarkWebp,
|
||||
alt: "SolenOS",
|
||||
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, plus a
|
||||
* hairline brightness lift: the masters matte out at 252–254, which multiply
|
||||
* would otherwise leave as a visible 1 % box on a white card. 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 brightness-[1.015]",
|
||||
imgClassName
|
||||
)}
|
||||
/>
|
||||
</picture>
|
||||
)
|
||||
}
|
||||
|
||||
/** glossy glass numerals, index 0 = "1" */
|
||||
const glassNumbers = [
|
||||
renders.glassBadge1,
|
||||
renders.glassBadge2,
|
||||
renders.glassBadge3,
|
||||
renders.glassBadge4,
|
||||
renders.glassBadge5,
|
||||
renders.glassBadge6,
|
||||
]
|
||||
|
||||
/**
|
||||
* Glass numeral for a step in an ordered list. Decorative on purpose: the
|
||||
* enclosing <ol> already carries the ordinal, so the badge is not announced a
|
||||
* second time. Only 1–6 exist as renders; a longer list falls back to the
|
||||
* numeral on a teal disc rather than losing its number.
|
||||
*/
|
||||
function GlassNumber({
|
||||
n,
|
||||
className,
|
||||
loading = "lazy",
|
||||
}: {
|
||||
n: number
|
||||
/** sizes the badge – pass a `size-*` utility */
|
||||
className?: string
|
||||
loading?: "lazy" | "eager"
|
||||
}) {
|
||||
const render = glassNumbers[n - 1]
|
||||
|
||||
if (!render) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex shrink-0 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground shadow-pill",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AssetRender
|
||||
render={render}
|
||||
alt=""
|
||||
className={cn("flex shrink-0 items-center justify-center", className)}
|
||||
imgClassName="size-full"
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface VideoAsset {
|
||||
mp4: string
|
||||
webm: string
|
||||
/** static frame shown before playback starts / while loading */
|
||||
poster: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
/** Looping background/product clips (opaque compositions, not matte assets). */
|
||||
const videos = {
|
||||
houseWithSprinter: {
|
||||
mp4: houseWithSprinterMp4,
|
||||
webm: houseWithSprinterWebm,
|
||||
poster: houseWithSprinterPosterWebp,
|
||||
alt: "Servicefahrzeug fährt zum Gebäude vor",
|
||||
},
|
||||
} satisfies Record<string, VideoAsset>
|
||||
|
||||
/**
|
||||
* Renders a kit video as WebM→MP4 <video>, muted/looping/autoplaying by
|
||||
* default for ambient hero use. Falls back to the poster frame if the
|
||||
* browser can't play either source.
|
||||
*/
|
||||
function VideoRender({
|
||||
video,
|
||||
className,
|
||||
autoPlay = true,
|
||||
loop = true,
|
||||
muted = true,
|
||||
playsInline = true,
|
||||
}: {
|
||||
video: VideoAsset
|
||||
className?: string
|
||||
autoPlay?: boolean
|
||||
loop?: boolean
|
||||
muted?: boolean
|
||||
playsInline?: boolean
|
||||
}) {
|
||||
return (
|
||||
<video
|
||||
className={cn("object-contain", className)}
|
||||
poster={video.poster}
|
||||
autoPlay={autoPlay}
|
||||
loop={loop}
|
||||
muted={muted}
|
||||
playsInline={playsInline}
|
||||
aria-label={video.alt}
|
||||
>
|
||||
<source src={video.webm} type="video/webm" />
|
||||
<source src={video.mp4} type="video/mp4" />
|
||||
</video>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AssetRender,
|
||||
GlassNumber,
|
||||
icons,
|
||||
renders,
|
||||
VideoRender,
|
||||
vectors,
|
||||
videos,
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { IconBadge } from "@/components/solenos/icon-tile"
|
||||
|
||||
/** The paragraph of a regulation a property is cited against. */
|
||||
type LegalSource = {
|
||||
cite: string
|
||||
href: string
|
||||
}
|
||||
|
||||
/** One technical property: line icon, title, body, optional legal citation. */
|
||||
type Topic = {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
body: string
|
||||
source?: LegalSource
|
||||
}
|
||||
|
||||
/** Cited source line, pinned to the card's baseline so rows stay aligned. */
|
||||
function LegalSourceNote({ source }: { source: LegalSource }) {
|
||||
return (
|
||||
<p className="mt-auto pt-4 text-xs text-muted-foreground">
|
||||
Quelle:{" "}
|
||||
<a
|
||||
href={source.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium text-brand-700 underline-offset-2 transition-colors hover:text-brand-800 hover:underline"
|
||||
>
|
||||
{source.cite}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
/** Property grid: line icon over a navy title, body copy, optional citation. */
|
||||
function TopicCards({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: Topic[]
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("mt-10 grid gap-5", className)}>
|
||||
{items.map((item) => (
|
||||
<Card key={item.title}>
|
||||
<CardContent className="flex flex-1 flex-col">
|
||||
<IconBadge variant="outline" shape="squircle" size="lg">
|
||||
{item.icon}
|
||||
</IconBadge>
|
||||
<h3 className="mt-5 text-base font-bold text-balance text-navy">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||
{item.body}
|
||||
</p>
|
||||
{item.source ? <LegalSourceNote source={item.source} /> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { LegalSourceNote, TopicCards }
|
||||
export type { LegalSource, Topic }
|
||||
@@ -1,117 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { AssetRender, renders } from "@/components/solenos/site-assets"
|
||||
|
||||
type TrustPanelItem = {
|
||||
icon: React.ReactNode
|
||||
label: React.ReactNode
|
||||
to?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust panel: a pale plate carrying the headline and the technical properties
|
||||
* as white pills (teal line icon beside a short label), two across from `sm`
|
||||
* so the labels stay on one line next to the security render, with the
|
||||
* follow-up link on the baseline underneath.
|
||||
*/
|
||||
function TrustProperty({ item }: { item: TrustPanelItem }) {
|
||||
const content = (
|
||||
<>
|
||||
<span className="flex shrink-0 text-brand-600 [&_svg]:size-7">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 text-[0.9375rem] leading-snug font-medium text-balance text-navy/75">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.to ? (
|
||||
<ArrowRight
|
||||
aria-hidden="true"
|
||||
className="size-4 shrink-0 text-brand-600"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
const className =
|
||||
"flex items-center gap-4 rounded-xl bg-card px-5 py-3.5 shadow-card"
|
||||
|
||||
return item.to ? (
|
||||
<Link
|
||||
to={item.to}
|
||||
className={`${className} outline-none transition-colors hover:bg-brand-50 focus-visible:ring-[3px] focus-visible:ring-ring/40`}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={className}>{content}</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrustPanel({
|
||||
title,
|
||||
items,
|
||||
visual,
|
||||
action,
|
||||
className,
|
||||
}: {
|
||||
title: React.ReactNode
|
||||
items: TrustPanelItem[]
|
||||
visual?: React.ReactNode
|
||||
/** follow-up affordance, right-aligned under the pills */
|
||||
action?: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
/* the plate is a wash, not a card: it stays translucent so the page
|
||||
reads through it and the white pills keep their contrast */
|
||||
"border-border/70 rounded-3xl border bg-gradient-to-br from-white/70 via-white/40 to-brand-50/40 p-6 sm:p-8 lg:p-10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="lg:flex lg:items-center lg:gap-10">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-navy text-lg font-bold tracking-tight text-balance sm:text-xl">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
{items.map((item, i) => (
|
||||
<TrustProperty key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{visual ? (
|
||||
/* stacked layouts hang the render off the right edge instead of
|
||||
centring it, so it anchors the link underneath rather than
|
||||
leaving the row beside it empty */
|
||||
<div className="mt-8 flex justify-end lg:mt-0 lg:shrink-0">
|
||||
{visual}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? (
|
||||
<div className="mt-8 flex justify-end lg:mt-6">{action}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel's security render: the glass shield with the lock inside it.
|
||||
* Decorative — the pills next to it carry the meaning.
|
||||
*/
|
||||
function TrustShieldArt({ className }: { className?: string }) {
|
||||
return (
|
||||
<AssetRender
|
||||
render={renders.glassShieldLock}
|
||||
alt=""
|
||||
className={cn("block w-40 lg:w-56", className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { TrustPanel, TrustShieldArt }
|
||||
export type { TrustPanelItem }
|
||||
@@ -1,9 +0,0 @@
|
||||
/** gesetze-im-internet base for the Heizkostenverordnung. */
|
||||
const HEIZKOSTENV = "https://www.gesetze-im-internet.de/heizkostenv"
|
||||
|
||||
/** Permalink to a single paragraph, e.g. `heizkostenv("6a")`. */
|
||||
function heizkostenv(paragraph: string) {
|
||||
return `${HEIZKOSTENV}/__${paragraph}.html`
|
||||
}
|
||||
|
||||
export { heizkostenv }
|
||||
@@ -1,811 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import {
|
||||
ArrowRight,
|
||||
Calculator,
|
||||
ChevronRight,
|
||||
Lock,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Container, Section, SectionHeading } from "@/components/solenos/section"
|
||||
import { PageHero } from "@/components/solenos/page-hero"
|
||||
import { HeroArt } from "@/components/solenos/hero-art"
|
||||
import { InfoStrip } from "@/components/solenos/info-strip"
|
||||
import { NumberedList, StepFlow } from "@/components/solenos/process-steps"
|
||||
import { Checklist } from "@/components/solenos/checklist"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import {
|
||||
ExpandingCardGrid,
|
||||
type ExpandingCard,
|
||||
} from "@/components/solenos/expanding-card-grid"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
|
||||
import { NoticeArt, NoticeBanner } from "@/components/solenos/notice-banner"
|
||||
import { GermanyIcon } from "@/components/solenos/line-icons"
|
||||
import { TrustPanel, TrustShieldArt } from "@/components/solenos/trust-panel"
|
||||
import {
|
||||
AssetRender,
|
||||
icons,
|
||||
renders,
|
||||
type RenderAsset,
|
||||
} from "@/components/solenos/site-assets"
|
||||
|
||||
function StripIcon({ render }: { render: RenderAsset }) {
|
||||
return (
|
||||
<AssetRender
|
||||
render={render}
|
||||
alt=""
|
||||
className="flex size-14 items-center justify-center"
|
||||
imgClassName="w-14"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const infoStripItems = [
|
||||
{
|
||||
media: <StripIcon render={renders.uviGlassPane} />,
|
||||
eyebrow: "UVI",
|
||||
title: "Monatliche UVI",
|
||||
},
|
||||
{
|
||||
media: <StripIcon render={renders.glassMoney} />,
|
||||
eyebrow: "Abrechnung",
|
||||
title: "Heizkosten\u00adabrechnung",
|
||||
},
|
||||
{
|
||||
media: <StripIcon render={renders.glassSmokeDetector} />,
|
||||
eyebrow: "Sicherheit",
|
||||
title: "Rauchwarnmelder",
|
||||
},
|
||||
{
|
||||
media: <StripIcon render={renders.glassGateway} />,
|
||||
eyebrow: "Technik",
|
||||
title: "Fernablesbare Messtechnik",
|
||||
},
|
||||
{
|
||||
media: <StripIcon render={renders.glassLaptop} />,
|
||||
eyebrow: "Software",
|
||||
title: "Portal & Verwaltung",
|
||||
},
|
||||
]
|
||||
|
||||
const operationsScope: ExpandingCard[] = [
|
||||
{
|
||||
id: "uvi",
|
||||
media: <img src={icons.uvi} alt="" className="size-12" loading="lazy" />,
|
||||
visual: (
|
||||
<AssetRender
|
||||
render={renders.uviGlassPane}
|
||||
className="flex w-full justify-center"
|
||||
imgClassName="w-full max-w-60"
|
||||
/>
|
||||
),
|
||||
title: "Unterjährige Verbrauchsinformation",
|
||||
text: "Verbrauchsdaten werden automatisch erfasst und die regelmäßigen Verbrauchsinformationen für die Bewohner bereitgestellt.",
|
||||
points: [
|
||||
"monatliche Erzeugung der Verbrauchsinformation",
|
||||
"Bereitstellung im Mieterportal für jede Wohnung",
|
||||
"Nutzerwechsel ohne Ablesetermin vor Ort",
|
||||
],
|
||||
to: "/produkte/uvi",
|
||||
},
|
||||
{
|
||||
id: "heizkostenabrechnung",
|
||||
media: (
|
||||
<img src={icons.billing} alt="" className="size-12" loading="lazy" />
|
||||
),
|
||||
visual: (
|
||||
<AssetRender
|
||||
render={renders.glassMoney}
|
||||
className="flex w-full justify-center"
|
||||
imgClassName="w-full max-w-52"
|
||||
/>
|
||||
),
|
||||
title: "Heizkostenabrechnung",
|
||||
text: "Aus den erfassten Verbrauchsdaten wird die jährliche Heizkostenabrechnung im Online-Portal erstellt und bereitgestellt.",
|
||||
points: [
|
||||
"Verteilung der Wärme- und Warmwasserkosten nach HeizkostenV",
|
||||
"Berücksichtigung von Nutzerwechseln und Leerständen",
|
||||
"versandfertige Einzelabrechnung je Wohnung im Online-Portal",
|
||||
"Pflichtangaben nach § 6a Absatz 3 HeizkostenV",
|
||||
],
|
||||
to: "/produkte/heizkostenabrechnung",
|
||||
},
|
||||
{
|
||||
id: "rauchwarnmelder",
|
||||
media: (
|
||||
<img src={icons.smokeAlarm} alt="" className="size-12" loading="lazy" />
|
||||
),
|
||||
visual: (
|
||||
<AssetRender
|
||||
render={renders.glassSmokeDetector}
|
||||
className="flex w-full justify-center"
|
||||
imgClassName="w-full max-w-48"
|
||||
/>
|
||||
),
|
||||
title: "Rauchwarnmelder",
|
||||
text: "Rauchwarnmelder können gemeinsam mit der übrigen Gebäudetechnik geplant, installiert und zentral verwaltet werden.",
|
||||
points: [
|
||||
"Rauchwarnmelder inklusive Montage und Eigentumsübertragung",
|
||||
"Bedarfsermittlung je Wohnung aus den Gebäudedaten",
|
||||
"regelmäßige Ferninspektion und Störungsmeldungen",
|
||||
],
|
||||
to: "/produkte/rauchwarnmelder",
|
||||
},
|
||||
{
|
||||
id: "messtechnik",
|
||||
media: <img src={icons.radio} alt="" className="size-12" loading="lazy" />,
|
||||
visual: (
|
||||
<AssetRender
|
||||
render={renders.heatMeter}
|
||||
className="flex w-full justify-center"
|
||||
imgClassName="w-full max-w-52"
|
||||
/>
|
||||
),
|
||||
title: "Fernauslesbare Messtechnik",
|
||||
text: "Wärme- und Wasserverbrauch werden über kompatible Geräte automatisch erfasst.",
|
||||
points: [
|
||||
"Wärmezähler, Heizkostenverteiler und Wasserzähler messen laufend je Wohnung",
|
||||
"Geräte sind fernablesbar, also ohne Zugang zur Wohnung auslesbar",
|
||||
"die Funkschnittstelle folgt einem offenen Standard (OMS)",
|
||||
],
|
||||
to: "/produkte/messtechnik-infrastruktur",
|
||||
},
|
||||
{
|
||||
id: "gateway",
|
||||
media: (
|
||||
<AssetRender
|
||||
render={renders.gateway}
|
||||
className="flex size-12 items-center justify-center"
|
||||
imgClassName="w-12"
|
||||
/>
|
||||
),
|
||||
visual: (
|
||||
<AssetRender
|
||||
render={renders.glassGateway}
|
||||
className="flex w-full justify-center"
|
||||
imgClassName="w-full max-w-52"
|
||||
/>
|
||||
),
|
||||
title: "Gateway und Datenübertragung",
|
||||
text: "Die Messdaten werden zentral gesammelt und an SolenOS übertragen.",
|
||||
points: [
|
||||
"die erforderliche Gateway-Infrastruktur wird passend zum Objekt bestimmt",
|
||||
"Übertragung per Mobilfunk, ohne Internetanschluss im Gebäude",
|
||||
"TLS-verschlüsselte Übertragung an die Plattform",
|
||||
],
|
||||
to: "/produkte/messtechnik-infrastruktur",
|
||||
linkLabel: "Messtechnik und Infrastruktur",
|
||||
},
|
||||
{
|
||||
id: "portale",
|
||||
media: (
|
||||
<img src={icons.portal} alt="" className="size-12" loading="lazy" />
|
||||
),
|
||||
visual: (
|
||||
<AssetRender
|
||||
render={renders.laptopDashboard}
|
||||
className="flex w-full justify-center"
|
||||
imgClassName="w-full"
|
||||
/>
|
||||
),
|
||||
title: "Portale und Verwaltung",
|
||||
text: "Eigentümer betreuen Gebäude, Wohnungen, Geräte und Verbrauchsdaten zentral. Bewohner haben ihre persönliche Übersicht im Mieterportal.",
|
||||
points: [
|
||||
"Portalzugang für Eigentümer und Verwaltung",
|
||||
"eigener Mieterportal-Zugang für jede Wohnung",
|
||||
"ausschließlich Daten der eigenen Wohnung und des eigenen Nutzungszeitraums sichtbar",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const processSteps = [
|
||||
{
|
||||
media: <AssetRender render={renders.building} imgClassName="w-52" />,
|
||||
title: "Gebäudedaten online erfassen",
|
||||
description:
|
||||
"Adresse, Wohnungen und Heizart online angeben. SolenOS ermittelt daraus die notwendige Ausstattung und prüft sie vor dem Versand.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.shippingBoxMeters} imgClassName="w-36" />
|
||||
),
|
||||
title: "Vorkonfiguriert erhalten",
|
||||
description:
|
||||
"Die geprüften Geräte werden bereits den vorgesehenen Wohnungen zugeordnet geliefert.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.drillTool} imgClassName="w-32" />,
|
||||
title: "Vor Ort montieren",
|
||||
description:
|
||||
"Selbst, mit dem eigenen Fachbetrieb oder durch SolenOS montieren lassen. Eine Softwarekonfiguration vor Ort ist nicht erforderlich.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.laptopDashboard} imgClassName="w-44" />
|
||||
),
|
||||
title: "Betrieb übernehmen lassen",
|
||||
description:
|
||||
"SolenOS übernimmt Einrichtung, Datenübertragung und laufenden Betrieb; Bewohner erhalten ihren Portalzugang.",
|
||||
},
|
||||
]
|
||||
|
||||
const lessEffort = [
|
||||
{
|
||||
title: "Zum Einbau vorkonfigurierte Messeinrichtungen",
|
||||
text: "SolenOS bestimmt die Ausstattung aus den Objektdaten, prüft sie und ordnet die Geräte vor dem Versand zu.",
|
||||
},
|
||||
{
|
||||
title: "Keine Softwarekonfiguration vor Ort",
|
||||
text: "Vor Ort wird nur montiert. Eine Softwarekonfiguration ist nicht erforderlich.",
|
||||
},
|
||||
{
|
||||
title: "Bestehende Fachbetriebe nutzen",
|
||||
text: "Kein spezieller SolenOS-Partner erforderlich – eigene Hausmeister, Heizungsbauer oder bestehende Installationspartner können weiterhin eingesetzt werden.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
to: "/produkte/uvi",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
to: "/produkte/heizkostenabrechnung",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<img src={icons.smokeAlarm} alt="" className="size-10" loading="lazy" />
|
||||
),
|
||||
title: "Rauchwarnmelder",
|
||||
text: "Rauchwarnmelder gemeinsam mit der übrigen Gebäudetechnik planen und verwalten.",
|
||||
to: "/produkte/rauchwarnmelder",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
to: "/produkte/messtechnik-infrastruktur",
|
||||
},
|
||||
]
|
||||
|
||||
const audiences = [
|
||||
{
|
||||
media: (
|
||||
<AssetRender
|
||||
render={renders.houseSmallIso}
|
||||
alt=""
|
||||
className="flex size-14 shrink-0 items-center justify-center"
|
||||
imgClassName="w-14"
|
||||
/>
|
||||
),
|
||||
name: "Hauseigentümer",
|
||||
headline: "Mein Mehrfamilienhaus ausstatten",
|
||||
text: "Messtechnik, UVI, Heizkostenabrechnung und Rauchwarnmelder in einem System – selbst installieren oder als Komplettlösung.",
|
||||
points: [
|
||||
"Ausstattung aus Gebäudedaten ermitteln",
|
||||
"vorkonfigurierte Geräte erhalten",
|
||||
"eigenen Fachbetrieb nutzen",
|
||||
"laufenden Messbetrieb digital betreuen",
|
||||
],
|
||||
cta: "SolenOS für Hauseigentümer",
|
||||
to: "/fuer-wen/hauseigentuemer",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender
|
||||
render={renders.housesSmallIso}
|
||||
alt=""
|
||||
className="flex size-14 shrink-0 items-center justify-center"
|
||||
imgClassName="w-14"
|
||||
/>
|
||||
),
|
||||
name: "Hausverwaltung",
|
||||
headline: "Viele Gebäude. Ein strukturierter Rollout.",
|
||||
text: "Gebäude gesammelt konfigurieren, vorkonfigurierte Technik ausrollen und den Fortschritt zentral verfolgen.",
|
||||
points: [
|
||||
"Rollout pro Liegenschaft",
|
||||
"Gebäude und Wohnungen zentral verwalten",
|
||||
"offene Aufgaben sichtbar",
|
||||
"Bewohner-Einladungen verfolgen",
|
||||
"UVI und Abrechnung über mehrere Gebäude",
|
||||
],
|
||||
cta: "SolenOS für Hausverwaltungen",
|
||||
to: "/fuer-wen/hausverwaltungen",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender
|
||||
render={renders.sprinter}
|
||||
alt=""
|
||||
className="flex size-14 shrink-0 items-center justify-center"
|
||||
imgClassName="w-14"
|
||||
/>
|
||||
),
|
||||
name: "Messdienstleister",
|
||||
headline: "Ihre Messdienstleistung. Unsere Infrastruktur.",
|
||||
text: "SolenOS liefert die technische Plattform für Fernauslesung, Geräteverwaltung, Verbrauchsdaten und digitale Messdienstleistungen.",
|
||||
points: [
|
||||
"zentrale Infrastruktur",
|
||||
"mehrere Kunden und Gebäude",
|
||||
"Geräte- und Verbrauchsdaten",
|
||||
"UVI",
|
||||
"Heizkostenabrechnungsprozesse",
|
||||
"Optionen für eigenes Logo und Eigenmarke",
|
||||
],
|
||||
cta: "SolenOS für Messdienstleister",
|
||||
to: "/fuer-wen/messdienstleister",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender
|
||||
render={renders.workerTablet}
|
||||
alt=""
|
||||
className="flex size-14 shrink-0 items-center justify-center"
|
||||
imgClassName="w-14"
|
||||
/>
|
||||
),
|
||||
name: "Fachbetrieb",
|
||||
headline: "SolenOS beim Kunden installieren",
|
||||
text: "Keine neue Vertriebsrolle notwendig. Technik montieren und die bestehende Kundenbeziehung um moderne Messdienstleistungen ergänzen.",
|
||||
points: [
|
||||
"vorkonfigurierte Geräte",
|
||||
"keine manuelle Geräteanlage vor Ort",
|
||||
"keine SolenOS-Vertriebspartnerschaft erforderlich",
|
||||
"Installation für bestehende Kunden",
|
||||
],
|
||||
cta: "SolenOS für Fachbetriebe",
|
||||
to: "/fuer-wen/installateure",
|
||||
},
|
||||
]
|
||||
|
||||
const portfolioScopes = [
|
||||
{
|
||||
render: renders.buildingIso,
|
||||
headline: "1 Gebäude",
|
||||
example: "18 Wohnungen · 75 Geräte",
|
||||
capabilities: [
|
||||
"Gebäude konfigurieren",
|
||||
"Wohnungen verwalten",
|
||||
"Bewohner einladen",
|
||||
"Geräte überwachen",
|
||||
"UVI und Abrechnung verwalten",
|
||||
],
|
||||
},
|
||||
{
|
||||
render: renders.housesSmallIso,
|
||||
headline: "50 Gebäude",
|
||||
example: "642 Wohnungen · 2.481 Geräte",
|
||||
capabilities: [
|
||||
"Rollout-Status je Gebäude",
|
||||
"mehrere Liegenschaften zentral verwalten",
|
||||
"offene Aufgaben portfolioübergreifend sehen",
|
||||
"Geräte- und Nutzerstatus vergleichen",
|
||||
"UVI- und Abrechnungsstatus verfolgen",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const priceFlow = [
|
||||
{ title: "Leistungen auswählen" },
|
||||
{ title: "Gebäudedaten online erfassen" },
|
||||
{ title: "Persönlichen Link erhalten" },
|
||||
{ title: "Details und Fotos im Portal ergänzen" },
|
||||
{ title: "Geprüftes Gesamtangebot erhalten" },
|
||||
]
|
||||
|
||||
const faqs: FaqItem[] = [
|
||||
{
|
||||
question: "Muss ich SolenOS selbst installieren?",
|
||||
answer:
|
||||
"Nein. Je nach Ausstattung können geeignete Arbeiten selbst bzw. durch einen vorhandenen Fachbetrieb durchgeführt werden. Alternativ kann die Installation als Komplettleistung beauftragt werden.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Kann mein bestehender Heizungs- oder Sanitärbetrieb die Geräte montieren?",
|
||||
answer:
|
||||
"In vielen Fällen kann ein vorhandener geeigneter Fachbetrieb eingesetzt werden. Welche Arbeiten erforderlich sind, hängt von der jeweiligen Messtechnik und Einbausituation ab.",
|
||||
},
|
||||
{
|
||||
question: "Muss vor Ort Software konfiguriert werden?",
|
||||
answer:
|
||||
"Nein. SolenOS ordnet die Geräte vor dem Versand dem Gebäude und den vorgesehenen Einheiten zu. Vor Ort wird nur montiert; eine Softwarekonfiguration ist nicht erforderlich.",
|
||||
},
|
||||
{
|
||||
question: "Wann erhalte ich den verbindlichen Gesamtpreis?",
|
||||
answer:
|
||||
"Nach dem Schnellkonfigurator ergänzen Sie Gebäudedetails und Fotos vorhandener Einbausituationen im Portal. SolenOS prüft die Angaben manuell, bestimmt die konkrete Ausstattung und erstellt anschließend das verbindliche Gesamtangebot.",
|
||||
},
|
||||
{
|
||||
question: "Welche Leistungen kann ich auswählen?",
|
||||
answer:
|
||||
"Monatliche Verbrauchsinformation, Heizkostenabrechnung und Rauchwarnmelder lassen sich im Schnellkonfigurator unabhängig voneinander hinzufügen oder abwählen."
|
||||
},
|
||||
{
|
||||
question: "Kann ich bestehende Messtechnik weiterverwenden?",
|
||||
answer:
|
||||
"Das hängt von den vorhandenen Geräten ab: Voraussetzung ist, dass sie fernauslesbar sind und auf geeigneten Standards basieren – dann lassen sich unterschiedliche Zähler- und Gerätetypen einbinden. Vorhandene Messtechnik wird deshalb bei der vollständigen Konfiguration je Gebäude und Einheit erfasst und geprüft; nicht fernablesbare Verbrauchserfassung muss ohnehin bis zum 31.12.2026 nachgerüstet oder ersetzt werden. Was übernommen werden kann, wirkt sich auf die Geräteliste und damit auf den finalen Preis aus.",
|
||||
},
|
||||
{
|
||||
question: "Was passiert bei einem Nutzerwechsel?",
|
||||
answer:
|
||||
"Der Wechsel wird für die betroffene Einheit im SolenOS Portal hinterlegt. Weil die Geräte fernauslesbar sind, werden die Werte zum Stichtag aus der laufenden Datenerfassung übernommen – ein Ablesetermin vor Ort ist dafür in der Regel nicht erforderlich. Die Verbräuche werden anschließend den jeweiligen Nutzungszeiträumen zugeordnet, der neue Bewohner erhält seinen Portalzugang, der bisherige verliert ihn. An der Messtechnik selbst ändert sich nichts.",
|
||||
},
|
||||
{
|
||||
question: "Funktioniert SolenOS auch für mehrere Gebäude?",
|
||||
answer:
|
||||
"Ja. Gebäude werden einzeln strukturiert, können aber gemeinsam innerhalb eines Portfolios verwaltet werden.",
|
||||
},
|
||||
]
|
||||
|
||||
const trustProperties = [
|
||||
{
|
||||
icon: <GermanyIcon />,
|
||||
label: "Daten ausschließlich in Deutschland gehostet",
|
||||
to: "/wissen/datenschutz-sicherheit",
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck />,
|
||||
label: "AES-Verschlüsselung schützt gespeicherte Messdaten",
|
||||
to: "/wissen/datenschutz-sicherheit",
|
||||
},
|
||||
{
|
||||
icon: <Lock />,
|
||||
label: "TLS schützt Daten während der Übertragung",
|
||||
to: "/wissen/datenschutz-sicherheit",
|
||||
},
|
||||
{
|
||||
icon: <Users />,
|
||||
label:
|
||||
"Bewohner sehen nur Daten ihrer eigenen Wohnung und ihres eigenen Nutzungszeitraums",
|
||||
to: "/wissen/datenschutz-sicherheit",
|
||||
},
|
||||
]
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={
|
||||
<>
|
||||
Messdienstleistungen
|
||||
<br className="hidden sm:block" />{" "}
|
||||
<span className="text-primary">einfach</span> gemacht.
|
||||
</>
|
||||
}
|
||||
lead="Fernauslesbare Messtechnik, monatliche Verbrauchsinformation (UVI), Heizkostenabrechnung und Rauchwarnmelder – vorkonfiguriert geliefert, digital betreut und auf Wunsch komplett installiert."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link to="/so-funktionierts">So funktioniert SolenOS</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
|
||||
visual={<HeroArt />}
|
||||
/>
|
||||
|
||||
<Container>
|
||||
<InfoStrip
|
||||
items={infoStripItems}
|
||||
className="-mt-6 mb-12 sm:-mt-8 sm:mb-14 lg:-mt-10"
|
||||
/>
|
||||
|
||||
<NoticeBanner
|
||||
title="Nachrüstung bis 31.12.2026 erforderlich"
|
||||
source={{
|
||||
cite: "§ 5 HeizkostenV",
|
||||
href: "https://www.gesetze-im-internet.de/heizkostenv/__5.html",
|
||||
}}
|
||||
visual={<NoticeArt className="max-lg:hidden" />}
|
||||
action={
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="text-brand-800 hover:text-navy decoration-brand-500 hover:decoration-brand-800 h-auto p-0 text-[15px] font-bold underline decoration-2 underline-offset-[6px]"
|
||||
>
|
||||
<Link to="/wissen">
|
||||
Mehr erfahren
|
||||
<ChevronRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Bestehende nicht fernablesbare Verbrauchserfassung muss grundsätzlich
|
||||
bis zum 31.12.2026 nachgerüstet oder ersetzt werden. SolenOS verbindet
|
||||
die neue Messtechnik direkt mit UVI und Abrechnung.
|
||||
</NoticeBanner>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Das System"
|
||||
title="Alles, was für den laufenden Messbetrieb benötigt wird"
|
||||
lead="SolenOS verbindet Messtechnik, Datenerfassung und Software in einem durchgängigen System. Bestellung, Hardware, Konfiguration und laufender Betrieb greifen dabei ineinander."
|
||||
/>
|
||||
<ExpandingCardGrid
|
||||
items={operationsScope}
|
||||
railSide="right"
|
||||
className="mt-12"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<div className="rounded-3xl bg-gradient-to-b from-secondary to-card px-6 py-10 sm:px-10 sm:py-12 lg:px-12">
|
||||
<SectionHeading
|
||||
eyebrow="So funktioniert's"
|
||||
title="Online in vier Schritten zum laufenden Messbetrieb"
|
||||
lead="Sie starten ohne Erstgespräch: Objektdaten online erfassen, die passende Ausstattung von SolenOS bestimmen lassen, vorkonfiguriert erhalten und montieren."
|
||||
/>
|
||||
</div>
|
||||
<StepFlow steps={processSteps} className="mt-4" />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Installation"
|
||||
title="Vor Ort nur montieren"
|
||||
lead="Vor Ort wird nur montiert. Eine Softwarekonfiguration ist nicht erforderlich. SolenOS bereitet Gebäudestruktur und Gerätezuordnung vor dem Versand 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">
|
||||
<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="SolenOS 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={task.to}>
|
||||
Mehr erfahren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Portfolio"
|
||||
title="Vom einzelnen Mehrfamilienhaus bis zum Portfolio"
|
||||
lead="SolenOS nutzt dieselbe Struktur unabhängig davon, ob Sie ein Gebäude oder viele Liegenschaften verwalten."
|
||||
/>
|
||||
<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">
|
||||
{portfolioScopes.map((scope) => (
|
||||
<div
|
||||
key={scope.headline}
|
||||
className="flex flex-col gap-6 p-6 sm:p-8"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-extrabold tracking-tight text-navy sm:text-3xl">
|
||||
{scope.headline}
|
||||
</h3>
|
||||
<AssetRender
|
||||
render={scope.render}
|
||||
alt=""
|
||||
className="flex w-20 shrink-0 items-center justify-center sm:w-24"
|
||||
imgClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
{/* demo data, flagged as such right where it is shown */}
|
||||
<div className="rounded-xl border border-border bg-secondary/40 px-4 py-3">
|
||||
<p className="text-[0.6875rem] font-bold tracking-[0.14em] text-primary uppercase">
|
||||
Beispiel
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm font-semibold text-navy">
|
||||
{scope.example}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold tracking-wide text-navy uppercase">
|
||||
Im Portal
|
||||
</p>
|
||||
<Checklist items={scope.capabilities} className="mt-3" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<p className="mx-auto mt-5 max-w-2xl text-center text-xs leading-relaxed text-muted-foreground italic">
|
||||
Die Beispielzahlen zeigen, was im Portal dargestellt werden kann –
|
||||
sie sind keine Angaben zu bestehenden SolenOS-Kunden.
|
||||
</p>
|
||||
<div className="mt-8 flex justify-center">
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link to="/fuer-wen/hausverwaltungen">
|
||||
SolenOS für Hausverwaltungen
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<div className="grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow="Preis"
|
||||
title="Wie entsteht der Gesamtpreis?"
|
||||
lead="Im Konfigurator wählen Sie Leistungen und erfassen die grundlegenden Gebäudedaten online. Der verbindliche Gesamtpreis folgt nach der vollständigen Konfiguration und manuellen Prüfung."
|
||||
/>
|
||||
<p className="mt-10 text-sm leading-relaxed text-muted-foreground">
|
||||
Für die erste Ermittlung der Ausstattung benötigen wir:
|
||||
</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">
|
||||
SolenOS bestimmt daraus den voraussichtlichen Gerätebedarf. Im
|
||||
Folgeprozess ergänzen Sie Details und Fotos; danach erhalten Sie
|
||||
den geprüften Gesamtpreis.
|
||||
</p>
|
||||
<Button asChild size="lg" className="mt-6">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Card className="p-6 gap-0 sm:p-8">
|
||||
<CardContent className="p-0">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-navy">
|
||||
In fünf Schritten zum verbindlichen Gesamtangebot
|
||||
</h3>
|
||||
</div>
|
||||
<NumberedList steps={priceFlow} className="mt-6" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p className="mt-5 text-xs leading-relaxed text-muted-foreground italic">
|
||||
SolenOS veröffentlicht keine ungeprüften Preiswerte. Das
|
||||
Gesamtangebot enthält die gewählten Leistungen, erforderliche
|
||||
Gateway-Infrastruktur und die gewählte Montagevariante.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Für wen?"
|
||||
title="Was möchten Sie mit SolenOS machen?"
|
||||
lead="SolenOS kann unterschiedlich eingesetzt werden. Wählen Sie die Situation, die 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">
|
||||
{audience.media}
|
||||
<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">
|
||||
Im Überblick
|
||||
</p>
|
||||
<Checklist items={audience.points} className="mb-2" />
|
||||
<Button asChild variant="outline" className="mt-auto">
|
||||
<Link to={audience.to}>
|
||||
{audience.cta}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<TrustPanel
|
||||
title="Datenschutz & Sicherheit"
|
||||
items={trustProperties}
|
||||
visual={<TrustShieldArt />}
|
||||
action={
|
||||
<Button asChild variant="link" className="h-auto p-0 font-bold">
|
||||
<Link to="/wissen/datenschutz-sicherheit">
|
||||
Mehr zu Datenschutz & Sicherheit
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zu SolenOS"
|
||||
lead="Von Installation und Ausstattung bis zu Preis und laufendem Betrieb."
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={faqs} />
|
||||
</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">Konfiguration starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/so-funktionierts">So funktioniert SolenOS</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import {
|
||||
ArrowRight,
|
||||
CalendarClock,
|
||||
Euro,
|
||||
FileCheck2,
|
||||
Flame,
|
||||
Layers,
|
||||
Droplets,
|
||||
UserRoundCog,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Checklist } from "@/components/solenos/checklist"
|
||||
import {
|
||||
ComparisonTable,
|
||||
type ComparisonColumn,
|
||||
type ComparisonRow,
|
||||
} from "@/components/solenos/comparison-table"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
|
||||
import { EuroCircleIcon } from "@/components/solenos/line-icons"
|
||||
import { NumberedList } from "@/components/solenos/process-steps"
|
||||
import { Section, SectionHeading } from "@/components/solenos/section"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
vectors,
|
||||
} from "@/components/solenos/site-assets"
|
||||
import { TopicCards, type Topic } from "@/components/solenos/topic-cards"
|
||||
import { heizkostenv } from "@/lib/legal"
|
||||
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
|
||||
|
||||
/**
|
||||
* Hero art: the glass money render as the subject – the costs being
|
||||
* distributed – with the consumption bars as their basis and the euro orb as
|
||||
* the accent. Decorative; the copy carries the meaning.
|
||||
*/
|
||||
function BillingHeroArt() {
|
||||
return (
|
||||
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
|
||||
<img
|
||||
src={vectors.euroOrb}
|
||||
alt=""
|
||||
className="absolute top-[2%] right-[4%] w-[22%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassMoney}
|
||||
alt=""
|
||||
className="mx-auto w-[68%]"
|
||||
loading="eager"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.barChartManyFull}
|
||||
alt=""
|
||||
className="absolute bottom-0 left-0 w-[32%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DISTRIBUTION: Topic[] = [
|
||||
{
|
||||
icon: <Flame />,
|
||||
title: "Kosten der Wärmeversorgung",
|
||||
body: "Mindestens 50 und höchstens 70 Prozent der Kosten werden nach dem erfassten Wärmeverbrauch verteilt, die übrigen nach Wohn- oder Nutzfläche beziehungsweise umbautem Raum.",
|
||||
source: { cite: "§ 7 HeizkostenV", href: heizkostenv("7") },
|
||||
},
|
||||
{
|
||||
icon: <Droplets />,
|
||||
title: "Kosten der Warmwasserversorgung",
|
||||
body: "Für die zentrale Warmwasserversorgung gilt derselbe Rahmen: 50 bis 70 Prozent nach erfasstem Warmwasserverbrauch, der Rest nach Wohn- oder Nutzfläche.",
|
||||
source: { cite: "§ 8 HeizkostenV", href: heizkostenv("8") },
|
||||
},
|
||||
{
|
||||
icon: <Layers />,
|
||||
title: "Verbundene Anlagen",
|
||||
body: "Erzeugt eine Anlage Wärme und Warmwasser gemeinsam, werden die einheitlich entstandenen Betriebskosten zuerst zwischen beiden Zwecken aufgeteilt – anhand der auf die Warmwasserbereitung entfallenden Wärmemenge, die dafür mit einem Wärmezähler gemessen wird.",
|
||||
source: { cite: "§ 9 HeizkostenV", href: heizkostenv("9") },
|
||||
},
|
||||
{
|
||||
icon: <UserRoundCog />,
|
||||
title: "Nutzungszeiträume und Nutzerwechsel",
|
||||
body: "Weil die Ausstattung fernablesbar ist, werden Zwischenwerte zum Stichtag aus der laufenden Datenerfassung übernommen und die Verbräuche dem jeweiligen Nutzungszeitraum zugeordnet.",
|
||||
},
|
||||
]
|
||||
|
||||
const YEAR_FLOW = [
|
||||
{
|
||||
title: "Abrechnungszeitraum festlegen",
|
||||
description:
|
||||
"Zeitraum, Verteilungsmaßstäbe und Gebäudedaten werden im Portal hinterlegt.",
|
||||
},
|
||||
{
|
||||
title: "Verbrauchsdaten laufen automatisch mit",
|
||||
description:
|
||||
"Die fernablesbare Messtechnik liefert Monatswerte – dieselbe Datenbasis wie für die UVI.",
|
||||
},
|
||||
{
|
||||
title: "Kosten des Abrechnungszeitraums erfassen",
|
||||
description:
|
||||
"Brennstoff- oder Wärmelieferkosten, Betriebsstrom, Wartung und Messdienstkosten werden zugeordnet.",
|
||||
},
|
||||
{
|
||||
title: "Abrechnung erstellen und prüfen",
|
||||
description:
|
||||
"Verteilung nach den gewählten Maßstäben, inklusive Nutzerwechsel und Leerstand.",
|
||||
},
|
||||
{
|
||||
title: "Dokumente bereitstellen",
|
||||
description:
|
||||
"Einzelabrechnungen und Pflichtangaben stehen für Eigentümer, Verwaltung und Bewohner im Portal bereit.",
|
||||
},
|
||||
]
|
||||
|
||||
const BASIS_COLUMNS: ComparisonColumn[] = [
|
||||
{ title: "Mit SolenOS", highlight: true },
|
||||
{ title: "Klassischer Ableseweg", check: false },
|
||||
]
|
||||
|
||||
const BASIS_ROWS: ComparisonRow[] = [
|
||||
{
|
||||
label: "Datenerhebung",
|
||||
cells: ["laufend fernabgelesen", "Ablesetermin je Wohnung"],
|
||||
},
|
||||
{
|
||||
label: "Zugang zur Wohnung",
|
||||
cells: ["nicht erforderlich", "erforderlich, inklusive Nachtermine"],
|
||||
},
|
||||
{
|
||||
label: "Nutzerwechsel",
|
||||
cells: ["Zwischenwerte aus der laufenden Erfassung", "Zwischenablesung vor Ort"],
|
||||
},
|
||||
{
|
||||
label: "Unterjährige Information",
|
||||
cells: ["monatlich aus derselben Datenbasis", "separat zu organisieren"],
|
||||
},
|
||||
{
|
||||
label: "Datenbestand",
|
||||
cells: [
|
||||
"Gebäude, Wohnungen und Geräte einmalig strukturiert",
|
||||
"je Leistung erneut zu übermitteln",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const INCLUDED = [
|
||||
"Verteilung der Wärme- und Warmwasserkosten nach HeizkostenV",
|
||||
"Berücksichtigung von Nutzerwechseln und Leerständen",
|
||||
"Einzelabrechnung je Wohnung mit den Pflichtangaben nach § 6a Absatz 3",
|
||||
"Bereitstellung der Dokumente im Portal für Verwaltung und Bewohner",
|
||||
"Übernahme der Verbrauchsdaten aus dem laufenden Messbetrieb",
|
||||
]
|
||||
|
||||
const NOT_INCLUDED = [
|
||||
"Betriebskostenabrechnung des Gebäudes (kalte Betriebskosten)",
|
||||
"Beschaffung von Brennstoff oder Wärmelieferung",
|
||||
"Mietvertragliche Prüfung und Mahnwesen",
|
||||
]
|
||||
|
||||
const FAQS: FaqItem[] = [
|
||||
{
|
||||
question: "Brauche ich für die Abrechnung einen Ablesetermin?",
|
||||
answer:
|
||||
"Nein. Die Werte stammen aus der fernablesbaren Messtechnik und werden zum Ende des Abrechnungszeitraums automatisch übernommen. Auch Zwischenwerte bei Nutzerwechseln entstehen ohne Termin in der Wohnung.",
|
||||
},
|
||||
{
|
||||
question: "Welche Verteilungsmaßstäbe kann ich wählen?",
|
||||
answer:
|
||||
"Für die Wärmekosten sind mindestens 50 und höchstens 70 Prozent nach erfasstem Verbrauch zu verteilen (§ 7 Absatz 1 HeizkostenV); für Warmwasser gilt derselbe Rahmen (§ 8 Absatz 1). Die Wahl der Maßstäbe bleibt dem Gebäudeeigentümer überlassen und wird für künftige Abrechnungszeiträume im Portal hinterlegt (§ 6 Absatz 4).",
|
||||
},
|
||||
{
|
||||
question: "Was passiert, wenn nicht verbrauchsabhängig abgerechnet wird?",
|
||||
answer:
|
||||
"Nach § 12 Absatz 1 HeizkostenV kann der Nutzer seinen Kostenanteil um 15 Prozent kürzen, soweit entgegen der Verordnung nicht verbrauchsabhängig abgerechnet wird. Fehlt die fernablesbare Ausstattung oder werden die Informationen nach § 6a nicht vollständig mitgeteilt, sind es 3 Prozent.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich die Abrechnung ohne UVI buchen?",
|
||||
answer:
|
||||
"Die Abrechnung setzt erfasste Verbrauchsdaten voraus. Läuft der Messbetrieb über SolenOS, ist die Datenbasis vorhanden und die Abrechnung nutzt sie direkt – die monatliche Verbrauchsinformation entsteht dann aus demselben Datenbestand.",
|
||||
},
|
||||
{
|
||||
question: "Wie sehen Bewohner ihre Abrechnung?",
|
||||
answer:
|
||||
"Die Einzelabrechnung wird im Mieterportal bereitgestellt und kann als Dokument ausgegeben werden. Bewohner sehen ausschließlich die Unterlagen ihres eigenen Nutzungsverhältnisses.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function HeizkostenabrechnungProductPage() {
|
||||
return (
|
||||
<ProductPage
|
||||
breadcrumb="Heizkostenabrechnung"
|
||||
eyebrow="Produkt"
|
||||
title={<>Heizkosten­abrechnung</>}
|
||||
tagline="Aus den Daten, die ohnehin laufen."
|
||||
lead="Die jährliche Abrechnung der Heiz- und Warmwasserkosten entsteht aus denselben fernabgelesenen Verbrauchswerten wie die monatliche Verbrauchsinformation (UVI) – verteilt nach den Maßstäben der Heizkostenverordnung, ohne Ablesetermin im Gebäude."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link to="/produkte/uvi">Verbrauchsinformation ansehen</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
|
||||
visual={<BillingHeroArt />}
|
||||
features={[
|
||||
{
|
||||
icon: <EuroCircleIcon />,
|
||||
title: "Verbrauchsabhängig verteilt",
|
||||
sub: "50 bis 70 Prozent nach Verbrauch, der Rest nach Fläche.",
|
||||
},
|
||||
{
|
||||
icon: <CalendarClock strokeWidth={1.6} />,
|
||||
title: "Nutzerwechsel inklusive",
|
||||
sub: "Zwischenwerte zum Stichtag ohne Termin in der Wohnung.",
|
||||
},
|
||||
{
|
||||
icon: <FileCheck2 strokeWidth={1.6} />,
|
||||
title: "Mit Pflichtangaben",
|
||||
sub: "Energieträger, Entgelte und Vergleichswerte nach § 6a.",
|
||||
},
|
||||
{
|
||||
icon: <Layers strokeWidth={1.6} />,
|
||||
title: "Ein Datenbestand",
|
||||
sub: "Gebäude, Wohnungen und Geräte werden einmal strukturiert.",
|
||||
},
|
||||
]}
|
||||
closing={
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<CtaBanner
|
||||
icon={<Euro />}
|
||||
title="Abrechnung und Messbetrieb aus einer Hand"
|
||||
description="Wir gehen Abrechnungszeitraum, Verteilungsmaßstäbe und Ausstattung für Ihr Gebäude konkret durch."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="inverse" size="lg">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/kontakt">Kontakt aufnehmen</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
}
|
||||
>
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Grundlagen"
|
||||
title="Was in die Abrechnung einfließt"
|
||||
lead="Die Heizkostenverordnung gibt vor, welche Kosten verbrauchsabhängig zu verteilen sind und in welchem Rahmen. SolenOS setzt diese Regeln auf die erfassten Werte des Gebäudes an."
|
||||
/>
|
||||
<TopicCards
|
||||
items={DISTRIBUTION}
|
||||
className="sm:grid-cols-2 lg:grid-cols-4"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<div className="grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow="Ablauf"
|
||||
title="Ein Abrechnungsjahr mit SolenOS"
|
||||
lead="Die Abrechnung ist kein eigenes Projekt am Jahresende, sondern das Ergebnis des laufenden Messbetriebs."
|
||||
/>
|
||||
<Button asChild variant="outline" size="lg" className="mt-8">
|
||||
<Link to="/so-funktionierts">
|
||||
So funktioniert SolenOS
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Card className="gap-0 p-6 sm:p-8">
|
||||
<NumberedList steps={YEAR_FLOW} />
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Im Vergleich"
|
||||
title="Woher die Abrechnungsdaten kommen"
|
||||
lead="Der Unterschied liegt nicht in der Rechenvorschrift, sondern in der Datenbeschaffung."
|
||||
/>
|
||||
<ComparisonTable
|
||||
className="mt-10"
|
||||
columns={BASIS_COLUMNS}
|
||||
rows={BASIS_ROWS}
|
||||
labelHead="Abrechnungsgrundlage"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Leistungsumfang"
|
||||
title="Enthalten und nicht enthalten"
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-2">
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-base font-bold text-navy">Enthalten</h3>
|
||||
<Checklist items={INCLUDED} className="mt-5" />
|
||||
</Card>
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-base font-bold text-navy">
|
||||
Nicht Teil der Heizkostenabrechnung
|
||||
</h3>
|
||||
<Checklist items={NOT_INCLUDED} className="mt-5" tone="excluded" />
|
||||
<p className="mt-6 text-sm leading-relaxed text-muted-foreground">
|
||||
Die Kosten der Verbrauchserfassung, ihrer Verwendung und der
|
||||
Abrechnungs- und Verbrauchsinformationen gehören zu den
|
||||
umlagefähigen Betriebskosten der Heizungsanlage.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
§ 7 Absatz 2 HeizkostenV
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zur Heizkostenabrechnung"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={FAQS} />
|
||||
</Section>
|
||||
|
||||
<ProductCrossLinks current="/produkte/heizkostenabrechnung" />
|
||||
</ProductPage>
|
||||
)
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronRight,
|
||||
Droplets,
|
||||
Flame,
|
||||
Gauge,
|
||||
Lock,
|
||||
Puzzle,
|
||||
RadioTower,
|
||||
Router,
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Checklist } from "@/components/solenos/checklist"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
|
||||
import {
|
||||
GermanyIcon,
|
||||
PreconfiguredIcon,
|
||||
} from "@/components/solenos/line-icons"
|
||||
import { NoticeArt, NoticeBanner } from "@/components/solenos/notice-banner"
|
||||
import { StepFlow } from "@/components/solenos/process-steps"
|
||||
import { Section, SectionHeading } from "@/components/solenos/section"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
vectors,
|
||||
} from "@/components/solenos/site-assets"
|
||||
import { TopicCards, type Topic } from "@/components/solenos/topic-cards"
|
||||
import { TrustPanel, TrustShieldArt } from "@/components/solenos/trust-panel"
|
||||
import { heizkostenv } from "@/lib/legal"
|
||||
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
|
||||
|
||||
const MESSEV_ANLAGE_7 =
|
||||
"https://www.gesetze-im-internet.de/messev/anlage_7.html"
|
||||
|
||||
/**
|
||||
* Hero art: the gateway as the subject – the piece that ties the building
|
||||
* together – with the heat meter beside it and the wireless orb as the accent
|
||||
* for the radio link between them. Decorative; the copy carries the meaning.
|
||||
*/
|
||||
function InfrastructureHeroArt() {
|
||||
return (
|
||||
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
|
||||
<img
|
||||
src={vectors.wirelessOrb}
|
||||
alt=""
|
||||
className="absolute top-[2%] right-[6%] w-[22%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.gateway}
|
||||
className="mx-auto w-[62%]"
|
||||
loading="eager"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.heatMeter}
|
||||
className="absolute bottom-0 left-0 w-[34%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const COMPONENTS: Topic[] = [
|
||||
{
|
||||
icon: <Flame />,
|
||||
title: "Heizkostenverteiler",
|
||||
body: "Elektronische Heizkostenverteiler erfassen an jedem Heizkörper den anteiligen Wärmeverbrauch. Sie sind keine Messgeräte im Sinne des Eichrechts und daher nicht eichpflichtig; getauscht werden sie nach Ablauf ihrer Nutzungsdauer.",
|
||||
source: { cite: "§ 5 HeizkostenV", href: heizkostenv("5") },
|
||||
},
|
||||
{
|
||||
icon: <Gauge />,
|
||||
title: "Wärmezähler",
|
||||
body: "Wo die Wärme je Einheit direkt gemessen wird – etwa bei Fußbodenheizung oder Wohnungsstationen – erfassen Wärmezähler die Wärmemenge in Kilowattstunden. Ihre Eichfrist beträgt sechs Jahre.",
|
||||
source: { cite: "Anlage 7 MessEV", href: MESSEV_ANLAGE_7 },
|
||||
},
|
||||
{
|
||||
icon: <Droplets />,
|
||||
title: "Kalt- und Warmwasserzähler",
|
||||
body: "Warmwasserzähler sind für die verbrauchsabhängige Abrechnung erforderlich, Kaltwasserzähler ergänzen die Erfassung. Für beide gilt eine Eichfrist von sechs Jahren.",
|
||||
source: { cite: "Anlage 7 MessEV", href: MESSEV_ANLAGE_7 },
|
||||
},
|
||||
{
|
||||
icon: <Router />,
|
||||
title: "Gateway und Datenübertragung",
|
||||
body: "Das Gateway empfängt die Funkdaten der Geräte im Gebäude, puffert sie und überträgt sie an SolenOS. Ein Zugang zu den einzelnen Wohnungen ist dafür nicht erforderlich.",
|
||||
},
|
||||
]
|
||||
|
||||
const DATA_PATH = [
|
||||
{
|
||||
media: <AssetRender render={renders.heatMeter} imgClassName="w-28" />,
|
||||
title: "Gerät misst und funkt",
|
||||
description:
|
||||
"Zähler und Heizkostenverteiler senden ihre Werte per Funk; die Nutzdaten sind dabei AES-geschützt.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.gateway} imgClassName="w-28" />,
|
||||
title: "Gateway sammelt",
|
||||
description:
|
||||
"Die erforderliche Gateway-Infrastruktur empfängt die Funkpakete der angebundenen Geräte und wird von SolenOS passend zum Objekt bestimmt.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.refreshLoopCloud} imgClassName="w-28" />
|
||||
),
|
||||
title: "Übertragung an SolenOS",
|
||||
description:
|
||||
"Die Daten werden TLS-verschlüsselt an die Plattform übertragen und dort verarbeitet.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.laptopDashboard} imgClassName="w-40" />
|
||||
),
|
||||
title: "Nutzung im Portal",
|
||||
description:
|
||||
"Aus denselben Werten entstehen Geräteüberwachung, monatliche UVI und Jahresabrechnung.",
|
||||
},
|
||||
]
|
||||
|
||||
const LIFECYCLE = [
|
||||
{
|
||||
device: "Wärmezähler",
|
||||
interval: "Eichfrist 6 Jahre",
|
||||
note: "Austausch oder Nacheichung vor Ablauf der Eichfrist",
|
||||
},
|
||||
{
|
||||
device: "Kalt- und Warmwasserzähler",
|
||||
interval: "Eichfrist 6 Jahre",
|
||||
note: "seit der Novelle der MessEV einheitlich sechs Jahre",
|
||||
},
|
||||
{
|
||||
device: "Elektronische Heizkostenverteiler",
|
||||
interval: "nicht eichpflichtig",
|
||||
note: "Austausch nach Ablauf der Nutzungsdauer des Geräts",
|
||||
},
|
||||
{
|
||||
device: "Rauchwarnmelder",
|
||||
interval: "Austausch nach 10 Jahren",
|
||||
note: "jährliche Inspektion nach DIN 14676-1, per Ferninspektion möglich",
|
||||
},
|
||||
]
|
||||
|
||||
const REUSE_POSSIBLE = [
|
||||
"Geräte sind fernablesbar, also ohne Zugang zur Wohnung auslesbar",
|
||||
"die Funkschnittstelle folgt einem offenen Standard (OMS)",
|
||||
"das Schlüsselmaterial der Geräte liegt vor",
|
||||
"die Eichfrist der betroffenen Zähler ist nicht abgelaufen",
|
||||
]
|
||||
|
||||
const REUSE_IMPOSSIBLE = [
|
||||
"nicht fernablesbare Verbrauchserfassung",
|
||||
"herstellergebundene Funkprotokolle ohne verfügbares Schlüsselmaterial",
|
||||
"Zähler mit abgelaufener Eichfrist",
|
||||
]
|
||||
|
||||
const TRUST_PROPERTIES = [
|
||||
{ icon: <GermanyIcon />, label: "Hosting in Deutschland" },
|
||||
{ icon: <Lock />, label: "AES-geschützte Funkdaten" },
|
||||
{ icon: <RadioTower />, label: "TLS-Übertragung ab Gateway" },
|
||||
{ icon: <Users />, label: "Rollenbasierte Zugriffsrechte" },
|
||||
]
|
||||
|
||||
const FAQS: FaqItem[] = [
|
||||
{
|
||||
question: "Was kostet die Messtechnik?",
|
||||
answer:
|
||||
"SolenOS bestimmt die benötigten Sensoren und die erforderliche Gateway-Infrastruktur aus der vollständigen Gebäudekonfiguration. Beides wird im geprüften Gesamtangebot gemeinsam mit den gewählten Leistungen ausgewiesen; ungeprüfte Einzelpreise veröffentlichen wir nicht.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich vorhandene Zähler weiterverwenden?",
|
||||
answer:
|
||||
"Das hängt von den Geräten ab. Voraussetzung ist, dass sie fernablesbar sind, auf offenen Standards basieren und das Schlüsselmaterial vorliegt. Vorhandene Messtechnik wird deshalb bei der vollständigen Konfiguration je Gebäude und Einheit erfasst und geprüft.",
|
||||
},
|
||||
{
|
||||
question: "Was bedeutet Interoperabilität konkret?",
|
||||
answer:
|
||||
"Nach § 5 Absatz 5 HeizkostenV dürfen seit dem 1. Dezember 2022 nur noch fernauslesbare Ausstattungen installiert werden, die mit Geräten gleicher Art anderer Hersteller interoperabel sind – ein anderer Dienstleister muss sie also selbst fernablesen können. Das Schlüsselmaterial ist dem Gebäudeeigentümer kostenfrei zur Verfügung zu stellen.",
|
||||
},
|
||||
{
|
||||
question: "Braucht das Gateway einen Internetanschluss im Gebäude?",
|
||||
answer:
|
||||
"Nein. Das Gateway überträgt die gesammelten Daten über Mobilfunk, sodass kein Anschluss im Haus und keine Mitnutzung eines fremden Netzes erforderlich ist.",
|
||||
},
|
||||
{
|
||||
question: "Wird gemeldet, wenn ein Gerät ausfällt?",
|
||||
answer:
|
||||
"Ja. Ausbleibende Werte, Batteriewarnungen und Manipulationsmeldungen laufen im Portal auf, sodass ein Gerät vor der nächsten Abrechnung geprüft werden kann – nicht erst am Jahresende.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function MesstechnikProductPage() {
|
||||
return (
|
||||
<ProductPage
|
||||
breadcrumb="Messtechnik und Infrastruktur"
|
||||
eyebrow="Produkt"
|
||||
title={
|
||||
<span className="block text-[2.25rem] text-pretty sm:text-[2.75rem]">
|
||||
Messtechnik und Infrastruktur
|
||||
</span>
|
||||
}
|
||||
tagline="Die Grundlage für alles andere."
|
||||
lead="Zähler, Heizkostenverteiler und Gateway erfassen und übertragen die Werte, aus denen monatliche Verbrauchsinformation (UVI) und Heizkostenabrechnung entstehen – fernablesbar, interoperabel und objektbezogen vorkonfiguriert geliefert."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/konfigurator">Ausstattung ermitteln</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link to="/wissen/datenschutz-sicherheit">
|
||||
Datenschutz & Sicherheit
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
|
||||
visual={<InfrastructureHeroArt />}
|
||||
features={[
|
||||
{
|
||||
icon: <RadioTower strokeWidth={1.6} />,
|
||||
title: "Fernablesbar",
|
||||
sub: "Ablesung ohne Zugang zu den einzelnen Wohnungen.",
|
||||
},
|
||||
{
|
||||
icon: <Puzzle strokeWidth={1.6} />,
|
||||
title: "Interoperabel",
|
||||
sub: "Offene Funkstandards, Schlüsselmaterial bleibt verfügbar.",
|
||||
},
|
||||
{
|
||||
icon: <PreconfiguredIcon />,
|
||||
title: "Vorkonfiguriert",
|
||||
sub: "Geräte kommen Gebäude und Einheit zugeordnet an.",
|
||||
},
|
||||
{
|
||||
icon: <GermanyIcon />,
|
||||
title: "Daten in Deutschland",
|
||||
sub: "Verschlüsselte Übertragung, Hosting in Falkenstein.",
|
||||
},
|
||||
]}
|
||||
closing={
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<CtaBanner
|
||||
icon={<Gauge />}
|
||||
title="Welche Ausstattung braucht Ihr Gebäude?"
|
||||
description="Aus Gebäudedaten, Wärmeverteilung und Wohnungszahl ermitteln wir Geräteart und Menge – und daraus den Preis."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="inverse" size="lg">
|
||||
<Link to="/konfigurator">Ausstattung ermitteln</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/kontakt">Bestand prüfen lassen</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
}
|
||||
>
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Komponenten"
|
||||
title="Was im Gebäude verbaut wird"
|
||||
lead="Welche Geräte zum Einsatz kommen, hängt von der Wärmeverteilung und der Einbausituation ab. Zusammengestellt und vorbereitet wird die Ausstattung objektbezogen."
|
||||
/>
|
||||
<TopicCards
|
||||
items={COMPONENTS}
|
||||
className="sm:grid-cols-2 lg:grid-cols-4"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Datenweg"
|
||||
title="Vom Heizkörper bis ins Portal"
|
||||
lead="Jeder Abschnitt hat seine eigene Schutzmaßnahme – von der Funkstrecke in der Wohnung bis zur Verarbeitung auf der Plattform."
|
||||
/>
|
||||
<StepFlow steps={DATA_PATH} className="mt-10" />
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<NoticeBanner
|
||||
title="Nachrüstung bis 31.12.2026 erforderlich"
|
||||
source={{ cite: "§ 5 HeizkostenV", href: heizkostenv("5") }}
|
||||
visual={<NoticeArt className="max-lg:hidden" />}
|
||||
action={
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="h-auto p-0 text-[15px] font-bold text-brand-800 underline decoration-brand-500 decoration-2 underline-offset-[6px] hover:text-navy hover:decoration-brand-800"
|
||||
>
|
||||
<Link to="/wissen/gesetzliche-anforderungen">
|
||||
Mehr erfahren
|
||||
<ChevronRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Nicht fernablesbare Verbrauchserfassung muss grundsätzlich bis zum
|
||||
31. Dezember 2026 durch Nachrüstung oder Austausch die Anforderungen
|
||||
an Fernablesbarkeit und Interoperabilität erfüllen.
|
||||
</NoticeBanner>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Bestand"
|
||||
title="Was übernommen werden kann"
|
||||
lead="Nicht jedes Gebäude beginnt bei null. Entscheidend ist, ob die vorhandenen Geräte fernablesbar und offen angebunden sind."
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-2">
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-base font-bold text-navy">
|
||||
Weiterverwendung möglich, wenn
|
||||
</h3>
|
||||
<Checklist items={REUSE_POSSIBLE} className="mt-5" />
|
||||
</Card>
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-base font-bold text-navy">
|
||||
Austausch erforderlich bei
|
||||
</h3>
|
||||
<Checklist
|
||||
items={REUSE_IMPOSSIBLE}
|
||||
className="mt-5"
|
||||
tone="excluded"
|
||||
/>
|
||||
<p className="mt-6 text-sm leading-relaxed text-muted-foreground">
|
||||
Was übernommen werden kann, wirkt sich unmittelbar auf die
|
||||
Geräteliste und damit auf den Preis aus. Der Bestand wird deshalb
|
||||
bei der vollständigen Objektkonfiguration je Einheit erfasst.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Fristen"
|
||||
title="Eichfristen und Nutzungsdauern"
|
||||
lead="Wann ein Gerät getauscht werden muss, ergibt sich aus dem Eichrecht oder aus seiner Nutzungsdauer. Fällige Termine werden je Gebäude im Portal geführt."
|
||||
/>
|
||||
<Card className="mt-10 gap-0 divide-y divide-border p-0">
|
||||
{LIFECYCLE.map((item) => (
|
||||
<div
|
||||
key={item.device}
|
||||
className="flex flex-col gap-1 p-5 sm:flex-row sm:items-center sm:gap-6 sm:px-7"
|
||||
>
|
||||
<h3 className="text-sm font-bold text-navy sm:w-72 sm:shrink-0">
|
||||
{item.device}
|
||||
</h3>
|
||||
<p className="text-sm font-semibold text-primary sm:w-56 sm:shrink-0">
|
||||
{item.interval}
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
{item.note}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
<p className="mt-5 text-xs leading-relaxed text-muted-foreground">
|
||||
Eichfristen nach Anlage 7 der Mess- und Eichverordnung. Elektronische
|
||||
Heizkostenverteiler unterliegen nicht dem Eichrecht.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<TrustPanel
|
||||
title="Messdaten sind auf jedem Abschnitt geschützt"
|
||||
items={TRUST_PROPERTIES}
|
||||
visual={<TrustShieldArt />}
|
||||
action={
|
||||
<Button asChild variant="link" className="h-auto p-0 font-bold">
|
||||
<Link to="/wissen/datenschutz-sicherheit">
|
||||
Mehr zu Datenschutz & Sicherheit
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zu Messtechnik und Infrastruktur"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={FAQS} />
|
||||
</Section>
|
||||
|
||||
<ProductCrossLinks current="/produkte/messtechnik-infrastruktur" />
|
||||
</ProductPage>
|
||||
)
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { FeatureBand, type FeatureBandItem } from "@/components/solenos/feature-band"
|
||||
import { PageBreadcrumb } from "@/components/solenos/page-breadcrumb"
|
||||
import { PageHero } from "@/components/solenos/page-hero"
|
||||
import { Container, Section, SectionHeading } from "@/components/solenos/section"
|
||||
import { icons } from "@/components/solenos/site-assets"
|
||||
|
||||
type Product = {
|
||||
to: string
|
||||
/** short label for navigation and cross-links */
|
||||
label: string
|
||||
/** headline used on the overview cards */
|
||||
title: string
|
||||
/** one-line positioning under the card title */
|
||||
sub: string
|
||||
summary: string
|
||||
/** baked-in disc icon, used wherever the four products are listed */
|
||||
icon: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The four products, in the order they are presented everywhere
|
||||
* (overview grid, header menu, cross-links at the foot of each product page).
|
||||
*/
|
||||
const PRODUCTS: Product[] = [
|
||||
{
|
||||
to: "/produkte/uvi",
|
||||
label: "Verbrauchsinformation (UVI)",
|
||||
title: "Verbrauchsinformation (UVI)",
|
||||
sub: "Monatlich für Bewohner",
|
||||
summary:
|
||||
"Verbrauchsdaten werden automatisch erfasst und die monatliche Verbrauchsinformation für die Bewohner erzeugt und bereitgestellt – ohne Ablesetermin in der Wohnung.",
|
||||
icon: icons.uvi,
|
||||
},
|
||||
{
|
||||
to: "/produkte/heizkostenabrechnung",
|
||||
label: "Heizkostenabrechnung",
|
||||
title: "Heizkostenabrechnung",
|
||||
sub: "Verbrauchsdaten korrekt abrechnen",
|
||||
summary:
|
||||
"Die jährliche Abrechnung entsteht aus denselben erfassten Verbrauchsdaten: Verteilung nach HeizkostenV, Nutzerwechsel berücksichtigt, Dokumente im Portal.",
|
||||
icon: icons.billing,
|
||||
},
|
||||
{
|
||||
to: "/produkte/rauchwarnmelder",
|
||||
label: "Rauchwarnmelder",
|
||||
title: "Rauchwarnmelder",
|
||||
sub: "Sicher überwacht, zentral verwaltet",
|
||||
summary:
|
||||
"Ferninspizierbare Rauchwarnmelder inklusive Montage, jährlicher Inspektion nach DIN 14676-1 und lückenloser Dokumentation im Portal.",
|
||||
icon: icons.smokeAlarm,
|
||||
},
|
||||
{
|
||||
to: "/produkte/messtechnik-infrastruktur",
|
||||
label: "Messtechnik und Infrastruktur",
|
||||
title: "Messtechnik und Infrastruktur",
|
||||
sub: "Die technische Grundlage",
|
||||
summary:
|
||||
"Fernablesbare Zähler und Heizkostenverteiler, Gateway und Datenübertragung – vorkonfiguriert geliefert und laufend überwacht.",
|
||||
icon: icons.radio,
|
||||
},
|
||||
]
|
||||
|
||||
function ProductIcon({
|
||||
src,
|
||||
className = "size-12",
|
||||
}: {
|
||||
src: string
|
||||
className?: string
|
||||
}) {
|
||||
return <img src={src} alt="" className={className} loading="lazy" />
|
||||
}
|
||||
|
||||
function ProductBreadcrumb({ current }: { current: string }) {
|
||||
return (
|
||||
<PageBreadcrumb
|
||||
items={[
|
||||
{ label: "Startseite", to: "/" },
|
||||
{ label: "Produkte", to: "/produkte" },
|
||||
{ label: current },
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared product page shell: split PageHero with the
|
||||
* Startseite → Produkte → product breadcrumb, a feature band summarising the
|
||||
* product in four properties, the product's own sections, then the closing
|
||||
* slot (CtaBanner).
|
||||
*/
|
||||
function ProductPage({
|
||||
breadcrumb,
|
||||
eyebrow,
|
||||
title,
|
||||
tagline,
|
||||
lead,
|
||||
actions,
|
||||
note,
|
||||
extra,
|
||||
visual,
|
||||
features,
|
||||
children,
|
||||
closing,
|
||||
}: {
|
||||
/** last breadcrumb crumb; also the product's own name */
|
||||
breadcrumb: string
|
||||
eyebrow?: React.ReactNode
|
||||
title: React.ReactNode
|
||||
/** short bold subline under the H1 */
|
||||
tagline?: React.ReactNode
|
||||
lead?: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
note?: React.ReactNode
|
||||
extra?: React.ReactNode
|
||||
visual?: React.ReactNode
|
||||
features: FeatureBandItem[]
|
||||
/** the product's spec sections between feature band and closing */
|
||||
children?: React.ReactNode
|
||||
closing?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
breadcrumb={<ProductBreadcrumb current={breadcrumb} />}
|
||||
eyebrow={eyebrow}
|
||||
title={title}
|
||||
lead={
|
||||
<>
|
||||
{tagline ? (
|
||||
<span className="mb-2 block text-lg font-bold text-navy sm:text-xl">
|
||||
{tagline}
|
||||
</span>
|
||||
) : null}
|
||||
{lead}
|
||||
</>
|
||||
}
|
||||
actions={actions}
|
||||
note={note}
|
||||
extra={extra}
|
||||
visual={visual}
|
||||
/>
|
||||
<Container>
|
||||
<Section className="pt-2 sm:pt-4 lg:pt-6">
|
||||
<FeatureBand items={features} />
|
||||
</Section>
|
||||
{children}
|
||||
{closing}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a product page out with the three other products, so the four pages
|
||||
* form one navigable set instead of dead ends.
|
||||
*/
|
||||
function ProductCrossLinks({ current }: { current: string }) {
|
||||
const others = PRODUCTS.filter((product) => product.to !== current)
|
||||
|
||||
return (
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Weitere Produkte"
|
||||
title="Läuft auf derselben Grundlage"
|
||||
lead="Alle SolenOS-Leistungen nutzen dieselbe Gebäudestruktur und dieselbe Messtechnik – einzeln buchbar, jederzeit erweiterbar."
|
||||
/>
|
||||
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{others.map((product) => (
|
||||
<Card key={product.to} className="gap-0 p-6 sm:p-7">
|
||||
<ProductIcon src={product.icon} />
|
||||
<h3 className="mt-5 text-lg font-extrabold tracking-tight text-balance text-navy">
|
||||
{product.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||
{product.summary}
|
||||
</p>
|
||||
<div className="mt-6 flex-1 content-end">
|
||||
<Button asChild variant="outline">
|
||||
<Link to={product.to}>
|
||||
Mehr erfahren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export { PRODUCTS, ProductCrossLinks, ProductIcon, ProductPage }
|
||||
export type { Product }
|
||||
@@ -1,341 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import {
|
||||
ArrowRight,
|
||||
BellRing,
|
||||
ClipboardCheck,
|
||||
ScrollText,
|
||||
ShieldCheck,
|
||||
Timer,
|
||||
Wifi,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Checklist } from "@/components/solenos/checklist"
|
||||
import {
|
||||
ComparisonTable,
|
||||
type ComparisonColumn,
|
||||
type ComparisonRow,
|
||||
} from "@/components/solenos/comparison-table"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
|
||||
import { PortalGaugeIcon } from "@/components/solenos/line-icons"
|
||||
import { PriceCard } from "@/components/solenos/price-card"
|
||||
import { StepRail } from "@/components/solenos/process-steps"
|
||||
import { Section, SectionHeading } from "@/components/solenos/section"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
vectors,
|
||||
} from "@/components/solenos/site-assets"
|
||||
import { TopicCards, type Topic } from "@/components/solenos/topic-cards"
|
||||
import { SmokeDetectorIllustration } from "@/components/solenos/illustrations"
|
||||
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
|
||||
|
||||
/**
|
||||
* Hero art: the glass smoke detector as the subject with the real device
|
||||
* beside it and the check orb as the accent – device plus verified operational
|
||||
* readiness. Decorative; the copy carries the meaning.
|
||||
*/
|
||||
function SmokeAlarmHeroArt() {
|
||||
return (
|
||||
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
|
||||
<img
|
||||
src={vectors.checkOrb}
|
||||
alt=""
|
||||
className="absolute top-[4%] right-[6%] w-[20%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassSmokeDetector}
|
||||
alt=""
|
||||
className="mx-auto w-[64%]"
|
||||
loading="eager"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.smokeAlarmAlpha}
|
||||
className="absolute bottom-[2%] left-[2%] w-[30%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DUTIES: Topic[] = [
|
||||
{
|
||||
icon: <ScrollText />,
|
||||
title: "Einbau und Betriebsbereitschaft",
|
||||
body: "Rauchwarnmelder sind in Wohnungen nach den Bauordnungen der Länder vorgeschrieben. Wer für die Sicherstellung der Betriebsbereitschaft verantwortlich ist, regelt das jeweilige Landesrecht – in den meisten Ländern der Eigentümer.",
|
||||
},
|
||||
{
|
||||
icon: <ClipboardCheck />,
|
||||
title: "Jährliche Inspektion",
|
||||
body: "Die Anwendungsnorm DIN 14676-1 verlangt eine Inspektion mindestens einmal jährlich: Energieversorgung, Rauchzutrittsöffnungen und Umfeld des Melders werden geprüft.",
|
||||
},
|
||||
{
|
||||
icon: <Wifi />,
|
||||
title: "Ferninspektion nach Norm",
|
||||
body: "Die DIN 14676-1 kennt neben der Vor-Ort-Inspektion die Teil- und die vollständige Ferninspektion. Geeignete Melder melden Verschmutzung, Batteriezustand und Demontage selbst – ohne Termin in der Wohnung.",
|
||||
},
|
||||
{
|
||||
icon: <Timer />,
|
||||
title: "Austausch nach zehn Jahren",
|
||||
body: "Rauchwarnmelder werden spätestens nach zehn Jahren Nutzungsdauer ausgetauscht. Verbaute Melder, Einbauorte und Austauschtermine sind im Portal je Wohnung dokumentiert.",
|
||||
},
|
||||
]
|
||||
|
||||
const ROLLOUT_STEPS = [
|
||||
{
|
||||
media: <AssetRender render={renders.houseSmallIso} imgClassName="w-24" />,
|
||||
title: "Bedarf ermitteln",
|
||||
description:
|
||||
"Aus Wohnungen und Zimmerzahl ergibt sich, wie viele Melder je Einheit benötigt werden.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.shippingBoxClosed} imgClassName="w-32" />
|
||||
),
|
||||
title: "Vorkonfiguriert erhalten",
|
||||
description:
|
||||
"Die Melder kommen bereits dem Gebäude und den geplanten Einheiten zugeordnet an.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.drillTool} imgClassName="w-28" />,
|
||||
title: "Montieren",
|
||||
description:
|
||||
"Montage durch Sie, Ihren Fachbetrieb oder als Komplettleistung durch SolenOS.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.gateway} imgClassName="w-24" />,
|
||||
title: "Anbinden",
|
||||
description:
|
||||
"Die Melder funken über dieselbe Gebäudeinfrastruktur wie die Verbrauchserfassung.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.laptopDashboard} imgClassName="w-40" />
|
||||
),
|
||||
title: "Ferninspektion läuft",
|
||||
description:
|
||||
"Status, Meldungen und Inspektionsnachweise stehen laufend im Portal bereit.",
|
||||
},
|
||||
]
|
||||
|
||||
const INSPECTION_COLUMNS: ComparisonColumn[] = [
|
||||
{ title: "Ferninspektion", highlight: true },
|
||||
{ title: "Vor-Ort-Inspektion", check: false },
|
||||
]
|
||||
|
||||
const INSPECTION_ROWS: ComparisonRow[] = [
|
||||
{
|
||||
label: "Termin in der Wohnung",
|
||||
cells: ["nicht erforderlich", "je Wohnung, inklusive Nachterminen"],
|
||||
},
|
||||
{
|
||||
label: "Prüfung",
|
||||
cells: ["laufend durch den Melder", "einmal jährlich vor Ort"],
|
||||
},
|
||||
{
|
||||
label: "Störungen",
|
||||
cells: ["werden gemeldet, sobald sie auftreten", "fallen beim Jahrestermin auf"],
|
||||
},
|
||||
{
|
||||
label: "Nachweis",
|
||||
cells: ["automatisch dokumentiert", "Protokoll je Begehung"],
|
||||
},
|
||||
{
|
||||
label: "Aufwand für Bewohner",
|
||||
cells: ["keine Anwesenheitspflicht", "Anwesenheit erforderlich"],
|
||||
},
|
||||
]
|
||||
|
||||
const INCLUDED = [
|
||||
"Rauchwarnmelder inklusive Montage und Eigentumsübertragung",
|
||||
"Bedarfsermittlung je Wohnung aus den Gebäudedaten",
|
||||
"regelmäßige Ferninspektion und Störungsmeldungen",
|
||||
"Inspektionsnachweise je Wohnung im Portal",
|
||||
"Support für Rückfragen von Verwaltung und Bewohnern",
|
||||
]
|
||||
|
||||
const FAQS: FaqItem[] = [
|
||||
{
|
||||
question: "Wie oft müssen Rauchwarnmelder geprüft werden?",
|
||||
answer:
|
||||
"Mindestens einmal jährlich. Die Anwendungsnorm DIN 14676-1 lässt dafür neben der Vor-Ort-Inspektion auch die Teil- und die vollständige Ferninspektion zu, sofern die eingesetzten Melder dafür geeignet sind.",
|
||||
},
|
||||
{
|
||||
question: "Ersetzt die Ferninspektion die Vor-Ort-Prüfung vollständig?",
|
||||
answer:
|
||||
"Bei Meldern, die für die vollständige Ferninspektion geeignet sind, kann die Sicherstellung der Betriebsbereitschaft über die Gerätelebensdauer aus der Ferne erfolgen. Anlassbezogene Einsätze bleiben möglich – etwa wenn ein Melder demontiert wurde, dauerhaft verschmutzt ist oder ausgetauscht werden muss.",
|
||||
},
|
||||
{
|
||||
question: "Wer ist für die Rauchwarnmelder verantwortlich?",
|
||||
answer:
|
||||
"Einbau und Sicherstellung der Betriebsbereitschaft richten sich nach der Bauordnung des jeweiligen Landes. In den meisten Ländern trägt der Eigentümer die Verantwortung für die Betriebsbereitschaft; SolenOS liefert die Melder, die Inspektion und die Nachweise, mit denen diese Pflicht erfüllt werden kann.",
|
||||
},
|
||||
{
|
||||
question: "Wann müssen die Melder ausgetauscht werden?",
|
||||
answer:
|
||||
"Spätestens nach zehn Jahren Nutzungsdauer. Einbaudatum und fälliger Austausch sind je Gerät und Wohnung im Portal hinterlegt, sodass der Austausch mit dem übrigen Gebäudebetrieb geplant werden kann.",
|
||||
},
|
||||
{
|
||||
question: "Sind die Kosten umlagefähig?",
|
||||
answer:
|
||||
"Die laufenden Kosten der Ferninspektion sind grundsätzlich als Betriebskosten umlagefähig. Die einmaligen Kosten der Hardware inklusive Montage und Eigentumsübertragung sind es nicht – sie sind Anschaffungskosten des Eigentümers.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich Rauchwarnmelder später ergänzen?",
|
||||
answer:
|
||||
"Ja. Läuft der Messbetrieb bereits über SolenOS, wird die vorhandene Gebäudestruktur weiterverwendet: Wohnungen und Einheiten sind erfasst, die Melder werden lediglich ergänzt und angebunden.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function RauchwarnmelderProductPage() {
|
||||
return (
|
||||
<ProductPage
|
||||
breadcrumb="Rauchwarnmelder"
|
||||
eyebrow="Produkt"
|
||||
title="Rauchwarnmelder"
|
||||
tagline="Geprüft, ohne zu klingeln."
|
||||
lead="Ferninspizierbare Rauchwarnmelder inklusive Montage, jährlicher Inspektion nach DIN 14676-1 und lückenloser Dokumentation – über dieselbe Gebäudeinfrastruktur wie die Verbrauchserfassung."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link to="/klarpreis">Gesamtangebot ansehen</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
|
||||
visual={<SmokeAlarmHeroArt />}
|
||||
features={[
|
||||
{
|
||||
icon: <Wifi strokeWidth={1.6} />,
|
||||
title: "Ferninspizierbar",
|
||||
sub: "Prüfung ohne Termin und ohne Anwesenheit der Bewohner.",
|
||||
},
|
||||
{
|
||||
icon: <BellRing strokeWidth={1.6} />,
|
||||
title: "Störung fällt sofort auf",
|
||||
sub: "Verschmutzung, Batterie und Demontage werden gemeldet.",
|
||||
},
|
||||
{
|
||||
icon: <ClipboardCheck strokeWidth={1.6} />,
|
||||
title: "Nachweis je Wohnung",
|
||||
sub: "Inspektionen und Austauschfristen sind dokumentiert.",
|
||||
},
|
||||
{
|
||||
icon: <PortalGaugeIcon />,
|
||||
title: "Im selben Portal",
|
||||
sub: "Melder stehen neben Zählern und Verbrauchsdaten.",
|
||||
},
|
||||
]}
|
||||
closing={
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<CtaBanner
|
||||
icon={<ShieldCheck />}
|
||||
title="Rauchwarnmelder für Ihr Gebäude"
|
||||
description="Bedarf, Montage und Ferninspektion in einem Schritt – auch zusätzlich zu einem laufenden Messbetrieb."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="inverse" size="lg">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/kontakt">Kontakt aufnehmen</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
}
|
||||
>
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Pflichten"
|
||||
title="Was für Rauchwarnmelder gilt"
|
||||
lead="Einbaupflicht, jährliche Inspektion und Austausch nach zehn Jahren sind gesetzt. Unterschiedlich ist nur, wie viel Aufwand daraus entsteht."
|
||||
/>
|
||||
<TopicCards items={DUTIES} className="sm:grid-cols-2 lg:grid-cols-4" />
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Schritt für Schritt"
|
||||
title="Von der Bedarfsermittlung zur laufenden Inspektion"
|
||||
/>
|
||||
<StepRail steps={ROLLOUT_STEPS} className="mt-10" />
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Im Vergleich"
|
||||
title="Ferninspektion oder Vor-Ort-Inspektion"
|
||||
lead="Beide Verfahren erfüllen die Norm. Der Unterschied liegt im Aufwand für Verwaltung und Bewohner – und darin, wie früh eine Störung auffällt."
|
||||
/>
|
||||
<ComparisonTable
|
||||
className="mt-10"
|
||||
columns={INSPECTION_COLUMNS}
|
||||
rows={INSPECTION_ROWS}
|
||||
labelHead="Inspektionsverfahren"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Gesamtangebot"
|
||||
title="Hardware, Ferninspektion und Montage transparent zusammenführen"
|
||||
lead="Geräteanzahl, Inspektionsumfang und gewählte Montagevariante werden nach der vollständigen Objektkonfiguration geprüft und gemeinsam angeboten."
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-[1fr_1fr_1.1fr]">
|
||||
<PriceCard
|
||||
title="Rauchwarnmelder"
|
||||
cadence="Gesamtangebot"
|
||||
price="Nach Prüfung"
|
||||
unit="Geräteanzahl und Montagevariante"
|
||||
description="Benötigte Rauchwarnmelder, vereinbarte Montage und Eigentumsübertragung."
|
||||
media={<SmokeDetectorIllustration />}
|
||||
/>
|
||||
<PriceCard
|
||||
title="Ferninspektion"
|
||||
cadence="Gesamtangebot"
|
||||
price="Nach Prüfung"
|
||||
unit="passend zum vereinbarten Leistungsumfang"
|
||||
description="Regelmäßige Prüfung, Nachweise, Meldungen und Support."
|
||||
media={
|
||||
<div className="relative">
|
||||
<SmokeDetectorIllustration />
|
||||
<Wifi className="absolute top-1 -right-1 size-5 text-primary" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-base font-bold text-navy">Enthalten</h3>
|
||||
<Checklist items={INCLUDED} className="mt-5" />
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="mt-6 h-auto self-start p-0 font-bold"
|
||||
>
|
||||
<Link to="/konfigurator">
|
||||
Gesamtangebot konfigurieren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zu Rauchwarnmeldern"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={FAQS} />
|
||||
</Section>
|
||||
|
||||
<ProductCrossLinks current="/produkte/rauchwarnmelder" />
|
||||
</ProductPage>
|
||||
)
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import {
|
||||
ArrowRight,
|
||||
CalendarClock,
|
||||
Euro,
|
||||
FileCheck2,
|
||||
RadioTower,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Checklist } from "@/components/solenos/checklist"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
|
||||
import { PortalGaugeIcon } from "@/components/solenos/line-icons"
|
||||
import { NoticeArt, NoticeBanner } from "@/components/solenos/notice-banner"
|
||||
import { PriceCard } from "@/components/solenos/price-card"
|
||||
import { StepRail } from "@/components/solenos/process-steps"
|
||||
import { Section, SectionHeading } from "@/components/solenos/section"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
vectors,
|
||||
} from "@/components/solenos/site-assets"
|
||||
import { heizkostenv } from "@/lib/legal"
|
||||
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
|
||||
|
||||
/**
|
||||
* Hero art: the UVI pane as the subject – what the resident receives – with
|
||||
* the consumption bars behind it and the consumption ring as the accent.
|
||||
* Decorative; the copy carries the meaning.
|
||||
*/
|
||||
function UviHeroArt() {
|
||||
return (
|
||||
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
|
||||
<img
|
||||
src={vectors.consumptionRing}
|
||||
alt=""
|
||||
className="absolute top-0 right-[2%] w-[24%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.uviGlassPane}
|
||||
alt=""
|
||||
className="mx-auto w-[76%]"
|
||||
loading="eager"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.barChart}
|
||||
alt=""
|
||||
className="absolute bottom-[2%] left-0 w-[28%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MONTHLY_STEPS = [
|
||||
{
|
||||
media: <AssetRender render={renders.heatMeter} imgClassName="w-24" />,
|
||||
title: "Verbrauch erfassen",
|
||||
description:
|
||||
"Wärmezähler, Heizkostenverteiler und Wasserzähler messen laufend je Wohnung.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.gateway} imgClassName="w-24" />,
|
||||
title: "Werte übertragen",
|
||||
description:
|
||||
"Das Gateway sammelt die Funkdaten im Gebäude und überträgt sie verschlüsselt an SolenOS.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.refreshLoopCloud} imgClassName="w-28" />,
|
||||
title: "Daten prüfen und zuordnen",
|
||||
description:
|
||||
"Die Werte werden plausibilisiert und Wohnung, Gerät und Nutzungszeitraum zugeordnet.",
|
||||
},
|
||||
{
|
||||
media: <AssetRender render={renders.uviGlassPane} alt="" className="w-28" />,
|
||||
title: "Verbrauchsinformation erzeugen",
|
||||
description:
|
||||
"Monatswert, Vergleichswerte und die nach § 6a HeizkostenV geforderten Angaben werden aufbereitet.",
|
||||
},
|
||||
{
|
||||
media: (
|
||||
<AssetRender render={renders.laptopDashboard} imgClassName="w-40" />
|
||||
),
|
||||
title: "Bewohnern bereitstellen",
|
||||
description:
|
||||
"Bewohner rufen ihre Information im Mieterportal ab, Eigentümer sehen den Versandstatus.",
|
||||
},
|
||||
]
|
||||
|
||||
const MONTHLY_CONTENT = [
|
||||
"Verbrauch des Nutzers im letzten Monat in Kilowattstunden",
|
||||
"Vergleich mit dem Vormonat desselben Nutzers",
|
||||
"Vergleich mit dem entsprechenden Monat des Vorjahres, soweit die Daten erhoben wurden",
|
||||
"Vergleich mit dem Verbrauch eines normierten Durchschnittsnutzers derselben Nutzerkategorie",
|
||||
]
|
||||
|
||||
const BILLING_CONTENT = [
|
||||
"Anteil der eingesetzten Energieträger, bei Fernwärme zusätzlich Treibhausgasemissionen und Primärenergiefaktor",
|
||||
"erhobene Steuern, Abgaben und Zölle",
|
||||
"Entgelte für Messtechnik, Eichung, Ablesung und Abrechnung",
|
||||
"grafischer Vergleich des witterungsbereinigten Energieverbrauchs mit dem vorherigen Abrechnungszeitraum",
|
||||
"Kontaktinformationen zu Energieeffizienz und Hinweise zur Streitbeilegung",
|
||||
]
|
||||
|
||||
const INCLUDED = [
|
||||
"fernablesbare Messtechnik für Wärme und Warmwasser",
|
||||
"Gateway, Datenübertragung und Geräteüberwachung",
|
||||
"monatliche Erzeugung der Verbrauchsinformation",
|
||||
"Bereitstellung im Mieterportal für jede Wohnung",
|
||||
"Nutzerwechsel ohne Ablesetermin vor Ort",
|
||||
"Portalzugang für Eigentümer und Verwaltung",
|
||||
]
|
||||
|
||||
const NOT_INCLUDED = [
|
||||
"Jahresabrechnung der Heiz- und Warmwasserkosten",
|
||||
"Rauchwarnmelder und deren Inspektion",
|
||||
"Betriebskostenabrechnung des gesamten Gebäudes",
|
||||
]
|
||||
|
||||
const FAQS: FaqItem[] = [
|
||||
{
|
||||
question: "Ab wann muss die Verbrauchsinformation monatlich erfolgen?",
|
||||
answer:
|
||||
"Sobald fernablesbare Ausstattungen zur Verbrauchserfassung installiert sind, sind die Verbrauchsinformationen nach § 6a Absatz 1 Nummer 2 HeizkostenV seit dem 1. Januar 2022 monatlich mitzuteilen. Für Abrechnungszeiträume ab dem 1. Dezember 2021 galt zunächst ein mindestens vierteljährlicher bzw. halbjährlicher Rhythmus.",
|
||||
},
|
||||
{
|
||||
question: "Wie erhalten die Bewohner ihre Verbrauchsinformation?",
|
||||
answer:
|
||||
"Über das Mieterportal. Jeder Bewohner erhält einen eigenen Zugang und sieht ausschließlich die Werte der eigenen Wohnung und des eigenen Nutzungszeitraums – nicht die Daten von Vor- oder Nachmietern.",
|
||||
},
|
||||
{
|
||||
question: "Was passiert, wenn ein Bewohner keinen digitalen Zugang nutzt?",
|
||||
answer:
|
||||
"Die Information wird im Portal bereitgestellt und kann als Dokument ausgegeben werden, sodass die Verwaltung sie auf dem für das Gebäude vereinbarten Weg zustellen kann. Die Pflicht trifft den Gebäudeeigentümer; SolenOS liefert die dafür benötigten Inhalte vollständig aufbereitet.",
|
||||
},
|
||||
{
|
||||
question: "Muss für die UVI jemand in die Wohnung?",
|
||||
answer:
|
||||
"Nein. Fernablesbar heißt, dass die Ausstattung ohne Zugang zu den einzelnen Nutzeinheiten abgelesen werden kann (§ 5 Absatz 2 HeizkostenV). Auch bei einem Nutzerwechsel werden die Werte zum Stichtag aus der laufenden Datenerfassung übernommen.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich die UVI ohne Heizkostenabrechnung buchen?",
|
||||
answer:
|
||||
"Ja. Der UVI-Komplettbetrieb umfasst die benötigte Messtechnik und die zugehörigen digitalen Dienstleistungen. Die Heizkostenabrechnung baut auf denselben erfassten Verbrauchsdaten auf und kann jederzeit ergänzt werden.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function UviProductPage() {
|
||||
return (
|
||||
<ProductPage
|
||||
breadcrumb="Verbrauchsinformation (UVI)"
|
||||
eyebrow="Produkt"
|
||||
title={
|
||||
<span className="block text-[2.25rem] text-pretty sm:text-[2.75rem]">
|
||||
Unterjährige Verbrauchs­information (UVI)
|
||||
</span>
|
||||
}
|
||||
tagline="Monatlich. Automatisch. Nachweisbar."
|
||||
lead="Sind fernablesbare Zähler installiert, muss jeder Nutzer monatlich über seinen Verbrauch informiert werden. SolenOS erzeugt diese Information aus den laufend erfassten Werten und stellt sie den Bewohnern im Mieterportal bereit."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link to="/wissen/gesetzliche-anforderungen">
|
||||
Gesetzliche Anforderungen
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
|
||||
extra={
|
||||
<div
|
||||
role="note"
|
||||
className="rounded-2xl border border-brand-300 bg-brand-50/80 p-4 shadow-card"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Euro
|
||||
aria-hidden="true"
|
||||
className="mt-0.5 size-6 shrink-0 text-brand-700"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-extrabold text-navy">
|
||||
Bis zu 3 % Kürzungsrecht
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
|
||||
Fehlt die monatliche Verbrauchsinformation oder ist sie
|
||||
unvollständig, können Nutzer ihren Kostenanteil um 3 %
|
||||
kürzen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm font-semibold">
|
||||
<Link
|
||||
to="/wissen/gesetzliche-anforderungen"
|
||||
className="inline-flex items-center gap-1 text-brand-800 underline decoration-brand-400 underline-offset-4"
|
||||
>
|
||||
Anforderungen im Detail
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
<a
|
||||
href={heizkostenv("6a")}
|
||||
className="text-muted-foreground underline underline-offset-4"
|
||||
>
|
||||
Quelle: § 6a HeizkostenV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
visual={<UviHeroArt />}
|
||||
features={[
|
||||
{
|
||||
icon: <CalendarClock strokeWidth={1.6} />,
|
||||
title: "Monatlich automatisch",
|
||||
sub: "Erfassung, Aufbereitung und Bereitstellung laufen ohne Zutun.",
|
||||
},
|
||||
{
|
||||
icon: <RadioTower strokeWidth={1.6} />,
|
||||
title: "Ohne Wohnungszugang",
|
||||
sub: "Fernablesung statt Ablesetermin – auch beim Nutzerwechsel.",
|
||||
},
|
||||
{
|
||||
icon: <PortalGaugeIcon />,
|
||||
title: "Eigener Bewohnerzugang",
|
||||
sub: "Jede Wohnung sieht ausschließlich ihren eigenen Verbrauch.",
|
||||
},
|
||||
{
|
||||
icon: <FileCheck2 strokeWidth={1.6} />,
|
||||
title: "Pflichtangaben enthalten",
|
||||
sub: "Monatswert und Vergleiche nach § 6a HeizkostenV.",
|
||||
},
|
||||
]}
|
||||
closing={
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<CtaBanner
|
||||
icon={<Euro />}
|
||||
title="Verbrauchsinformation für Ihr Gebäude konfigurieren"
|
||||
description="Gebäude und Leistungen online erfassen; SolenOS prüft anschließend Ausstattung und Gesamtpreis."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="inverse" size="lg">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/kontakt">Kontakt aufnehmen</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
}
|
||||
>
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<NoticeBanner
|
||||
title="Monatliche Verbrauchsinformation ist Pflicht"
|
||||
source={{ cite: "§ 6a HeizkostenV", href: heizkostenv("6a") }}
|
||||
visual={<NoticeArt className="max-lg:hidden" />}
|
||||
action={
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="h-auto p-0 text-[15px] font-bold text-brand-800 underline decoration-brand-500 decoration-2 underline-offset-[6px] hover:text-navy hover:decoration-brand-800"
|
||||
>
|
||||
<Link to="/wissen/gesetzliche-anforderungen">
|
||||
Anforderungen im Detail
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Wo fernablesbare Ausstattungen installiert sind, sind die
|
||||
Verbrauchsinformationen seit dem 1. Januar 2022 monatlich
|
||||
mitzuteilen. Werden sie nicht oder nicht vollständig mitgeteilt,
|
||||
können Nutzer ihren Kostenanteil um 3 % kürzen.
|
||||
</NoticeBanner>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Inhalte"
|
||||
title="Was die Bewohner erhalten"
|
||||
lead="Die monatliche Information und die Angaben zur Jahresabrechnung sind in der HeizkostenV unterschiedlich geregelt. SolenOS bereitet beide Sätze aus derselben Datenbasis auf."
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-2">
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
|
||||
Monatlich
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-extrabold tracking-tight text-navy">
|
||||
Verbrauchsinformation je Wohnung
|
||||
</h3>
|
||||
<Checklist items={MONTHLY_CONTENT} className="mt-5" />
|
||||
<p className="mt-5 text-xs text-muted-foreground">
|
||||
§ 6a Absatz 2 HeizkostenV
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
|
||||
Zur Abrechnung
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-extrabold tracking-tight text-navy">
|
||||
Zusätzliche Informationen
|
||||
</h3>
|
||||
<Checklist items={BILLING_CONTENT} className="mt-5" />
|
||||
<p className="mt-5 text-xs text-muted-foreground">
|
||||
§ 6a Absatz 3 HeizkostenV
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Schritt für Schritt"
|
||||
title="So entsteht die monatliche UVI"
|
||||
lead="Vom Messwert in der Wohnung bis zur Information im Mieterportal – ein Ablauf, der nach der Installation ohne weitere Eingriffe läuft."
|
||||
/>
|
||||
<StepRail steps={MONTHLY_STEPS} className="mt-10" />
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
eyebrow="Leistungsumfang"
|
||||
title="Was im UVI-Betrieb enthalten ist"
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
|
||||
<Card className="gap-0 p-6 sm:p-7">
|
||||
<h3 className="text-base font-bold text-navy">Enthalten</h3>
|
||||
<Checklist items={INCLUDED} className="mt-5" />
|
||||
<h3 className="mt-8 text-base font-bold text-navy">
|
||||
Nicht im UVI-Betrieb enthalten
|
||||
</h3>
|
||||
<Checklist items={NOT_INCLUDED} className="mt-5" tone="excluded" />
|
||||
<p className="mt-5 text-sm leading-relaxed text-muted-foreground">
|
||||
Heizkostenabrechnung und Rauchwarnmelder sind eigene Leistungen –
|
||||
einzeln buchbar und jederzeit ergänzbar.
|
||||
</p>
|
||||
</Card>
|
||||
<div>
|
||||
<PriceCard
|
||||
title="Monatliche Verbrauchsinformation (UVI)"
|
||||
cadence="Gesamtangebot"
|
||||
price="Nach Prüfung"
|
||||
unit="nach vollständiger Objektkonfiguration"
|
||||
description="Messtechnik, erforderliche Gateway-Infrastruktur, Daten, Portal und Verbrauchsinformation werden gemeinsam angeboten."
|
||||
media={
|
||||
<AssetRender
|
||||
render={renders.uviGlassPane}
|
||||
alt=""
|
||||
className="w-full"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Button asChild variant="link" className="mt-4 h-auto p-0 font-bold">
|
||||
<Link to="/konfigurator">
|
||||
Gesamtangebot konfigurieren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Häufige Fragen"
|
||||
title="Fragen zur Verbrauchsinformation"
|
||||
/>
|
||||
<FaqAccordion className="mt-10" items={FAQS} />
|
||||
</Section>
|
||||
|
||||
<ProductCrossLinks current="/produkte/uvi" />
|
||||
</ProductPage>
|
||||
)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { KeyRound, Lock, MapPin, ShieldCheck, Users } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import { Section, SectionHeading } from "@/components/solenos/section"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
vectors,
|
||||
} from "@/components/solenos/site-assets"
|
||||
import {
|
||||
TopicCards,
|
||||
type Topic,
|
||||
} from "@/components/solenos/topic-cards"
|
||||
import { WissenArticle, WissenRelated } from "@/pages/wissen/wissen-article"
|
||||
|
||||
const safeguards: Topic[] = [
|
||||
{
|
||||
icon: <MapPin />,
|
||||
title: "Datenstandort",
|
||||
body: "Anwendungs- und Messdaten werden in Falkenstein, Deutschland, gehostet.",
|
||||
},
|
||||
{
|
||||
icon: <Lock />,
|
||||
title: "Verschlüsselte Übertragung",
|
||||
body: "Messdaten werden bereits auf der Funkstrecke mittels AES geschützt. Die Übertragung zwischen Gateway und SolenOS sowie der Zugriff auf das Portal erfolgen TLS-verschlüsselt.",
|
||||
},
|
||||
{
|
||||
icon: <Users />,
|
||||
title: "Rollen und Zugriffsrechte",
|
||||
body: "Zugriffe werden nach Rolle, Gebäude, Wohnung und Nutzungszeitraum begrenzt. Bewohner sehen ausschließlich die ihrem eigenen Nutzungsverhältnis zugeordneten Verbrauchsdaten – nicht die Daten von Vor- oder Nachmietern.",
|
||||
},
|
||||
{
|
||||
icon: <KeyRound />,
|
||||
title: "Kontrollierbare Geräteschlüssel",
|
||||
body: "Die Schlüssel der installierten Verbrauchserfassungsgeräte können ausgelesen und dem Gebäudeeigentümer zur Verfügung gestellt werden. Die Messtechnik bleibt dadurch nicht dauerhaft an SolenOS gebunden.",
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Hero art: the glass shield with the lock as the subject, the wireless orb
|
||||
* and the gateway beside it – the chain the copy describes
|
||||
* (Funkstrecke → Gateway → Plattform). Decorative; the copy carries meaning.
|
||||
*/
|
||||
function SecurityHeroArt() {
|
||||
return (
|
||||
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
|
||||
<img
|
||||
src={vectors.wirelessOrb}
|
||||
alt=""
|
||||
className="absolute top-0 right-[4%] w-[22%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassShieldLock}
|
||||
alt=""
|
||||
className="mx-auto w-[74%]"
|
||||
loading="eager"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassGateway}
|
||||
alt=""
|
||||
className="absolute bottom-[2%] left-0 w-[26%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DatenschutzSicherheitPage() {
|
||||
return (
|
||||
<WissenArticle
|
||||
breadcrumb="Datenschutz & Sicherheit"
|
||||
eyebrow="Technik & Sicherheit"
|
||||
title="Datenschutz und technische Sicherheit"
|
||||
lead="Wo die Messdaten liegen, wie sie übertragen werden und wer sie sehen darf – jeweils auf die Ebene begrenzt, die für den Messbetrieb erforderlich ist."
|
||||
visual={<SecurityHeroArt />}
|
||||
>
|
||||
<Section className="pt-2 sm:pt-4 lg:pt-6">
|
||||
<SectionHeading
|
||||
eyebrow="Sicherheit"
|
||||
title="Technische und organisatorische Maßnahmen"
|
||||
lead="Messdaten entstehen in der Wohnung und werden zentral verarbeitet. Auf jedem Abschnitt dieses Wegs gilt eine eigene Schutzmaßnahme."
|
||||
/>
|
||||
<TopicCards items={safeguards} className="sm:grid-cols-2" />
|
||||
</Section>
|
||||
|
||||
<WissenRelated
|
||||
to="/wissen/gesetzliche-anforderungen"
|
||||
title="Gesetzliche Anforderungen an den Messbetrieb"
|
||||
summary="Unterjährige Verbrauchsinformation, Fernablesbarkeit und interoperable Messtechnik nach HeizkostenV."
|
||||
/>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<CtaBanner
|
||||
icon={<ShieldCheck />}
|
||||
title="Fragen zu Datenschutz oder technischer Sicherheit?"
|
||||
description="Wir gehen Datenhaltung, Datenübertragung und Zugriffsrechte für Ihr Gebäude konkret durch."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="inverse" size="lg">
|
||||
<Link to="/kontakt">Kontakt aufnehmen</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
</WissenArticle>
|
||||
)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { BarChart3, KeyRound, Puzzle, RadioTower, ShieldCheck } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CtaBanner } from "@/components/solenos/cta-banner"
|
||||
import { IconBadge } from "@/components/solenos/icon-tile"
|
||||
import { Section, SectionHeading } from "@/components/solenos/section"
|
||||
import {
|
||||
AssetRender,
|
||||
renders,
|
||||
vectors,
|
||||
} from "@/components/solenos/site-assets"
|
||||
import {
|
||||
TopicCards,
|
||||
type Topic,
|
||||
} from "@/components/solenos/topic-cards"
|
||||
import { heizkostenv } from "@/lib/legal"
|
||||
import { WissenArticle, WissenRelated } from "@/pages/wissen/wissen-article"
|
||||
|
||||
const requirements: Topic[] = [
|
||||
{
|
||||
icon: <BarChart3 />,
|
||||
title: "Unterjährige Verbrauchsinformation",
|
||||
body: "Monatliche Verbrauchsinformationen auf Basis der erfassten Verbrauchs- und Ablesewerte – zur Umsetzung der Anforderungen des § 6a HeizkostenV.",
|
||||
source: { cite: "§ 6a HeizkostenV", href: heizkostenv("6a") },
|
||||
},
|
||||
{
|
||||
icon: <RadioTower />,
|
||||
title: "Fernablesbarkeit",
|
||||
body: "Verbrauchswerte werden ohne Zugang zu den einzelnen Wohnungen ausgelesen und zentral für Verbrauchsinformation und Abrechnung bereitgestellt.",
|
||||
},
|
||||
{
|
||||
icon: <Puzzle />,
|
||||
title: "Interoperable Messtechnik",
|
||||
body: "Die eingesetzten Verbrauchserfassungsgeräte sind OMS-fähig. Soweit für die jeweilige Ausstattung anwendbar, erfüllt die gelieferte Messtechnik die Anforderungen an Interoperabilität und Stand der Technik nach § 5 HeizkostenV.",
|
||||
source: { cite: "§ 5 HeizkostenV", href: heizkostenv("5") },
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Hero art: the UVI pane as the subject with the consumption donut and the
|
||||
* UVI badge beside it – what the requirements produce for the resident.
|
||||
* Decorative; the copy carries the meaning.
|
||||
*/
|
||||
function RequirementsHeroArt() {
|
||||
return (
|
||||
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
|
||||
<img
|
||||
src={vectors.uviBadge}
|
||||
alt=""
|
||||
className="absolute top-0 right-[4%] w-[22%]"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.uviGlassPane}
|
||||
alt=""
|
||||
className="mx-auto w-[74%]"
|
||||
loading="eager"
|
||||
/>
|
||||
<AssetRender
|
||||
render={renders.glassDonutShadow}
|
||||
alt=""
|
||||
className="absolute bottom-0 left-0 w-[30%]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GesetzlicheAnforderungenPage() {
|
||||
return (
|
||||
<WissenArticle
|
||||
breadcrumb="Gesetzliche Anforderungen"
|
||||
eyebrow="Technik & Anforderungen"
|
||||
title="Gesetzliche Anforderungen an den Messbetrieb"
|
||||
tagline="Für den Messbetrieb gebaut."
|
||||
lead="SolenOS verbindet fernablesbare Messtechnik, Verbrauchsinformationen und Abrechnung in einer klaren technischen und organisatorischen Struktur."
|
||||
visual={<RequirementsHeroArt />}
|
||||
>
|
||||
<Section className="pt-2 sm:pt-4 lg:pt-6">
|
||||
<SectionHeading
|
||||
eyebrow="Anforderungen"
|
||||
title="Was die Heizkostenverordnung verlangt"
|
||||
lead="Verbrauchsinformation, Fernablesbarkeit und Interoperabilität sind keine getrennten Themen – sie hängen an derselben Messtechnik und derselben Datenerfassung."
|
||||
/>
|
||||
<TopicCards
|
||||
items={requirements}
|
||||
className="sm:grid-cols-2 lg:grid-cols-3"
|
||||
/>
|
||||
|
||||
<aside className="mt-6 flex flex-col gap-5 rounded-3xl border border-brand-200 bg-brand-50/50 p-6 sm:flex-row sm:items-center sm:gap-6 sm:p-8">
|
||||
<IconBadge
|
||||
variant="solid"
|
||||
shape="squircle"
|
||||
size="lg"
|
||||
className="shadow-band"
|
||||
>
|
||||
<KeyRound />
|
||||
</IconBadge>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-bold text-balance text-navy sm:text-lg">
|
||||
Ein Wechsel des Messdienstleisters bleibt möglich
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-pretty text-navy/80 sm:text-base">
|
||||
Die AES-Schlüssel der Verbrauchserfassungsgeräte bleiben verfügbar
|
||||
und können dem Gebäudeeigentümer bereitgestellt werden. Dadurch
|
||||
bleibt ein späterer Wechsel des Messdienstleisters technisch
|
||||
möglich.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</Section>
|
||||
|
||||
<WissenRelated
|
||||
to="/wissen/datenschutz-sicherheit"
|
||||
title="Datenschutz und technische Sicherheit"
|
||||
summary="Datenstandort, verschlüsselte Übertragung, Rollen und Zugriffsrechte sowie kontrollierbare Geräteschlüssel."
|
||||
/>
|
||||
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<CtaBanner
|
||||
icon={<ShieldCheck />}
|
||||
title="Erfüllt Ihr Gebäude die Anforderungen bereits?"
|
||||
description="Wir prüfen Ausstattung, Fernablesbarkeit und Verbrauchsinformation für Ihr Gebäude konkret."
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="inverse" size="lg">
|
||||
<Link to="/kontakt">Kontakt aufnehmen</Link>
|
||||
</Button>
|
||||
<Button asChild variant="inverseOutline" size="lg">
|
||||
<Link to="/konfigurator">Konfiguration starten</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
</WissenArticle>
|
||||
)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import type * as React from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowRight, ScrollText, ShieldCheck } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { IconBadge } from "@/components/solenos/icon-tile"
|
||||
import { PageBreadcrumb } from "@/components/solenos/page-breadcrumb"
|
||||
import { Container, Section, SectionHeading } from "@/components/solenos/section"
|
||||
|
||||
type Article = {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
summary: string
|
||||
to: string
|
||||
}
|
||||
|
||||
const articles: Article[] = [
|
||||
{
|
||||
icon: <ScrollText />,
|
||||
title: "Gesetzliche Anforderungen an den Messbetrieb",
|
||||
summary:
|
||||
"Unterjährige Verbrauchsinformation nach § 6a HeizkostenV, Fernablesbarkeit ohne Zugang zur Wohnung und interoperable, OMS-fähige Messtechnik nach § 5 HeizkostenV.",
|
||||
to: "/wissen/gesetzliche-anforderungen",
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck />,
|
||||
title: "Datenschutz und technische Sicherheit",
|
||||
summary:
|
||||
"Datenstandort in Deutschland, AES- und TLS-verschlüsselte Übertragung, Zugriffsrechte nach Rolle und Nutzungszeitraum sowie kontrollierbare Geräteschlüssel.",
|
||||
to: "/wissen/datenschutz-sicherheit",
|
||||
},
|
||||
]
|
||||
|
||||
export default function WissenPage() {
|
||||
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: "Wissen" }]}
|
||||
/>
|
||||
<SectionHeading
|
||||
title="Wissen"
|
||||
lead="Hintergründe zu Messtechnik, Abrechnung und gesetzlichen Vorgaben rund um Wohngebäude."
|
||||
/>
|
||||
|
||||
<div className="mt-10 grid gap-5 lg:grid-cols-2">
|
||||
{articles.map((article) => (
|
||||
<Card key={article.to} className="gap-0 p-7">
|
||||
<IconBadge variant="outline" shape="squircle" size="lg">
|
||||
{article.icon}
|
||||
</IconBadge>
|
||||
<h2 className="mt-5 text-xl font-extrabold tracking-tight text-balance text-navy">
|
||||
{article.title}
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||
{article.summary}
|
||||
</p>
|
||||
<div className="mt-6 flex-1 content-end">
|
||||
<Button asChild variant="outline">
|
||||
<Link to={article.to}>
|
||||
Mehr erfahren
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import type * as React from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
||||
import { IconBadge } from "@/components/solenos/icon-tile"
|
||||
import { PageBreadcrumb } from "@/components/solenos/page-breadcrumb"
|
||||
import { PageHero } from "@/components/solenos/page-hero"
|
||||
import { Container, Section } from "@/components/solenos/section"
|
||||
|
||||
/**
|
||||
* Shared shell for the /wissen articles: split PageHero with the
|
||||
* Startseite → Wissen → article breadcrumb, then the article's own sections.
|
||||
*/
|
||||
function WissenArticle({
|
||||
breadcrumb,
|
||||
eyebrow,
|
||||
title,
|
||||
tagline,
|
||||
lead,
|
||||
visual,
|
||||
children,
|
||||
}: {
|
||||
/** label of the current page, appended to Startseite → Wissen */
|
||||
breadcrumb: string
|
||||
eyebrow?: React.ReactNode
|
||||
title: React.ReactNode
|
||||
/** short bold subline under the H1 (e.g. "Für den Messbetrieb gebaut.") */
|
||||
tagline?: React.ReactNode
|
||||
lead?: React.ReactNode
|
||||
visual?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
breadcrumb={
|
||||
<PageBreadcrumb
|
||||
items={[
|
||||
{ label: "Startseite", to: "/" },
|
||||
{ label: "Wissen", to: "/wissen" },
|
||||
{ label: breadcrumb },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
eyebrow={eyebrow}
|
||||
title={title}
|
||||
lead={
|
||||
<>
|
||||
{tagline ? (
|
||||
<span className="mb-2 block text-lg font-bold text-navy sm:text-xl">
|
||||
{tagline}
|
||||
</span>
|
||||
) : null}
|
||||
{lead}
|
||||
</>
|
||||
}
|
||||
visual={visual}
|
||||
/>
|
||||
<Container>{children}</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Baseline link to the sibling article, closing an article out. */
|
||||
function WissenRelated({
|
||||
to,
|
||||
title,
|
||||
summary,
|
||||
}: {
|
||||
to: string
|
||||
title: string
|
||||
summary: string
|
||||
}) {
|
||||
return (
|
||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||
<Link
|
||||
to={to}
|
||||
className="group flex items-center gap-6 rounded-3xl border border-border bg-card p-6 shadow-card transition-colors hover:border-brand-200 sm:p-8"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
|
||||
Weiterlesen
|
||||
</p>
|
||||
<h2 className="mt-2 text-xl font-extrabold tracking-tight text-balance text-navy">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-pretty text-muted-foreground">
|
||||
{summary}
|
||||
</p>
|
||||
</div>
|
||||
<IconBadge
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="transition-transform group-hover:translate-x-0.5"
|
||||
>
|
||||
<ArrowRight />
|
||||
</IconBadge>
|
||||
</Link>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export { WissenArticle, WissenRelated }
|
||||
@@ -3,10 +3,7 @@ name: Deploy Static Site
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "component-library/**"
|
||||
- ".gitea/workflows/deploy.yml"
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -18,38 +15,34 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: component-library/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: component-library
|
||||
run: npm ci
|
||||
|
||||
- name: Build static site
|
||||
working-directory: component-library
|
||||
run: npm run build
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
|
||||
- name: Deploy static files to server
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
REMOTE_DIR: /home/deploy/projects/solenos/static
|
||||
REMOTE_DIR: /home/deploy/projects/ge-bos/static
|
||||
run: |
|
||||
SSH="ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new"
|
||||
# Recreate the target dir so removed files don't linger (rsync --delete equivalent)
|
||||
$SSH deploy@"$DEPLOY_HOST" "rm -rf '$REMOTE_DIR' && mkdir -p '$REMOTE_DIR'"
|
||||
# Stream the build output over ssh and extract it on the server
|
||||
tar -czf - -C component-library/dist . | $SSH deploy@"$DEPLOY_HOST" "tar -xzf - -C '$REMOTE_DIR'"
|
||||
tar -czf - -C out . | $SSH deploy@"$DEPLOY_HOST" "tar -xzf - -C '$REMOTE_DIR'"
|
||||
|
||||
- name: Run deployment script
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new \
|
||||
deploy@${{ secrets.DEPLOY_HOST }} \
|
||||
"cd /home/deploy/projects/solenos && ./deploy.sh"
|
||||
"cd /home/deploy/projects/ge-bos && ./deploy.sh"
|
||||
@@ -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>
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "solenos-component-library",
|
||||
"name": "gebos-component-library",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "solenos-component-library",
|
||||
"name": "gebos-component-library",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.5",
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "solenos-component-library",
|
||||
"name": "gebos-component-library",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect } from "react"
|
||||
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"
|
||||
|
||||
import { SiteFooter } from "@/components/solenos/site-footer"
|
||||
import { SiteHeader } from "@/components/solenos/site-header"
|
||||
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"
|
||||
@@ -13,14 +13,7 @@ import HomePage from "@/pages/home"
|
||||
import HowItWorksPage from "@/pages/how-it-works"
|
||||
import KlarpreisPage from "@/pages/klarpreis"
|
||||
import { SimplePage } from "@/pages/placeholder"
|
||||
import HeizkostenabrechnungProductPage from "@/pages/products/heizkostenabrechnung"
|
||||
import MesstechnikProductPage from "@/pages/products/messtechnik-infrastruktur"
|
||||
import ProductsPage from "@/pages/products"
|
||||
import RauchwarnmelderProductPage from "@/pages/products/rauchwarnmelder"
|
||||
import UviProductPage from "@/pages/products/uvi"
|
||||
import WissenPage from "@/pages/wissen"
|
||||
import DatenschutzSicherheitPage from "@/pages/wissen/datenschutz-sicherheit"
|
||||
import GesetzlicheAnforderungenPage from "@/pages/wissen/gesetzliche-anforderungen"
|
||||
import SolutionsPage from "@/pages/solutions"
|
||||
|
||||
function ScrollToTop() {
|
||||
const { pathname } = useLocation()
|
||||
@@ -42,20 +35,7 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/so-funktionierts" element={<HowItWorksPage />} />
|
||||
<Route path="/produkte" element={<ProductsPage />} />
|
||||
<Route path="/produkte/uvi" element={<UviProductPage />} />
|
||||
<Route
|
||||
path="/produkte/heizkostenabrechnung"
|
||||
element={<HeizkostenabrechnungProductPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/produkte/rauchwarnmelder"
|
||||
element={<RauchwarnmelderProductPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/produkte/messtechnik-infrastruktur"
|
||||
element={<MesstechnikProductPage />}
|
||||
/>
|
||||
<Route path="/loesungen" element={<SolutionsPage />} />
|
||||
<Route path="/fuer-wen" element={<ForWhomPage />} />
|
||||
<Route
|
||||
path="/fuer-wen/hauseigentuemer"
|
||||
@@ -75,15 +55,6 @@ export default function App() {
|
||||
/>
|
||||
<Route path="/konfigurator" element={<ConfiguratorPage />} />
|
||||
<Route path="/klarpreis" element={<KlarpreisPage />} />
|
||||
<Route path="/wissen" element={<WissenPage />} />
|
||||
<Route
|
||||
path="/wissen/gesetzliche-anforderungen"
|
||||
element={<GesetzlicheAnforderungenPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/wissen/datenschutz-sicherheit"
|
||||
element={<DatenschutzSicherheitPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/unternehmen"
|
||||
element={
|
||||
@@ -93,7 +64,7 @@ export default function App() {
|
||||
{ label: "Unternehmen" },
|
||||
]}
|
||||
title="Unternehmen"
|
||||
lead="SolenOS verbindet Messtechnik, Datenerfassung und Software zu einem durchgängigen System für Wohngebäude."
|
||||
lead="GebOS verbindet Messtechnik, Datenerfassung und Software zu einem durchgängigen System für Wohngebäude."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 908 B After Width: | Height: | Size: 908 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 138 KiB |