{"id":1,"date":"2026-05-02T11:08:00","date_gmt":"2026-05-02T11:08:00","guid":{"rendered":"https:\/\/livingtotaal.nl\/?p=1"},"modified":"2026-05-02T11:34:47","modified_gmt":"2026-05-02T11:34:47","slug":"hello-world","status":"publish","type":"post","link":"https:\/\/livingtotaal.nl\/?p=1","title":{"rendered":"Hello world!"},"content":{"rendered":"<p>import React, { useMemo, useState } from &#8220;react&#8221;;<br \/>\nimport { motion } from &#8220;framer-motion&#8221;;<br \/>\nimport { Plus, Trash2, RotateCcw, Ruler, Euro, Download, Home } from &#8220;lucide-react&#8221;;<br \/>\nimport { Button } from &#8220;@\/components\/ui\/button&#8221;;<br \/>\nimport { Card, CardContent } from &#8220;@\/components\/ui\/card&#8221;;<\/p>\n<p>const MODULES = [<br \/>\n  { id: &#8220;base60&#8221;, name: &#8220;Onderkast 60&#8221;, width: 60, depth: 60, price: 240, type: &#8220;Onderkast&#8221; },<br \/>\n  { id: &#8220;base80&#8221;, name: &#8220;Onderkast 80&#8221;, width: 80, depth: 60, price: 310, type: &#8220;Onderkast&#8221; },<br \/>\n  { id: &#8220;sink80&#8221;, name: &#8220;Spoelkast 80&#8221;, width: 80, depth: 60, price: 390, type: &#8220;Spoelzone&#8221; },<br \/>\n  { id: &#8220;hob60&#8221;, name: &#8220;Kookplaatkast 60&#8221;, width: 60, depth: 60, price: 430, type: &#8220;Kookzone&#8221; },<br \/>\n  { id: &#8220;drawer90&#8221;, name: &#8220;Ladekast 90&#8221;, width: 90, depth: 60, price: 520, type: &#8220;Opbergen&#8221; },<br \/>\n  { id: &#8220;tall60&#8221;, name: &#8220;Hoge kast 60&#8221;, width: 60, depth: 60, price: 690, type: &#8220;Hoge kast&#8221; },<br \/>\n  { id: &#8220;fridge60&#8221;, name: &#8220;Koelkastkast 60&#8221;, width: 60, depth: 60, price: 850, type: &#8220;Apparatuur&#8221; },<br \/>\n  { id: &#8220;oven60&#8221;, name: &#8220;Ovenkast 60&#8221;, width: 60, depth: 60, price: 760, type: &#8220;Apparatuur&#8221; },<br \/>\n];<\/p>\n<p>const LAYOUTS = [<br \/>\n  { id: &#8220;straight&#8221;, label: &#8220;Rechte keuken&#8221;, description: &#8220;Alle modules langs \u00e9\u00e9n wand.&#8221; },<br \/>\n  { id: &#8220;lshape&#8221;, label: &#8220;L-keuken&#8221;, description: &#8220;Modules verdeeld over twee wanden.&#8221; },<br \/>\n  { id: &#8220;island&#8221;, label: &#8220;Keuken met eiland&#8221;, description: &#8220;Wandopstelling plus los eiland.&#8221; },<br \/>\n];<\/p>\n<p>function currency(value) {<br \/>\n  return new Intl.NumberFormat(&#8220;nl-NL&#8221;, { style: &#8220;currency&#8221;, currency: &#8220;EUR&#8221;, maximumFractionDigits: 0 }).format(value);<br \/>\n}<\/p>\n<p>function fitStatus(totalWidth, roomWidth) {<br \/>\n  if (totalWidth <= roomWidth) return { label: \"Past\", tone: \"bg-emerald-100 text-emerald-800\" };\n  return { label: \"Te breed\", tone: \"bg-red-100 text-red-800\" };\n}\n\nexport default function KitchenDesigner() {\n  const [roomWidth, setRoomWidth] = useState(360);\n  const [roomDepth, setRoomDepth] = useState(260);\n  const [layout, setLayout] = useState(\"straight\");\n  const [selectedType, setSelectedType] = useState(\"Alles\");\n  const [items, setItems] = useState([\n    { uid: crypto.randomUUID(), ...MODULES[1], zone: \"wand\" },\n    { uid: crypto.randomUUID(), ...MODULES[2], zone: \"wand\" },\n    { uid: crypto.randomUUID(), ...MODULES[3], zone: \"wand\" },\n  ]);\n\n  const filteredModules = useMemo(() => {<br \/>\n    if (selectedType === &#8220;Alles&#8221;) return MODULES;<br \/>\n    return MODULES.filter((m) => m.type === selectedType);<br \/>\n  }, [selectedType]);<\/p>\n<p>  const totalWidth = items.filter((i) => i.zone === &#8220;wand&#8221;).reduce((sum, i) => sum + i.width, 0);<br \/>\n  const islandWidth = items.filter((i) => i.zone === &#8220;eiland&#8221;).reduce((sum, i) => sum + i.width, 0);<br \/>\n  const cabinetsPrice = items.reduce((sum, i) => sum + i.price, 0);<br \/>\n  const worktopPrice = Math.ceil((totalWidth + islandWidth) \/ 100) * 180;<br \/>\n  const installPrice = Math.max(850, Math.ceil(items.length * 115));<br \/>\n  const totalPrice = cabinetsPrice + worktopPrice + installPrice;<br \/>\n  const status = fitStatus(totalWidth, roomWidth);<\/p>\n<p>  const addModule = (module) => {<br \/>\n    const zone = layout === &#8220;island&#8221; &#038;&#038; [&#8220;drawer90&#8221;, &#8220;base80&#8221;, &#8220;base60&#8221;].includes(module.id) &#038;&#038; islandWidth < 180 ? \"eiland\" : \"wand\";\n    setItems((prev) => [&#8230;prev, { uid: crypto.randomUUID(), &#8230;module, zone }]);<br \/>\n  };<\/p>\n<p>  const removeModule = (uid) => setItems((prev) => prev.filter((item) => item.uid !== uid));<\/p>\n<p>  const reset = () => {<br \/>\n    setLayout(&#8220;straight&#8221;);<br \/>\n    setRoomWidth(360);<br \/>\n    setRoomDepth(260);<br \/>\n    setItems([<br \/>\n      { uid: crypto.randomUUID(), &#8230;MODULES[1], zone: &#8220;wand&#8221; },<br \/>\n      { uid: crypto.randomUUID(), &#8230;MODULES[2], zone: &#8220;wand&#8221; },<br \/>\n      { uid: crypto.randomUUID(), &#8230;MODULES[3], zone: &#8220;wand&#8221; },<br \/>\n    ]);<br \/>\n  };<\/p>\n<p>  const exportDesign = () => {<br \/>\n    const data = {<br \/>\n      ruimte: { breedte_cm: roomWidth, diepte_cm: roomDepth },<br \/>\n      layout,<br \/>\n      modules: items.map(({ uid, name, width, depth, price, zone }) => ({ uid, name, width, depth, price, zone })),<br \/>\n      prijsindicatie: { kasten: cabinetsPrice, werkblad: worktopPrice, montage: installPrice, totaal: totalPrice },<br \/>\n    };<br \/>\n    const blob = new Blob([JSON.stringify(data, null, 2)], { type: &#8220;application\/json&#8221; });<br \/>\n    const url = URL.createObjectURL(blob);<br \/>\n    const a = document.createElement(&#8220;a&#8221;);<br \/>\n    a.href = url;<br \/>\n    a.download = &#8220;keukenontwerp.json&#8221;;<br \/>\n    a.click();<br \/>\n    URL.revokeObjectURL(url);<br \/>\n  };<\/p>\n<p>  const scale = 1.35;<br \/>\n  const roomSvgWidth = Math.max(420, roomWidth * scale);<br \/>\n  const roomSvgHeight = Math.max(260, roomDepth * scale);<\/p>\n<p>  return (<\/p>\n<div className=\"min-h-screen bg-neutral-50 p-4 text-neutral-900 md:p-8\">\n<div className=\"mx-auto max-w-7xl space-y-6\">\n<header className=\"flex flex-col justify-between gap-4 rounded-3xl bg-white p-6 shadow-sm md:flex-row md:items-center\">\n<div>\n<p className=\"mb-2 text-sm font-medium uppercase tracking-wide text-neutral-500\">Online configurator<\/p>\n<h1 className=\"text-3xl font-bold md:text-4xl\">Keukenontwerp systeem<\/h1>\n<p className=\"mt-2 max-w-2xl text-neutral-600\">Stel een keuken samen, controleer of de indeling past en exporteer het ontwerp als JSON.<\/p>\n<\/p><\/div>\n<div className=\"flex gap-2\">\n            <Button variant=\"outline\" onClick={reset} className=\"rounded-2xl\"><RotateCcw className=\"mr-2 h-4 w-4\" \/>Reset<\/Button><br \/>\n            <Button onClick={exportDesign} className=\"rounded-2xl\"><Download className=\"mr-2 h-4 w-4\" \/>Exporteer<\/Button>\n          <\/div>\n<\/header>\n<div className=\"grid gap-6 lg:grid-cols-[390px_1fr]\">\n<aside className=\"space-y-6\">\n            <Card className=\"rounded-3xl border-0 shadow-sm\"><br \/>\n              <CardContent className=\"space-y-4 p-5\"><\/p>\n<div className=\"flex items-center gap-2\">\n                  <Home className=\"h-5 w-5\" \/><\/p>\n<h2 className=\"text-xl font-semibold\">Ruimte<\/h2>\n<\/p><\/div>\n<p>                <label className=\"block text-sm font-medium\">Breedte ruimte: {roomWidth} cm<\/label><br \/>\n                <input type=\"range\" min=\"240\" max=\"600\" value={roomWidth} onChange={(e) => setRoomWidth(Number(e.target.value))} className=&#8221;w-full&#8221; \/><br \/>\n                <label className=\"block text-sm font-medium\">Diepte ruimte: {roomDepth} cm<\/label><br \/>\n                <input type=\"range\" min=\"180\" max=\"500\" value={roomDepth} onChange={(e) => setRoomDepth(Number(e.target.value))} className=&#8221;w-full&#8221; \/><br \/>\n              <\/CardContent><br \/>\n            <\/Card><\/p>\n<p>            <Card className=\"rounded-3xl border-0 shadow-sm\"><br \/>\n              <CardContent className=\"space-y-4 p-5\"><\/p>\n<h2 className=\"text-xl font-semibold\">Indeling<\/h2>\n<div className=\"grid gap-2\">\n                  {LAYOUTS.map((option) => (<br \/>\n                    <button\n                      key={option.id}\n                      onClick={() => setLayout(option.id)}<br \/>\n                      className={`rounded-2xl border p-4 text-left transition ${layout === option.id ? &#8220;border-neutral-900 bg-neutral-900 text-white&#8221; : &#8220;border-neutral-200 bg-white hover:bg-neutral-100&#8221;}`}<br \/>\n                    ><\/p>\n<div className=\"font-semibold\">{option.label}<\/div>\n<div className={`text-sm ${layout === option.id ? \"text-neutral-200\" : \"text-neutral-500\"}`}>{option.description}<\/div>\n<p>                    <\/button><br \/>\n                  ))}\n                <\/div>\n<p>              <\/CardContent><br \/>\n            <\/Card><\/p>\n<p>            <Card className=\"rounded-3xl border-0 shadow-sm\"><br \/>\n              <CardContent className=\"space-y-4 p-5\"><\/p>\n<h2 className=\"text-xl font-semibold\">Modules toevoegen<\/h2>\n<p>                <select value={selectedType} onChange={(e) => setSelectedType(e.target.value)} className=&#8221;w-full rounded-2xl border border-neutral-200 bg-white p-3&#8243;><br \/>\n                  {[&#8220;Alles&#8221;, &#8230;new Set(MODULES.map((m) => m.type))].map((type) =><option key={type}>{type}<\/option>)}<br \/>\n                <\/select><\/p>\n<div className=\"grid gap-2\">\n                  {filteredModules.map((module) => (<br \/>\n                    <button key={module.id} onClick={() => addModule(module)} className=&#8221;flex items-center justify-between rounded-2xl border border-neutral-200 bg-white p-3 text-left hover:bg-neutral-100&#8243;><br \/>\n                      <span><br \/>\n                        <span className=\"block font-medium\">{module.name}<\/span><br \/>\n                        <span className=\"text-sm text-neutral-500\">{module.width} \u00d7 {module.depth} cm \u00b7 {currency(module.price)}<\/span><br \/>\n                      <\/span><br \/>\n                      <Plus className=\"h-5 w-5\" \/><br \/>\n                    <\/button><br \/>\n                  ))}\n                <\/div>\n<p>              <\/CardContent><br \/>\n            <\/Card><br \/>\n          <\/aside>\n<p>          <main className=\"space-y-6\"><\/p>\n<section className=\"grid gap-4 md:grid-cols-4\">\n              <Metric icon={<Ruler className=\"h-5 w-5\" \/>} label=&#8221;Wandbreedte&#8221; value={`${totalWidth} cm`} badge={status} \/><br \/>\n              <Metric icon={<Ruler className=\"h-5 w-5\" \/>} label=&#8221;Eiland&#8221; value={`${islandWidth} cm`} \/><br \/>\n              <Metric icon={<Euro className=\"h-5 w-5\" \/>} label=&#8221;Prijsindicatie&#8221; value={currency(totalPrice)} \/><br \/>\n              <Metric icon={<Home className=\"h-5 w-5\" \/>} label=&#8221;Modules&#8221; value={items.length} \/><br \/>\n            <\/section>\n<p>            <Card className=\"overflow-hidden rounded-3xl border-0 shadow-sm\"><br \/>\n              <CardContent className=\"p-5\"><\/p>\n<div className=\"mb-4 flex flex-col justify-between gap-2 md:flex-row md:items-center\">\n<div>\n<h2 className=\"text-xl font-semibold\">Live plattegrond<\/h2>\n<p className=\"text-sm text-neutral-500\">Schaal is indicatief. Grijze vlakken zijn keukenmodules.<\/p>\n<\/p><\/div>\n<p>                  <span className={`w-fit rounded-full px-3 py-1 text-sm font-medium ${status.tone}`}>{status.label}<\/span>\n                <\/div>\n<div className=\"overflow-auto rounded-3xl border border-neutral-200 bg-white p-4\">\n                  <svg width={roomSvgWidth} height={roomSvgHeight} className=\"max-w-none\">\n                    <rect x=\"8\" y=\"8\" width={roomWidth * scale} height={roomDepth * scale} rx=\"18\" fill=\"white\" stroke=\"#d4d4d4\" strokeWidth=\"2\" \/>\n                    <text x=\"20\" y=\"32\" fontSize=\"14\" fill=\"#737373\">{roomWidth} \u00d7 {roomDepth} cm<\/text>\n                    {items.filter((i) => i.zone === &#8220;wand&#8221;).map((item, index) => {\n                      const x = 14 + items.filter((i) => i.zone === &#8220;wand&#8221;).slice(0, index).reduce((sum, i) => sum + i.width, 0) * scale;\n                      return <KitchenBlock key={item.uid} x={x} y={52} w={item.width * scale} h={item.depth * scale} label={item.name} \/>;\n                    })}\n                    {layout === &#8220;lshape&#8221; &#038;&#038; items.slice(0, Math.min(3, items.length)).map((item, index) => (\n                      <KitchenBlock key={`l-${item.uid}`} x={14} y={52 + (index + 1) * item.depth * scale} w={item.depth * scale} h={item.width * scale} label=\"L-deel\" \/>\n                    ))}\n                    {layout === &#8220;island&#8221; &#038;&#038; items.filter((i) => i.zone === &#8220;eiland&#8221;).map((item, index) => {\n                      const x = roomWidth * scale \/ 2 &#8211; islandWidth * scale \/ 2 + items.filter((i) => i.zone === &#8220;eiland&#8221;).slice(0, index).reduce((sum, i) => sum + i.width, 0) * scale;\n                      const y = Math.min(roomDepth * scale &#8211; 95, 190);\n                      return <KitchenBlock key={`island-${item.uid}`} x={x} y={y} w={item.width * scale} h={item.depth * scale} label={item.name} \/>;\n                    })}\n                  <\/svg>\n                <\/div>\n<p>              <\/CardContent><br \/>\n            <\/Card><\/p>\n<div className=\"grid gap-6 lg:grid-cols-2\">\n              <Card className=\"rounded-3xl border-0 shadow-sm\"><br \/>\n                <CardContent className=\"p-5\"><\/p>\n<h2 className=\"mb-4 text-xl font-semibold\">Geselecteerde modules<\/h2>\n<div className=\"space-y-2\">\n                    {items.map((item) => (<\/p>\n<div key={item.uid} className=\"flex items-center justify-between rounded-2xl bg-neutral-100 p-3\">\n<div>\n<div className=\"font-medium\">{item.name}<\/div>\n<div className=\"text-sm text-neutral-500\">{item.zone} \u00b7 {item.width} cm \u00b7 {currency(item.price)}<\/div>\n<\/p><\/div>\n<p>                        <Button variant=\"ghost\" size=\"icon\" onClick={() => removeModule(item.uid)} className=&#8221;rounded-xl&#8221;><Trash2 className=\"h-4 w-4\" \/><\/Button>\n                      <\/div>\n<p>                    ))}\n                  <\/p><\/div>\n<p>                <\/CardContent><br \/>\n              <\/Card><\/p>\n<p>              <Card className=\"rounded-3xl border-0 shadow-sm\"><br \/>\n                <CardContent className=\"p-5\"><\/p>\n<h2 className=\"mb-4 text-xl font-semibold\">Prijsopbouw<\/h2>\n<p>                  <PriceRow label=\"Kasten en apparatuur\" value={cabinetsPrice} \/><br \/>\n                  <PriceRow label=\"Werkblad indicatie\" value={worktopPrice} \/><br \/>\n                  <PriceRow label=\"Montage indicatie\" value={installPrice} \/><\/p>\n<div className=\"mt-4 border-t pt-4\">\n                    <PriceRow label=\"Totaal\" value={totalPrice} strong \/>\n                  <\/div>\n<p className=\"mt-4 rounded-2xl bg-neutral-100 p-3 text-sm text-neutral-600\">Deze prijs is een indicatie. Koppel dit systeem aan je eigen productdatabase voor actuele voorraad, materialen en marges.<\/p>\n<p>                <\/CardContent><br \/>\n              <\/Card>\n            <\/div>\n<p>          <\/main>\n        <\/div>\n<\/p><\/div>\n<\/p><\/div>\n<p>  );<br \/>\n}<\/p>\n<p>function Metric({ icon, label, value, badge }) {<br \/>\n  return (<br \/>\n    <Card className=\"rounded-3xl border-0 shadow-sm\"><br \/>\n      <CardContent className=\"p-5\"><\/p>\n<div className=\"mb-3 flex items-center justify-between\">\n<div className=\"rounded-2xl bg-neutral-100 p-2\">{icon}<\/div>\n<p>          {badge &#038;&#038; <span className={`rounded-full px-2 py-1 text-xs font-medium ${badge.tone}`}>{badge.label}<\/span>}\n        <\/div>\n<div className=\"text-sm text-neutral-500\">{label}<\/div>\n<div className=\"mt-1 text-2xl font-bold\">{value}<\/div>\n<p>      <\/CardContent><br \/>\n    <\/Card><br \/>\n  );<br \/>\n}<\/p>\n<p>function KitchenBlock({ x, y, w, h, label }) {<br \/>\n  return (<br \/>\n    <motion.g initial={{ opacity: 0, scale: 0.96 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.2 }}><br \/>\n      <rect x={x} y={y} width={w} height={h} rx=\"10\" fill=\"#e5e5e5\" stroke=\"#525252\" strokeWidth=\"1.5\" \/>\n      <line x1={x} y1={y + h \/ 2} x2={x + w} y2={y + h \/ 2} stroke=\"#a3a3a3\" \/>\n      <text x={x + 8} y={y + 22} fontSize=\"12\" fill=\"#262626\">{label}<\/text><br \/>\n    <\/motion.g><br \/>\n  );<br \/>\n}<\/p>\n<p>function PriceRow({ label, value, strong = false }) {<br \/>\n  return (<\/p>\n<div className={`flex items-center justify-between py-2 ${strong ? \"text-lg font-bold\" : \"text-neutral-700\"}`}>\n      <span>{label}<\/span><br \/>\n      <span>{currency(value)}<\/span>\n    <\/div>\n<p>  );<br \/>\n}<\/p>\n","protected":false},"excerpt":{"rendered":"<p>import React, { useMemo, useState } from &#8220;react&#8221;; import { motion } from &#8220;framer-motion&#8221;; import { Plus, Trash2, RotateCcw, Ruler, Euro, Download, Home } from &#8220;lucide-react&#8221;; import { Button } from &#8220;@\/components\/ui\/button&#8221;; import { Card, CardContent } from &#8220;@\/components\/ui\/card&#8221;; const MODULES = [ { id: &#8220;base60&#8221;, name: &#8220;Onderkast 60&#8221;, width: 60, depth: 60, price: 240, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=\/wp\/v2\/posts\/1","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1"}],"version-history":[{"count":3,"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=\/wp\/v2\/posts\/1\/revisions"}],"predecessor-version":[{"id":7,"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=\/wp\/v2\/posts\/1\/revisions\/7"}],"wp:attachment":[{"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/livingtotaal.nl\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}