From 3d788740cbe79db9fbd26339552fdd652676cede Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 1 Mar 2026 14:50:31 +0000 Subject: [PATCH] feat: Latest production version with interior scene and glass Includes room interior with floor, walls, glass you can see through, and all uncommitted production changes that were running live. Co-Authored-By: Claude Opus 4.6 --- .claude/agents/3d-visual-lead.md | 14 +- .claude/agents/frontend-stylist.md | 10 +- actions/send-quote.ts | 234 + app/contact/page.tsx | 218 + app/offerte/page.tsx | 28 +- app/over-ons/page.tsx | 212 + app/privacy/page.tsx | 135 + app/producten/page.tsx | 246 + app/voorwaarden/page.tsx | 217 + bron/App.js | 16 + bron/Appp.js | 16 + bron/BubblesManager.js | 161 + bron/BubblesManagerr.js | 161 + bron/ColorSelector.js | 84 + bron/DoorHole.js | 1210 + bron/Handle%203.glb | Bin 0 -> 4456 bytes bron/Handle%204.glb | Bin 0 -> 5748 bytes bron/InfoIcon.js | 51 + bron/RequestConfirmation.js | 145 + bron/RequestConfirmationn.js | 145 + bron/RequestForm.js | 416 + bron/Stalen.js | 146 + bron/Structure.js | 2742 + bron/TechInformation.js | 477 + bron/Tooltip.js | 19 + bron/bootstrap | 27 + bron/compat get default export | 8 + bron/config.cloozdoors.nl-1761175825660.log | 601 + bron/contextapi.js | 3 + bron/create fake namespace object | 26 + bron/define property getters | 8 + bron/design.js | 4831 ++ bron/door.js | 735 + bron/door_type_1_staging.jpg | Bin 0 -> 23239 bytes bron/door_type_2_staging.jpg | Bin 0 -> 22119 bytes bron/door_type_3_staging.jpg | Bin 0 -> 36702 bytes bron/door_type_4.jpg | Bin 0 -> 21929 bytes bron/double-door-type_staging.jpg | Bin 0 -> 220419 bytes bron/draco_decoder.wasm | Bin 0 -> 283091 bytes bron/draco_decover.js.rtf | 47 + bron/endpoint.js | 653 + bron/extends.js | 10 + bron/extra.js | 148 + bron/global | 8 + bron/handle.js | 50 + bron/hasOwnProperty shorthand | 1 + bron/index.css | 19 + bron/index.js | 16 + bron/indexx.js | 16 + bron/information.js | 198 + bron/main.4524c4d6.css | 6 + bron/main.45858049.js | 72323 ++++++++++++++++ bron/main.458580499.js | 72323 ++++++++++++++++ bron/make namespace object | 7 + bron/node module decorator | 5 + bron/preloader.js | 9 + bron/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2 | Bin 0 -> 7848 bytes bron/samenstelling_link.png | Bin 0 -> 25318 bytes bron/samenstling.js | 910 + bron/style.css | 246 + bron/style0.css | 132 + bron/tailwind.css | 3 + components/configurator/door-3d-enhanced.tsx | 180 +- components/configurator/door-3d.tsx | 7 +- components/configurator/door-visualizer.tsx | 57 +- components/configurator/handles-3d.tsx | 47 +- components/configurator/scene.tsx | 945 +- components/configurator/texture-loader.tsx | 6 +- components/home/hero.tsx | 71 +- components/layout/footer.tsx | 68 +- components/layout/mobile-menu.tsx | 12 +- components/layout/navbar.tsx | 2 +- components/layout/top-bar.tsx | 4 +- components/offerte/form-context.tsx | 15 +- components/offerte/step-contact.tsx | 21 +- components/offerte/step-extras.tsx | 89 + components/offerte/step-options.tsx | 324 +- components/offerte/step-product.tsx | 211 +- components/offerte/step-summary.tsx | 331 +- lib/asset-map.ts | 63 +- lib/door-models.ts | 241 +- lib/pricing.ts | 83 +- lib/store.ts | 88 +- lib/validators.ts | 34 +- package-lock.json | 73 + package.json | 1 + .../aluwdoors-ref/configurator.css?v=mlaxicsg | 1 - .../configurator.iife.js?v=mlaxicsg | 12059 --- public/aluwdoors-ref/opentype.js | 118 - .../proinn-draairichting-DIN-links.svg} | 0 .../proinn-draairichting-DIN-rechts.svg} | 0 .../proinn-fineer-handgreep-beugelgreep.svg} | 0 .../proinn-fineer-handgreep-geen.svg} | 0 .../proinn-fineer-handgreep-hoekgreep.svg} | 0 .../proinn-fineer-handgreep-maangreep.svg} | 0 .../proinn-fineer-handgreep-ovaalgreep.svg} | 0 .../proinn-glaskleur-blank.jpg} | Bin .../proinn-glaskleur-brons.jpg} | Bin .../proinn-glaskleur-grijs.jpg} | Bin .../proinn-glaskleur-mat-blank.jpg} | Bin .../proinn-glaskleur-mat-brons.jpg} | Bin .../proinn-glaskleur-mat-zwart.jpg} | Bin .../proinn-metaalkleur-antraciet.jpg} | Bin .../proinn-metaalkleur-beige.jpg} | Bin .../proinn-metaalkleur-brons.jpg} | Bin .../proinn-metaalkleur-goud.jpg} | Bin .../proinn-metaalkleur-ral-keuze.jpg} | Bin .../proinn-metaalkleur-zwart.jpg} | Bin .../proinn-roedetype-platte-roede.svg} | 0 .../proinn-roedetype-t-roede.svg} | 0 110 files changed, 162553 insertions(+), 13070 deletions(-) create mode 100644 actions/send-quote.ts create mode 100644 app/contact/page.tsx create mode 100644 app/over-ons/page.tsx create mode 100644 app/privacy/page.tsx create mode 100644 app/producten/page.tsx create mode 100644 app/voorwaarden/page.tsx create mode 100644 bron/App.js create mode 100644 bron/Appp.js create mode 100644 bron/BubblesManager.js create mode 100644 bron/BubblesManagerr.js create mode 100644 bron/ColorSelector.js create mode 100644 bron/DoorHole.js create mode 100644 bron/Handle%203.glb create mode 100644 bron/Handle%204.glb create mode 100644 bron/InfoIcon.js create mode 100644 bron/RequestConfirmation.js create mode 100644 bron/RequestConfirmationn.js create mode 100644 bron/RequestForm.js create mode 100644 bron/Stalen.js create mode 100644 bron/Structure.js create mode 100644 bron/TechInformation.js create mode 100644 bron/Tooltip.js create mode 100644 bron/bootstrap create mode 100644 bron/compat get default export create mode 100644 bron/config.cloozdoors.nl-1761175825660.log create mode 100644 bron/contextapi.js create mode 100644 bron/create fake namespace object create mode 100644 bron/define property getters create mode 100644 bron/design.js create mode 100644 bron/door.js create mode 100644 bron/door_type_1_staging.jpg create mode 100644 bron/door_type_2_staging.jpg create mode 100644 bron/door_type_3_staging.jpg create mode 100644 bron/door_type_4.jpg create mode 100644 bron/double-door-type_staging.jpg create mode 100644 bron/draco_decoder.wasm create mode 100644 bron/draco_decover.js.rtf create mode 100644 bron/endpoint.js create mode 100644 bron/extends.js create mode 100644 bron/extra.js create mode 100644 bron/global create mode 100644 bron/handle.js create mode 100644 bron/hasOwnProperty shorthand create mode 100644 bron/index.css create mode 100644 bron/index.js create mode 100644 bron/indexx.js create mode 100644 bron/information.js create mode 100644 bron/main.4524c4d6.css create mode 100644 bron/main.45858049.js create mode 100644 bron/main.458580499.js create mode 100644 bron/make namespace object create mode 100644 bron/node module decorator create mode 100644 bron/preloader.js create mode 100644 bron/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2 create mode 100644 bron/samenstelling_link.png create mode 100644 bron/samenstling.js create mode 100644 bron/style.css create mode 100644 bron/style0.css create mode 100644 bron/tailwind.css create mode 100644 components/offerte/step-extras.tsx delete mode 100644 public/aluwdoors-ref/configurator.css?v=mlaxicsg delete mode 100644 public/aluwdoors-ref/configurator.iife.js?v=mlaxicsg delete mode 100644 public/aluwdoors-ref/opentype.js rename public/textures/{aluwdoors/aluwdoors-configurator-draairichting-DIN-links.svg => proinn/proinn-draairichting-DIN-links.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-draairichting-DIN-rechts.svg => proinn/proinn-draairichting-DIN-rechts.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-fineer-handgreep-beugelgreep.svg => proinn/proinn-fineer-handgreep-beugelgreep.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-fineer-handgreep-geen.svg => proinn/proinn-fineer-handgreep-geen.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-fineer-handgreep-hoekgreep.svg => proinn/proinn-fineer-handgreep-hoekgreep.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-fineer-handgreep-maangreep.svg => proinn/proinn-fineer-handgreep-maangreep.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-fineer-handgreep-ovaalgreep.svg => proinn/proinn-fineer-handgreep-ovaalgreep.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-glaskleur-blank.jpg => proinn/proinn-glaskleur-blank.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-glaskleur-brons.jpg => proinn/proinn-glaskleur-brons.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-glaskleur-grijs.jpg => proinn/proinn-glaskleur-grijs.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-glaskleur-mat-blank.jpg => proinn/proinn-glaskleur-mat-blank.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-glaskleur-mat-brons.jpg => proinn/proinn-glaskleur-mat-brons.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-glaskleur-mat-zwart.jpg => proinn/proinn-glaskleur-mat-zwart.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-metaalkleur-antraciet.jpg => proinn/proinn-metaalkleur-antraciet.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-metaalkleur-beige.jpg => proinn/proinn-metaalkleur-beige.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-metaalkleur-brons.jpg => proinn/proinn-metaalkleur-brons.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-metaalkleur-goud.jpg => proinn/proinn-metaalkleur-goud.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-metaalkleur-ral-keuze.jpg => proinn/proinn-metaalkleur-ral-keuze.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg => proinn/proinn-metaalkleur-zwart.jpg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-roedetype-platte-roede.svg => proinn/proinn-roedetype-platte-roede.svg} (100%) rename public/textures/{aluwdoors/aluwdoors-configurator-roedetype-t-roede.svg => proinn/proinn-roedetype-t-roede.svg} (100%) diff --git a/.claude/agents/3d-visual-lead.md b/.claude/agents/3d-visual-lead.md index 2442b35..c7cb778 100644 --- a/.claude/agents/3d-visual-lead.md +++ b/.claude/agents/3d-visual-lead.md @@ -26,7 +26,7 @@ You are a senior 3D technical artist who has shipped production product configur - **3D Stack**: React Three Fiber (`@react-three/fiber`), Drei (`@react-three/drei`), Three.js - **Styling**: Tailwind CSS v4 - **Design Language**: Industrial, clean, heavy — Dark Grey/Black primary, Orange & Blue accents -- **Texture Assets**: Located in `public/textures/aluwdoors/` +- **Texture Assets**: Located in `public/textures/proinn/` ## Hard Constraints — DO NOT VIOLATE @@ -46,7 +46,7 @@ You are a senior 3D technical artist who has shipped production product configur - Build door geometry from composed primitives (frame, panels, glass inserts, handles) — not a single box. 4. **Texture-First Approach**: - - ALWAYS check `public/textures/aluwdoors/` for available texture assets BEFORE falling back to procedural or flat colors. + - ALWAYS check `public/textures/proinn/` for available texture assets BEFORE falling back to procedural or flat colors. - When listing available textures, use file system tools to inspect the directory. - Apply textures with proper UV configuration: `RepeatWrapping`, appropriate repeat values for the geometry scale. @@ -60,7 +60,7 @@ When tasked with visual improvements, follow this workflow: - Identify gaps: flat colors where textures should be, missing shadows, poor lighting, sharp edges. ### Step 2: Inventory Available Assets -- Scan `public/textures/aluwdoors/` and any other texture directories. +- Scan `public/textures/proinn/` and any other texture directories. - Catalog available maps: diffuse, normal, roughness, metalness, AO. - Note texture resolutions and naming conventions. @@ -69,9 +69,9 @@ When tasked with visual improvements, follow this workflow: - Use `useTexture` from Drei for loading, with proper configuration: ```tsx const [diffuse, normal, roughness] = useTexture([ - '/textures/aluwdoors/diffuse.jpg', - '/textures/aluwdoors/normal.jpg', - '/textures/aluwdoors/roughness.jpg', + '/textures/proinn/diffuse.jpg', + '/textures/proinn/normal.jpg', + '/textures/proinn/roughness.jpg', ]) // Configure wrapping and repeat ;[diffuse, normal, roughness].forEach(t => { @@ -113,7 +113,7 @@ When tasked with visual improvements, follow this workflow: Before considering any visual task complete, verify: - [ ] No sharp BoxGeometry edges visible — all using RoundedBox -- [ ] Textures from `public/textures/aluwdoors/` are applied where available +- [ ] Textures from `public/textures/proinn/` are applied where available - [ ] Materials have physically plausible PBR values (metalness, roughness, etc.) - [ ] Lighting creates depth with visible highlights, mid-tones, and shadows - [ ] Contact shadows ground the object in space diff --git a/.claude/agents/frontend-stylist.md b/.claude/agents/frontend-stylist.md index f33ad3f..0d0a3d7 100644 --- a/.claude/agents/frontend-stylist.md +++ b/.claude/agents/frontend-stylist.md @@ -1,6 +1,6 @@ --- name: frontend-stylist -description: "Use this agent when you need to implement or refine visual styling, design system tokens, Tailwind CSS configurations, UI polish, responsive layouts, or translate reference CSS into Tailwind utility classes for the Proinn Configurator project. This agent is specifically for the 'Anti-Gravity' design system: floating cards, soft shadows, premium typography, and mobile-first responsive design.\\n\\nExamples:\\n\\n\\nContext: The user wants to apply the competitor's color palette from the scraped CSS file to the Tailwind config.\\nuser: \"Apply the colors from the aluwdoors reference CSS to our Tailwind config\"\\nassistant: \"I'll use the frontend-stylist agent to read the reference CSS and translate those color values into our Tailwind configuration.\"\\n\\nSince this is a styling task involving translating reference CSS into Tailwind config, use the Task tool to launch the frontend-stylist agent.\\n\\n\\n\\n\\nContext: The user has just built a new configurator step component and it needs styling.\\nuser: \"I just created step-dimensions.tsx, can you style it to match our design system?\"\\nassistant: \"I'll launch the frontend-stylist agent to apply the Anti-Gravity design system styles to the new step component.\"\\n\\nSince a new UI component needs styling with floating cards, shadows, and responsive design, use the Task tool to launch the frontend-stylist agent.\\n\\n\\n\\n\\nContext: The user notices the configurator looks broken on mobile.\\nuser: \"The configurator buttons are overlapping on iPhone, fix the mobile layout\"\\nassistant: \"I'll use the frontend-stylist agent to fix the mobile-first responsive layout for the configurator buttons.\"\\n\\nSince this is a mobile responsive styling issue in the configurator UI, use the Task tool to launch the frontend-stylist agent.\\n\\n\\n\\n\\nContext: The user wants to add smooth transitions and hover effects to the step cards.\\nuser: \"Make the option cards feel more premium with hover animations\"\\nassistant: \"I'll launch the frontend-stylist agent to implement smooth transitions and premium hover effects on the option cards.\"\\n\\nSince this involves UI polish, transitions, and visual refinement, use the Task tool to launch the frontend-stylist agent.\\n\\n" +description: "Use this agent when you need to implement or refine visual styling, design system tokens, Tailwind CSS configurations, UI polish, responsive layouts, or translate reference CSS into Tailwind utility classes for the Proinn Configurator project. This agent is specifically for the 'Anti-Gravity' design system: floating cards, soft shadows, premium typography, and mobile-first responsive design.\\n\\nExamples:\\n\\n\\nContext: The user wants to apply the competitor's color palette from the scraped CSS file to the Tailwind config.\\nuser: \"Apply the colors from the proinn reference CSS to our Tailwind config\"\\nassistant: \"I'll use the frontend-stylist agent to read the reference CSS and translate those color values into our Tailwind configuration.\"\\n\\nSince this is a styling task involving translating reference CSS into Tailwind config, use the Task tool to launch the frontend-stylist agent.\\n\\n\\n\\n\\nContext: The user has just built a new configurator step component and it needs styling.\\nuser: \"I just created step-dimensions.tsx, can you style it to match our design system?\"\\nassistant: \"I'll launch the frontend-stylist agent to apply the Anti-Gravity design system styles to the new step component.\"\\n\\nSince a new UI component needs styling with floating cards, shadows, and responsive design, use the Task tool to launch the frontend-stylist agent.\\n\\n\\n\\n\\nContext: The user notices the configurator looks broken on mobile.\\nuser: \"The configurator buttons are overlapping on iPhone, fix the mobile layout\"\\nassistant: \"I'll use the frontend-stylist agent to fix the mobile-first responsive layout for the configurator buttons.\"\\n\\nSince this is a mobile responsive styling issue in the configurator UI, use the Task tool to launch the frontend-stylist agent.\\n\\n\\n\\n\\nContext: The user wants to add smooth transitions and hover effects to the step cards.\\nuser: \"Make the option cards feel more premium with hover animations\"\\nassistant: \"I'll launch the frontend-stylist agent to implement smooth transitions and premium hover effects on the option cards.\"\\n\\nSince this involves UI polish, transitions, and visual refinement, use the Task tool to launch the frontend-stylist agent.\\n\\n" model: sonnet color: cyan memory: project @@ -15,7 +15,7 @@ You are a world-class frontend stylist who thinks in Tailwind utility classes. Y 1. **Tailwind CSS**: Utility classes, arbitrary values (e.g., `h-[calc(100vh-80px)]`), `@apply` directives, responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`), dark mode, animation utilities, and custom theme configuration. 2. **UI/UX Craft**: You ensure every interactive element feels tactile and intentional — buttons have satisfying hover states, transitions are buttery smooth (200-300ms ease-out), and spacing creates visual breathing room. 3. **Mobile-First Design**: You ALWAYS write mobile styles first, then layer on tablet and desktop enhancements. Every component must be fully functional and beautiful on a 375px viewport before you consider larger screens. -4. **CSS Translation**: You excel at reading raw CSS files (especially `public/aluwdoors-ref/configurator.css`) and translating exact values — colors, border-radii, shadows, fonts, spacing — into precise Tailwind config entries or utility classes. +4. **CSS Translation**: You excel at reading raw CSS files (especially `public/proinn-ref/configurator.css`) and translating exact values — colors, border-radii, shadows, fonts, spacing — into precise Tailwind config entries or utility classes. ## Tech Stack Context @@ -71,7 +71,7 @@ Your design system principles: ## Workflow -1. **Read First**: Before making changes, read the target file AND `public/aluwdoors-ref/configurator.css` (if relevant) to understand current state and reference values. +1. **Read First**: Before making changes, read the target file AND `public/proinn-ref/configurator.css` (if relevant) to understand current state and reference values. 2. **Plan**: Briefly describe what styles you'll apply and why. 3. **Implement Mobile-First**: Write the mobile layout first. Test mentally at 375px. 4. **Layer Up**: Add `sm:`, `md:`, `lg:` responsive variants. @@ -84,7 +84,7 @@ Your design system principles: ## Reference CSS Translation Protocol -When translating from `public/aluwdoors-ref/configurator.css`: +When translating from `public/proinn-ref/configurator.css`: 1. Read the CSS file carefully, extracting: - Color values → Add to `tailwind.config.ts` under `theme.extend.colors` @@ -102,7 +102,7 @@ When translating from `public/aluwdoors-ref/configurator.css`: ## Current Mission -Your immediate task is to translate the scraped competitor styles from `public/aluwdoors-ref/configurator.css` — specifically colors, border-radius values, and shadow definitions — into our Tailwind config (`tailwind.config.ts`) and then apply them systematically to the configurator interface components. Ensure the result feels premium, industrial, and distinctly "Proinn" while borrowing the best UX patterns from the reference. +Your immediate task is to translate the scraped competitor styles from `public/proinn-ref/configurator.css` — specifically colors, border-radius values, and shadow definitions — into our Tailwind config (`tailwind.config.ts`) and then apply them systematically to the configurator interface components. Ensure the result feels premium, industrial, and distinctly "Proinn" while borrowing the best UX patterns from the reference. **Update your agent memory** as you discover design tokens, component styling patterns, responsive breakpoint decisions, and any CSS quirks or workarounds specific to this project. This builds up institutional knowledge across conversations. Write concise notes about what you found and where. diff --git a/actions/send-quote.ts b/actions/send-quote.ts new file mode 100644 index 0000000..b07bc91 --- /dev/null +++ b/actions/send-quote.ts @@ -0,0 +1,234 @@ +"use server"; + +import { Resend } from "resend"; + +const resend = new Resend(process.env.RESEND_API_KEY); + +interface QuoteRequest { + // Product + doorType: string; + gridType: string; + doorConfig: string; + sidePanel: string; + + // Dimensions + width: number; + height: number; + doorLeafWidth: number; + + // Options + finish: string; + glassColor: string; + handle: string; + frameSize: number; + glassPattern: string; + + // Extras + extraOptions: string[]; + + // Contact + name: string; + email: string; + phone: string; + note: string; + + // Pricing + totalPrice: number; + steelCost: number; + glassCost: number; + baseFee: number; + mechanismSurcharge: number; + sidePanelSurcharge: number; + handleCost: number; + finishSurcharge: number; + + // Screenshot + screenshotDataUrl: string | null; +} + +const LABEL_MAP: Record = { + taats: "Taatsdeur", + scharnier: "Scharnierdeur", + paneel: "Vast Paneel", + enkele: "Enkele deur", + dubbele: "Dubbele deur", + geen: "Geen", + links: "Links", + rechts: "Rechts", + beide: "Beide zijden", + zwart: "Mat Zwart", + brons: "Brons", + grijs: "Antraciet", + goud: "Goud", + beige: "Beige", + ral: "RAL Kleur", + helder: "Helder glas", + "mat-blank": "Mat Blank", + "mat-brons": "Mat Brons", + "mat-zwart": "Mat Zwart", + beugelgreep: "Beugelgreep", + hoekgreep: "Hoekgreep", + maangreep: "Maangreep", + ovaalgreep: "Ovaalgreep", + klink: "Deurklink", + "u-greep": "U-Greep", + standard: "Standaard", + "dt9-rounded": "DT9 Afgerond", + "dt10-ushape": "DT10 U-vorm", +}; + +function label(key: string): string { + return LABEL_MAP[key] || key; +} + +function formatPrice(cents: number): string { + return new Intl.NumberFormat("nl-NL", { + style: "currency", + currency: "EUR", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(cents); +} + +function buildHtmlEmail(data: QuoteRequest): string { + const rows = [ + ["Deurtype", label(data.doorType)], + ["Verdeling", data.gridType], + ["Configuratie", label(data.doorConfig)], + ["Zijpanelen", label(data.sidePanel)], + ["", ""], + ["Breedte (wandopening)", `${data.width} mm`], + ["Hoogte", `${data.height} mm`], + ["Deurblad breedte", `${Math.round(data.doorLeafWidth)} mm`], + ["", ""], + ["Afwerking", label(data.finish)], + ["Glaskleur", label(data.glassColor)], + ["Greep", label(data.handle)], + ["Profielbreedte", `${data.frameSize} mm`], + ["Glaspatroon", label(data.glassPattern)], + ]; + + if (data.extraOptions.length > 0) { + rows.push(["", ""], ["Extra opties", data.extraOptions.join(", ")]); + } + + const tableRows = rows + .filter(([k]) => k !== "") + .map( + ([k, v]) => + `${k}${v}` + ) + .join(""); + + const priceRows = [ + ["Staal", formatPrice(data.steelCost)], + ["Glas", formatPrice(data.glassCost)], + ["Basiskost", formatPrice(data.baseFee)], + ...(data.mechanismSurcharge > 0 + ? [["Mechanisme toeslag", formatPrice(data.mechanismSurcharge)]] + : []), + ...(data.sidePanelSurcharge > 0 + ? [["Zijpaneel toeslag", formatPrice(data.sidePanelSurcharge)]] + : []), + ...(data.handleCost > 0 ? [["Greep", formatPrice(data.handleCost)]] : []), + ...(data.finishSurcharge > 0 + ? [["Kleur toeslag", formatPrice(data.finishSurcharge)]] + : []), + ]; + + const priceTableRows = priceRows + .map( + ([k, v]) => + `${k}${v}` + ) + .join(""); + + return ` +
+
+

