From 9fc1344177db4b457f4b7c3a4326ce4e01ae8bdc Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 10 Feb 2026 16:14:28 +0000 Subject: [PATCH] Complete configurator overhaul with premium design Major Changes: - Replaced static preview with dynamic SVG door visualizer - Integrated Zustand for real-time state management - Redesigned UI with Dark Green (#1A2E2E) and Pistachio (#C4D668) colors - Removed all orange branding (brand-orange) - Premium selection tiles with active/inactive states - Live door rendering that updates instantly on selection Features: - Dynamic SVG draws door based on configuration - Real-time updates: doorType, gridType, finish, handle - Background room image with semi-transparent overlay - Animated "Live Voorbeeld" badge with pulse effect - Configuration info card showing current selections - Premium typography with Inter font Technical: - Added Zustand state management (lib/store.ts) - Created DoorVisualizer component with SVG logic - Updated step-product and step-options with premium tiles - Color swatches for finish selection - Check icons for selected states Co-Authored-By: Claude Sonnet 4.5 --- app/offerte/page.tsx | 49 +++-- components/configurator/door-visualizer.tsx | 201 ++++++++++++++++++++ components/offerte/step-options.tsx | 182 ++++++++++++------ components/offerte/step-product.tsx | 200 +++++++++++++------ lib/store.ts | 36 ++++ package-lock.json | 32 +++- package.json | 3 +- 7 files changed, 559 insertions(+), 144 deletions(-) create mode 100644 components/configurator/door-visualizer.tsx create mode 100644 lib/store.ts diff --git a/app/offerte/page.tsx b/app/offerte/page.tsx index c017bb0..152024c 100644 --- a/app/offerte/page.tsx +++ b/app/offerte/page.tsx @@ -30,15 +30,15 @@ function StepIndicator() {
{i + 1}
{label} @@ -47,7 +47,7 @@ function StepIndicator() { {i < totalSteps - 1 && (
)} @@ -72,13 +72,17 @@ function WizardContent() { {/* Navigation — hidden on step 1 (auto-advances) and summary (has its own button) */} {!isFirstStep && !isLastStep && (
- + ); + })}
+
- {/* Finish */} -
- - updateData({ finish: val as typeof formData.finish })} - className="space-y-2" - > - {finishTypes.map((type) => ( -
diff --git a/components/offerte/step-product.tsx b/components/offerte/step-product.tsx index a78b55e..72929df 100644 --- a/components/offerte/step-product.tsx +++ b/components/offerte/step-product.tsx @@ -1,79 +1,157 @@ "use client"; -import Image from "next/image"; +import { useConfiguratorStore, type DoorType } from "@/lib/store"; import { useFormContext } from "@/components/offerte/form-context"; -import { productTypes } from "@/lib/validators"; -import { cn } from "@/lib/utils"; +import { Check } from "lucide-react"; -const productImages: Record = { - Taatsdeur: "/images/taats.jpg", - Scharnierdeur: "/images/scharnier.jpg", - "Vast Paneel": "/images/paneel.jpg", -}; +const doorTypes: Array<{ + value: DoorType; + label: string; + description: string; +}> = [ + { + value: "taats", + label: "Taatsdeur", + description: "Pivoterende deur met verticaal draaimechanisme", + }, + { + value: "scharnier", + label: "Scharnierdeur", + description: "Klassieke deur met zijscharnieren", + }, + { + value: "paneel", + label: "Vast Paneel", + description: "Vast glaspaneel zonder bewegend mechanisme", + }, +]; -const productDescriptions: Record = { - Taatsdeur: "Pivoterende deur", - Scharnierdeur: "Klassiek scharnier", - "Vast Paneel": "Vast glaspaneel", -}; +const gridTypes: Array<{ + value: "3-vlak" | "4-vlak" | "geen"; + label: string; + description: string; +}> = [ + { value: "geen", label: "Geen verdeling", description: "Volledig vlak" }, + { value: "3-vlak", label: "3-vlaks", description: "2 horizontale balken" }, + { value: "4-vlak", label: "4-vlaks", description: "3 horizontale balken" }, +]; export function StepProduct() { - const { formData, updateData, nextStep } = useFormContext(); + const { nextStep } = useFormContext(); + const { doorType, gridType, setDoorType, setGridType } = + useConfiguratorStore(); - function select(type: (typeof productTypes)[number]) { - updateData({ productType: type }); + function handleDoorTypeSelect(type: DoorType) { + setDoorType(type); + } + + function handleGridTypeSelect(type: "3-vlak" | "4-vlak" | "geen") { + setGridType(type); + } + + function handleContinue() { nextStep(); } return ( -
-

Kies uw product

-

- Selecteer het type stalen element dat u wilt configureren. -

+
+ {/* Door Type Selection */} +
+

Kies uw deurtype

+

+ Selecteer het type stalen deur dat u wilt configureren. +

-
- {productTypes.map((type) => { - const selected = formData.productType === type; +
+ {doorTypes.map((type) => { + const selected = doorType === type.value; - return ( - - ); - })} + return ( + + ); + })} +
+ + {/* Grid Type Selection */} +
+

Verdeling

+

+ Kies het aantal horizontale vlakken. +

+ +
+ {gridTypes.map((type) => { + const selected = gridType === type.value; + + return ( + + ); + })} +
+
+ + {/* Continue Button */} +
); } diff --git a/lib/store.ts b/lib/store.ts new file mode 100644 index 0000000..eafd9ab --- /dev/null +++ b/lib/store.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand'; + +export type DoorType = 'taats' | 'scharnier' | 'paneel'; +export type GridType = '3-vlak' | '4-vlak' | 'geen'; +export type Finish = 'zwart' | 'brons' | 'grijs'; +export type Handle = 'u-greep' | 'klink' | 'geen'; + +interface ConfiguratorState { + doorType: DoorType; + gridType: GridType; + finish: Finish; + handle: Handle; + width: number; + height: number; + + setDoorType: (type: DoorType) => void; + setGridType: (type: GridType) => void; + setFinish: (finish: Finish) => void; + setHandle: (handle: Handle) => void; + setDimensions: (width: number, height: number) => void; +} + +export const useConfiguratorStore = create((set) => ({ + doorType: 'taats', + gridType: '3-vlak', + finish: 'zwart', + handle: 'u-greep', + width: 1000, + height: 2400, + + setDoorType: (doorType) => set({ doorType }), + setGridType: (gridType) => set({ gridType }), + setFinish: (finish) => set({ finish }), + setHandle: (handle) => set({ handle }), + setDimensions: (width, height) => set({ width, height }), +})); diff --git a/package-lock.json b/package-lock.json index 56dbb9a..2a4ece6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "react-dom": "19.2.3", "react-hook-form": "^7.71.1", "tailwind-merge": "^3.4.0", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -12243,6 +12244,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 85f93bd..09a191d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "react-dom": "19.2.3", "react-hook-form": "^7.71.1", "tailwind-merge": "^3.4.0", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/postcss": "^4",