// WattIQ Product Detail Page — single component, content-driven
// Alle 9 secties renderen vanuit window.WATTIQ_PRODUCTS data
const { useState, useMemo, useEffect } = React;
// ============== ICONS (inline SVG) ==============
const Icon = ({ name, size = 24, stroke = 1.6 }) => {
const props = {
width: size, height: size, fill: "none", stroke: "currentColor",
strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round",
};
switch (name) {
// problem icons (used inside .icon)
case "queue": return ;
case "scale": return ;
case "isolated": return ;
case "report": return ;
case "hours": return ;
case "noise": return ;
case "cost": return ;
case "wait": return ;
case "fixed": return ;
case "spike": return ;
case "solar": return ;
case "fuel": return ;
case "ban": return ;
case "refuel": return ;
case "noroof": return ;
case "time": return ;
case "loss": return ;
// flow icons
case "grid": return ;
case "core": return ;
case "ems": return ;
case "balance": return ;
case "load": return ;
case "battery": return ;
case "genset": return ;
case "bridge": return ;
case "charger": return ;
case "ev": return ;
case "fleet": return ;
case "container": return ;
case "deploy": return ;
case "led": return ;
case "auto": return ;
case "dc": return ;
case "rental": return ;
case "stage": return ;
// feature icons
case "peak": return ;
case "modular": return ;
case "plug": return ;
case "remote": return ;
case "hybrid": return ;
default: return ;
}
};
// ============== HERO STAGE (product image + meta overlay) ==============
const HeroStage = ({ product }) => {
return (
{product.stageMeta.map((m, i) => {m} )}
{product.stageStatus}
{product.stageCorner[0]}
{product.stageCorner[1]}
);
};
// ============== HERO (Section 1) ==============
const Hero = ({ product }) => (
Product · {product.family} — {product.category}
{product.headline}
{product.sublead}
{product.heroMeta.map((m, i) => (
))}
);
// ============== PROBLEM ICONS (mini diagrams) ==============
const ProblemIcon = ({ kind }) => (
{/* grid */}
{kind === "queue" && (<>
12
24
36 mnd
project
>)}
{kind === "hours" && (<>
24/7
DRAAIUREN
>)}
{kind === "noise" && (<>
{[1,2,3,4,5,6].map((i) => (
))}
65+ dB(A)
>)}
{kind === "cost" && (<>
€/dag
+340%
>)}
{kind === "report" && (<>
CSRD ?
>)}
{kind === "scale" && (<>
VAST
>)}
{kind === "isolated" && (<>
BATT
>)}
{kind === "wait" && (<>
{[0,1,2,3,4,5].map((i) => (
))}
12 / 24 / 36 MND
>)}
{kind === "fixed" && (<>
FIXED
€280k CapEx
>)}
{kind === "spike" && (<>
MAX
>)}
{kind === "solar" && (<>
EXPORT 0,02€
>)}
{kind === "fuel" && (<>
1L/uur
VOOR 1.2 kW LED
>)}
{kind === "ban" && (<>
STAGE IIIA · BANNED
>)}
{kind === "refuel" && (<>
REFUEL
02:00 · OPERATOR
>)}
{kind === "noroof" && (<>
NO ROOF
>)}
{kind === "time" && (<>
{[0,1,2,3,4].map((i) => (
{i+1}d
))}
>)}
{kind === "loss" && (<>
AC
DC
AC
−18% LOSS
>)}
);
// ============== PROBLEM (Section 2) — Diagonal Hero (V6) ==============
const Problem = ({ product }) => (
Vier blokkades.
Eén oorzaak:
starre infrastructuur.
{product.problemLead} Hieronder de patronen die we op elke locatie tegenkomen — en waarom geen losse oplossing volstaat.
{product.problems.map((p, i) => (
{p.num}
{p.title}
{p.body}
))}
);
// ============== FLOW (Section 3) ==============
const Flow = ({ product }) => (
03 — Hoe het werkt
{product.flowHead}
{product.flowLead}
{product.flow.map((n, i) => (
))}
{product.flowCaption.map((c, i) => (
))}
);
// ============== FEATURES (Section 4) — Icon Strip Compact ==============
const Features = ({ product }) => (
04 — Eigenschappen
{product.featuresHead}
Iedere feature toont operationeel voordeel — niet alleen wat het is.
{product.features.map((f, i) => (
{String(i + 1).padStart(2, "0")}
{f.title}
{f.body}
))}
);
// ============== SCENARIO STAGE (mini diagram per type) ==============
const ScenarioStage = ({ kind }) => (
SIM_001 REAL-WORLD
LIVE
{/* y-axis labels */}
100%
50%
0%
{kind === "site-load" && (<>
{/* battery SoC area */}
{/* genset bursts */}
SoC
GENSET
06:00
12:00
22:00
14-DAY · ROTTERDAM SITE
>)}
{kind === "fuel-curve" && (<>
{/* stacked curves */}
BATTERY
GENSET
DAG 1
DAG 11
DAG 21
21-DAY OFF-GRID PROJECT
>)}
{kind === "peak-shave" && (<>
CAP
{/* shaved peak */}
SHAVED
AANSLUITINGSLAST
07:00
12:00
18:00
>)}
{kind === "charge-curve" && (<>
{/* multiple charging trucks */}
{[0,1,2,3].map(i => (
))}
8 TRUCKS · DC SESSIONS
18:00
00:00
06:00
2.4 MWh DELIVERED · 80kW GRID
>)}
{kind === "backup-curve" && (<>
FORGE 22 MIN
BATTERY SoC
SCHALEN VAN PIEK · 4-UURS WINDOW
>)}
{kind === "light-cycle" && (<>
{/* solar/battery cycle */}
{[0,1,2].map(d => (
DAG{d+1}
))}
SOLAR HARVEST + LED RUNTIME
14 NACHTEN · 0L DIESEL
>)}
{kind === "harvest-curve" && (<>
30 kWp PIEK
SOLAR HARVEST
06:00
13:00
20:00
>)}
);
// ============== SCENARIO (Section 5) — Three-Phase Cards + Sparklines ==============
const SCN_LINES = [
"M0,40 L40,38 L80,18 L120,8 L160,12 L200,30 L240,42",
"M0,30 L40,22 L80,16 L120,18 L160,28 L200,38 L240,42",
"M0,40 L40,42 L80,32 L120,18 L160,10 L200,12 L240,22",
];
const Scenario = ({ product }) => {
// pak eerste 3 stappen — als minder, vul aan
const phases = product.scenarioSteps.slice(0, 3);
return (
05 — Scenario
{product.scenarioHead}
{product.scenarioLead}
{phases.map((p, i) => (
PHASE 0{i + 1}
{p.t}
{p.title}
{p.body}
SAMPLE / 5 MIN
LIVE
))}
);
};
// ============== VARIANTS (Section 6a) — Sub-tab strip + Hero variant in focus ==============
// Parses a model name into a series label + short variant identifier
// "WattIQ Core 260" → { series: "CORE", variant: "260" }
// "WattIQ Core Lite 40" → { series: "LITE", variant: "40" }
// "WattIQ Forge D60" → { series: "FORGE", variant: "D60" }
// "WattIQ Beacon H" → { series: "BEACON", variant: "H" }
const parseModelName = (fullName, productName) => {
let n = (fullName || "").replace(/^WattIQ\s+/i, "");
const base = (productName || "").replace(/^WattIQ\s+/i, "");
if (n.toLowerCase().startsWith(base.toLowerCase() + " ")) n = n.slice(base.length + 1);
const parts = n.split(/\s+/).filter(Boolean);
if (parts.length === 0) return { series: base.toUpperCase(), variant: "" };
const looksLikeSeries = /^[A-Za-z]+$/.test(parts[0]) && parts.length > 1;
if (looksLikeSeries) {
return { series: parts[0].toUpperCase(), variant: parts.slice(1).join(" ") };
}
return { series: base.toUpperCase(), variant: parts.join(" ") };
};
// Per-variant photos — slug = lowercased model name with spaces → hyphens, "WattIQ " prefix stripped.
// Fallback is the product's main image when no specific photo exists.
const VARIANT_IMAGES = {
// CORE
"core-125": "assets/products/core-125.png",
"core-260": "assets/products/core-260.png",
"core-1000": "assets/products/core-2000.png",
"core-2000": "assets/products/core-2000.png",
"core-lite-40": "assets/products/core-lite-100.png",
"core-lite-100": "assets/products/core-lite-100.png",
"core-flex-50": "assets/products/core-flex-100-1.png",
"core-flex-100": "assets/products/core-flex-100-1.png",
// FUSION
"fusion-gb30": "assets/products/fusion.png",
"fusion-gsb20": "assets/products/fusion-2.png",
"fusion-gsb60": "assets/products/fusion-3.png",
// BRIDGE
"bridge-30": "assets/products/bridge-1.png",
"bridge-60": "assets/products/bridge-2.png",
// GRID
"grid-80": "assets/products/grid-80-1.png",
"grid-200": "assets/products/grid-200-1.png",
"grid-600": "assets/products/grid-600-1.png",
"grid-800": "assets/products/grid-1000-1.png",
"grid-1000": "assets/products/grid-1000-1.png",
// FORGE
"forge-d30": "assets/products/forge-d60.png",
"forge-d60": "assets/products/forge-d60.png",
"forge-d120": "assets/products/forge-d120.png",
// BEACON
"beacon-h": "assets/products/beacon-h-1.png",
"beacon-d-hydra": "assets/products/beacon-d-hydra-1.png",
"beacon-d-manual": "assets/products/beacon-d-manual.png",
// RAY
"ray-fold-10": "assets/products/ray-fold-1.png",
"ray-fold-30": "assets/products/ray-fold-2.png",
"ray-flex-10": "assets/products/ray-flex-1.png",
"ray-flex-30": "assets/products/ray-flex-2.png",
};
const getVariantImage = (fullName, fallback) => {
if (!fullName) return fallback;
const slug = fullName.replace(/^WattIQ\s+/i, "").replace(/\s+/g, "-").toLowerCase();
return VARIANT_IMAGES[slug] || fallback;
};
const Variants = ({ product }) => {
const models = product.models || [];
if (models.length === 0) return null;
// Reset selection back to first variant whenever the product family changes
const [pickIdx, setPickIdx] = useState(0);
useEffect(() => { setPickIdx(0); }, [product.id]);
const m = models[pickIdx] || models[0];
const [name, useCase, capacity, power, specific, housing] = m;
const code = name.replace(/^WattIQ\s+/i, "").replace(/\s+/g, "-").toUpperCase();
const photo = getVariantImage(name, product.image);
return (
06 — Varianten
{models.length} configuraties. Eén platform.
Kies de variant die past bij je locatie. Onder elke configuratie zit hetzelfde EMS en dezelfde Control-koppeling — alleen capaciteit, vermogen en behuizing verschillen.
{models.map((row, i) => {
const p = parseModelName(row[0], product.name);
return (
setPickIdx(i)}
>
{p.series}
{p.variant}
{row[2]}
);
})}
WIQ-{code}
SELECTED
WIQ-{code}
{name}
— Capaciteit / Output
— Vermogen / Battery
— Specifiek
— Behuizing
— Use-case
);
};
// ============== SPECS (Section 6) — Card Grid Dense ==============
// Pakt 3 representatieve modellen uit product.models — middelste = "POPULAIR"
const Specs = ({ product }) => {
const all = product.models || [];
// pick 3: first, middle, last (or all if fewer)
let picks;
if (all.length >= 3) {
picks = [all[0], all[Math.floor(all.length / 2)], all[all.length - 1]];
} else {
picks = all.slice(0, 3);
}
// common spec rows pulled from product.specs (k/v pairs)
const sp = product.specs || [];
const findSpec = (re) => {
const m = sp.find((s) => re.test(s.k));
return m ? m.v : "";
};
const ipRow = findSpec(/behuizing|housing|ip/i) || "IP54";
const tempRow = findSpec(/temp|operating/i) || "−20 / +50 °C";
const warrRow = findSpec(/garantie|warranty|inzetduur/i) || "10 jaar";
return (
06 — Specificaties
{product.specsHead}
{product.specsLead}
{picks.map((m, i) => {
const isMid = i === 1 && picks.length === 3;
return (
{isMid &&
POPULAIR
}
SKU 0{i + 1}
{m[0]}
{(m[1] || "").toUpperCase()}
{[
["Capaciteit / Output", m[2]],
["Vermogen / Battery", m[3]],
["Specifiek", m[4]],
["Behuizing", m[5] || ipRow],
["Temp", tempRow],
].map(([k, v], j) => (
{k}
))}
);
})}
{all.length} configuraties beschikbaar
·
PDF datasheet op aanvraag
·
Garantie: {warrRow}
);
};
// ============== ECOSYSTEM (Section 7) — Constellation Map (V19) ==============
const ECO_FAMILY = [
{ id: "core", code: "CORE", t: "Core", role: "Storage", desc: "Modulaire BESS · 40 kWh – 2 MWh" },
{ id: "fusion", code: "FUSN", t: "Fusion", role: "Hybrid", desc: "Hybrid skid · genset + battery" },
{ id: "bridge", code: "BRDG", t: "Bridge", role: "Bridge", desc: "Net-tijdelijke aansluiting" },
{ id: "grid", code: "GRID", t: "Grid", role: "Grid", desc: "Vaste netinfrastructuur" },
{ id: "forge", code: "FRGE", t: "Forge", role: "Charge", desc: "EV-laadcluster · 50–400 kW" },
{ id: "beacon", code: "BCON", t: "Beacon", role: "Light", desc: "Mobile lighting · solar-first" },
{ id: "ray", code: "RAY", t: "Ray", role: "Solar", desc: "Solar harvest · direct-DC" },
];
// 6 omringende posities (60° intervallen) — actief product zit altijd in centrum
const ECO_ORBIT = [
{ x: 50, y: 14 }, { x: 84, y: 30 },
{ x: 86, y: 72 }, { x: 50, y: 88 },
{ x: 14, y: 72 }, { x: 16, y: 30 },
];
const Ecosystem = ({ product }) => {
const others = ECO_FAMILY.filter((p) => p.id !== product.id).slice(0, 6);
const hub = ECO_FAMILY.find((p) => p.id === product.id) || { code: product.family, t: product.name };
return (
07 — Ecosystem
Werkt zelfstandig. Schaalt als systeem.
{product.ecoNote} {product.name} klikt in op WattIQ Control en werkt samen met andere modules.
{/* tiny background stars */}
{Array.from({ length: 80 }).map((_, i) => {
const x = (i * 37) % 100;
const y = (i * 53) % 100;
const s = ((i * 7) % 3) + 1;
return ;
})}
{/* connecting lines from hub to others */}
{others.map((_, i) => {
const o = ECO_ORBIT[i];
return (
);
})}
{/* hub (active product, big orange) */}
{hub.code}
{hub.role || "Hub"}
{/* orbiting nodes */}
{others.map((p, i) => {
const o = ECO_ORBIT[i];
return (
);
})}
{/* footer caption */}
● MAP · 7 PRODUCTS · ONE PLATFORM
{hub.code} ★ ACTIVE
{/* legend grid below */}
{others.map((p) => (
{p.code}
WattIQ {p.t}
{p.desc}
))}
);
};
// ============== RESULTS (Section 8) — Live Dashboard ==============
const Results = ({ product }) => {
// pak 4 meest indrukwekkende uit 6 — eerste 4 voldoet
const metrics = product.results.slice(0, 4);
const sites = ["ALMERE-DC1 99.98%", "SCHIPHOL-B 99.91%", "R'DAM-FEST 100%", "UTRECHT-S2 99.87%", "ANTWERPEN-P3 99.94%"];
return (
08 — Resultaten
{product.resultsHead}
Operationele cijfers per product. Gemeten op real-world deployments.
LIVE · ALL {product.name.replace("WattIQ ", "").toUpperCase()} DEPLOYMENTS
UPDATED 14:32 · NL 21 · BE 7
{metrics.map((m, i) => (
{m.title.toUpperCase()}
▲ TRENDING UP · 12W
{m.body}
))}
{sites.map((s, i) => ● {s} )}
);
};
// ============== CTA (Section 9) ==============
const CTA = ({ product }) => (
09 — Twee paden
Wat past bij nu ?
Twee manieren om verder te gaan met {product.name}. Eén voor mensen die concreet zoeken; één voor mensen die eerst willen begrijpen.
);
// ============== PICKER ==============
const Picker = ({ products, current, onPick }) => (
Productfamilies →
{products.map((p) => (
onPick(p.id)}
>
{p.family}
WattIQ {p.name.replace("WattIQ ", "")}
))}
);
// ============== TICKER ==============
const Ticker = ({ product }) => (
WIQ-PRODUCT-DOC v3.4
·
{product.family} · {product.name.toUpperCase()}
·
{product.category}
·
EMS-INTEGRATED
·
NL-ROTTERDAM
);
// ============== APP ==============
const ProductDetailApp = () => {
const products = window.WATTIQ_PRODUCTS;
const [currentId, setCurrentId] = useState(products[0].id);
// Allow URL hash to drive product
useEffect(() => {
const fromHash = window.location.hash.replace("#", "");
if (fromHash && products.find((p) => p.id === fromHash)) {
setCurrentId(fromHash);
}
const handler = () => {
const h = window.location.hash.replace("#", "");
if (h && products.find((p) => p.id === h)) setCurrentId(h);
};
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
const product = useMemo(
() => products.find((p) => p.id === currentId) || products[0],
[currentId]
);
const handlePick = (id) => {
setCurrentId(id);
window.history.replaceState(null, "", `#${id}`);
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
);
};
window.ProductDetailApp = ProductDetailApp;