Nieuwe Offerte Aanvraag

+

Via Proinn Configurator

+
+ +
+

Contactgegevens

+

${data.name}

+

${data.email} | ${data.phone}

+ ${data.note ? `

${data.note}

` : ""} +
+ +
+

Configuratie

+ + ${tableRows} +
+
+ +
+

Indicatieprijs

+ + ${priceTableRows} +
+
+ Indicatieprijs totaal
+ ${formatPrice(data.totalPrice)} +
+

* Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.

+
+
+ `; +} + +export async function sendQuoteAction( + data: QuoteRequest +): Promise<{ success: boolean; error?: string }> { + try { + const html = buildHtmlEmail(data); + + const attachments: Array<{ filename: string; content: string }> = []; + if (data.screenshotDataUrl) { + const base64Data = data.screenshotDataUrl.replace(/^data:image\/\w+;base64,/, ""); + attachments.push({ + filename: "deur-configuratie.png", + content: base64Data, + }); + } + + await resend.emails.send({ + from: "Proinn Configurator ", + to: ["info@proinn.nl"], + replyTo: data.email, + subject: `Offerte Aanvraag - ${data.name} - ${label(data.doorType)}`, + html, + ...(attachments.length > 0 ? { attachments } : {}), + }); + + // Send confirmation to customer + await resend.emails.send({ + from: "Proinn ", + to: [data.email], + subject: "Uw offerte aanvraag is ontvangen - Proinn", + html: ` +
+

Bedankt voor uw aanvraag, ${data.name}!

+

Wij hebben uw configuratie ontvangen en nemen zo snel mogelijk contact met u op.

+

+ Indicatieprijs: ${formatPrice(data.totalPrice)}
+ Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting. +

+

+ Proinn Stalen Deuren | proinn.nl +

+
+ `, + }); + + return { success: true }; + } catch (error) { + console.error("Failed to send quote email:", error); + return { + success: false, + error: "Er is iets misgegaan bij het versturen. Probeer het opnieuw.", + }; + } +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 0000000..2d86086 --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,218 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Mail, Phone, MapPin, Clock, ArrowRight } from "lucide-react"; + +export const metadata: Metadata = { + title: "Contact | PROINN Stalen Deuren", + description: + "Neem contact op met Proinn voor vragen over stalen deuren, offertes of advies. Bel 0165 311 490 of mail info@proinn.nl.", +}; + +const contactMethods = [ + { + icon: Phone, + title: "Bel ons", + value: "0165 311 490", + href: "tel:0165311490", + description: "Ma t/m vr: 08:00 - 17:00", + }, + { + icon: Mail, + title: "E-mail", + value: "info@proinn.nl", + href: "mailto:info@proinn.nl", + description: "Reactie binnen 24 uur", + }, + { + icon: MapPin, + title: "Bezoekadres", + value: "Schotsbossenstraat 2", + href: "https://maps.google.com/?q=Schotsbossenstraat+2+4705AG+Roosendaal", + description: "4705AG Roosendaal", + }, + { + icon: Clock, + title: "Openingstijden", + value: "Ma t/m vr: 08:00 - 17:00", + href: undefined, + description: "Weekend op afspraak", + }, +]; + +const faqItems = [ + { + question: "Hoe lang duurt de levertijd?", + answer: + "De gemiddelde levertijd bedraagt 4 tot 6 weken na goedkeuring van de offerte. Dit kan vari\u00ebren afhankelijk van de complexiteit van het project en de drukte in onze werkplaats.", + }, + { + question: "Verzorgen jullie ook de montage?", + answer: + "Ja, wij bieden een complete service van ontwerp tot montage. Onze eigen monteurs installeren uw deur vakkundig op locatie. U kunt er ook voor kiezen de deur zelf te (laten) plaatsen.", + }, + { + question: "Kan ik een showroom bezoeken?", + answer: + "U bent van harte welkom in onze werkplaats in Roosendaal om onze producten in het echt te bekijken. Neem vooraf contact op zodat wij u de aandacht kunnen geven die u verdient.", + }, + { + question: "Wat kost een stalen deur?", + answer: + "De prijs is afhankelijk van de afmetingen, het type deur, de glassoort en de gewenste afwerking. Gebruik onze online configurator voor een directe indicatieprijs, of vraag een offerte op maat aan.", + }, + { + question: "Welke kleuren zijn beschikbaar?", + answer: + "Onze standaard kleuren zijn Zwart (RAL 9005), Antraciet, Brons, Goud en Beige. Daarnaast kunt u kiezen uit het volledige RAL-kleurenpalet voor een kleur die precies aansluit bij uw interieur.", + }, + { + question: "Leveren jullie door heel Nederland?", + answer: + "Ja, wij leveren en monteren door heel Nederland en Belgi\u00eb. Onze monteurs komen bij u op locatie voor een perfecte installatie.", + }, +]; + +export default function ContactPage() { + return ( + <> + {/* Hero */} +
+
+
+
+
+ + Contact + +
+

+ Neem contact op +

+

+ Heeft u vragen over onze stalen deuren, wilt u advies of bent u + klaar om een offerte aan te vragen? Wij staan voor u klaar. +

+
+
+
+ + {/* Contact Methods */} +
+
+
+ {contactMethods.map((method) => { + const Icon = method.icon; + const content = ( + <> +
+ +
+

+ {method.title} +

+

+ {method.value} +

+

+ {method.description} +

+ + ); + + const className = + "group rounded-2xl bg-[#F5F5F3] p-8 transition-colors hover:bg-[#1A2E2E]"; + + if (method.href) { + return ( + + {content} + + ); + } + + return ( +
+ {content} +
+ ); + })} +
+
+
+ + {/* FAQ */} +
+
+
+
+
+
+ + Veelgestelde vragen + +
+

+ Alles wat u wilt weten +

+
+
+ {faqItems.map((item) => ( +
+ + {item.question} + + + + + +
+

+ {item.answer} +

+
+
+ ))} +
+
+
+
+ + {/* CTA */} +
+
+

+ Liever direct aan de slag? +

+

+ Ontwerp uw stalen deur stap voor stap in onze online configurator + en ontvang direct een indicatieprijs. +

+ + Start de configurator + + +
+
+ + ); +} diff --git a/app/offerte/page.tsx b/app/offerte/page.tsx index 152024c..df0a30f 100644 --- a/app/offerte/page.tsx +++ b/app/offerte/page.tsx @@ -4,35 +4,43 @@ import { FormProvider, useFormContext } from "@/components/offerte/form-context" import { StepProduct } from "@/components/offerte/step-product"; import { StepDimensions } from "@/components/offerte/step-dimensions"; import { StepOptions } from "@/components/offerte/step-options"; +import { StepExtras } from "@/components/offerte/step-extras"; import { StepContact } from "@/components/offerte/step-contact"; import { StepSummary } from "@/components/offerte/step-summary"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DoorVisualizer } from "@/components/configurator/door-visualizer"; -const stepLabels = ["Product", "Afmetingen", "Opties", "Contact", "Overzicht"]; +const stepLabels = ["Product", "Afmetingen", "Opties", "Extra", "Contact", "Overzicht"]; const stepComponents = [ StepProduct, StepDimensions, StepOptions, + StepExtras, StepContact, StepSummary, ]; function StepIndicator() { - const { currentStep, totalSteps } = useFormContext(); + const { currentStep, totalSteps, goToStep } = useFormContext(); return ( -
+
{stepLabels.map((label, i) => ( -
-
+
+
+ {i < totalSteps - 1 && (
@@ -69,7 +77,7 @@ function WizardContent() {
- {/* Navigation — hidden on step 1 (auto-advances) and summary (has its own button) */} + {/* Navigation -- hidden on step 1 (auto-advances) and summary (has its own button) */} {!isFirstStep && !isLastStep && (
+ ) : ( + + )} +
*/} + {isEditing && ( +
+ setBubbleName(e.target.value)} + className="border rounded p-2 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-400" + placeholder="Enter bubble name" + /> + setBubbleText(e.target.value)} + className="border rounded p-2 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-400" + placeholder="Enter bubble text" + /> + +
+ )} + +
    + {bubbles.map((bubble, index) => ( +
  • +
    +

    {bubble.name}

    +

    {bubble.text}

    +
    +
    + +
    +
  • + ))} +
+
+ ); +}; + +export default BubblesManager; diff --git a/bron/BubblesManagerr.js b/bron/BubblesManagerr.js new file mode 100644 index 0000000..09d9788 --- /dev/null +++ b/bron/BubblesManagerr.js @@ -0,0 +1,161 @@ +// src/BubblesManager.js +import React, { useEffect, useState } from "react"; +import axios from "axios"; + +const BubblesManager = () => { + const [bubbles, setBubbles] = useState([]); + const [bubbleName, setBubbleName] = useState(""); + const [bubbleText, setBubbleText] = useState(""); + const [isEditing, setIsEditing] = useState(false); + const [currentBubbleIndex, setCurrentBubbleIndex] = useState(null); + + useEffect(() => { + fetchBubbles(); + }, []); + + const fetchBubbles = async () => { + try { + const response = await axios.get( + "https://api.config-fencing.com/api/get-bubbles" + ); + setBubbles(response.data.bubbles); + } catch (error) { + console.error("Error fetching bubbles:", error); + } + }; + + const addBubble = async () => { + if (bubbleName.trim() !== "" && bubbleText.trim() !== "") { + const response = await axios.post( + "https://api.config-fencing.com/api/create-bubble", + { + name: bubbleName, + text: bubbleText, + } + ); + setBubbles([ + ...bubbles, + { id: response.data.bubble.id, name: bubbleName, text: bubbleText }, + ]); + setBubbleName(""); + setBubbleText(""); + } + }; + + const editBubble = (index) => { + setBubbleName(bubbles[index].name); + setBubbleText(bubbles[index].text); + setIsEditing(true); + setCurrentBubbleIndex(index); + }; + + const updateBubble = async () => { + const response = await axios.post( + `https://api.config-fencing.com/api/update-bubble/${bubbles[currentBubbleIndex].id}`, + { + name: bubbleName, + text: bubbleText, + } + ); + const updatedBubbles = bubbles.map((bubble, index) => + index === currentBubbleIndex + ? { id: bubble.id, name: bubbleName, text: bubbleText } + : bubble + ); + setBubbles(updatedBubbles); + setBubbleName(""); + setBubbleText(""); + setIsEditing(false); + setCurrentBubbleIndex(null); + }; + + const removeBubble = (index) => { + const newBubbles = bubbles.filter((_, i) => i !== index); + setBubbles(newBubbles); + }; + + return ( +
+

Bubble Manager

+ {/*
+ setBubbleName(e.target.value)} + className="border rounded p-2 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-400" + placeholder="Enter bubble name" + /> + setBubbleText(e.target.value)} + className="border rounded p-2 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-400" + placeholder="Enter bubble text" + /> + {isEditing ? ( + + ) : ( + + )} +
*/} + {isEditing && ( +
+ setBubbleName(e.target.value)} + className="border rounded p-2 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-400" + placeholder="Enter bubble name" + /> + setBubbleText(e.target.value)} + className="border rounded p-2 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-400" + placeholder="Enter bubble text" + /> + +
+ )} + +
    + {bubbles.map((bubble, index) => ( +
  • +
    +

    {bubble.name}

    +

    {bubble.text}

    +
    +
    + +
    +
  • + ))} +
+
+ ); +}; + +export default BubblesManager; diff --git a/bron/ColorSelector.js b/bron/ColorSelector.js new file mode 100644 index 0000000..dec66d3 --- /dev/null +++ b/bron/ColorSelector.js @@ -0,0 +1,84 @@ +import { IoMdClose } from "react-icons/io"; +import { useEffect, useState } from "react"; +import { useContext } from "react"; +import { RiSearchLine } from "react-icons/ri"; +import rals from "./logic/data/colors.json"; +import { MyContext } from "./logic/data/contextapi"; + +export default function ColorSelector() { + const { colorPickerOpened, setColorPickerOpened, setFrameType, setCustomFrameType } = useContext(MyContext); + const iconSize = "16px"; + const [value, setValue] = useState(""); + const handleChange = (event) => setValue(event.target.value); + + const colors = rals; + + let resColors = []; + + for (const [key, value] of Object.entries(colors)) { + resColors.push(value); + } + + if (value) { + resColors = resColors.filter((x) => + x.code.startsWith(value.toLowerCase().trim()) + ); + } + + return ( +
+
+
Color selector
+
+ setColorPickerOpened(false)} + size={iconSize} + /> +
+
+ +
+
+ + + + +
+ +
+
+ {resColors.map((c) => { + return ( +
{ + setFrameType(`./images/custom_colors/${c.img}`); + setCustomFrameType({name: c.names.en, color: c.color.hex}) + setColorPickerOpened(false); + }} + key={c.code} + className="flex h-5 w-9 text-xs items-center justify-center text-white cursor-pointer" + style={{ background: c.color.hex }} + > + {c.code} +
+ ); + })} +
+
+
+
+ ); +} diff --git a/bron/DoorHole.js b/bron/DoorHole.js new file mode 100644 index 0000000..51c1c49 --- /dev/null +++ b/bron/DoorHole.js @@ -0,0 +1,1210 @@ +import { Base, Geometry, Subtraction } from "@react-three/csg"; +import { useTexture, useGLTF, Stats } from "@react-three/drei"; +import { + useContext, + useEffect, + useRef, + useState, + useCallback, + useMemo, +} from "react"; +import { debounce, throttle } from "lodash"; +import * as THREE from "three"; +import { MyContext } from "./data/contextapi"; +import { invalidate, useLoader, useThree } from "@react-three/fiber"; +import { gsap } from "gsap/gsap-core"; +import Stalen from "./Stalen"; +import Structure from "./Structure"; +import Handle from "./handle"; +import { Box } from "@react-three/drei"; +// Add these definitions at the top +const bulbGeometry = new THREE.SphereGeometry(0.0002, 16, 8); +const bulbMat = new THREE.MeshStandardMaterial({ + emissive: 0xffffff, + emissiveIntensity: 1, + color: 0x000000, +}); +export default function DoorHole() { + const { + type, + width, + height, + frameType, + frameSize, + doorConfig, + sidePannel, + sidePannelSize, + setStalenPart, + stalenType, + setWidth, + stalenPart, + sidePannelConfig, + open, + setOpen, + holeWidth, + setHoleWidth, + inprogress, + setInprogress, + } = useContext(MyContext); + const [maxWidth, setMaxWidth] = useState(); + const containerRef = useRef(null); + const [sizePannel, setSizePannel] = useState(0); + const [rSizePannel, setRSizePannel] = useState(0); + const [firstStructure, setFirstStructure] = useState({ + fixedPosition: 0, + viewPosition: 0, + handlePostion: 0, + }); + const [secondStructure, setSecondStructure] = useState({ + fixedPosition: 0, + viewPosition: 0, + handlePostion: 0, + sizePannel: 0, + }); + const [thirdStructure, setThirdStructure] = useState({ + fixedPosition: 0, + viewPosition: 0, + handlePostion: 0, + sizePannel: 0, + }); + const [wallStructure, setWallStructure] = useState({ + position: 0, + width: 0, + }); + + const secondStructureRef = useRef({}); + const firstStructureRef = useRef({}); + const thirdStructureRef = useRef({}); + + const [updated, setUpdated] = useState(false); + + const isInitialRender = useRef(true); + + const texture_bottom = useTexture(frameType); + const texture_wall = useTexture(frameType); + const texture = useTexture(frameType); + const texture_wall_door = useTexture(frameType); + + const bulbLight = useRef(); + const ambientLight = useRef(); + // Inside your component, probably near the return statement + + useEffect(() => { + // Create a new directional light + // const light = new THREE.DirectionalLight(0xffffff, 2, 0, 0); // Increased intensity to 2 + // light.add(new THREE.Mesh(bulbGeometry, bulbMat)); + // light.position.set(-5, 7, 15); // Adjust the position as needed + // light.castShadow = true; // Enable shadow casting + // // Set the rotatio9n of the light + // light.rotation.set(0, 0, 0); // Example rotation values in radians + + // // Assign the light to a variable or add to the scene + // bulbLight.current = light; + + // Create a SpotLight + const directionalLight = new THREE.PointLight(0xffffe3, 70); // White light with intensity 1 + + // Set the position of the light + directionalLight.position.set(-5, 7, 15); + + // Configure shadow properties + directionalLight.castShadow = true; + + // Shadow map size (higher values give better quality shadows) + directionalLight.shadow.mapSize.width = 4096; // Default is 512 + directionalLight.shadow.mapSize.height = 4096; // Default is 512 + + // Shadow camera properties + directionalLight.shadow.camera.left = -10; + directionalLight.shadow.camera.right = 20; + directionalLight.shadow.camera.top = 10; + directionalLight.shadow.camera.bottom = -10; + directionalLight.shadow.camera.near = 1; + directionalLight.shadow.camera.far = 20; + + // Optionally adjust shadow bias to reduce shadow artifacts + directionalLight.shadow.bias = -0.001; + + bulbLight.current = directionalLight; + // Create an ambient light with a specified intensity + const ambientlight = new THREE.AmbientLight(0xffffff, 30); // Color is white (0xffffff) and intensity is 0.5 + + // Optionally, you can change the intensity or color later + ambientlight.intensity = 1.3; // Adjust intensity as needed + + ambientLight.current = ambientlight; // = console.log("Light setup completed"); + }, []); + + useEffect(() => { + const wallPosition = + doorConfig == "enkele" && + (sidePannel == "links" || sidePannel == "rechts") + ? sidePannel == "links" + ? (doorWidth - sidePannelWidth) / 2 + : -(doorWidth - sidePannelWidth) / 2 + : doorConfig == "enkele" && sidePannel == "beide" + ? 0 + : doorConfig == "dubbele" && + (sidePannel == "links" || + sidePannel == "rechts" || + sidePannel == "beide") + ? 0 + : sizePannel; + + const wallWidth = + type == "vast-stalen" && + (stalenType == "divider" || stalenType == "tussen") + ? stalenType == "divider" + ? 4 * (doorWidth + 4 * frameSize) + : (0.06 * width + 4 * frameSize + (stalenPart - 1) * 3.5 * frameSize) + : ((doorConfig == "enkele" && + (sidePannel == "links" || sidePannel == "rechts")) || + (doorConfig != "enkele" && sidePannel == "geen") + ? doorConfig != "enkele" && sidePannel == "geen" + ? doorWidth * 2 + 8 * frameSize + : doorWidth + sidePannelWidth + 8 * frameSize + : (doorConfig == "enkele" && sidePannel == "beide") || + (doorConfig == "dubbele" && + (sidePannel == "rechts" || sidePannel == "links")) + ? doorConfig == "enkele" + ? doorWidth + 2 * sidePannelWidth + 12 * frameSize + : 2 * doorWidth + sidePannelWidth + 12 * frameSize + : doorConfig == "dubbele" && sidePannel == "beide" + ? 2 * doorWidth + 2 * sidePannelWidth + 16 * frameSize + : doorWidth + 4 * frameSize) + + (type == "Scharnier" ? 2 * frameWidth : 0); + + setWallStructure({ + position: wallPosition, + width: wallWidth, + }); + + const firstDoorWidth = + (doorConfig == "enkele" && sidePannel == "links" + ? sidePannelSize + : width) * + 0.06 + + 3 * frameSize; + + const secondDoorWidth = + (doorConfig == "enkele" && + (sidePannel == "rechts" || sidePannel == "beide") + ? sidePannelSize + : doorConfig == "dubbele" && sidePannel == "rechts" + ? sidePannelSize + : width) * + 0.06 + + 3 * frameSize; + + const thirdDoorWidth = + (doorConfig == "dubbele" && + (sidePannel == "links" || sidePannel == "beide") + ? sidePannelSize + : doorConfig == "enkele" && sidePannel == "beide" + ? sidePannelSize + : width) * + 0.06 + + 3 * frameSize; + + let fixedPosition = -firstDoorWidth / 2 - 1.25 * frameSize; + let viewPosition = firstDoorWidth / 2; + let handlePostion = firstDoorWidth / 2; + + let secondFixedPosition = -secondDoorWidth / 2 - 1.25 * frameSize; + let secondViewPosition = secondDoorWidth / 2; + let secondHandlePostion = secondDoorWidth / 2; + + let thirdFixedPosition = -thirdDoorWidth / 2 - 1.25 * frameSize; + let thirdViewPosition = thirdDoorWidth / 2; + let thirdHandlePostion = thirdDoorWidth / 2; + + if (doorConfig == "enkele") { + if (sidePannel == "links" || sidePannel == "rechts") { + fixedPosition = -firstDoorWidth - 1.25 * frameSize; + secondFixedPosition = secondDoorWidth - 1.25 * frameSize; + secondHandlePostion = secondDoorWidth / 2; + } else if (sidePannel == "beide") { + secondFixedPosition = + doorWidth / 2 + secondDoorWidth + 0.55 * frameSize; + thirdFixedPosition = -(doorWidth / 2 + secondDoorWidth + 3 * frameSize); + } + } else { + if (sidePannel == "geen") { + fixedPosition = -firstDoorWidth - 1.25 * frameSize; + secondFixedPosition = secondDoorWidth - 1.25 * frameSize; + } else if (sidePannel == "rechts") { + fixedPosition = doorWidth - sidePannelWidth / 2 + 0.5 * frameSize; + secondFixedPosition = doorWidth + sidePannelWidth / 2 + 4 * frameSize; + viewPosition = -firstDoorWidth / 2; + handlePostion = -firstDoorWidth / 2; + thirdFixedPosition = -doorWidth - sidePannelWidth / 2 - 6 * frameSize; + } else if (sidePannel == "beide") { + fixedPosition = -firstDoorWidth - 1.25 * frameSize; + secondFixedPosition = secondDoorWidth - 1 * frameSize; + // thirdFixedPosition = (-maxWidth / 2) * 0.06 - 7.5 * frameSize; + thirdFixedPosition = + -doorWidth - sidePannelSize * 0.06 - 7.5 * frameSize; + } else if (sidePannel == "links") { + fixedPosition = -doorWidth + sidePannelWidth / 2 - 2.5 * frameSize; + secondFixedPosition = doorWidth + sidePannelWidth / 2 + 4 * frameSize; + + thirdFixedPosition = -doorWidth - sidePannelWidth / 2 - 6 * frameSize; + } + } + + const firstStructure = { + fixedPosition: fixedPosition, + viewPosition: viewPosition, + handlePostion: handlePostion, + }; + const secondStructure = { + fixedPosition: secondFixedPosition, + viewPosition: secondViewPosition, + handlePostion: secondHandlePostion, + }; + const thirdStructure = { + fixedPosition: thirdFixedPosition, + viewPosition: thirdViewPosition, + handlePostion: thirdHandlePostion, + }; + + setFirstStructure(firstStructure); + setSecondStructure(secondStructure); + setThirdStructure(thirdStructure); + + firstStructureRef.current = firstStructure; + secondStructureRef.current = secondStructure; + thirdStructureRef.current = thirdStructure; + }, [ + doorConfig, + maxWidth, + sidePannel, + sizePannel, + sidePannelSize, + stalenType, + type, + width, + stalenPart, + ]); + // Load textures using useLoader + const [diffuse_bottom, bump_bottom, roughness_bottom] = useLoader( + THREE.TextureLoader, + [ + "textures/floor texture.jpeg", + "textures/bump_map.png", + "textures/roughness_map.png", + ] + ); + const [diffuse_wall, bump_wall, roughness_wall] = useLoader( + THREE.TextureLoader, + ["textures/wall-1.jpg", "textures/wall-1.jpg", "textures/wall-1.jpg"] + ); + + const [diffuse_wall_door, bump_wall_door, roughness_wall_door] = useLoader( + THREE.TextureLoader, + ["textures/wall-2.jpg", "textures/wall-2.jpg", "textures/wall-2.jpg"] + ); + // Set texture properties + [diffuse_bottom, bump_bottom, roughness_bottom].forEach((texture_bottom) => { + texture_bottom.wrapS = THREE.RepeatWrapping; + texture_bottom.wrapT = THREE.RepeatWrapping; + texture_bottom.anisotropy = 4; + texture_bottom.repeat.set(2, 4); + texture_bottom.bumpScale = 0.00001; // Set bumpScale on the texture itself + }); + + [diffuse_wall, bump_wall, roughness_wall].forEach((texture_wall) => { + texture_wall.wrapS = THREE.RepeatWrapping; + texture_wall.wrapT = THREE.RepeatWrapping; + texture_wall.anisotropy = 4; + texture_wall.repeat.set(25.2, 25.2); + texture_wall.bumpScale = 0.01; // Set bumpScale on the texture itself + }); + + [diffuse_wall_door, bump_wall_door, roughness_wall_door].forEach( + (texture_wall_door) => { + texture_wall_door.wrapS = THREE.RepeatWrapping; + texture_wall_door.wrapT = THREE.RepeatWrapping; + texture_wall_door.anisotropy = 4; + texture_wall_door.repeat.set(25.2, 25.2); + texture_wall_door.bumpScale = 0.01; // Set bumpScale on the texture itself + } + ); + + const frameWidth = 0.19; + const frameHeight = 0.053 * height; + const frameDepth = 0.19; + + const doorWidth = useMemo(() => 0.06 * width, [width]); + const sidePannelWidth = useMemo( + () => 0.06 * sidePannelSize, + [sidePannelSize] + ); + const doorHeightn = useMemo(() => 0.053 * height, [height]); + const floorMat = useMemo(() => { + const material = new THREE.MeshStandardMaterial({ + roughness: 0.8, + color: 0xffffff, + metalness: 0.2, + map: diffuse_bottom, + bumpMap: bump_bottom, + roughnessMap: roughness_bottom, + castShadow: true, + }); + material.bumpScale = 1; + return material; + }, [diffuse_bottom, bump_bottom, roughness_bottom]); + + const wallMat = useMemo(() => { + const material = new THREE.MeshStandardMaterial({ + roughness: 0.8, + color: 0x777777, + metalness: 0.2, + map: diffuse_wall, + bumpMap: bump_wall, + roughnessMap: roughness_wall, + castShadow: true, + }); + material.bumpScale = 1; + return material; + }, [diffuse_wall, bump_wall, roughness_wall]); + const behindWallMat = useMemo(() => { + const material = new THREE.MeshStandardMaterial({ + roughness: 0.8, + color: 0x777777, + metalness: 0.2, + map: diffuse_wall_door, + bumpMap: bump_wall_door, + roughnessMap: roughness_wall_door, + castShadow: true, + }); + material.bumpScale = 1; + return material; + }, [diffuse_wall_door, bump_wall_door, roughness_wall_door]); + const basewidth = useMemo(() => { + return ( + 0.06 * + width * + (sidePannel === "beide" && doorConfig === "enkele" + ? 3 + : sidePannel !== "geen" && doorConfig === "dubbele" + ? 4 + : 2) + ); + }, [width, sidePannel, doorConfig]); + + const Csg = useRef(); + const groupRef = useRef(); + const groupref2 = useRef(); + const groupref3 = useRef(); + + const handleAnimation = () => { + setOpen((prevOpen) => !prevOpen); + startAnimation(); + }; + + const startAnimation = (isClosing = false) => { + if (doorConfig === "enkele") { + if (type === "Schuifdeur") { + if (sidePannel === "links") { + gsap.to(groupref2.current.position, { + x: isClosing + ? secondStructureRef.current.fixedPosition + : open + ? secondStructureRef.current.fixedPosition + : doorConfig === "dubbele" || + sidePannel === "links" || + sidePannel === "rechts" + ? -doorWidth * 0.01 + : doorWidth * 1.3, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } else { + gsap.to(groupRef.current.position, { + x: isClosing + ? firstStructureRef.current.fixedPosition + : open + ? firstStructureRef.current.fixedPosition + : sidePannel === "rechts" + ? -doorWidth * 0.05 + : -doorWidth * 1.3, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } + } else { + if (sidePannel !== "links") { + gsap.to(groupRef.current.rotation, { + y: isClosing ? 0 : open ? 0 : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } + if (doorConfig === "dubbele" || sidePannel === "links") { + gsap.to(groupref2.current.rotation, { + y: isClosing ? -Math.PI : open ? -Math.PI : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } + } + } else { + if (type === "Schuifdeur") { + if (sidePannel === "links") { + gsap.to(groupref2.current.position, { + x: isClosing + ? secondStructureRef.current.fixedPosition + : open + ? secondStructureRef.current.fixedPosition + : doorWidth * 2.2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + gsap.to(groupRef.current.position, { + x: isClosing + ? firstStructureRef.current.fixedPosition + : open + ? firstStructureRef.current.fixedPosition + : -doorWidth * 1.5, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } else if (sidePannel === "rechts") { + gsap.to(groupRef.current.position, { + x: isClosing + ? firstStructureRef.current.fixedPosition + : open + ? firstStructureRef.current.fixedPosition + : doorWidth * 1.4, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + gsap.to(thirdStructure.fixedPosition, { + x: isClosing + ? (-doorWidth / 2) * 3.15 + : open + ? (-doorWidth / 2) * 3.15 + : -doorWidth * 2.2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } else { + gsap.to(groupref2.current.position, { + x: isClosing + ? secondStructureRef.current.fixedPosition + : open + ? secondStructureRef.current.fixedPosition + : doorConfig === "dubbele" || + sidePannel === "links" || + sidePannel === "rechts" + ? doorWidth * 1.8 + : doorWidth * 1.3, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + gsap.to(groupRef.current.position, { + x: isClosing + ? firstStructureRef.current.fixedPosition + : open + ? firstStructureRef.current.fixedPosition + : doorConfig === "dubbele" || + sidePannel === "links" || + sidePannel === "rechts" + ? -doorWidth * 1.8 + : -doorWidth * 1.3, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } + } else { + if (sidePannel === "rechts") { + gsap.to(groupRef.current.rotation, { + y: isClosing ? 0 : open ? 0 : Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + gsap.to(groupref3.current.rotation, { + y: isClosing ? 0 : open ? 0 : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } else if (sidePannel === "links") { + gsap.to(groupref2.current.rotation, { + y: isClosing ? -Math.PI : open ? -Math.PI : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + gsap.to(groupRef.current.rotation, { + y: isClosing ? 0 : open ? 0 : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } else { + gsap.to(groupref2.current.rotation, { + y: isClosing ? -Math.PI : open ? -Math.PI : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + gsap.to(groupRef.current.rotation, { + y: isClosing ? 0 : open ? 0 : -Math.PI / 2, + duration: 1.5, + ease: "power3.out", + onUpdate: invalidate, + }); + } + } + } + + if (type === "Schuifdeur" && isClosing) { + setTimeout(() => { + setInprogress(false); + }, 1500); + } else { + setInprogress(isClosing ? false : !open); + } + }; + + useEffect(() => { + if (isInitialRender.current) { + isInitialRender.current = false; + return; + } + + const openDoor = () => { + setOpen(false); + startAnimation(); + }; + + const closeDoor = () => { + setOpen(false); + + startAnimation(true); + }; + + let closeTimeout; + if (type != "vast-stalen") { + openDoor(); + closeTimeout = setTimeout(() => { + closeDoor(); + }, 1500); + } + + return () => clearTimeout(closeTimeout); + }, [type, doorConfig, sidePannel]); + + return ( + <> + + + + + + + + + + + + + + {/* bottom */} + + + + + {/* celling */} + {/* + + + */} + {/* behind */} + {/* + + + */} + {/* front */} + + + + + + + {/* wall-right */} + {bulbLight.current && } + {ambientLight.current && } + + + {type == "Scharnier" && ( + <> + + + + + + + + + + + + + + + + )} + + {type == "Schuifdeur" && ( + <> + {doorConfig == "enkele" && sidePannel != "geen" && ( + + + + + + + )} + {doorConfig == "enkele" && sidePannel == "geen" && ( + + + + + + + )} + {doorConfig == "dubbele" && ( + <> + {sidePannel == "geen" || sidePannel == "beide" ? ( + <> + + + + + + + + + + + + + + ) : ( + <> + + + + + + + + )} + + )} + + )} + {type != "vast-stalen" ? ( + <> + { + event.stopPropagation(); + handleAnimation(); + }} + > + + + + {doorConfig == "enkele" && sidePannel == "links" ? ( + <> + ) : ( + + + + )} + + {(doorConfig == "dubbele" || + sidePannel == "links" || + sidePannel == "rechts" || + sidePannel == "beide") && ( + { + event.stopPropagation(); + handleAnimation(); + }} + > + + + + {((doorConfig == "enkele" && sidePannel == "links") || + (doorConfig == "dubbele" && sidePannel != "rechts")) && ( + + + + )} + + )} + {(sidePannel == "beide" || + (doorConfig == "dubbele" && + (sidePannel == "links" || sidePannel == "rechts"))) && ( + { + event.stopPropagation(); + handleAnimation(); + }} + > + + + + {doorConfig == "dubbele" && sidePannel == "rechts" && ( + + + + )} + + )} + {sidePannel == "beide" && doorConfig == "dubbele" && ( + { + event.stopPropagation(); + handleAnimation(); + }} + > + + + + + )} + + ) : ( + <> + + + )} + + + ); +} diff --git a/bron/Handle%203.glb b/bron/Handle%203.glb new file mode 100644 index 0000000000000000000000000000000000000000..dbf60737c87e29034331d4e8f7f78ebf56ec374c GIT binary patch literal 4456 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 YEaHWqSwIb { + const { bubbles } = useContext(MyContext); + + const bubbleText = bubbles?.find(({ name }) => name === title) + ? bubbles.find(({ name }) => name === title).text + : ""; + + + + return ( + + {title} + + + + + + + + + ); +}; + +export default InfoIcon; diff --git a/bron/RequestConfirmation.js b/bron/RequestConfirmation.js new file mode 100644 index 0000000..8471d1f --- /dev/null +++ b/bron/RequestConfirmation.js @@ -0,0 +1,145 @@ +import React, { useEffect } from "react"; + +export default function RequestConfirmation() { + useEffect(() => { + // Function to retrieve the GA Client ID + const getGAClientId = () => { + const trackers = window.ga?.getAll?.(); + if (trackers && trackers.length) { + return trackers[0].get('clientId'); + } + return null; + }; + + const timer = setTimeout(() => { + const vid = window?.gaGlobal?.vid; + console.log("gaClientId", vid) + // If GA Client ID is found, append it to the URL + const redirectUrl = vid + ? `https://cloozdoors.nl?gaClientId=${vid}` + : "https://cloozdoors.nl"; // fallback if GA Client ID is not found + window.location.href = redirectUrl; + }, 20000); + + return () => clearTimeout(timer); + }, []); + + return ( +
+
+
+
+
+
+ +
+
+
+

+ BEDANKT VOOR UW AANVRAAG! +

+

+ Binnen 48 uur neemt onze verkoop binnendienst contact met u op om + deze online aanvraag volledig te optimaliseren en u van de juiste + offerte te voorzien. +

+
+
+
+
+
+
+
+

+ Waarom kies +

+

+ Je Clooz doors +

+
+
+ +
+ +
+
    +
  • Eigen fabriek
  • +
  • Uniek eigen profiel
  • +
  • Snelste levering
  • +
  • Perfecte afwerking
  • +
  • Kindveilig product
  • +
+
+
+
+
+

+ Waarom het +

+

+ Clooz doors profiel? +

+
+
+ +
+ +
+
    +
  • Tweezijdig hetzelfde aanzicht
  • +
  • Elke design mogelijk
  • +
  • Ongedeelde glasplaat
  • +
  • Rankste profiel
  • +
+
+
+
+
+

+ Bezoek onze +

+

SHOWROOM

+
+
+ +
+
+
    +
  • Met persoonlijk advies
  • +
  • Alle deuren
  • +
  • Ontwerp samen
  • +
  • Ook op zaterdag
  • +
  • Centraal in NL
  • +
+
+
+
+
+

+ Gebruik onze +

+

+ inmeetservice +

+
+
+ +
+ +
+
    +
  • Door heel NL
  • +
  • Voorkom fouten
  • +
  • Voorkom kosten
  • +
  • Met het beste advies
  • +
  • Voor het gemak
  • +
+
+
+
+
+
+
+
+ ); +} diff --git a/bron/RequestConfirmationn.js b/bron/RequestConfirmationn.js new file mode 100644 index 0000000..8471d1f --- /dev/null +++ b/bron/RequestConfirmationn.js @@ -0,0 +1,145 @@ +import React, { useEffect } from "react"; + +export default function RequestConfirmation() { + useEffect(() => { + // Function to retrieve the GA Client ID + const getGAClientId = () => { + const trackers = window.ga?.getAll?.(); + if (trackers && trackers.length) { + return trackers[0].get('clientId'); + } + return null; + }; + + const timer = setTimeout(() => { + const vid = window?.gaGlobal?.vid; + console.log("gaClientId", vid) + // If GA Client ID is found, append it to the URL + const redirectUrl = vid + ? `https://cloozdoors.nl?gaClientId=${vid}` + : "https://cloozdoors.nl"; // fallback if GA Client ID is not found + window.location.href = redirectUrl; + }, 20000); + + return () => clearTimeout(timer); + }, []); + + return ( +
+
+
+
+
+
+ +
+
+
+

+ BEDANKT VOOR UW AANVRAAG! +

+

+ Binnen 48 uur neemt onze verkoop binnendienst contact met u op om + deze online aanvraag volledig te optimaliseren en u van de juiste + offerte te voorzien. +

+
+
+
+
+
+
+
+

+ Waarom kies +

+

+ Je Clooz doors +

+
+
+ +
+ +
+
    +
  • Eigen fabriek
  • +
  • Uniek eigen profiel
  • +
  • Snelste levering
  • +
  • Perfecte afwerking
  • +
  • Kindveilig product
  • +
+
+
+
+
+

+ Waarom het +

+

+ Clooz doors profiel? +

+
+
+ +
+ +
+
    +
  • Tweezijdig hetzelfde aanzicht
  • +
  • Elke design mogelijk
  • +
  • Ongedeelde glasplaat
  • +
  • Rankste profiel
  • +
+
+
+
+
+

+ Bezoek onze +

+

SHOWROOM

+
+
+ +
+
+
    +
  • Met persoonlijk advies
  • +
  • Alle deuren
  • +
  • Ontwerp samen
  • +
  • Ook op zaterdag
  • +
  • Centraal in NL
  • +
+
+
+
+
+

+ Gebruik onze +

+

+ inmeetservice +

+
+
+ +
+ +
+
    +
  • Door heel NL
  • +
  • Voorkom fouten
  • +
  • Voorkom kosten
  • +
  • Met het beste advies
  • +
  • Voor het gemak
  • +
+
+
+
+
+
+
+
+ ); +} diff --git a/bron/RequestForm.js b/bron/RequestForm.js new file mode 100644 index 0000000..e075c3a --- /dev/null +++ b/bron/RequestForm.js @@ -0,0 +1,416 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import TechInformation from "./TechInformation"; + +const RequestForm = ({ techInformation, attachDesign, glContext, sceneContext, cameraContext }) => { + const navigate = useNavigate(); + + const [formData, setFormData] = useState({ + aanhef: "Dhr", + voornaam: "", + tussenvoegsel: "", + achternaam: "", + straatnaam: "", + huisnummer: "", + postcode: "", + woonplaats: "", + land: "Nederland", + emailadres: "", + telefoonnummer: "", + comment: "", + file: null, + }); + + useEffect(() => { + setFormData({...formData, file: attachDesign}); + }, [attachDesign]) + + const [isSubmitting, setIsSubmitting] = useState(false); + const [isClearSubmitting, setIsClearSubmitting] = useState(false); + const [formErrors, setFormErrors] = useState({}); + + const handleChange = (e) => { + const { name, value, files } = e.target; + if (name === "file") { + setFormData({ + ...formData, + [name]: files[0], + }); + } else { + setFormData({ + ...formData, + [name]: value, + }); + } + }; + + const validate = () => { + const errors = {}; + Object.keys(formData).forEach((key) => { + if (["comment", "tussenvoegsel", "file"].includes(key)) return; + if (!formData[key]) { + errors[key] = "Dit veld is verplicht"; + } + }); + return errors; + }; + + const dataURLToBlob = (dataURL) => { + const byteString = atob(dataURL.split(',')[1]); + const mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]; + const arrayBuffer = new ArrayBuffer(byteString.length); + const uint8Array = new Uint8Array(arrayBuffer); + + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i); + } + + return new Blob([arrayBuffer], { type: mimeString }); + }; + + const handleSubmit = async (isReset = false) => { + const errors = validate(); + if (Object.keys(errors).length === 0) { + setIsSubmitting(true); + setIsClearSubmitting(isReset); + try { + glContext.render(sceneContext, cameraContext); + const image = glContext.domElement.toDataURL('image/png'); + const imageBlob = dataURLToBlob(image); + + const formDataToSubmit = new FormData(); + Object.keys(formData).forEach((key) => { + formDataToSubmit.append(key, formData[key]); + }); + + formDataToSubmit.append('constructImage', imageBlob, 'constructImage.png'); + + formDataToSubmit.append( + "techInformation", + JSON.stringify(techInformation) + ); + + formDataToSubmit.append( + "platform", "clooz" + ); + + const response = await axios.post( + "https://api-lumbronch.agreatidea.studio/api/request-a-quote", + formDataToSubmit, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + + navigate("/request-confirmation"); + + if (isReset) { + setFormData({ + aanhef: "Dhr", + voornaam: "", + tussenvoegsel: "", + achternaam: "", + straatnaam: "", + huisnummer: "", + postcode: "", + woonplaats: "", + land: "Nederland", + emailadres: "", + telefoonnummer: "", + comment: "", + file: null, + }); + } + // Handle successful form submission + } catch (error) { + console.error(error); + // Handle error in form submission + } finally { + setIsSubmitting(false); + } + } else { + setFormErrors(errors); + } + }; + + const formStyle = { + maxWidth: "400px", + margin: "0 auto", + backgroundColor: "#2D3748", + padding: "20px", + borderRadius: "8px", + }; + + const inputStyle = { + width: "100%", + padding: "10px", + marginBottom: "10px", + borderRadius: "4px", + backgroundColor: "#fff", + color: "#333", + border: "none", + boxSizing: "border-box", + }; + + const fileInputStyle = { + display: "none", + }; + + const labelStyle = { + color: "#fff", + fontSize: "14px", + fontWeight: "bold", + display: "block", + }; + + const errorStyle = { + color: "red", + fontSize: "12px", + margin: 0, + }; + + const buttonStyle = { + backgroundColor: "#48BB78", + color: "#FFF", + fontSize: "16px", + padding: "10px 20px", + borderRadius: "100px", + border: "none", + cursor: "pointer", + }; + + const linkButtonStyle = { + fontSize: "14px", + color: "#FFF", + cursor: "pointer", + display: "block", + }; + + const fileUploadLabelStyle = { + display: "flex", + alignItems: "center", + gap: "10px", + cursor: "pointer", + backgroundColor: "#48BB78", + padding: "8px 15px", + borderRadius: "50px", + color: "#FFF", + fontSize: "14px", + textAlign: "center", + }; + + return ( +
+ +
+

+ Vul uw gegevens in +

+
+ + {formErrors.aanhef &&

{formErrors.aanhef}

} +
+ {[ + "voornaam", + "tussenvoegsel", + "achternaam", + "straatnaam", + "huisnummer", + "postcode", + "woonplaats", + "emailadres", + "telefoonnummer", + ].map((field) => ( +
+ + {formErrors[field] &&

{formErrors[field]}

} +
+ ))} +
+ + {formErrors.land &&

{formErrors.land}

} +
+
+