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 <noreply@anthropic.com>
@@ -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
|
- **3D Stack**: React Three Fiber (`@react-three/fiber`), Drei (`@react-three/drei`), Three.js
|
||||||
- **Styling**: Tailwind CSS v4
|
- **Styling**: Tailwind CSS v4
|
||||||
- **Design Language**: Industrial, clean, heavy — Dark Grey/Black primary, Orange & Blue accents
|
- **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
|
## 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.
|
- Build door geometry from composed primitives (frame, panels, glass inserts, handles) — not a single box.
|
||||||
|
|
||||||
4. **Texture-First Approach**:
|
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.
|
- 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.
|
- 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.
|
- Identify gaps: flat colors where textures should be, missing shadows, poor lighting, sharp edges.
|
||||||
|
|
||||||
### Step 2: Inventory Available Assets
|
### 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.
|
- Catalog available maps: diffuse, normal, roughness, metalness, AO.
|
||||||
- Note texture resolutions and naming conventions.
|
- 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:
|
- Use `useTexture` from Drei for loading, with proper configuration:
|
||||||
```tsx
|
```tsx
|
||||||
const [diffuse, normal, roughness] = useTexture([
|
const [diffuse, normal, roughness] = useTexture([
|
||||||
'/textures/aluwdoors/diffuse.jpg',
|
'/textures/proinn/diffuse.jpg',
|
||||||
'/textures/aluwdoors/normal.jpg',
|
'/textures/proinn/normal.jpg',
|
||||||
'/textures/aluwdoors/roughness.jpg',
|
'/textures/proinn/roughness.jpg',
|
||||||
])
|
])
|
||||||
// Configure wrapping and repeat
|
// Configure wrapping and repeat
|
||||||
;[diffuse, normal, roughness].forEach(t => {
|
;[diffuse, normal, roughness].forEach(t => {
|
||||||
@@ -113,7 +113,7 @@ When tasked with visual improvements, follow this workflow:
|
|||||||
|
|
||||||
Before considering any visual task complete, verify:
|
Before considering any visual task complete, verify:
|
||||||
- [ ] No sharp BoxGeometry edges visible — all using RoundedBox
|
- [ ] 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.)
|
- [ ] Materials have physically plausible PBR values (metalness, roughness, etc.)
|
||||||
- [ ] Lighting creates depth with visible highlights, mid-tones, and shadows
|
- [ ] Lighting creates depth with visible highlights, mid-tones, and shadows
|
||||||
- [ ] Contact shadows ground the object in space
|
- [ ] Contact shadows ground the object in space
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: frontend-stylist
|
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<example>\\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<commentary>\\nSince this is a styling task involving translating reference CSS into Tailwind config, use the Task tool to launch the frontend-stylist agent.\\n</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\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</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\nSince this is a mobile responsive styling issue in the configurator UI, use the Task tool to launch the frontend-stylist agent.\\n</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\nSince this involves UI polish, transitions, and visual refinement, use the Task tool to launch the frontend-stylist agent.\\n</commentary>\\n</example>"
|
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<example>\\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<commentary>\\nSince this is a styling task involving translating reference CSS into Tailwind config, use the Task tool to launch the frontend-stylist agent.\\n</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\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</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\nSince this is a mobile responsive styling issue in the configurator UI, use the Task tool to launch the frontend-stylist agent.\\n</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\nSince this involves UI polish, transitions, and visual refinement, use the Task tool to launch the frontend-stylist agent.\\n</commentary>\\n</example>"
|
||||||
model: sonnet
|
model: sonnet
|
||||||
color: cyan
|
color: cyan
|
||||||
memory: project
|
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.
|
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.
|
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.
|
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
|
## Tech Stack Context
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ Your design system principles:
|
|||||||
|
|
||||||
## Workflow
|
## 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.
|
2. **Plan**: Briefly describe what styles you'll apply and why.
|
||||||
3. **Implement Mobile-First**: Write the mobile layout first. Test mentally at 375px.
|
3. **Implement Mobile-First**: Write the mobile layout first. Test mentally at 375px.
|
||||||
4. **Layer Up**: Add `sm:`, `md:`, `lg:` responsive variants.
|
4. **Layer Up**: Add `sm:`, `md:`, `lg:` responsive variants.
|
||||||
@@ -84,7 +84,7 @@ Your design system principles:
|
|||||||
|
|
||||||
## Reference CSS Translation Protocol
|
## 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:
|
1. Read the CSS file carefully, extracting:
|
||||||
- Color values → Add to `tailwind.config.ts` under `theme.extend.colors`
|
- 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
|
## 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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
234
actions/send-quote.ts
Normal file
@@ -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<string, string> = {
|
||||||
|
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]) =>
|
||||||
|
`<tr><td style="padding:8px 12px;border-bottom:1px solid #eee;color:#666;width:40%">${k}</td><td style="padding:8px 12px;border-bottom:1px solid #eee;font-weight:600">${v}</td></tr>`
|
||||||
|
)
|
||||||
|
.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]) =>
|
||||||
|
`<tr><td style="padding:6px 12px;color:#666">${k}</td><td style="padding:6px 12px;text-align:right">${v}</td></tr>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;background:#fff">
|
||||||
|
<div style="background:#1A2E2E;padding:24px;border-radius:12px 12px 0 0">
|
||||||
|
<h1 style="color:#C4D668;margin:0;font-size:22px">Nieuwe Offerte Aanvraag</h1>
|
||||||
|
<p style="color:#fff;margin:8px 0 0;font-size:14px">Via Proinn Configurator</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:24px;border:1px solid #eee;border-top:0">
|
||||||
|
<h2 style="color:#1A2E2E;font-size:16px;margin:0 0 4px">Contactgegevens</h2>
|
||||||
|
<p style="margin:0;font-size:15px"><strong>${data.name}</strong></p>
|
||||||
|
<p style="margin:4px 0;font-size:14px;color:#666">${data.email} | ${data.phone}</p>
|
||||||
|
${data.note ? `<p style="margin:8px 0 0;font-size:14px;color:#333;background:#f7f7f7;padding:12px;border-radius:8px">${data.note}</p>` : ""}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:24px;border:1px solid #eee;border-top:0">
|
||||||
|
<h2 style="color:#1A2E2E;font-size:16px;margin:0 0 12px">Configuratie</h2>
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:14px">
|
||||||
|
${tableRows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:24px;border:1px solid #eee;border-top:0;border-radius:0 0 12px 12px;background:#f9f9f9">
|
||||||
|
<h2 style="color:#1A2E2E;font-size:16px;margin:0 0 12px">Indicatieprijs</h2>
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:14px">
|
||||||
|
${priceTableRows}
|
||||||
|
</table>
|
||||||
|
<div style="margin-top:12px;padding-top:12px;border-top:2px solid #1A2E2E;text-align:right">
|
||||||
|
<span style="font-size:12px;color:#666">Indicatieprijs totaal</span><br>
|
||||||
|
<span style="font-size:24px;font-weight:700;color:#1A2E2E">${formatPrice(data.totalPrice)}</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:8px 0 0;font-size:11px;color:#999">* Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <noreply@proinn.youztech.nl>",
|
||||||
|
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 <noreply@proinn.youztech.nl>",
|
||||||
|
to: [data.email],
|
||||||
|
subject: "Uw offerte aanvraag is ontvangen - Proinn",
|
||||||
|
html: `
|
||||||
|
<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto">
|
||||||
|
<h2 style="color:#1A2E2E">Bedankt voor uw aanvraag, ${data.name}!</h2>
|
||||||
|
<p>Wij hebben uw configuratie ontvangen en nemen zo snel mogelijk contact met u op.</p>
|
||||||
|
<p style="margin-top:16px;padding:16px;background:#f7f7f7;border-radius:8px">
|
||||||
|
<strong>Indicatieprijs:</strong> ${formatPrice(data.totalPrice)}<br>
|
||||||
|
<small style="color:#666">Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.</small>
|
||||||
|
</p>
|
||||||
|
<p style="color:#999;font-size:12px;margin-top:24px">
|
||||||
|
Proinn Stalen Deuren | proinn.nl
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
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.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
218
app/contact/page.tsx
Normal file
@@ -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 */}
|
||||||
|
<section className="bg-[#1A2E2E] pt-32 pb-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-[#C4D668]">
|
||||||
|
Contact
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-light leading-tight text-white sm:text-5xl">
|
||||||
|
Neem <span className="font-semibold">contact</span> op
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 max-w-xl text-base leading-relaxed text-gray-400">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Methods */}
|
||||||
|
<section className="bg-white py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{contactMethods.map((method) => {
|
||||||
|
const Icon = method.icon;
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 flex size-12 items-center justify-center rounded-xl bg-[#1A2E2E] transition-colors group-hover:bg-[#C4D668]">
|
||||||
|
<Icon
|
||||||
|
className="size-5 text-[#C4D668] transition-colors group-hover:text-black"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-medium uppercase tracking-wider text-gray-400 transition-colors group-hover:text-gray-400">
|
||||||
|
{method.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold text-gray-900 transition-colors group-hover:text-white">
|
||||||
|
{method.value}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 transition-colors group-hover:text-gray-400">
|
||||||
|
{method.description}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const className =
|
||||||
|
"group rounded-2xl bg-[#F5F5F3] p-8 transition-colors hover:bg-[#1A2E2E]";
|
||||||
|
|
||||||
|
if (method.href) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={method.title}
|
||||||
|
href={method.href}
|
||||||
|
target={
|
||||||
|
method.href.startsWith("http") ? "_blank" : undefined
|
||||||
|
}
|
||||||
|
rel={
|
||||||
|
method.href.startsWith("http")
|
||||||
|
? "noopener noreferrer"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={method.title} className={className}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ */}
|
||||||
|
<section className="bg-[#F5F5F3] py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
<div className="mb-10 text-center">
|
||||||
|
<div className="mb-4 flex items-center justify-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-gray-500">
|
||||||
|
Veelgestelde vragen
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-light text-gray-900 lg:text-4xl">
|
||||||
|
Alles wat u wilt <span className="font-semibold">weten</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{faqItems.map((item) => (
|
||||||
|
<details
|
||||||
|
key={item.question}
|
||||||
|
className="group rounded-xl bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<summary className="flex cursor-pointer items-center justify-between px-6 py-5 text-sm font-semibold text-gray-900 [&::-webkit-details-marker]:hidden">
|
||||||
|
{item.question}
|
||||||
|
<span className="ml-4 flex size-6 shrink-0 items-center justify-center rounded-full bg-gray-100 text-xs text-gray-500 transition-transform group-open:rotate-45">
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="px-6 pb-5">
|
||||||
|
<p className="text-sm leading-relaxed text-gray-600">
|
||||||
|
{item.answer}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="bg-[#1A2E2E] py-16">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
||||||
|
<h2 className="mb-4 text-3xl font-bold text-white">
|
||||||
|
Liever direct aan de slag?
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mb-8 max-w-md text-sm text-gray-400">
|
||||||
|
Ontwerp uw stalen deur stap voor stap in onze online configurator
|
||||||
|
en ontvang direct een indicatieprijs.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/offerte"
|
||||||
|
className="inline-flex items-center gap-2 rounded-md bg-[#C4D668] px-6 py-3 text-sm font-semibold text-black transition-colors hover:bg-[#b5c75a]"
|
||||||
|
>
|
||||||
|
Start de configurator
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,35 +4,43 @@ import { FormProvider, useFormContext } from "@/components/offerte/form-context"
|
|||||||
import { StepProduct } from "@/components/offerte/step-product";
|
import { StepProduct } from "@/components/offerte/step-product";
|
||||||
import { StepDimensions } from "@/components/offerte/step-dimensions";
|
import { StepDimensions } from "@/components/offerte/step-dimensions";
|
||||||
import { StepOptions } from "@/components/offerte/step-options";
|
import { StepOptions } from "@/components/offerte/step-options";
|
||||||
|
import { StepExtras } from "@/components/offerte/step-extras";
|
||||||
import { StepContact } from "@/components/offerte/step-contact";
|
import { StepContact } from "@/components/offerte/step-contact";
|
||||||
import { StepSummary } from "@/components/offerte/step-summary";
|
import { StepSummary } from "@/components/offerte/step-summary";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
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 = [
|
const stepComponents = [
|
||||||
StepProduct,
|
StepProduct,
|
||||||
StepDimensions,
|
StepDimensions,
|
||||||
StepOptions,
|
StepOptions,
|
||||||
|
StepExtras,
|
||||||
StepContact,
|
StepContact,
|
||||||
StepSummary,
|
StepSummary,
|
||||||
];
|
];
|
||||||
|
|
||||||
function StepIndicator() {
|
function StepIndicator() {
|
||||||
const { currentStep, totalSteps } = useFormContext();
|
const { currentStep, totalSteps, goToStep } = useFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-8 flex items-center gap-2">
|
<div className="mb-8 flex items-center gap-1.5">
|
||||||
{stepLabels.map((label, i) => (
|
{stepLabels.map((label, i) => (
|
||||||
<div key={label} className="flex items-center gap-2">
|
<div key={label} className="flex items-center gap-1.5">
|
||||||
<div className="flex flex-col items-center gap-1">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => i < currentStep && goToStep(i)}
|
||||||
|
className="flex flex-col items-center gap-1"
|
||||||
|
disabled={i > currentStep}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex size-8 items-center justify-center rounded-full text-sm font-semibold transition-colors ${
|
className={`flex size-8 items-center justify-center rounded-full text-sm font-semibold transition-colors ${
|
||||||
i <= currentStep
|
i <= currentStep
|
||||||
? "bg-[#1A2E2E] text-white"
|
? "bg-[#1A2E2E] text-white"
|
||||||
: "bg-gray-200 text-gray-500"
|
: "bg-gray-200 text-gray-500"
|
||||||
}`}
|
} ${i < currentStep ? "cursor-pointer hover:bg-[#1A2E2E]/80" : ""}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</div>
|
</div>
|
||||||
@@ -43,10 +51,10 @@ function StepIndicator() {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
{i < totalSteps - 1 && (
|
{i < totalSteps - 1 && (
|
||||||
<div
|
<div
|
||||||
className={`h-px w-4 transition-colors lg:w-6 ${
|
className={`h-px w-3 transition-colors lg:w-4 ${
|
||||||
i < currentStep ? "bg-[#1A2E2E]" : "bg-gray-300"
|
i < currentStep ? "bg-[#1A2E2E]" : "bg-gray-300"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -69,7 +77,7 @@ function WizardContent() {
|
|||||||
<CurrentStepComponent />
|
<CurrentStepComponent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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 && (
|
{!isFirstStep && !isLastStep && (
|
||||||
<div className="mt-6 flex justify-between gap-4">
|
<div className="mt-6 flex justify-between gap-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -103,8 +111,6 @@ function WizardContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
import { DoorVisualizer } from "@/components/configurator/door-visualizer";
|
|
||||||
|
|
||||||
export default function OffertePage() {
|
export default function OffertePage() {
|
||||||
return (
|
return (
|
||||||
<FormProvider>
|
<FormProvider>
|
||||||
|
|||||||
212
app/over-ons/page.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { ArrowRight, Factory, Ruler, Shield, Users } from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Over Ons | PROINN Stalen Deuren",
|
||||||
|
description:
|
||||||
|
"Leer meer over Proinn. Handgemaakte stalen deuren, op maat geleverd en geinstalleerd vanuit onze werkplaats in Roosendaal.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
icon: Factory,
|
||||||
|
title: "Eigen productie",
|
||||||
|
description:
|
||||||
|
"Alle deuren worden volledig in onze eigen werkplaats in Roosendaal geproduceerd. Zo houden wij controle over ieder detail.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Ruler,
|
||||||
|
title: "100% maatwerk",
|
||||||
|
description:
|
||||||
|
"Geen deur is hetzelfde. Wij produceren uitsluitend op maat, afgestemd op uw specifieke wensen en afmetingen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: "Kwaliteitsgarantie",
|
||||||
|
description:
|
||||||
|
"Wij werken met hoogwaardige staalprofielen en duurzame poedercoating voor een resultaat dat jarenlang meegaat.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: "Persoonlijk advies",
|
||||||
|
description:
|
||||||
|
"Van het eerste contact tot de oplevering: u heeft altijd een vast aanspreekpunt dat met u meedenkt.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ value: "500+", label: "Deuren geleverd" },
|
||||||
|
{ value: "100%", label: "Op maat gemaakt" },
|
||||||
|
{ value: "4.8/5", label: "Klantwaardering" },
|
||||||
|
{ value: "Roosendaal", label: "Eigen werkplaats" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function OverOnsPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="bg-[#1A2E2E] pt-32 pb-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-[#C4D668]">
|
||||||
|
Over Proinn
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-light leading-tight text-white sm:text-5xl">
|
||||||
|
Vakmanschap in{" "}
|
||||||
|
<span className="font-semibold">staal</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 max-w-xl text-base leading-relaxed text-gray-400">
|
||||||
|
Proinn is gespecialiseerd in het ontwerpen en produceren van
|
||||||
|
handgemaakte stalen deuren, kozijnen en wanden. Vanuit onze
|
||||||
|
werkplaats in Roosendaal combineren wij ambachtelijke technieken
|
||||||
|
met moderne technologie.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<section className="border-b border-gray-200 bg-white">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-2 divide-x divide-gray-200 lg:grid-cols-4">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<div key={stat.label} className="px-6 py-10 text-center">
|
||||||
|
<p className="text-3xl font-bold text-gray-900">{stat.value}</p>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">{stat.label}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Story */}
|
||||||
|
<section className="bg-[#F5F5F3] py-20">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-12 px-4 sm:px-6 lg:grid-cols-2 lg:gap-16 lg:px-8">
|
||||||
|
<div className="relative min-h-[400px] overflow-hidden rounded-2xl lg:min-h-[500px]">
|
||||||
|
<Image
|
||||||
|
src="/images/proinn-spuiten.png"
|
||||||
|
alt="Proinn werkplaats - poedercoating"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-gray-500">
|
||||||
|
Ons verhaal
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-5 text-3xl font-light leading-tight text-gray-900 lg:text-4xl">
|
||||||
|
Van ontwerp tot
|
||||||
|
<br />
|
||||||
|
<span className="font-semibold">montage op locatie</span>
|
||||||
|
</h2>
|
||||||
|
<p className="mb-4 text-sm leading-relaxed text-gray-600">
|
||||||
|
Bij Proinn geloven wij dat een stalen deur meer is dan een
|
||||||
|
functioneel element. Het is een statement dat de sfeer van uw
|
||||||
|
ruimte bepaalt. Daarom besteden wij aan ieder project dezelfde
|
||||||
|
aandacht en toewijding, of het nu gaat om een enkele binnendeur
|
||||||
|
of een complete kantoorindeling.
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 text-sm leading-relaxed text-gray-600">
|
||||||
|
Ons team van vakmensen beheerst het volledige traject: van het
|
||||||
|
eerste adviesgesprek en het technisch ontwerp, tot de productie
|
||||||
|
in onze eigen werkplaats en de zorgvuldige montage bij u op
|
||||||
|
locatie.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed text-gray-600">
|
||||||
|
Alle deuren worden geproduceerd met hoogwaardige staalprofielen en
|
||||||
|
afgewerkt met een duurzame poedercoating in de kleur van uw keuze.
|
||||||
|
Het resultaat: een product dat niet alleen mooi oogt, maar ook
|
||||||
|
jarenlang meegaat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Values */}
|
||||||
|
<section className="bg-white py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-12 max-w-xl">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-gray-500">
|
||||||
|
Waar wij voor staan
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-light text-gray-900 lg:text-4xl">
|
||||||
|
Kwaliteit in elk <span className="font-semibold">detail</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{values.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<div key={item.title} className="rounded-2xl bg-[#F5F5F3] p-8">
|
||||||
|
<div className="mb-4 flex size-12 items-center justify-center rounded-xl bg-[#1A2E2E]">
|
||||||
|
<Icon className="size-6 text-[#C4D668]" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
<h3 className="mb-2 text-base font-semibold text-gray-900">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-gray-500">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Team Image + CTA */}
|
||||||
|
<section className="bg-[#1A2E2E] py-20">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-12 px-4 sm:px-6 lg:grid-cols-2 lg:gap-16 lg:px-8">
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h2 className="mb-5 text-3xl font-bold leading-tight text-white lg:text-4xl">
|
||||||
|
Samen uw ideale
|
||||||
|
<br />
|
||||||
|
deur realiseren?
|
||||||
|
</h2>
|
||||||
|
<p className="mb-8 max-w-md text-sm leading-relaxed text-gray-300">
|
||||||
|
Neem vrijblijvend contact met ons op of ontwerp direct uw eigen
|
||||||
|
stalen deur via onze online configurator. Wij denken graag met
|
||||||
|
u mee.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Link
|
||||||
|
href="/offerte"
|
||||||
|
className="inline-flex items-center gap-2 rounded-md bg-[#C4D668] px-6 py-3 text-sm font-semibold text-black transition-colors hover:bg-[#b5c75a]"
|
||||||
|
>
|
||||||
|
Configureer uw deur
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/contact"
|
||||||
|
className="inline-flex items-center gap-2 rounded-md border border-white/20 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Neem contact op
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative min-h-[350px] overflow-hidden rounded-2xl lg:min-h-[420px]">
|
||||||
|
<Image
|
||||||
|
src="/images/image-people1.png"
|
||||||
|
alt="Het Proinn team"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
app/privacy/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Privacybeleid | PROINN",
|
||||||
|
description: "Privacybeleid van Proinn - hoe wij omgaan met uw persoonsgegevens.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="bg-[#1A2E2E] pt-32 pb-16">
|
||||||
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 className="text-3xl font-light text-white sm:text-4xl">
|
||||||
|
Privacybeleid
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-sm text-gray-400">
|
||||||
|
Laatst bijgewerkt: februari 2025
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-white py-16">
|
||||||
|
<div className="prose prose-sm prose-gray mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="space-y-8 text-sm leading-relaxed text-gray-600">
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
1. Wie zijn wij?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Proinn is gevestigd aan de Schotsbossenstraat 2, 4705AG
|
||||||
|
Roosendaal en ingeschreven bij de Kamer van Koophandel onder
|
||||||
|
nummer 85086991. Wij zijn verantwoordelijk voor de verwerking
|
||||||
|
van uw persoonsgegevens zoals beschreven in dit privacybeleid.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
2. Welke gegevens verzamelen wij?
|
||||||
|
</h2>
|
||||||
|
<p className="mb-2">
|
||||||
|
Wanneer u contact met ons opneemt of een offerte aanvraagt via
|
||||||
|
onze website, kunnen wij de volgende gegevens verwerken:
|
||||||
|
</p>
|
||||||
|
<ul className="ml-4 list-disc space-y-1 text-gray-600">
|
||||||
|
<li>Naam</li>
|
||||||
|
<li>E-mailadres</li>
|
||||||
|
<li>Telefoonnummer</li>
|
||||||
|
<li>De configuratie van uw gewenste product</li>
|
||||||
|
<li>Eventuele opmerkingen die u bij uw aanvraag plaatst</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
3. Waarvoor gebruiken wij uw gegevens?
|
||||||
|
</h2>
|
||||||
|
<p className="mb-2">Wij verwerken uw persoonsgegevens voor de volgende doeleinden:</p>
|
||||||
|
<ul className="ml-4 list-disc space-y-1 text-gray-600">
|
||||||
|
<li>Het verwerken en opvolgen van uw offerteaanvraag</li>
|
||||||
|
<li>Het beantwoorden van uw vragen via e-mail of telefoon</li>
|
||||||
|
<li>Het verbeteren van onze dienstverlening en website</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
4. Hoe lang bewaren wij uw gegevens?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Wij bewaren uw persoonsgegevens niet langer dan strikt
|
||||||
|
noodzakelijk is voor de doeleinden waarvoor zij worden
|
||||||
|
verzameld. In de regel hanteren wij een bewaartermijn van
|
||||||
|
maximaal 2 jaar na het laatste contact, tenzij wettelijke
|
||||||
|
verplichtingen een langere bewaartermijn vereisen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
5. Delen wij uw gegevens?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Wij delen uw persoonsgegevens niet met derden, tenzij dit
|
||||||
|
noodzakelijk is voor de uitvoering van onze dienstverlening
|
||||||
|
(bijvoorbeeld een monteur die bij u op locatie komt) of
|
||||||
|
wanneer wij hiertoe wettelijk verplicht zijn.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
6. Cookies
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Onze website maakt gebruik van technisch noodzakelijke cookies
|
||||||
|
om de website goed te laten functioneren. Wij plaatsen geen
|
||||||
|
tracking- of marketingcookies.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
7. Uw rechten
|
||||||
|
</h2>
|
||||||
|
<p className="mb-2">
|
||||||
|
U heeft het recht om uw persoonsgegevens in te zien, te
|
||||||
|
corrigeren of te verwijderen. Daarnaast heeft u het recht om
|
||||||
|
bezwaar te maken tegen de verwerking van uw gegevens. U kunt
|
||||||
|
een verzoek indienen via:
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-gray-900">
|
||||||
|
E-mail: info@proinn.nl
|
||||||
|
<br />
|
||||||
|
Telefoon: 0165 311 490
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
8. Klachten
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Mocht u een klacht hebben over de verwerking van uw
|
||||||
|
persoonsgegevens, dan kunt u contact met ons opnemen. U heeft
|
||||||
|
daarnaast altijd het recht een klacht in te dienen bij de
|
||||||
|
Autoriteit Persoonsgegevens (autoriteitpersoonsgegevens.nl).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
246
app/producten/page.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
DoorOpen,
|
||||||
|
Home,
|
||||||
|
Grid2x2,
|
||||||
|
Ruler,
|
||||||
|
Palette,
|
||||||
|
Shield,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Producten | PROINN Stalen Deuren",
|
||||||
|
description:
|
||||||
|
"Bekijk ons assortiment stalen deuren: binnendeuren, buitendeuren en kantoorwanden. Alles op maat, handgemaakt in Roosendaal.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const products = [
|
||||||
|
{
|
||||||
|
id: "binnendeuren",
|
||||||
|
icon: DoorOpen,
|
||||||
|
title: "Stalen binnendeuren",
|
||||||
|
description:
|
||||||
|
"Onze stalen binnendeuren combineren een industrieel karakter met een strakke, moderne uitstraling. Verkrijgbaar als taatsdeur, scharnierdeur of schuifdeur, in iedere gewenste afmeting en glasverdeling.",
|
||||||
|
features: [
|
||||||
|
"Taats-, scharnier- of schuifdeur",
|
||||||
|
"Enkele of dubbele uitvoering",
|
||||||
|
"Diverse glasverdelingen (2 t/m 8 vlakken)",
|
||||||
|
"Inclusief kozijn en beglazingskit",
|
||||||
|
],
|
||||||
|
image: "/images/taats.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "buitendeuren",
|
||||||
|
icon: Home,
|
||||||
|
title: "Stalen buitendeuren",
|
||||||
|
description:
|
||||||
|
"Stalen buitendeuren van Proinn zijn ontworpen om jarenlang mee te gaan. Met thermisch onderbroken profielen en veiligheidsbeglazingen vormen zij een duurzame en stijlvolle entree voor uw woning of bedrijfspand.",
|
||||||
|
features: [
|
||||||
|
"Thermisch onderbroken profielen",
|
||||||
|
"HR++ of triple beglazing",
|
||||||
|
"Inbraakwerend (WK2 mogelijk)",
|
||||||
|
"Poedercoating in RAL-kleur naar keuze",
|
||||||
|
],
|
||||||
|
image: "/images/scharnier.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kantoorwanden",
|
||||||
|
icon: Grid2x2,
|
||||||
|
title: "Stalen kantoorwanden",
|
||||||
|
description:
|
||||||
|
"Creeer een open en lichte werkomgeving met onze stalen kantoorwanden. De slanke staalprofielen laten maximaal licht door en zorgen tegelijkertijd voor een visuele scheiding tussen werkplekken.",
|
||||||
|
features: [
|
||||||
|
"Vaste of opendraaiende panelen",
|
||||||
|
"Akoestisch isolerend glas beschikbaar",
|
||||||
|
"Diverse afmetingen en indelingen",
|
||||||
|
"Passend in bestaande kantoorruimtes",
|
||||||
|
],
|
||||||
|
image: "/images/paneel.jpg",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const uspItems = [
|
||||||
|
{
|
||||||
|
icon: Ruler,
|
||||||
|
title: "100% maatwerk",
|
||||||
|
description: "Elke deur wordt op maat gemaakt voor een perfecte pasvorm.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Palette,
|
||||||
|
title: "Kleur naar keuze",
|
||||||
|
description:
|
||||||
|
"Standaard kleuren of het volledige RAL-palet. U kiest, wij coaten.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: "Duurzame kwaliteit",
|
||||||
|
description:
|
||||||
|
"Poedercoating en hoogwaardig staal voor jarenlang resultaat.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ProductenPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="bg-[#1A2E2E] pt-32 pb-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-[#C4D668]">
|
||||||
|
Ons assortiment
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-light leading-tight text-white sm:text-5xl">
|
||||||
|
Stalen deuren &{" "}
|
||||||
|
<span className="font-semibold">wanden</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 max-w-xl text-base leading-relaxed text-gray-400">
|
||||||
|
Van stijlvolle binnendeuren tot robuuste buitendeuren en
|
||||||
|
functionele kantoorwanden. Alles volledig op maat, handgemaakt
|
||||||
|
in onze werkplaats in Roosendaal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* USP Bar */}
|
||||||
|
<section className="border-b border-gray-200 bg-white">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 divide-y divide-gray-200 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
|
||||||
|
{uspItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.title}
|
||||||
|
className="flex items-center gap-4 px-6 py-8"
|
||||||
|
>
|
||||||
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-[#F5F5F3]">
|
||||||
|
<Icon className="size-5 text-[#1A2E2E]" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Product Sections */}
|
||||||
|
{products.map((product, index) => {
|
||||||
|
const Icon = product.icon;
|
||||||
|
const isReversed = index % 2 !== 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
key={product.id}
|
||||||
|
id={product.id}
|
||||||
|
className={index % 2 === 0 ? "bg-[#F5F5F3] py-20" : "bg-white py-20"}
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div
|
||||||
|
className={`grid gap-12 lg:grid-cols-2 lg:gap-16 ${
|
||||||
|
isReversed ? "lg:[direction:rtl] lg:[&>*]:[direction:ltr]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative min-h-[350px] overflow-hidden rounded-2xl lg:min-h-[450px]">
|
||||||
|
<Image
|
||||||
|
src={product.image}
|
||||||
|
alt={product.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<div className="flex size-10 items-center justify-center rounded-lg bg-[#1A2E2E]">
|
||||||
|
<Icon className="size-5 text-[#C4D668]" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-light text-gray-900 lg:text-4xl">
|
||||||
|
{product.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mb-6 text-sm leading-relaxed text-gray-600">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mb-8 space-y-2.5">
|
||||||
|
{product.features.map((feature) => (
|
||||||
|
<li
|
||||||
|
key={feature}
|
||||||
|
className="flex items-start gap-2.5 text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
<span className="mt-1.5 block size-1.5 shrink-0 rounded-full bg-[#C4D668]" />
|
||||||
|
{feature}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/offerte"
|
||||||
|
className="inline-flex w-fit items-center gap-2 rounded-md bg-[#C4D668] px-6 py-3 text-sm font-semibold text-black transition-colors hover:bg-[#b5c75a]"
|
||||||
|
>
|
||||||
|
Configureer uw deur
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Maatwerk Section */}
|
||||||
|
<section id="maatwerk" className="bg-[#1A2E2E] py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-2xl">
|
||||||
|
<div className="mb-4 flex items-center justify-center gap-2">
|
||||||
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
|
<span className="text-xs font-medium tracking-widest uppercase text-[#C4D668]">
|
||||||
|
Maatwerk
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-5 text-3xl font-bold text-white lg:text-4xl">
|
||||||
|
Iets speciaals in gedachten?
|
||||||
|
</h2>
|
||||||
|
<p className="mb-8 text-sm leading-relaxed text-gray-400">
|
||||||
|
Heeft u een uniek ontwerp in gedachten dat niet in onze standaard
|
||||||
|
configurator past? Geen probleem. Neem contact met ons op en wij
|
||||||
|
bekijken samen wat er mogelijk is. Van bijzondere glasverdelingen
|
||||||
|
tot speciale afwerkingen: als het in staal kan, maken wij het.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||||
|
<Link
|
||||||
|
href="/offerte"
|
||||||
|
className="inline-flex items-center gap-2 rounded-md bg-[#C4D668] px-6 py-3 text-sm font-semibold text-black transition-colors hover:bg-[#b5c75a]"
|
||||||
|
>
|
||||||
|
Start de configurator
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/contact"
|
||||||
|
className="inline-flex items-center gap-2 rounded-md border border-white/20 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Neem contact op
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
217
app/voorwaarden/page.tsx
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Algemene Voorwaarden | PROINN",
|
||||||
|
description:
|
||||||
|
"Algemene voorwaarden van Proinn voor de levering van stalen deuren en aanverwante producten.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function VoorwaardenPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="bg-[#1A2E2E] pt-32 pb-16">
|
||||||
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 className="text-3xl font-light text-white sm:text-4xl">
|
||||||
|
Algemene Voorwaarden
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-sm text-gray-400">
|
||||||
|
Laatst bijgewerkt: februari 2025
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-white py-16">
|
||||||
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="space-y-8 text-sm leading-relaxed text-gray-600">
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 1 - Definities
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Proinn:</strong> gevestigd te Schotsbossenstraat 2,
|
||||||
|
4705AG Roosendaal, ingeschreven bij de KvK onder nummer
|
||||||
|
85086991.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Opdrachtgever:</strong> de natuurlijke of
|
||||||
|
rechtspersoon die met Proinn een overeenkomst aangaat.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Overeenkomst:</strong> iedere afspraak of
|
||||||
|
overeenkomst tussen Proinn en Opdrachtgever waarvan deze
|
||||||
|
algemene voorwaarden integraal onderdeel uitmaken.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Product:</strong> stalen deuren, kozijnen, wanden
|
||||||
|
en aanverwante producten die door Proinn worden vervaardigd
|
||||||
|
en geleverd.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 2 - Toepasselijkheid
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Deze algemene voorwaarden zijn van toepassing op alle
|
||||||
|
aanbiedingen, offertes en overeenkomsten van Proinn.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Afwijkingen van deze voorwaarden zijn uitsluitend geldig
|
||||||
|
indien schriftelijk overeengekomen.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Eventuele voorwaarden van de Opdrachtgever worden uitdrukkelijk
|
||||||
|
van de hand gewezen.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 3 - Offertes en aanbiedingen
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Alle offertes van Proinn zijn vrijblijvend en hebben een
|
||||||
|
geldigheid van 30 dagen, tenzij anders vermeld.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Prijzen in offertes zijn inclusief BTW, tenzij anders
|
||||||
|
aangegeven.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Een overeenkomst komt tot stand na schriftelijke bevestiging
|
||||||
|
door Proinn of na aanvang van de werkzaamheden.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 4 - Levering en montage
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Levertijden worden bij benadering opgegeven en zijn nimmer
|
||||||
|
te beschouwen als fatale termijnen.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Levering geschiedt op het door Opdrachtgever opgegeven adres.
|
||||||
|
De Opdrachtgever dient ervoor te zorgen dat de locatie
|
||||||
|
bereikbaar en geschikt is voor montage.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Het risico van de producten gaat over op de Opdrachtgever
|
||||||
|
op het moment van levering.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 5 - Betaling
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Betaling dient te geschieden binnen 14 dagen na
|
||||||
|
factuurdatum, tenzij anders overeengekomen.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Bij maatwerk producten kan Proinn een aanbetaling van 50%
|
||||||
|
verlangen voorafgaand aan de productie.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Bij niet-tijdige betaling is de Opdrachtgever van
|
||||||
|
rechtswege in verzuim en is Proinn gerechtigd de wettelijke
|
||||||
|
rente en incassokosten in rekening te brengen.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 6 - Garantie
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Proinn garandeert dat de geleverde producten voldoen aan de
|
||||||
|
in de overeenkomst vastgelegde specificaties.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Op de constructie van de stalen deuren geldt een garantie
|
||||||
|
van 5 jaar. Op de poedercoating geldt een garantie van 2
|
||||||
|
jaar bij normaal gebruik.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Garantie vervalt bij onjuist gebruik, gebrekkig onderhoud
|
||||||
|
of wijzigingen aan het product door derden.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 7 - Annulering
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
Bij annulering na akkoord op de offerte is Proinn gerechtigd
|
||||||
|
de reeds gemaakte kosten en een redelijke vergoeding voor
|
||||||
|
gederfde winst in rekening te brengen.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Maatwerk producten die reeds in productie zijn genomen
|
||||||
|
kunnen niet worden geannuleerd.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 8 - Aansprakelijkheid
|
||||||
|
</h2>
|
||||||
|
<ol className="ml-4 list-decimal space-y-1">
|
||||||
|
<li>
|
||||||
|
De aansprakelijkheid van Proinn is beperkt tot het bedrag
|
||||||
|
dat in het betreffende geval onder de
|
||||||
|
aansprakelijkheidsverzekering wordt uitbetaald.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Proinn is niet aansprakelijk voor indirecte schade,
|
||||||
|
gevolgschade of gederfde winst.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-gray-900">
|
||||||
|
Artikel 9 - Toepasselijk recht
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Op alle overeenkomsten tussen Proinn en de Opdrachtgever is
|
||||||
|
Nederlands recht van toepassing. Geschillen worden voorgelegd
|
||||||
|
aan de bevoegde rechter in het arrondissement Zeeland-West-Brabant.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl bg-[#F5F5F3] p-6">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
<strong className="text-gray-900">Proinn</strong>
|
||||||
|
<br />
|
||||||
|
Schotsbossenstraat 2, 4705AG Roosendaal
|
||||||
|
<br />
|
||||||
|
KVK: 85086991 | BTW: NL863503330.B01
|
||||||
|
<br />
|
||||||
|
Tel: 0165 311 490 | E-mail: info@proinn.nl
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
bron/App.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
|
||||||
|
import Endpoint from "./components/endpoint";
|
||||||
|
import RequestConfirmation from "./pages/RequestConfirmation";
|
||||||
|
import BubblesManager from "./pages/BubblesManager";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Endpoint />} />
|
||||||
|
<Route path="/request-confirmation" element={<RequestConfirmation />} />
|
||||||
|
<Route path="/manage-bubble" element={<BubblesManager />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
bron/Appp.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
|
||||||
|
import Endpoint from "./components/endpoint";
|
||||||
|
import RequestConfirmation from "./pages/RequestConfirmation";
|
||||||
|
import BubblesManager from "./pages/BubblesManager";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Endpoint />} />
|
||||||
|
<Route path="/request-confirmation" element={<RequestConfirmation />} />
|
||||||
|
<Route path="/manage-bubble" element={<BubblesManager />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
bron/BubblesManager.js
Normal file
@@ -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 (
|
||||||
|
<div className="container mx-auto p-6 max-w-md">
|
||||||
|
<h1 className="text-3xl font-bold mb-6 text-center">Bubble Manager</h1>
|
||||||
|
{/* <div className="mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleName}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleText}
|
||||||
|
onChange={(e) => 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 ? (
|
||||||
|
<button
|
||||||
|
onClick={updateBubble}
|
||||||
|
className="bg-yellow-500 text-white rounded p-2 w-full hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Update Bubble
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={addBubble}
|
||||||
|
className="bg-blue-500 text-white rounded p-2 w-full hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Add Bubble
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div> */}
|
||||||
|
{isEditing && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleName}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleText}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={updateBubble}
|
||||||
|
className="bg-yellow-500 text-white rounded p-2 w-full hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Update Bubble
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul className="list-none p-0">
|
||||||
|
{bubbles.map((bubble, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex justify-between items-center mb-2 bg-gray-100 p-2 rounded shadow"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="font-bold">{bubble.name}</h2>
|
||||||
|
<p>{bubble.text}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => editBubble(index)}
|
||||||
|
className="bg-yellow-500 text-white rounded p-1 mr-2 hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BubblesManager;
|
||||||
161
bron/BubblesManagerr.js
Normal file
@@ -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 (
|
||||||
|
<div className="container mx-auto p-6 max-w-md">
|
||||||
|
<h1 className="text-3xl font-bold mb-6 text-center">Bubble Manager</h1>
|
||||||
|
{/* <div className="mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleName}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleText}
|
||||||
|
onChange={(e) => 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 ? (
|
||||||
|
<button
|
||||||
|
onClick={updateBubble}
|
||||||
|
className="bg-yellow-500 text-white rounded p-2 w-full hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Update Bubble
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={addBubble}
|
||||||
|
className="bg-blue-500 text-white rounded p-2 w-full hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Add Bubble
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div> */}
|
||||||
|
{isEditing && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleName}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bubbleText}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={updateBubble}
|
||||||
|
className="bg-yellow-500 text-white rounded p-2 w-full hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Update Bubble
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul className="list-none p-0">
|
||||||
|
{bubbles.map((bubble, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex justify-between items-center mb-2 bg-gray-100 p-2 rounded shadow"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="font-bold">{bubble.name}</h2>
|
||||||
|
<p>{bubble.text}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => editBubble(index)}
|
||||||
|
className="bg-yellow-500 text-white rounded p-1 mr-2 hover:bg-yellow-600"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BubblesManager;
|
||||||
84
bron/ColorSelector.js
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"absolute bg-neutral-200 bottom-0 left-[108px] flex flex-col " +
|
||||||
|
(!colorPickerOpened ? "hidden" : "")
|
||||||
|
}
|
||||||
|
style={{ width: "180px" }}
|
||||||
|
>
|
||||||
|
<div className="bg-gray-200 p-2 text-xs flex flex-row items-center">
|
||||||
|
<div>Color selector</div>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<IoMdClose
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setColorPickerOpened(false)}
|
||||||
|
size={iconSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-1 flex flex-col gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||||
|
<RiSearchLine className="text-gray-300" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
type="text"
|
||||||
|
placeholder="RAL code"
|
||||||
|
className="pl-8 pr-2 py-1 border border-gray-300 rounded bg-white text-xs w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="" style={{ overflowY: "scroll", maxHeight: "350px" }}>
|
||||||
|
<div className="flex flex-row flex-wrap gap-1">
|
||||||
|
{resColors.map((c) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1210
bron/DoorHole.js
Normal file
BIN
bron/Handle%203.glb
Normal file
BIN
bron/Handle%204.glb
Normal file
51
bron/InfoIcon.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useContext, useEffect } from "react";
|
||||||
|
import { MyContext } from "../data/contextapi";
|
||||||
|
import Tooltip from "../../Tooltip";
|
||||||
|
|
||||||
|
const InfoIcon = ({ title, containerStyles }) => {
|
||||||
|
const { bubbles } = useContext(MyContext);
|
||||||
|
|
||||||
|
const bubbleText = bubbles?.find(({ name }) => name === title)
|
||||||
|
? bubbles.find(({ name }) => name === title).text
|
||||||
|
: "";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
...containerStyles,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<Tooltip message={bubbleText}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
borderRadius: "100%",
|
||||||
|
backgroundColor: "#bdc79d",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="currentColor"
|
||||||
|
className="bi bi-info"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoIcon;
|
||||||
145
bron/RequestConfirmation.js
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
<div className="w-full h-20 bg-[#bdc79d]"></div>
|
||||||
|
<div className="mt-10 p-8">
|
||||||
|
<div className="max-w-5xl mx-auto bg-[#ececec] shadow-lg">
|
||||||
|
<div className="p-6 pt-0 rounded-t-lg text-center">
|
||||||
|
<div className="w-1/6">
|
||||||
|
<img src="/images/Logo_CLOOZ.jpg" className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 text-left px-[15%]">
|
||||||
|
<h2 className="text-4xl font-bold mb-0">
|
||||||
|
BEDANKT VOOR UW AANVRAAG!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-8">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-5xl mx-auto mt-4 shadow-lg">
|
||||||
|
<div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Waarom kies
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Je Clooz doors
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Why choose.jpg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Eigen fabriek</li>
|
||||||
|
<li>Uniek eigen profiel</li>
|
||||||
|
<li>Snelste levering</li>
|
||||||
|
<li>Perfecte afwerking</li>
|
||||||
|
<li>Kindveilig product</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Waarom het
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Clooz doors profiel?
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Why it.jpg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Tweezijdig hetzelfde aanzicht</li>
|
||||||
|
<li>Elke design mogelijk</li>
|
||||||
|
<li>Ongedeelde glasplaat</li>
|
||||||
|
<li>Rankste profiel</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Bezoek onze
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">SHOWROOM</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Visit our.jpg" />
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Met persoonlijk advies</li>
|
||||||
|
<li>Alle deuren</li>
|
||||||
|
<li>Ontwerp samen</li>
|
||||||
|
<li>Ook op zaterdag</li>
|
||||||
|
<li>Centraal in NL</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Gebruik onze
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
inmeetservice
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Use our.jpg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Door heel NL</li>
|
||||||
|
<li>Voorkom fouten</li>
|
||||||
|
<li>Voorkom kosten</li>
|
||||||
|
<li>Met het beste advies</li>
|
||||||
|
<li>Voor het gemak</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
bron/RequestConfirmationn.js
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
<div className="w-full h-20 bg-[#bdc79d]"></div>
|
||||||
|
<div className="mt-10 p-8">
|
||||||
|
<div className="max-w-5xl mx-auto bg-[#ececec] shadow-lg">
|
||||||
|
<div className="p-6 pt-0 rounded-t-lg text-center">
|
||||||
|
<div className="w-1/6">
|
||||||
|
<img src="/images/Logo_CLOOZ.jpg" className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 text-left px-[15%]">
|
||||||
|
<h2 className="text-4xl font-bold mb-0">
|
||||||
|
BEDANKT VOOR UW AANVRAAG!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-8">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-5xl mx-auto mt-4 shadow-lg">
|
||||||
|
<div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Waarom kies
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Je Clooz doors
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Why choose.jpg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Eigen fabriek</li>
|
||||||
|
<li>Uniek eigen profiel</li>
|
||||||
|
<li>Snelste levering</li>
|
||||||
|
<li>Perfecte afwerking</li>
|
||||||
|
<li>Kindveilig product</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Waarom het
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Clooz doors profiel?
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Why it.jpg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Tweezijdig hetzelfde aanzicht</li>
|
||||||
|
<li>Elke design mogelijk</li>
|
||||||
|
<li>Ongedeelde glasplaat</li>
|
||||||
|
<li>Rankste profiel</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Bezoek onze
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">SHOWROOM</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Visit our.jpg" />
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Met persoonlijk advies</li>
|
||||||
|
<li>Alle deuren</li>
|
||||||
|
<li>Ontwerp samen</li>
|
||||||
|
<li>Ook op zaterdag</li>
|
||||||
|
<li>Centraal in NL</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#bdc79d] border">
|
||||||
|
<div className="px-4 pt-6 pb-4">
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
Gebruik onze
|
||||||
|
</h3>
|
||||||
|
<h3 className="font-semibold text-white text-xl">
|
||||||
|
inmeetservice
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-[#000]">
|
||||||
|
<img src="/images/request confirm/Use our.jpg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-3 pb-4">
|
||||||
|
<ul className="text-left text-white text-base list-disc list-inside">
|
||||||
|
<li>Door heel NL</li>
|
||||||
|
<li>Voorkom fouten</li>
|
||||||
|
<li>Voorkom kosten</li>
|
||||||
|
<li>Met het beste advies</li>
|
||||||
|
<li>Voor het gemak</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
416
bron/RequestForm.js
Normal file
@@ -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 (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<TechInformation techInformation={techInformation} />
|
||||||
|
<div style={formStyle}>
|
||||||
|
<h2 style={{ color: "#FFF", marginBottom: "20px", fontSize: "16px" }}>
|
||||||
|
Vul uw gegevens in
|
||||||
|
</h2>
|
||||||
|
<div style={{ marginBottom: "10px" }}>
|
||||||
|
<select
|
||||||
|
name="aanhef"
|
||||||
|
value={formData.aanhef}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{
|
||||||
|
...inputStyle,
|
||||||
|
border: formErrors.aanhef ? "1px solid red" : "none",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="Dhr">Dhr.</option>
|
||||||
|
<option value="Mevr">Mevr.</option>
|
||||||
|
</select>
|
||||||
|
{formErrors.aanhef && <p style={errorStyle}>{formErrors.aanhef}</p>}
|
||||||
|
</div>
|
||||||
|
{[
|
||||||
|
"voornaam",
|
||||||
|
"tussenvoegsel",
|
||||||
|
"achternaam",
|
||||||
|
"straatnaam",
|
||||||
|
"huisnummer",
|
||||||
|
"postcode",
|
||||||
|
"woonplaats",
|
||||||
|
"emailadres",
|
||||||
|
"telefoonnummer",
|
||||||
|
].map((field) => (
|
||||||
|
<div
|
||||||
|
key={field}
|
||||||
|
style={{
|
||||||
|
marginBottom: "10px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={field}
|
||||||
|
value={formData[field]}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={field.charAt(0).toUpperCase() + field.slice(1)}
|
||||||
|
style={{
|
||||||
|
...inputStyle,
|
||||||
|
border: formErrors[field] ? "1px solid red" : "none",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{formErrors[field] && <p style={errorStyle}>{formErrors[field]}</p>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ marginBottom: "10px" }}>
|
||||||
|
<select
|
||||||
|
name="land"
|
||||||
|
value={formData.land}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{
|
||||||
|
...inputStyle,
|
||||||
|
border: formErrors.land ? "1px solid red" : "none",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="Nederland">Nederland</option>
|
||||||
|
<option value="Spanje">Spanje</option>
|
||||||
|
<option value="Duitsland">Duitsland</option>
|
||||||
|
<option value="België">België</option>
|
||||||
|
{/* Add more options as needed */}
|
||||||
|
</select>
|
||||||
|
{formErrors.land && <p style={errorStyle}>{formErrors.land}</p>}
|
||||||
|
</div>
|
||||||
|
<div key="comment" style={{ marginBottom: "10px", display: "flex" }}>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
name="comment"
|
||||||
|
value={formData.comment}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Opmerkingen"
|
||||||
|
style={{
|
||||||
|
...inputStyle,
|
||||||
|
border: formErrors.comment ? "1px solid red" : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{formErrors.comment && <p style={errorStyle}>{formErrors.comment}</p>}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: "red", marginBottom: "20px" }}>
|
||||||
|
* Vul alle velden correct in.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: "10px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label htmlFor="file" style={labelStyle}>
|
||||||
|
Afbeelding bijvoegen:
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
htmlFor="file"
|
||||||
|
style={{
|
||||||
|
...fileUploadLabelStyle,
|
||||||
|
backgroundColor: formData.file ? "#48BB78" : "red",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src="/images/upload.png" width={20} />
|
||||||
|
Kies bestand
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
id="file"
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{
|
||||||
|
...fileInputStyle,
|
||||||
|
border: formErrors.file ? "1px solid red" : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{formErrors.file && <p style={errorStyle}>{formErrors.file}</p>}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "end",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSubmit(false)}
|
||||||
|
style={{ ...buttonStyle, marginTop: "10px", minHeight: "44px", minWidth: "187px" }}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`relative ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSubmitting && !isClearSubmitting ? (
|
||||||
|
<div className="absolute inset-0 flex justify-center items-center">
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Offerte aanvragen"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSubmit(true)}
|
||||||
|
style={{ ...buttonStyle, marginTop: "20px", width: "100%", minHeight: "44px" }}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`relative ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSubmitting && isClearSubmitting ? (
|
||||||
|
<div className="absolute inset-0 flex justify-center items-center">
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Versturen en nog een offerte aanvragen"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequestForm;
|
||||||
146
bron/Stalen.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { MyContext } from './data/contextapi';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import Structure from './Structure';
|
||||||
|
|
||||||
|
export default function Stalen() {
|
||||||
|
const { width, height, stalenPart, stalenType, frameSize } = useContext(MyContext);
|
||||||
|
|
||||||
|
const doorWidth = (0.06 * width) / stalenPart;
|
||||||
|
const doorHeightn = 0.053 * height;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{stalenType == 'tussen' ? (
|
||||||
|
<>
|
||||||
|
{stalenPart >= 1 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
(0.06 * width + 4 * frameSize + (stalenPart - 1) * 3.5 * frameSize) / 2 - (doorWidth + (4 * frameSize + (stalenPart - 1) * 4 * frameSize) / stalenPart) / 2 - 3 * frameSize/2,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<group position={[0, 0, 0]}>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
{stalenPart >= 2 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
(0.06 * width + 4 * frameSize + (stalenPart - 1) * 3.5 * frameSize) / 2 - 3 * (doorWidth + (4 * frameSize + (stalenPart - 1) * 4 * frameSize) / stalenPart) / 2 - frameSize,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<group
|
||||||
|
position={[0, 0, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
{stalenPart >= 3 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
(0.06 * width + 4 * frameSize + (stalenPart - 1) * 3.5 * frameSize) / 2 - 5 * (doorWidth + (4 * frameSize + (stalenPart - 1) * 4 * frameSize) / stalenPart) / 2,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<group position={[0, 0, 0]}>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
{stalenPart >= 4 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
(0.06 * width + 4 * frameSize + (stalenPart - 1) * 3.5 * frameSize) / 2 - 7 * (doorWidth + (4 * frameSize + (stalenPart - 1) * 4 * frameSize) / stalenPart) / 2 + frameSize / 2,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<group position={[0, 0, 0]}>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{stalenPart >= 1 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
2 * (0.06 * width + 4 * frameSize) - (doorWidth + 4 * frameSize),
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<group
|
||||||
|
position={[-doorWidth / 2, 0, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
{stalenPart >= 2 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
2 * (0.06 * width + 4 * frameSize) - 2 * (doorWidth + 4 * frameSize) + frameSize / 2,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<group
|
||||||
|
position={[-doorWidth / 2, 0, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
{stalenPart >= 3 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
2 * (0.06 * width + 4 * frameSize) - 3 * (doorWidth + 4 * frameSize) + frameSize,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<group
|
||||||
|
position={[-doorWidth / 2, 0, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
{stalenPart >= 4 && (
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
2 * (0.06 * width + 4 * frameSize) - 4 * (doorWidth + 4 * frameSize) + 3 * frameSize/2,
|
||||||
|
(-12 + doorHeightn) * 0.5,
|
||||||
|
-13.5,
|
||||||
|
]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<group
|
||||||
|
position={[-doorWidth / 2, 0, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
>
|
||||||
|
<Structure sizePannel={0} stalenWidth={doorWidth} />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
2742
bron/Structure.js
Normal file
477
bron/TechInformation.js
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function TechInformation({techInformation}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Technische informatie</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Type deur
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Deur type
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span>{techInformation.type}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aantal deuren in offerte
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Samenstelling deur
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.doorConfig}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aantal en plaats zijpanelen
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Samenstelling zijpaneel
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.sidePannel}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Verdeling elementen
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Paneel verdeling
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.sidePannelConfig}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{techInformation.sidePannelConfig === "eigen maat" && (
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Breedte deur
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Breedte deur
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.sidePannelSize}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Breedte deur
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{techInformation.width}
|
||||||
|
</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hoogte sparing
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.height}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Breedte sparing
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{techInformation.holeWidth}
|
||||||
|
</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Vlakverdeling
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Design
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.door}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Handgreep
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Handgreep
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.handle}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kleur glas
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Glas soort
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.colorGlass}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
RAL Kleur
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kleur
|
||||||
|
</span>
|
||||||
|
<span>{techInformation.steelColor}</span>
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%", marginTop: "15px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "2% 5% 2% 5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
borderBottom: "1px solid GrayText",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Verzending en montage
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "GrayText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Extra opties
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", flexDirection: "column", width: "80%" }}
|
||||||
|
>
|
||||||
|
{techInformation.extraOptions.map((extraOption, index) => {
|
||||||
|
return (
|
||||||
|
<span style={{ fontSize: "14px" }} key={`extra-${index}`}>
|
||||||
|
- {extraOption}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <span>€ 0,00</span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
bron/Tooltip.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Tooltip = ({ message, children }) => {
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
{children}
|
||||||
|
{message !== "" && (
|
||||||
|
<div className="absolute top-[30px] left-1/2 w-[140px] transform -translate-x-1/2 mb-2 hidden group-hover:block z-10">
|
||||||
|
<div className="relative bg-gray-800 text-white text-xs rounded py-1 px-4">
|
||||||
|
{message}
|
||||||
|
<div className="absolute top-[-5px] left-1/2 transform -translate-x-1/2 w-3 h-3 bg-gray-800 rotate-45"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
27
bron/bootstrap
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// The module cache
|
||||||
|
var __webpack_module_cache__ = {};
|
||||||
|
|
||||||
|
// The require function
|
||||||
|
function __webpack_require__(moduleId) {
|
||||||
|
// Check if module is in cache
|
||||||
|
var cachedModule = __webpack_module_cache__[moduleId];
|
||||||
|
if (cachedModule !== undefined) {
|
||||||
|
return cachedModule.exports;
|
||||||
|
}
|
||||||
|
// Create a new module (and put it into the cache)
|
||||||
|
var module = __webpack_module_cache__[moduleId] = {
|
||||||
|
id: moduleId,
|
||||||
|
loaded: false,
|
||||||
|
exports: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the module function
|
||||||
|
__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
|
||||||
|
// Flag the module as loaded
|
||||||
|
module.loaded = true;
|
||||||
|
|
||||||
|
// Return the exports of the module
|
||||||
|
return module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
8
bron/compat get default export
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
__webpack_require__.n = (module) => {
|
||||||
|
var getter = module && module.__esModule ?
|
||||||
|
() => (module['default']) :
|
||||||
|
() => (module);
|
||||||
|
__webpack_require__.d(getter, { a: getter });
|
||||||
|
return getter;
|
||||||
|
};
|
||||||
601
bron/config.cloozdoors.nl-1761175825660.log
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
DoorHole.js:345 THREE.Material: 'castShadow' is not a property of THREE.MeshStandardMaterial.
|
||||||
|
setValues @ three.module.js:9206
|
||||||
|
Ip @ three.module.js:40946
|
||||||
|
(anonymous) @ DoorHole.js:345
|
||||||
|
useMemo @ react-reconciler.production.min.js:93
|
||||||
|
t.useMemo @ react.production.min.js:26
|
||||||
|
zR @ DoorHole.js:344
|
||||||
|
kr @ react-reconciler.production.min.js:78
|
||||||
|
ca @ react-reconciler.production.min.js:196
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
DoorHole.js:359 THREE.Material: 'castShadow' is not a property of THREE.MeshStandardMaterial.
|
||||||
|
setValues @ three.module.js:9206
|
||||||
|
Ip @ three.module.js:40946
|
||||||
|
(anonymous) @ DoorHole.js:359
|
||||||
|
useMemo @ react-reconciler.production.min.js:93
|
||||||
|
t.useMemo @ react.production.min.js:26
|
||||||
|
zR @ DoorHole.js:358
|
||||||
|
kr @ react-reconciler.production.min.js:78
|
||||||
|
ca @ react-reconciler.production.min.js:196
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
DoorHole.js:372 THREE.Material: 'castShadow' is not a property of THREE.MeshStandardMaterial.
|
||||||
|
setValues @ three.module.js:9206
|
||||||
|
Ip @ three.module.js:40946
|
||||||
|
(anonymous) @ DoorHole.js:372
|
||||||
|
useMemo @ react-reconciler.production.min.js:93
|
||||||
|
t.useMemo @ react.production.min.js:26
|
||||||
|
zR @ DoorHole.js:371
|
||||||
|
kr @ react-reconciler.production.min.js:78
|
||||||
|
ca @ react-reconciler.production.min.js:196
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
DoorHole.js:345 THREE.Material: 'castShadow' is not a property of THREE.MeshStandardMaterial.
|
||||||
|
setValues @ three.module.js:9206
|
||||||
|
Ip @ three.module.js:40946
|
||||||
|
(anonymous) @ DoorHole.js:345
|
||||||
|
useMemo @ react-reconciler.production.min.js:93
|
||||||
|
t.useMemo @ react.production.min.js:26
|
||||||
|
zR @ DoorHole.js:344
|
||||||
|
kr @ react-reconciler.production.min.js:78
|
||||||
|
ca @ react-reconciler.production.min.js:196
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
DoorHole.js:359 THREE.Material: 'castShadow' is not a property of THREE.MeshStandardMaterial.
|
||||||
|
setValues @ three.module.js:9206
|
||||||
|
Ip @ three.module.js:40946
|
||||||
|
(anonymous) @ DoorHole.js:359
|
||||||
|
useMemo @ react-reconciler.production.min.js:93
|
||||||
|
t.useMemo @ react.production.min.js:26
|
||||||
|
zR @ DoorHole.js:358
|
||||||
|
kr @ react-reconciler.production.min.js:78
|
||||||
|
ca @ react-reconciler.production.min.js:196
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
DoorHole.js:372 THREE.Material: 'castShadow' is not a property of THREE.MeshStandardMaterial.
|
||||||
|
setValues @ three.module.js:9206
|
||||||
|
Ip @ three.module.js:40946
|
||||||
|
(anonymous) @ DoorHole.js:372
|
||||||
|
useMemo @ react-reconciler.production.min.js:93
|
||||||
|
t.useMemo @ react.production.min.js:26
|
||||||
|
zR @ DoorHole.js:371
|
||||||
|
kr @ react-reconciler.production.min.js:78
|
||||||
|
ca @ react-reconciler.production.min.js:196
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
n @ index-99983b2d.esm.js:64
|
||||||
|
Ii @ react-reconciler.production.min.js:109
|
||||||
|
Ua @ react-reconciler.production.min.js:187
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
n @ index-99983b2d.esm.js:64
|
||||||
|
Ii @ react-reconciler.production.min.js:109
|
||||||
|
Ua @ react-reconciler.production.min.js:187
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
n @ index-99983b2d.esm.js:64
|
||||||
|
Ii @ react-reconciler.production.min.js:109
|
||||||
|
Ua @ react-reconciler.production.min.js:187
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
n @ index-99983b2d.esm.js:64
|
||||||
|
Ii @ react-reconciler.production.min.js:109
|
||||||
|
Ua @ react-reconciler.production.min.js:187
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29710
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
n @ index-99983b2d.esm.js:64
|
||||||
|
Ii @ react-reconciler.production.min.js:109
|
||||||
|
Ua @ react-reconciler.production.min.js:187
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
Fa @ react-reconciler.production.min.js:186
|
||||||
|
Ma @ react-reconciler.production.min.js:175
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
commitUpdate @ index-99983b2d.esm.js:303
|
||||||
|
Ms @ react-reconciler.production.min.js:155
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:161
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:189
|
||||||
|
za @ react-reconciler.production.min.js:187
|
||||||
|
Ma @ react-reconciler.production.min.js:177
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
commitUpdate @ index-99983b2d.esm.js:303
|
||||||
|
Ms @ react-reconciler.production.min.js:155
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:161
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:189
|
||||||
|
za @ react-reconciler.production.min.js:187
|
||||||
|
Ma @ react-reconciler.production.min.js:177
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
commitUpdate @ index-99983b2d.esm.js:303
|
||||||
|
Ms @ react-reconciler.production.min.js:155
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:161
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:189
|
||||||
|
za @ react-reconciler.production.min.js:187
|
||||||
|
Ma @ react-reconciler.production.min.js:177
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29710
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
commitUpdate @ index-99983b2d.esm.js:303
|
||||||
|
Ms @ react-reconciler.production.min.js:155
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:161
|
||||||
|
(anonymous) @ react-reconciler.production.min.js:189
|
||||||
|
za @ react-reconciler.production.min.js:187
|
||||||
|
Ma @ react-reconciler.production.min.js:177
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
endpoint.js:133 Error fetching bubbles: Zy {message: 'Network Error', name: 'AxiosError', code: 'ERR_NETWORK', config: {…}, request: XMLHttpRequest, …}
|
||||||
|
console.error @ index.tsx:86
|
||||||
|
Qe @ endpoint.js:133
|
||||||
|
await in Qe
|
||||||
|
(anonymous) @ endpoint.js:116
|
||||||
|
rl @ react-dom.production.min.js:243
|
||||||
|
wc @ react-dom.production.min.js:285
|
||||||
|
(anonymous) @ react-dom.production.min.js:281
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
endpoint.js:128 GET https://api.config-fencing.com/api/get-bubbles net::ERR_CONNECTION_TIMED_OUT
|
||||||
|
(anonymous) @ xhr.js:188
|
||||||
|
xhr @ xhr.js:15
|
||||||
|
hb @ dispatchRequest.js:51
|
||||||
|
_request @ Axios.js:173
|
||||||
|
request @ Axios.js:40
|
||||||
|
Qy.forEach.yb.<computed> @ Axios.js:199
|
||||||
|
(anonymous) @ bind.js:5
|
||||||
|
Qe @ endpoint.js:128
|
||||||
|
(anonymous) @ endpoint.js:116
|
||||||
|
rl @ react-dom.production.min.js:243
|
||||||
|
wc @ react-dom.production.min.js:285
|
||||||
|
(anonymous) @ react-dom.production.min.js:281
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
invalidate @ index-99983b2d.esm.js:1300
|
||||||
|
lv @ index-99983b2d.esm.js:779
|
||||||
|
ov @ index-99983b2d.esm.js:758
|
||||||
|
n @ index-99983b2d.esm.js:64
|
||||||
|
Ii @ react-reconciler.production.min.js:109
|
||||||
|
Ua @ react-reconciler.production.min.js:187
|
||||||
|
Oa @ react-reconciler.production.min.js:186
|
||||||
|
ja @ react-reconciler.production.min.js:186
|
||||||
|
Da @ react-reconciler.production.min.js:186
|
||||||
|
Ra @ react-reconciler.production.min.js:179
|
||||||
|
Yt @ react-reconciler.production.min.js:39
|
||||||
|
w @ scheduler.production.min.js:13
|
||||||
|
B @ scheduler.production.min.js:14
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29708
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
(anonymous) @ index-99983b2d.esm.js:1590
|
||||||
|
t @ index-99983b2d.esm.js:1590
|
||||||
|
sE @ gsap-core.js:990
|
||||||
|
n.render @ gsap-core.js:3464
|
||||||
|
n.render @ gsap-core.js:2257
|
||||||
|
pC @ gsap-core.js:192
|
||||||
|
t.updateRoot @ gsap-core.js:2695
|
||||||
|
n @ gsap-core.js:1316
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
three.module.js:19367 THREE.WebGLProgram: Unsupported toneMapping: srgb
|
||||||
|
Bc @ three.module.js:19367
|
||||||
|
Gc @ three.module.js:20038
|
||||||
|
acquireProgram @ three.module.js:21011
|
||||||
|
Fe @ three.module.js:29960
|
||||||
|
(anonymous) @ three.module.js:30223
|
||||||
|
renderBufferDirect @ three.module.js:29037
|
||||||
|
je @ three.module.js:29896
|
||||||
|
De @ three.module.js:29865
|
||||||
|
Pe @ three.module.js:29710
|
||||||
|
render @ three.module.js:29526
|
||||||
|
Mv @ index-99983b2d.esm.js:1544
|
||||||
|
a @ index-99983b2d.esm.js:1570
|
||||||
|
requestAnimationFrame
|
||||||
|
t @ index-99983b2d.esm.js:1609
|
||||||
|
(anonymous) @ index-99983b2d.esm.js:1590
|
||||||
|
t @ index-99983b2d.esm.js:1590
|
||||||
|
sE @ gsap-core.js:990
|
||||||
|
n.render @ gsap-core.js:3464
|
||||||
|
n.render @ gsap-core.js:2257
|
||||||
|
pC @ gsap-core.js:192
|
||||||
|
t.updateRoot @ gsap-core.js:2695
|
||||||
|
n @ gsap-core.js:1316
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
requestAnimationFrame
|
||||||
|
n @ gsap-core.js:1311
|
||||||
|
Warning: Don’t paste code into the DevTools Console that you don’t understand or haven’t reviewed yourself. This could allow attackers to steal your identity or take control of your computer. Please type ‘allow pasting’ below and press Enter to allow pasting.
|
||||||
|
allow pasting
|
||||||
|
[...performance.getEntriesByType('resource')]
|
||||||
|
.map(e => decodeURIComponent(e.name))
|
||||||
|
.filter(u => u.endsWith('.glb'));
|
||||||
|
['https://config.cloozdoors.nl/models/Handle 1.glb']0: "https://config.cloozdoors.nl/models/Handle 1.glb"length: 1[[Prototype]]: Array(0)
|
||||||
|
(async () => {
|
||||||
|
// 1) Probeer de basis-map te vinden uit eerdere .glb-requests
|
||||||
|
const entries = performance.getEntriesByType('resource')
|
||||||
|
.map(e => e.name)
|
||||||
|
.filter(u => /\.glb(\?|#|$)/i.test(u));
|
||||||
|
|
||||||
|
let base = '';
|
||||||
|
if (entries.length) {
|
||||||
|
const u = new URL(entries[0]);
|
||||||
|
u.pathname = u.pathname.replace(/[^/]+$/, ''); // strip bestandsnaam
|
||||||
|
base = u.href;
|
||||||
|
} else {
|
||||||
|
// fallback (meest waarschijnlijke pad bij deze site)
|
||||||
|
base = `${location.origin}/static/js/components/logic/models/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('GLB base:', base);
|
||||||
|
|
||||||
|
// 2) Probe range en log welke bestaan
|
||||||
|
const found = [];
|
||||||
|
const tryFetch = async (url) => {
|
||||||
|
try {
|
||||||
|
// HEAD wordt soms geblokkeerd; daarom GET met no-store
|
||||||
|
const r = await fetch(url, { cache: 'no-store' });
|
||||||
|
return r.ok && /model\/gltf-binary|octet-stream/i.test(r.headers.get('content-type') || '');
|
||||||
|
} catch (e) { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 1; i <= 200; i++) {
|
||||||
|
const url = `${base}Handle%20${i}.glb`;
|
||||||
|
// %20 i.p.v. spatie – zo worden ze ook aangevraagd
|
||||||
|
if (await tryFetch(url)) {
|
||||||
|
found.push({ i, url: decodeURIComponent(url) });
|
||||||
|
console.log('GLB gevonden:', decodeURIComponent(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found.length) {
|
||||||
|
console.warn('Geen Handle {n}.glb gevonden in', base);
|
||||||
|
} else {
|
||||||
|
console.log('Totaal gevonden:', found.length, found.map(f => `Handle ${f.i}.glb`));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
VM925:17 GLB base: https://config.cloozdoors.nl/models/
|
||||||
|
Promise {<pending>}
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 1.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 2.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 3.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 4.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 5.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 6.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 7.glb
|
||||||
|
VM925:34 GLB gevonden: https://config.cloozdoors.nl/models/Handle 8.glb
|
||||||
|
VM925:41 Totaal gevonden: 8 (8) ['Handle 1.glb', 'Handle 2.glb', 'Handle 3.glb', 'Handle 4.glb', 'Handle 5.glb', 'Handle 6.glb', 'Handle 7.glb', 'Handle 8.glb']
|
||||||
3
bron/contextapi.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const MyContext = createContext("");
|
||||||
26
bron/create fake namespace object
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
|
||||||
|
var leafPrototypes;
|
||||||
|
// create a fake namespace object
|
||||||
|
// mode & 1: value is a module id, require it
|
||||||
|
// mode & 2: merge all properties of value into the ns
|
||||||
|
// mode & 4: return value when already ns object
|
||||||
|
// mode & 16: return value when it's Promise-like
|
||||||
|
// mode & 8|1: behave like require
|
||||||
|
__webpack_require__.t = function(value, mode) {
|
||||||
|
if(mode & 1) value = this(value);
|
||||||
|
if(mode & 8) return value;
|
||||||
|
if(typeof value === 'object' && value) {
|
||||||
|
if((mode & 4) && value.__esModule) return value;
|
||||||
|
if((mode & 16) && typeof value.then === 'function') return value;
|
||||||
|
}
|
||||||
|
var ns = Object.create(null);
|
||||||
|
__webpack_require__.r(ns);
|
||||||
|
var def = {};
|
||||||
|
leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
|
||||||
|
for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
|
||||||
|
Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
|
||||||
|
}
|
||||||
|
def['default'] = () => (value);
|
||||||
|
__webpack_require__.d(ns, def);
|
||||||
|
return ns;
|
||||||
|
};
|
||||||
8
bron/define property getters
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// define getter functions for harmony exports
|
||||||
|
__webpack_require__.d = (exports, definition) => {
|
||||||
|
for(var key in definition) {
|
||||||
|
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||||
|
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
4831
bron/design.js
Normal file
735
bron/door.js
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
import { useContext, useState, useEffect } from "react";
|
||||||
|
import { MyContext } from "../data/contextapi";
|
||||||
|
import InfoIcon from "./InfoIcon";
|
||||||
|
|
||||||
|
export default function Door() {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
settype,
|
||||||
|
setStep,
|
||||||
|
width,
|
||||||
|
setWidth,
|
||||||
|
doorConfig,
|
||||||
|
setDoorConfig,
|
||||||
|
sidePannel,
|
||||||
|
setSidePannel,
|
||||||
|
sidePannelConfig,
|
||||||
|
setSidePannelConfig,
|
||||||
|
sidePannelSize,
|
||||||
|
setSidePannelSize,
|
||||||
|
stalenPart,
|
||||||
|
setStalenPart,
|
||||||
|
stalenType,
|
||||||
|
setStalenType,
|
||||||
|
open,
|
||||||
|
inprogress,
|
||||||
|
setInprogress,
|
||||||
|
height,
|
||||||
|
setHeight,
|
||||||
|
holeWidth,
|
||||||
|
setHoleWidth,
|
||||||
|
} = useContext(MyContext);
|
||||||
|
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Function to check the window width
|
||||||
|
const checkScreenWidth = () => {
|
||||||
|
setIsMobile(window.innerWidth <= 800);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
checkScreenWidth();
|
||||||
|
|
||||||
|
// Add event listener to check screen width on resize
|
||||||
|
window.addEventListener("resize", checkScreenWidth);
|
||||||
|
|
||||||
|
// Clean up event listener on component unmount
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", checkScreenWidth);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
(doorConfig == "dubbele" && sidePannel === "beide") ||
|
||||||
|
sidePannel === "beide"
|
||||||
|
) {
|
||||||
|
setSidePannelSize(width);
|
||||||
|
} else {
|
||||||
|
setSidePannelSize(width);
|
||||||
|
}
|
||||||
|
setWidth(90);
|
||||||
|
setSidePannelSize(90);
|
||||||
|
setHoleWidth(calculateHoleWidth());
|
||||||
|
}, [doorConfig, sidePannelConfig, sidePannel]);
|
||||||
|
|
||||||
|
const handleHeightChange = (e) => {
|
||||||
|
if (e.target.value > 285) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const value = e.target.value;
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
setHeight(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateHoleMinWidth = () => {
|
||||||
|
if (
|
||||||
|
(doorConfig != "dubbele" &&
|
||||||
|
(sidePannel === "links" || sidePannel === "rechts")) ||
|
||||||
|
(doorConfig == "dubbele" && sidePannel == "geen")
|
||||||
|
) {
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig != "dubbele" && sidePannel === "beide") {
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
doorConfig == "dubbele" &&
|
||||||
|
(sidePannel == "links" || sidePannel == "rechts")
|
||||||
|
) {
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "beide") {
|
||||||
|
return 240;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 60;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateHoleMaxWidth = () => {
|
||||||
|
if (
|
||||||
|
(doorConfig != "dubbele" &&
|
||||||
|
(sidePannel === "links" || sidePannel === "rechts")) ||
|
||||||
|
(doorConfig == "dubbele" && sidePannel == "geen")
|
||||||
|
) {
|
||||||
|
return 240;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig != "dubbele" && sidePannel === "beide") {
|
||||||
|
return 360;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
doorConfig == "dubbele" &&
|
||||||
|
(sidePannel == "links" || sidePannel == "rechts")
|
||||||
|
) {
|
||||||
|
return 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "beide") {
|
||||||
|
return 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 120;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateHoleWidth = () => {
|
||||||
|
if (
|
||||||
|
(doorConfig != "dubbele" &&
|
||||||
|
(sidePannel === "links" || sidePannel === "rechts")) ||
|
||||||
|
(doorConfig == "dubbele" && sidePannel == "geen")
|
||||||
|
) {
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig != "dubbele" && sidePannel === "beide") {
|
||||||
|
return 270;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
doorConfig == "dubbele" &&
|
||||||
|
(sidePannel == "links" || sidePannel == "rechts")
|
||||||
|
) {
|
||||||
|
return 270;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "beide") {
|
||||||
|
return 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 90;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHoleWidth = (e) => {
|
||||||
|
if (e.target.value > calculateHoleMaxWidth()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setHoleWidth(e.target.value);
|
||||||
|
setSidePannelSize(calculateSidePanelWidth(e.target.value, width));
|
||||||
|
|
||||||
|
if (sidePannel == "geen") {
|
||||||
|
setWidth(doorConfig === "dubbele" ? e.target.value / 2 : e.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSidePanelWidth = (totalWidth, doorWidth) => {
|
||||||
|
if (
|
||||||
|
doorConfig != "dubbele" &&
|
||||||
|
(sidePannel === "links" || sidePannel === "rechts")
|
||||||
|
) {
|
||||||
|
return totalWidth - Number(doorWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "geen") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig != "dubbele" && sidePannel === "beide") {
|
||||||
|
return (totalWidth - Number(doorWidth)) / 2;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
doorConfig == "dubbele" &&
|
||||||
|
(sidePannel == "links" || sidePannel == "rechts")
|
||||||
|
) {
|
||||||
|
return totalWidth - Number(doorWidth) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "beide") {
|
||||||
|
return (totalWidth - Number(doorWidth) * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <div style={{ width: '100%' }}>
|
||||||
|
<div style={{ margin: '5%', paddingTop: '0.5rem', display: 'flex', justifyContent: 'space-between', fontWeight: 'bolder', fontSize: 'normal' }}>
|
||||||
|
<span>Type Deur</span>
|
||||||
|
<div style={{ backgroundColor: 'white', borderRadius: '20px', width: 'auto', padding: '10px', margin: '0 0 0 10px' }}>
|
||||||
|
<span className="body-txt" style={{}} onClick={() => { setStep('door') }}>€ 0,00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Afmeting</span>
|
||||||
|
</div>
|
||||||
|
<div className="slider-container" style={{ margin: "5%" }}>
|
||||||
|
<div className="">
|
||||||
|
<p style={{ marginBottom: "-5px" }}>Hoogte</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="slider-container"
|
||||||
|
style={{ display: "flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="180"
|
||||||
|
max="285"
|
||||||
|
value={height}
|
||||||
|
onChange={handleHeightChange}
|
||||||
|
className="slider"
|
||||||
|
id="range1"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
appearance: "none",
|
||||||
|
background: "#F2F2F3", // Grey line
|
||||||
|
outline: "none", // Remove default outline
|
||||||
|
marginTop: "10px", // Adjust margin to separate from the text above
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: "10px",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
paddingRight: "10px",
|
||||||
|
paddingTop: "5px",
|
||||||
|
paddingBottom: "5px",
|
||||||
|
backgroundColor: "#F2F2F3",
|
||||||
|
position: "relative",
|
||||||
|
width: "5rem",
|
||||||
|
fontSize: "small",
|
||||||
|
border: "1px solid",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
value={height}
|
||||||
|
onChange={handleHeightChange}
|
||||||
|
onInput={(e) => e.target.value = e.target.value.replace(/[^0-9]/g, '')}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "10px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="slider-container"
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
borderBottom: "1px solid #d1d1d1",
|
||||||
|
paddingBottom: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="">
|
||||||
|
<p style={{ marginBottom: "-5px" }}>Breedte</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="slider-container"
|
||||||
|
style={{ display: "flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={calculateHoleMinWidth()}
|
||||||
|
max={calculateHoleMaxWidth()}
|
||||||
|
value={holeWidth}
|
||||||
|
onChange={handleHoleWidth}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
appearance: "none",
|
||||||
|
background: "#F2F2F3", // Grey line
|
||||||
|
outline: "none", // Remove default outline
|
||||||
|
marginTop: "10px", // Adjust margin to separate from the text above
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: "10px",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
paddingRight: "10px",
|
||||||
|
paddingTop: "5px",
|
||||||
|
paddingBottom: "5px",
|
||||||
|
backgroundColor: "#F2F2F3",
|
||||||
|
position: "relative",
|
||||||
|
width: "5rem",
|
||||||
|
fontSize: "small",
|
||||||
|
border: "1px solid",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
value={holeWidth}
|
||||||
|
onChange={handleHoleWidth}
|
||||||
|
onInput={(e) => e.target.value = e.target.value.replace(/[^0-9]/g, '')}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "10px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoIcon title={"Deur type"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="door-content-2"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="door-content-2-1 door-content-custom-1"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
|
gridColumnGap: "10px",
|
||||||
|
fontSize: "11px",
|
||||||
|
margin: "0 5%",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="custom-card-1"
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) settype("Taatsdeur");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="main-img-div"
|
||||||
|
style={{
|
||||||
|
border: type === "Taatsdeur" ? "2px solid black" : "none",
|
||||||
|
borderRadius: "10px",
|
||||||
|
boxShadow: "-5px -2px 10px -2px rgba(0,0,0,0.6)",
|
||||||
|
padding: "10px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={type === "Taatsdeur"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => {
|
||||||
|
if (!inprogress) settype("Taatsdeur");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/door_type_1_staging.jpg"
|
||||||
|
style={{
|
||||||
|
maxWidth: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "80%",
|
||||||
|
borderRadius: "10px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor: type === "Taatsdeur" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Taatsdeur
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="custom-card-1"
|
||||||
|
style={{ position: "relative", margin: "5px", borderRadius: "5px" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) settype("Schuifdeur");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="main-img-div"
|
||||||
|
style={{
|
||||||
|
border: type === "Schuifdeur" ? "2px solid black" : "none",
|
||||||
|
borderRadius: "10px",
|
||||||
|
boxShadow: "-5px -2px 10px -2px rgba(0,0,0,0.6)",
|
||||||
|
padding: "10px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={type === "Schuifdeur"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => {
|
||||||
|
if (!inprogress) settype("Schuifdeur");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/door_type_2_staging.jpg"
|
||||||
|
style={{
|
||||||
|
maxWidth: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "80%",
|
||||||
|
borderRadius: "10px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor: type === "Schuifdeur" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SCHUIFDEUR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="custom-card-1"
|
||||||
|
style={{ position: "relative", margin: "5px", borderRadius: "5px" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) settype("Scharnier");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="main-img-div"
|
||||||
|
style={{
|
||||||
|
border: type === "Scharnier" ? "2px solid black" : "none",
|
||||||
|
borderRadius: "10px",
|
||||||
|
boxShadow: "-5px -2px 10px -2px rgba(0,0,0,0.6)",
|
||||||
|
padding: "10px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={type === "Scharnier"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => {
|
||||||
|
if (!inprogress) settype("Scharnier");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/door_type_3_staging.jpg"
|
||||||
|
style={{
|
||||||
|
maxWidth: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "80%",
|
||||||
|
borderRadius: "10px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor: type === "Scharnier" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KOZIJN DEUR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="custom-card-1"
|
||||||
|
style={{ position: "relative", margin: "5px", borderRadius: "5px" }}
|
||||||
|
onClick={() => {
|
||||||
|
settype("vast-stalen");
|
||||||
|
setDoorConfig("enkele");
|
||||||
|
setSidePannel("geen");
|
||||||
|
setInprogress(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="main-img-div"
|
||||||
|
style={{
|
||||||
|
border: type === "vast-stalen" ? "2px solid black" : "none",
|
||||||
|
borderRadius: "10px",
|
||||||
|
boxShadow: "-5px -2px 10px -2px rgba(0,0,0,0.6)",
|
||||||
|
padding: "10px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={type === "vast-stalen"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => {
|
||||||
|
settype("vast-stalen");
|
||||||
|
setDoorConfig("enkele");
|
||||||
|
setSidePannel("geen");
|
||||||
|
setInprogress(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/door_type_4.jpg"
|
||||||
|
style={{
|
||||||
|
maxWidth: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "80%",
|
||||||
|
borderRadius: "10px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor: type === "vast-stalen" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Vast stalen
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="volgende btn"
|
||||||
|
onClick={() => {
|
||||||
|
setStep("samenstling");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Volgende
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
bron/door_type_1_staging.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
bron/door_type_2_staging.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
bron/door_type_3_staging.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
bron/door_type_4.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
bron/double-door-type_staging.jpg
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
bron/draco_decoder.wasm
Normal file
47
bron/draco_decover.js.rtf
Normal file
653
bron/endpoint.js
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
import { Canvas, useThree } from "@react-three/fiber";
|
||||||
|
import axios from "axios";
|
||||||
|
// import { EffectComposer } from "@react-three/postprocessing";
|
||||||
|
import DoorHole from "./logic/DoorHole";
|
||||||
|
import { Suspense, useState, useEffect } from "react";
|
||||||
|
import Preloader from "./logic/preloader";
|
||||||
|
import Door from "./logic/UI/door";
|
||||||
|
import Design from "./logic/UI/design";
|
||||||
|
import Information from "./logic/UI/information";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { MyContext } from "./logic/data/contextapi";
|
||||||
|
import Extra from "./logic/UI/extra";
|
||||||
|
import Samenstling from "./logic/UI/samenstling";
|
||||||
|
import Arrow from "./logic/UI/arrow";
|
||||||
|
import { DirectionalLightHelper, CameraHelper } from "three";
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
export default function Endpoint() {
|
||||||
|
const [json, setJson] = useState({ Data: [] });
|
||||||
|
|
||||||
|
const [step, setStep] = useState("door");
|
||||||
|
const [type, settype] = useState("Taatsdeur");
|
||||||
|
const [door, setdoor] = useState("3panel");
|
||||||
|
const [frameSize, setFrameSize] = useState(0.125);
|
||||||
|
const [glassType, setGlassType] = useState(0x111111);
|
||||||
|
const [frameType, setFrameType] = useState(
|
||||||
|
"./images/doortypes/RAL 9005 fijn structuur.png"
|
||||||
|
);
|
||||||
|
const [width, setWidth] = useState(90); // State for width
|
||||||
|
const [height, setHeight] = useState(250); // State for height
|
||||||
|
const [holeWidth, setHoleWidth] = useState(90);
|
||||||
|
const [doorConfig, setDoorConfig] = useState("enkele");
|
||||||
|
const [sidePannel, setSidePannel] = useState("geen");
|
||||||
|
const [sidePannelConfig, setSidePannelConfig] = useState("eigen maat");
|
||||||
|
const [sidePannelSize, setSidePannelSize] = useState(width);
|
||||||
|
const [stalenType, setStalenType] = useState("tussen");
|
||||||
|
const [stalenPart, setStalenPart] = useState(1);
|
||||||
|
const [handle, setHandle] = useState(1);
|
||||||
|
const [maxWidth, setMaxWidth] = useState(105);
|
||||||
|
const [prwType, setPrwType] = useState("Enkele");
|
||||||
|
const [prwImage, setPrwImage] = useState(
|
||||||
|
"./images/doortypes/enkele deur.png"
|
||||||
|
);
|
||||||
|
const [extraOptions, setExtraOptions] = useState([
|
||||||
|
"Meetservice",
|
||||||
|
"Montage service",
|
||||||
|
]);
|
||||||
|
const [count, setCount] = useState(1);
|
||||||
|
|
||||||
|
const [coverSheetSteel, setCoverSheetSteel] = useState(false);
|
||||||
|
const [coverLearn, setCoverLearn] = useState(false);
|
||||||
|
const [coverWoodliness, setCoverWoodliness] = useState(false);
|
||||||
|
const [coverMirrors, setCoverMirrors] = useState(false);
|
||||||
|
const [coverCylinderLockKey, setCoverCylinderLockKey] = useState(false);
|
||||||
|
const [coverDontKnow, setCoverDontKnow] = useState(false);
|
||||||
|
|
||||||
|
const [handleSheetSteel, setHandleSheetSteel] = useState(false);
|
||||||
|
const [handleLearn, setHandleLearn] = useState(false);
|
||||||
|
const [handleWoodliness, setHandleWoodliness] = useState(false);
|
||||||
|
const [handleMirrors, setHandleMirrors] = useState(false);
|
||||||
|
const [handleCylinderLockKey, setHandleCylinderLockKey] = useState(false);
|
||||||
|
|
||||||
|
const [standardBlack, setStandardBlack] = useState(false);
|
||||||
|
const [alternativeRALColour, setAlternativeRALColour] = useState(false);
|
||||||
|
const [metallicCoating, setMetallicCoating] = useState(false);
|
||||||
|
|
||||||
|
const [bright, setBright] = useState(false);
|
||||||
|
const [canaleOrCrepi, setCanaleOrCrepi] = useState(false);
|
||||||
|
|
||||||
|
const [fireResistant, setFireResistant] = useState(false);
|
||||||
|
const [outdoorPlacement, setOutdoorPlacement] = useState(false);
|
||||||
|
|
||||||
|
const [maxHeight, setMaxHeight] = useState(0);
|
||||||
|
const [contentHeight, setContentHeight] = useState("calc(100vh - 320px)");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [inprogress, setInprogress] = useState(false);
|
||||||
|
|
||||||
|
const [bubbles, setBubbles] = useState();
|
||||||
|
|
||||||
|
const [attachDesign, setAttachDesign] = useState(null);
|
||||||
|
|
||||||
|
const [colorPickerOpened, setColorPickerOpened] = useState(false);
|
||||||
|
|
||||||
|
const [customFrameType, setCustomFrameType] = useState({name: "", color: ""});
|
||||||
|
|
||||||
|
const [compositionImage, setCompositionImage] = useState("https://config.livingsteel.nl/images/doortypes/3%20panel.png");
|
||||||
|
|
||||||
|
const [glContext, setGlContext] = useState(null);
|
||||||
|
const [sceneContext, setSceneContext] = useState(null); // Store the Scene context
|
||||||
|
const [cameraContext, setCameraContext] = useState(null); // Store the Camera context
|
||||||
|
|
||||||
|
// Calculate the maximum height based on the available screen height
|
||||||
|
useEffect(() => {
|
||||||
|
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||||
|
|
||||||
|
const isAndroid = /android/i.test(userAgent);
|
||||||
|
const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream;
|
||||||
|
const calculateMaxHeight = () => {
|
||||||
|
let availableHeight;
|
||||||
|
if (window.innerWidth > 800) {
|
||||||
|
const screenHeight = window.innerHeight;
|
||||||
|
// Adjust maxHeight to leave space for other elements like headers or footers
|
||||||
|
availableHeight = screenHeight - 100; // Adjust this value as needed
|
||||||
|
} else {
|
||||||
|
setContentHeight(
|
||||||
|
`calc(100vh - 25vh - ${isAndroid ? "270px" : "310px"})`
|
||||||
|
);
|
||||||
|
const screenHeight = window.innerHeight * 0.65;
|
||||||
|
// Adjust maxHeight to leave space for other elements like headers or footers 30
|
||||||
|
availableHeight = screenHeight + 210; // Adjust this value as needed
|
||||||
|
}
|
||||||
|
setMaxHeight(availableHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBubbles();
|
||||||
|
|
||||||
|
calculateMaxHeight(); // Call once initially
|
||||||
|
window.addEventListener("resize", calculateMaxHeight); // Recalculate on window resize
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", calculateMaxHeight); // Cleanup
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Create a ref for the SpotLight
|
||||||
|
const spotLightRef = useRef();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MyContext.Provider
|
||||||
|
value={{
|
||||||
|
json,
|
||||||
|
setJson,
|
||||||
|
setStep,
|
||||||
|
sidePannel,
|
||||||
|
setSidePannel,
|
||||||
|
type,
|
||||||
|
settype,
|
||||||
|
door,
|
||||||
|
setdoor,
|
||||||
|
width,
|
||||||
|
setWidth,
|
||||||
|
height,
|
||||||
|
setHeight,
|
||||||
|
holeWidth,
|
||||||
|
setHoleWidth,
|
||||||
|
doorConfig,
|
||||||
|
setDoorConfig,
|
||||||
|
prwType,
|
||||||
|
setPrwType,
|
||||||
|
prwImage,
|
||||||
|
setPrwImage,
|
||||||
|
count,
|
||||||
|
setCount,
|
||||||
|
frameSize,
|
||||||
|
setFrameSize,
|
||||||
|
glassType,
|
||||||
|
setGlassType,
|
||||||
|
frameType,
|
||||||
|
setFrameType,
|
||||||
|
sidePannelConfig,
|
||||||
|
setSidePannelConfig,
|
||||||
|
sidePannelSize,
|
||||||
|
setSidePannelSize,
|
||||||
|
stalenType,
|
||||||
|
setStalenType,
|
||||||
|
stalenPart,
|
||||||
|
setStalenPart,
|
||||||
|
handle,
|
||||||
|
setHandle,
|
||||||
|
maxWidth,
|
||||||
|
setMaxWidth,
|
||||||
|
coverSheetSteel,
|
||||||
|
setCoverSheetSteel,
|
||||||
|
coverLearn,
|
||||||
|
setCoverLearn,
|
||||||
|
coverWoodliness,
|
||||||
|
setCoverWoodliness,
|
||||||
|
coverMirrors,
|
||||||
|
setCoverMirrors,
|
||||||
|
coverCylinderLockKey,
|
||||||
|
setCoverCylinderLockKey,
|
||||||
|
coverDontKnow,
|
||||||
|
setCoverDontKnow,
|
||||||
|
handleSheetSteel,
|
||||||
|
setHandleSheetSteel,
|
||||||
|
handleLearn,
|
||||||
|
setHandleLearn,
|
||||||
|
handleWoodliness,
|
||||||
|
setHandleWoodliness,
|
||||||
|
handleMirrors,
|
||||||
|
setHandleMirrors,
|
||||||
|
handleCylinderLockKey,
|
||||||
|
setHandleCylinderLockKey,
|
||||||
|
standardBlack,
|
||||||
|
setStandardBlack,
|
||||||
|
alternativeRALColour,
|
||||||
|
setAlternativeRALColour,
|
||||||
|
metallicCoating,
|
||||||
|
setMetallicCoating,
|
||||||
|
bright,
|
||||||
|
setBright,
|
||||||
|
canaleOrCrepi,
|
||||||
|
setCanaleOrCrepi,
|
||||||
|
fireResistant,
|
||||||
|
setFireResistant,
|
||||||
|
outdoorPlacement,
|
||||||
|
setOutdoorPlacement,
|
||||||
|
setExtraOptions,
|
||||||
|
extraOptions,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
inprogress,
|
||||||
|
setInprogress,
|
||||||
|
bubbles,
|
||||||
|
setBubbles,
|
||||||
|
attachDesign,
|
||||||
|
setAttachDesign,
|
||||||
|
colorPickerOpened,
|
||||||
|
setColorPickerOpened,
|
||||||
|
customFrameType,
|
||||||
|
setCustomFrameType,
|
||||||
|
compositionImage,
|
||||||
|
setCompositionImage,
|
||||||
|
glContext,
|
||||||
|
sceneContext,
|
||||||
|
cameraContext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: "100vw", height: "100svh", overflow: "hidden" }}>
|
||||||
|
<div className="body-can-2" style={{}}>
|
||||||
|
<div className="body-can-2-1" style={{}}>
|
||||||
|
<div className="body-can-2-1-1" style={{}}>
|
||||||
|
<Suspense fallback={<Preloader />}>
|
||||||
|
<Canvas
|
||||||
|
frameloop="demand"
|
||||||
|
gl={{
|
||||||
|
antialias: true,
|
||||||
|
alpha: true,
|
||||||
|
toneMapping: THREE.SRGBColorSpace,
|
||||||
|
}}
|
||||||
|
camera={{ fov: 53 }}
|
||||||
|
onCreated={({ gl, scene, camera }) => {
|
||||||
|
setGlContext(gl);
|
||||||
|
setSceneContext(scene);
|
||||||
|
setCameraContext(camera);
|
||||||
|
}}
|
||||||
|
// shadows // Enable shadows
|
||||||
|
>
|
||||||
|
{/* Ambient light for basic illumination */}
|
||||||
|
|
||||||
|
<DoorHole />
|
||||||
|
</Canvas>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<div className="body-can-2-1-2" style={{ overflowY: "hidden" }}>
|
||||||
|
<div
|
||||||
|
className="quot"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#bdc79d",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <span
|
||||||
|
style={{
|
||||||
|
color: "white",
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginLeft: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Prijs incl. BTW
|
||||||
|
</span> */}
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "5px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
€ 2200,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`step-content ${
|
||||||
|
step === "door" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
marginTop: "auto",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`body-btn ${
|
||||||
|
step === "door" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={`body-txt ${
|
||||||
|
step === "door" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{ opacity: step === "door" ? 1 : 0.4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
DEURMAAT & TYPE
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "5px",
|
||||||
|
right: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={step === "door"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => setStep("door")}
|
||||||
|
className={`radio ${
|
||||||
|
step === "door" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{/* <Arrow direction={step === 'door' ? 'down' : 'up'} /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: step == "door" ? "10px" : 0,
|
||||||
|
height: step == "door" ? contentHeight : 0,
|
||||||
|
opacity: step == "door" ? 1 : 0,
|
||||||
|
transition:
|
||||||
|
"height 0.3s ease-in-out, opacity 0.5s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Door />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`step-content ${
|
||||||
|
step === "samenstling" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("samenstling");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`body-btn ${
|
||||||
|
step === "samenstling" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={`body-txt ${
|
||||||
|
step === "samenstling" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{ opacity: step === "samenstling" ? 1 : 0.4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("samenstling");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Samenstelling
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "5px",
|
||||||
|
right: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={step === "samenstling"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => setStep("samenstling")}
|
||||||
|
className={`radio ${
|
||||||
|
step === "samenstling" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: step == "samenstling" ? "10px" : 0,
|
||||||
|
height: step == "samenstling" ? contentHeight : 0,
|
||||||
|
opacity: step == "samenstling" ? 1 : 0,
|
||||||
|
transition:
|
||||||
|
"height 0.3s ease-in-out, opacity 0.5s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Samenstling />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`step-content ${
|
||||||
|
step === "design" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("design");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`body-btn ${
|
||||||
|
step === "design" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
3
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={`body-txt ${
|
||||||
|
step === "design" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{ opacity: step === "design" ? 1 : 0.4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("design");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Design, Kleur & GLAS
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "5px",
|
||||||
|
right: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={step === "design"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => setStep("design")}
|
||||||
|
className={`radio ${
|
||||||
|
step === "design" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: step == "design" ? "10px" : 0,
|
||||||
|
height: step == "design" ? contentHeight : 0,
|
||||||
|
opacity: step == "design" ? 1 : 0,
|
||||||
|
transition:
|
||||||
|
"height 0.3s ease-in-out, opacity 0.5s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Design />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`step-content ${
|
||||||
|
step === "extra" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("extra");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`body-btn ${
|
||||||
|
step === "extra" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
4
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={`body-txt ${
|
||||||
|
step === "extra" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{ opacity: step === "extra" ? 1 : 0.4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("extra");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Extra Opties
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "5px",
|
||||||
|
right: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={step === "extra"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => setStep("extra")}
|
||||||
|
className={`radio ${
|
||||||
|
step === "extra" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{/* <Arrow direction={step === 'extra' ? 'down' : 'up'} /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: step == "extra" ? "10px" : 0,
|
||||||
|
height: step == "extra" ? contentHeight : 0,
|
||||||
|
opacity: step == "extra" ? 1 : 0,
|
||||||
|
transition:
|
||||||
|
"height 0.3s ease-in-out, opacity 0.5s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Extra />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`pd step-content ${
|
||||||
|
step === "information" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("information");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`body-btn ${
|
||||||
|
step === "information" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
5
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={`body-txt ${
|
||||||
|
step === "information" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
style={{ opacity: step === "information" ? 1 : 0.4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("information");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Order informatie
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "5px",
|
||||||
|
right: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={step === "information"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => setStep("information")}
|
||||||
|
className={`radio ${
|
||||||
|
step === "information" ? "expand" : "collapsed"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{/* <Arrow
|
||||||
|
direction={step === 'information' ? 'down' : 'up'}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: step == "information" ? "10px" : 0,
|
||||||
|
height: step == "information" ? contentHeight : 0,
|
||||||
|
opacity: step == "information" ? 1 : 0,
|
||||||
|
transition:
|
||||||
|
"height 0.3s ease-in-out, opacity 0.5s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Information />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MyContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
bron/extends.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
function _extends() {
|
||||||
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
||||||
|
for (var e = 1; e < arguments.length; e++) {
|
||||||
|
var t = arguments[e];
|
||||||
|
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}, _extends.apply(null, arguments);
|
||||||
|
}
|
||||||
|
export { _extends as default };
|
||||||
148
bron/extra.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { MyContext } from "../data/contextapi";
|
||||||
|
import InfoIcon from "./InfoIcon";
|
||||||
|
|
||||||
|
export default function Extra() {
|
||||||
|
const { settype, type, setStep, extraOptions, setExtraOptions } =
|
||||||
|
useContext(MyContext);
|
||||||
|
const [extras, setExtra] = useState([]);
|
||||||
|
const [montage, setMontage] = useState([]);
|
||||||
|
|
||||||
|
const handleExtra = (val) =>
|
||||||
|
setExtraOptions((prev) =>
|
||||||
|
prev.includes(val)
|
||||||
|
? [...prev.filter((itm) => itm != val)]
|
||||||
|
: [...prev, val]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ backgroundColor: "#fff" }}>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoIcon title={"Kies hier wat van toepassing is"} />
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "2px 10px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
€ 400,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ margin: "5%", fontSize: "small" }}>
|
||||||
|
{[
|
||||||
|
"Meetservice",
|
||||||
|
"Montage service",
|
||||||
|
"Taatsbeslag in kleur",
|
||||||
|
"Vloerverwarming montageset",
|
||||||
|
"Adviesgesprek",
|
||||||
|
].map((option, idx) => (
|
||||||
|
<div
|
||||||
|
key={`${option}_${idx}`}
|
||||||
|
style={{ display: "flex", cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={extraOptions.includes(option)}
|
||||||
|
onChange={() => handleExtra(option)}
|
||||||
|
style={{ marginRight: "10px", width: "15px", height: "15px" }}
|
||||||
|
/>
|
||||||
|
<span onClick={() => handleExtra(option)}>{option}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
paddingLeft: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoIcon title={"Afhalen of verzenden"} />
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "2px 10px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
// onClick={() => {
|
||||||
|
// setStep('door');
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
€ 400,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
fontSize: "small",
|
||||||
|
borderBottom: "1px solid #d1d1d1",
|
||||||
|
paddingBottom: "25px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"Afleveren NL per koeriersdienst (excl. Waddeneilanden)",
|
||||||
|
"Afleveren buiten NL",
|
||||||
|
"Afhalen fabriek",
|
||||||
|
].map((option, idx) => (
|
||||||
|
<div key={`${option}_${idx}`} style={{ display: "flex" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={extraOptions.includes(option)}
|
||||||
|
onChange={() => handleExtra(option)}
|
||||||
|
style={{ marginRight: "10px", width: "15px", height: "15px" }}
|
||||||
|
/>
|
||||||
|
<span onClick={() => handleExtra(option)}>{option}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="volgende btn"
|
||||||
|
onClick={() => {
|
||||||
|
setStep("information");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Volgende
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
bron/global
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
__webpack_require__.g = (function() {
|
||||||
|
if (typeof globalThis === 'object') return globalThis;
|
||||||
|
try {
|
||||||
|
return this || new Function('return this')();
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof window === 'object') return window;
|
||||||
|
}
|
||||||
|
})();
|
||||||
50
bron/handle.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Clone, useGLTF, useTexture } from "@react-three/drei";
|
||||||
|
import { MyContext } from "./data/contextapi";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
export default function Handle() {
|
||||||
|
const { width, height, handle, frameType } = useContext(MyContext);
|
||||||
|
|
||||||
|
const grip = useGLTF(`./models/Handle ${handle}.glb`);
|
||||||
|
|
||||||
|
const texture = useTexture(frameType);
|
||||||
|
|
||||||
|
grip.scene.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.material = new THREE.MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Clone
|
||||||
|
object={grip.scene}
|
||||||
|
position={[
|
||||||
|
0.03 * width + 0.1 - (handle === 5 || handle === 6 ? 0.6 : 0),
|
||||||
|
-(0.053 * height) / 10000,
|
||||||
|
0.045,
|
||||||
|
]}
|
||||||
|
scale={handle === 7 ? 4.88 : handle === 4 ? 6 : 5}
|
||||||
|
rotation={[0, -Math.PI / 2, 0]}
|
||||||
|
/>
|
||||||
|
<Clone
|
||||||
|
object={grip.scene}
|
||||||
|
position={[
|
||||||
|
0.03 * width + 0.1 - (handle === 5 || handle === 6 ? 0.6 : 0),
|
||||||
|
-(0.053 * height) / 10000,
|
||||||
|
-0.045,
|
||||||
|
]}
|
||||||
|
scale={handle === 7 ? 4.88 : handle === 4 ? 6 : 5}
|
||||||
|
rotation={[
|
||||||
|
0,
|
||||||
|
[5, 6, 7, 8].includes(handle) ? Math.PI / 2 : -Math.PI / 2,
|
||||||
|
[5, 6, 7, 8].includes(handle) ? 0 : Math.PI,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
bron/hasOwnProperty shorthand
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
||||||
19
bron/index.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* poppins-latin-ext-400-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./files/poppins-latin-ext-400-normal.woff2) format('woff2'), url(./files/poppins-latin-ext-400-normal.woff) format('woff');
|
||||||
|
unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* poppins-latin-400-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./files/poppins-latin-400-normal.woff2) format('woff2'), url(./files/poppins-latin-400-normal.woff) format('woff');
|
||||||
|
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||||
|
}
|
||||||
16
bron/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import App from './App'
|
||||||
|
import './style.css'
|
||||||
|
import './style0.css'
|
||||||
|
import './tailwind.css'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import "@fontsource/poppins";
|
||||||
|
import ConsentBanner from './components/ConsentBanner'
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.querySelector('#root'))
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<>
|
||||||
|
<App />
|
||||||
|
{/* <ConsentBanner /> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
16
bron/indexx.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import App from './App'
|
||||||
|
import './style.css'
|
||||||
|
import './style0.css'
|
||||||
|
import './tailwind.css'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import "@fontsource/poppins";
|
||||||
|
import ConsentBanner from './components/ConsentBanner'
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.querySelector('#root'))
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<>
|
||||||
|
<App />
|
||||||
|
{/* <ConsentBanner /> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
198
bron/information.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { MyContext } from "../data/contextapi";
|
||||||
|
import RequestForm from "../../RequestForm";
|
||||||
|
const doorTypes = {
|
||||||
|
Taatsdeur: "TAATSDEUR",
|
||||||
|
Schuifdeur: "SCHUIFDEUR",
|
||||||
|
Scharnier: "KOZIJN DEUR",
|
||||||
|
"vast-stalen": "VAST STALEN",
|
||||||
|
};
|
||||||
|
|
||||||
|
const doorConfigs = {
|
||||||
|
enkele: "ENKELE DEUR",
|
||||||
|
dubbele: "DUBBELE DEUR",
|
||||||
|
};
|
||||||
|
|
||||||
|
const panels = {
|
||||||
|
links: "LINKS",
|
||||||
|
rechts: "RECHTS",
|
||||||
|
beide: "BEIDE",
|
||||||
|
geen: "GEEN",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sidePannelConfigs = {
|
||||||
|
"gelijk vardelen": "GELIJKE DELEN",
|
||||||
|
"eigen maat": "EIGEN MAAT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const doors = {
|
||||||
|
george: "1 paneel",
|
||||||
|
"2panel": "2 panelen",
|
||||||
|
"3panel": "3 panelen",
|
||||||
|
"4panel": "4 panelen",
|
||||||
|
"3pannel": "3 panelen ongelijk plus",
|
||||||
|
annelot: "Annelot",
|
||||||
|
notaris: "Notaris",
|
||||||
|
boerderij: "Boerderij",
|
||||||
|
herenhuis: "Herenhuis",
|
||||||
|
rond: "Rond",
|
||||||
|
rondPlus: "Rond Plus",
|
||||||
|
"low-deep": "Low Deep",
|
||||||
|
rivera: "Rivera",
|
||||||
|
porto: "Porto",
|
||||||
|
toog: "Toog",
|
||||||
|
toogPlus: "Toog plus",
|
||||||
|
boender: "Boender",
|
||||||
|
contempera: "Contempera",
|
||||||
|
fabric: "Fabric",
|
||||||
|
dt13: "Hoogh",
|
||||||
|
larino: "Larino",
|
||||||
|
dt11: "Mexico",
|
||||||
|
parallel: "Parallel",
|
||||||
|
grandma: "Grandma",
|
||||||
|
kasteel: "Kasteel",
|
||||||
|
kathedraal: "Kathedraal",
|
||||||
|
"the-judge": "The Judge",
|
||||||
|
prison: "Prison",
|
||||||
|
curved: "Curved",
|
||||||
|
lindsey: "Lindsey",
|
||||||
|
baku: "Baku",
|
||||||
|
supreme: "Supreme",
|
||||||
|
ultimate: "Ultimate",
|
||||||
|
fisherman: "Fisherman",
|
||||||
|
primier: "Primier",
|
||||||
|
elite: "Elite"
|
||||||
|
};
|
||||||
|
|
||||||
|
const handles = [
|
||||||
|
"Hoeklijn",
|
||||||
|
"Beugel",
|
||||||
|
"Circle M",
|
||||||
|
"Circle L",
|
||||||
|
"Koker M",
|
||||||
|
"Koker L",
|
||||||
|
"Greeploos",
|
||||||
|
"Handgreep met leer",
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorGlass = {
|
||||||
|
1118481: "Helderglas",
|
||||||
|
395529: "Rookglas",
|
||||||
|
13467442: "Bronsglas",
|
||||||
|
16777215: "Melkglas"
|
||||||
|
};
|
||||||
|
|
||||||
|
const steelColor = {
|
||||||
|
"./images/doortypes/RAL 9005 fijn structuur.png": "RAL 9005 fijn structuur",
|
||||||
|
"./images/doortypes/RAL 9004 fijn structuur.png": "RAL 9004 fijn structuur",
|
||||||
|
"./images/doortypes/RAL 7021 structuur.png": "RAL 7021 structuur",
|
||||||
|
"./images/doortypes/RAL 7016 structuur.png": "RAL 7016 structuur",
|
||||||
|
"./images/doortypes/RAL 9010 structuur.png": "RAL 9010 structuur",
|
||||||
|
"./images/doortypes/RAL 9016 structuur.png": "RAL 9016 structuur",
|
||||||
|
"./images/doortypes/Anodic brown.png": "Anodic brown",
|
||||||
|
"./images/doortypes/Sterling.png": "Sterling",
|
||||||
|
"./images/doortypes/Nobble bronze.png": "Nobble bronze",
|
||||||
|
"./images/doortypes/Halo 1036.png": "Halo 1036",
|
||||||
|
"./images/doortypes/Halo 1037.png": "Halo 1037",
|
||||||
|
"./images/doortypes/Anodic_bronze.png": "Anodic bronze",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Information() {
|
||||||
|
const {
|
||||||
|
step,
|
||||||
|
setStep,
|
||||||
|
width,
|
||||||
|
setWidth,
|
||||||
|
height,
|
||||||
|
door,
|
||||||
|
doorConfig,
|
||||||
|
sidePannel,
|
||||||
|
type,
|
||||||
|
sidePannelConfig,
|
||||||
|
sidePannelSize,
|
||||||
|
frameSize,
|
||||||
|
glassType,
|
||||||
|
handle,
|
||||||
|
frameType,
|
||||||
|
customFrameType,
|
||||||
|
extraOptions,
|
||||||
|
holeWidth,
|
||||||
|
attachDesign,
|
||||||
|
compositionImage,
|
||||||
|
glContext,
|
||||||
|
sceneContext,
|
||||||
|
cameraContext
|
||||||
|
} = useContext(MyContext);
|
||||||
|
|
||||||
|
const [techInformation, setTechInformation] = useState({
|
||||||
|
type: "",
|
||||||
|
doorConfig: "",
|
||||||
|
sidePannel: "",
|
||||||
|
sidePannelConfig: "",
|
||||||
|
sidePannelSize: "",
|
||||||
|
height: "",
|
||||||
|
doorConfig: "",
|
||||||
|
width: "",
|
||||||
|
door: "",
|
||||||
|
frameSize: "",
|
||||||
|
handle: "",
|
||||||
|
colorGlass: "",
|
||||||
|
steelColor: "",
|
||||||
|
extraOptions: [],
|
||||||
|
compositionImage: compositionImage,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTechInformation({
|
||||||
|
type: doorTypes[type],
|
||||||
|
doorConfig: doorConfigs[doorConfig],
|
||||||
|
sidePannel: panels[sidePannel],
|
||||||
|
sidePannelConfig: sidePannelConfigs[sidePannelConfig],
|
||||||
|
sidePannelSize,
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
holeWidth: holeWidth,
|
||||||
|
door: doors[door],
|
||||||
|
frameSize:
|
||||||
|
frameSize === 0.25 ? "40mm" : frameSize === 0.2 ? "30mm" : "20mm",
|
||||||
|
handle: handles[handle - 1],
|
||||||
|
colorGlass: colorGlass[glassType],
|
||||||
|
steelColor:
|
||||||
|
customFrameType.name === ""
|
||||||
|
? steelColor[frameType]
|
||||||
|
: customFrameType.name,
|
||||||
|
extraOptions,
|
||||||
|
compositionImage: compositionImage
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
type,
|
||||||
|
doorConfig,
|
||||||
|
sidePannel,
|
||||||
|
sidePannelConfig,
|
||||||
|
sidePannelSize,
|
||||||
|
height,
|
||||||
|
doorConfig,
|
||||||
|
width,
|
||||||
|
holeWidth,
|
||||||
|
door,
|
||||||
|
frameSize,
|
||||||
|
handle,
|
||||||
|
glassType,
|
||||||
|
steelColor,
|
||||||
|
extraOptions,
|
||||||
|
customFrameType,
|
||||||
|
compositionImage
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RequestForm
|
||||||
|
techInformation={techInformation}
|
||||||
|
attachDesign={attachDesign}
|
||||||
|
glContext={glContext}
|
||||||
|
sceneContext={sceneContext}
|
||||||
|
cameraContext={cameraContext}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
bron/main.4524c4d6.css
Normal file
72323
bron/main.45858049.js
Normal file
72323
bron/main.458580499.js
Normal file
7
bron/make namespace object
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// define __esModule on exports
|
||||||
|
__webpack_require__.r = (exports) => {
|
||||||
|
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||||
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||||
|
}
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
};
|
||||||
5
bron/node module decorator
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
__webpack_require__.nmd = (module) => {
|
||||||
|
module.paths = [];
|
||||||
|
if (!module.children) module.children = [];
|
||||||
|
return module;
|
||||||
|
};
|
||||||
9
bron/preloader.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default function Preloader() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', backgroundSize: 'cover' }}>
|
||||||
|
<div className="loader"></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
bron/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2
Normal file
BIN
bron/samenstelling_link.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
910
bron/samenstling.js
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { MyContext } from "../data/contextapi";
|
||||||
|
import InfoIcon from "./InfoIcon";
|
||||||
|
|
||||||
|
export default function Samenstling() {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
settype,
|
||||||
|
setStep,
|
||||||
|
width,
|
||||||
|
setWidth,
|
||||||
|
doorConfig,
|
||||||
|
setDoorConfig,
|
||||||
|
sidePannel,
|
||||||
|
setSidePannel,
|
||||||
|
sidePannelConfig,
|
||||||
|
setSidePannelConfig,
|
||||||
|
sidePannelSize,
|
||||||
|
setSidePannelSize,
|
||||||
|
stalenPart,
|
||||||
|
setStalenPart,
|
||||||
|
stalenType,
|
||||||
|
setStalenType,
|
||||||
|
open,
|
||||||
|
inprogress,
|
||||||
|
holeWidth,
|
||||||
|
setHoleWidth,
|
||||||
|
} = useContext(MyContext);
|
||||||
|
|
||||||
|
const doorImages = {
|
||||||
|
Taatsdeur: "door_type_1_staging.jpg",
|
||||||
|
Schuifdeur: "door_type_2_staging.jpg",
|
||||||
|
Scharnier: "door_type_3.jpg",
|
||||||
|
"vast-stalen": "door_type_4.jpg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStalenPart = (e) => {
|
||||||
|
setStalenPart(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWidthChange = (e) => {
|
||||||
|
if (doorConfig === "dubbele" || sidePannel != "geen") {
|
||||||
|
setSidePannelSize(calculateSidePanelWidth(holeWidth, e.target.value));
|
||||||
|
setWidth(e.target.value);
|
||||||
|
} else {
|
||||||
|
setWidth(e.target.value);
|
||||||
|
setSidePannelSize(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidePannel == "geen") {
|
||||||
|
setHoleWidth(
|
||||||
|
doorConfig === "dubbele" ? 2 * e.target.value : e.target.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSidePanelWidth = (totalWidth, doorWidth) => {
|
||||||
|
if (
|
||||||
|
doorConfig != "dubbele" &&
|
||||||
|
(sidePannel === "links" || sidePannel === "rechts")
|
||||||
|
) {
|
||||||
|
return totalWidth - Number(doorWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "geen") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig != "dubbele" && sidePannel === "beide") {
|
||||||
|
return (totalWidth - Number(doorWidth)) / 2;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
doorConfig == "dubbele" &&
|
||||||
|
(sidePannel == "links" || sidePannel == "rechts")
|
||||||
|
) {
|
||||||
|
return totalWidth - Number(doorWidth) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorConfig == "dubbele" && sidePannel == "beide") {
|
||||||
|
return (totalWidth - Number(doorWidth) * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ width: "100%", height: "100%" }}>
|
||||||
|
{type != "vast-stalen" ? (
|
||||||
|
<>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "0 5%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Samenstelling deur</span>
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "2px 10px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
€ 1200,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
|
gridColumnGap: "10px",
|
||||||
|
fontSize: "11px",
|
||||||
|
margin: "5%",
|
||||||
|
borderBottom: "1px solid #d1d1d1",
|
||||||
|
paddingBottom: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) setDoorConfig("enkele");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border:
|
||||||
|
doorConfig === "enkele" ? "2px solid black" : "none",
|
||||||
|
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: "white",
|
||||||
|
padding: "10px",
|
||||||
|
boxShadow:
|
||||||
|
doorConfig === "enkele"
|
||||||
|
? "none"
|
||||||
|
: "rgba(0, 0, 0, 0.6) -5px -2px 10px -2px",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src={`./images/doortypes/door_type_3_staging.jpg`}
|
||||||
|
style={{
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "80%",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", top: "2px", right: "2px" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={doorConfig === "enkele"}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor:
|
||||||
|
doorConfig === "enkele" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
border: `1px solid ${
|
||||||
|
doorConfig === "enkele" ? "black" : "white"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderBottom: `1px solid ${
|
||||||
|
doorConfig === "enkele" ? "black" : "white"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "inline-block",
|
||||||
|
flex: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
padding: "2px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enkele deur
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) setDoorConfig("dubbele");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border:
|
||||||
|
doorConfig === "dubbele" ? "2px solid black" : "none",
|
||||||
|
borderRadius: "10px",
|
||||||
|
boxShadow: "-5px -2px 10px -2px rgba(0,0,0,0.6)",
|
||||||
|
padding: "10px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src={`./images/doortypes/double-door-type_staging.jpg`}
|
||||||
|
style={{
|
||||||
|
maxWidth: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", top: "2px", right: "2px" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={doorConfig === "dubbele"}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
paddingTop: "5px",
|
||||||
|
backgroundColor:
|
||||||
|
doorConfig === "dubbele" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
border: `1px solid ${
|
||||||
|
doorConfig === "dubbele" ? "black" : "white"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderBottom: `1px solid ${
|
||||||
|
doorConfig === "dubbele" ? "black" : "white"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "inline-block",
|
||||||
|
flex: 1,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "2px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dubbele deur
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoIcon title={"Samenstelling zijpaneel"} />
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "2px 10px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
€ 900,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="door-content-2"
|
||||||
|
style={{
|
||||||
|
borderBottom: "25px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="door-content-2-1"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
|
gridColumnGap: "10px",
|
||||||
|
fontSize: "11px",
|
||||||
|
padding: "5%",
|
||||||
|
borderBottom: "2px solid #d1d1d1",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) setSidePannel("links");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border:
|
||||||
|
sidePannel === "links"
|
||||||
|
? "2px solid black"
|
||||||
|
: "2px solid #d7d7d7",
|
||||||
|
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow:
|
||||||
|
sidePannel === "links"
|
||||||
|
? "none"
|
||||||
|
: "rgba(0, 0, 0, 0.6) -5px -2px 10px -2px",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", top: "2px", right: "2px" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={sidePannel === "links"}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/samenstelling_link.png"
|
||||||
|
style={{
|
||||||
|
maxWidth: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor:
|
||||||
|
sidePannel === "links" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
paddingTop: "5px",
|
||||||
|
// border: '2px solid white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
links
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) setSidePannel("rechts");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border:
|
||||||
|
sidePannel === "rechts"
|
||||||
|
? "2px solid black"
|
||||||
|
: "2px solid #d7d7d7",
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow:
|
||||||
|
sidePannel === "rechts"
|
||||||
|
? "none"
|
||||||
|
: "rgba(0, 0, 0, 0.6) -5px -2px 10px -2px",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", top: "2px", right: "2px" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={sidePannel === "rechts"}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/samenstelling_rechts.png"
|
||||||
|
style={{ maxWidth: "80%", maxHeight: "100%" }}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor:
|
||||||
|
sidePannel === "rechts" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
rechts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) setSidePannel("beide");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border:
|
||||||
|
sidePannel === "beide"
|
||||||
|
? "2px solid black"
|
||||||
|
: "2px solid #d7d7d7",
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow:
|
||||||
|
sidePannel === "beide"
|
||||||
|
? "none"
|
||||||
|
: "rgba(0, 0, 0, 0.6) -5px -2px 10px -2px",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", top: "2px", right: "2px" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={sidePannel === "beide"}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/samenstelling_beide.png"
|
||||||
|
style={{ maxWidth: "80%", maxHeight: "100%" }}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor:
|
||||||
|
sidePannel === "beide" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
beide
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "5px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!inprogress) {
|
||||||
|
setSidePannel("geen");
|
||||||
|
setSidePannelSize(width);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border:
|
||||||
|
sidePannel === "geen"
|
||||||
|
? "2px solid black"
|
||||||
|
: "2px solid #d7d7d7",
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow:
|
||||||
|
sidePannel === "geen"
|
||||||
|
? "none"
|
||||||
|
: "rgba(0, 0, 0, 0.6) -5px -2px 10px -2px",
|
||||||
|
aspectRatio: "1/1",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", top: "2px", right: "2px" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={sidePannel === "geen"}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<center>
|
||||||
|
<img
|
||||||
|
src="./images/doortypes/samenstelling_geen.png"
|
||||||
|
style={{ maxWidth: "80%", maxHeight: "100%" }}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
paddingTop: "5px",
|
||||||
|
margin: "10px 0 0 0",
|
||||||
|
backgroundColor:
|
||||||
|
sidePannel === "geen" ? "black" : "#bdc79d",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-5px",
|
||||||
|
left: "0",
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "50px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="itm-txt"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
geen
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Situatie van het vaste paneel</span>
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "10px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
€ 0,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "small",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
margin: "5%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={stalenType == "divider"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => {
|
||||||
|
setStalenType("divider");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ marginLeft: "0.5rem" }}>Roomdivider</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="sm"
|
||||||
|
checked={stalenType == "tussen"}
|
||||||
|
readOnly
|
||||||
|
onChange={() => {
|
||||||
|
setStalenType("tussen");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ marginLeft: "0.5rem" }}>Tussen 2 wanden</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Aantal panelen</span>
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "20px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "10px",
|
||||||
|
margin: "0 0 0 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="body-txt"
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setStep("door");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
€ 0,00
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="slider-container"
|
||||||
|
style={{ display: "flex", alignItems: "center", padding: "5%" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="4"
|
||||||
|
value={stalenPart}
|
||||||
|
onChange={handleStalenPart}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
appearance: "none",
|
||||||
|
background: "#F2F2F3",
|
||||||
|
outline: "none",
|
||||||
|
marginTop: "10px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: "10px",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
paddingRight: "10px",
|
||||||
|
paddingTop: "5px",
|
||||||
|
paddingBottom: "5px",
|
||||||
|
backgroundColor: "#F2F2F3",
|
||||||
|
position: "relative",
|
||||||
|
width: "6rem",
|
||||||
|
fontSize: "small",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: "10px" }}>{stalenPart}</span>
|
||||||
|
<span>panelen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type !== "vast-stalen" && (
|
||||||
|
<>
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "5%",
|
||||||
|
paddingTop: "0.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
fontSize: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoIcon title={"Breedte deur"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={60}
|
||||||
|
max={120}
|
||||||
|
value={width}
|
||||||
|
onChange={handleWidthChange}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
appearance: "none",
|
||||||
|
background: "#F2F2F3", // Grey line
|
||||||
|
outline: "none", // Remove default outline
|
||||||
|
marginTop: "10px", // Adjust margin to separate from the text above
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: "10px",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
paddingRight: "10px",
|
||||||
|
paddingTop: "5px",
|
||||||
|
paddingBottom: "5px",
|
||||||
|
backgroundColor: "#F2F2F3",
|
||||||
|
position: "relative",
|
||||||
|
width: "5rem",
|
||||||
|
fontSize: "small",
|
||||||
|
border: "1px solid",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: "10px" }}>{width}</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "10px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="volgende btn"
|
||||||
|
onClick={() => {
|
||||||
|
setStep("design");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Volgende
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
246
bron/style.css
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/* CSS Styles for endpoint.js Starts*/
|
||||||
|
/* CSS Styles for Computer in endpoint.js Starts */
|
||||||
|
|
||||||
|
.body-can-2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-can-2-1 {
|
||||||
|
position: relative;
|
||||||
|
/* Change to relative */
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door-content-2-1{
|
||||||
|
grid-template-columns: repeat(2 , 1fr);
|
||||||
|
grid-template-rows: repeat(1 , 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
box-shadow: 0px -8px 10px -5px rgba(0, 0, 0, 0.8);
|
||||||
|
overflow-y: hidden;
|
||||||
|
z-index: 99999;
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-can-2-1-1 {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: calc(100% - 400px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-can-2-1-2 {
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
height: 100%;
|
||||||
|
width: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-btn {
|
||||||
|
margin: 0 10px 5px 10px;
|
||||||
|
width: 70px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
background-color: #bdc79d;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #bdc79d;
|
||||||
|
position: relative;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
/* Ensure the parent container is relative */
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-txt {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #110000;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itm-txt {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the scrollbar track (the background) */
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: white;
|
||||||
|
/* Use any background color you want */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customize the scrollbar thumb (the draggable part) */
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: gray;
|
||||||
|
/* Color of the scrollbar thumb */
|
||||||
|
border-radius: 10px;
|
||||||
|
/* Rounded corners of the scrollbar thumb */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the scrollbar buttons (arrows at the ends) */
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Customize the scrollbar corner */
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
/* Background color of the scrollbar corner */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the scrollbar on hover */
|
||||||
|
::-webkit-scrollbar:hover {
|
||||||
|
width: 8px;
|
||||||
|
/* Width of the scrollbar on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Add a shadow to the scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
/* Width of the scrollbar */
|
||||||
|
height: 8px;
|
||||||
|
/* Height of the scrollbar */
|
||||||
|
background-color: transparent;
|
||||||
|
/* Background color of the scrollbar area */
|
||||||
|
border-radius: 10px;
|
||||||
|
/* Rounded corners of the scrollbar */
|
||||||
|
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
/* Shadow inside the scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
#range1 {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: red;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
z-index: 12;
|
||||||
|
/* box-shadow: -407px 0 0 400px #f50; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.quot {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volgende.btn {
|
||||||
|
background-color: #bdc79d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-banner button {
|
||||||
|
margin: 0 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-banner button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* CSS Styles for Mobile in endpoint.js Ends */
|
||||||
|
/* CSS Styles for endpoint.js Ends*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
.ham-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-can-2-1-1 {
|
||||||
|
height: 33%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-can-2-1-2 {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 67%;
|
||||||
|
}
|
||||||
|
.door-content-2-1{
|
||||||
|
grid-template-columns: repeat(4 , 1fr);
|
||||||
|
grid-template-rows: repeat(1 , 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content.collapsed {
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-btn.collapsed {
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-txt.collapsed {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio.collapsed {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 490px) {
|
||||||
|
.body-btn {
|
||||||
|
margin: 7px 10px 3px 10px;
|
||||||
|
width: 45px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.quot {
|
||||||
|
height: 1rem;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.pd {
|
||||||
|
margin-bottom: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.itm-txt {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 350px) {
|
||||||
|
.itm-txt {
|
||||||
|
font-size: xx-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
132
bron/style0.css
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: #efefef;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='radio'] {
|
||||||
|
appearance: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-clip: content-box;
|
||||||
|
border: 2px solid #bdc79d;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='radio'].sm {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='radio']:checked {
|
||||||
|
background-color: #bdc79d;
|
||||||
|
/* padding: 2px; */
|
||||||
|
border: 2px solid #bdc79d;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
accent-color: #bdc79d;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 16px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 16px solid #3498db;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
-webkit-animation: spin 2s linear infinite;
|
||||||
|
/* Safari */
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safari */
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volgende {
|
||||||
|
background-color: #fab500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
margin: 5%;
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volgende:hover {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='range']::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #bdc79d;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number']::-webkit-inner-spin-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-title-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .door-content-2-1 {
|
||||||
|
display: flex;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* .door-content-2-1>div>div {
|
||||||
|
box-shadow: 0px 8px 0px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.door-content-2-1>div>div:hover {
|
||||||
|
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
|
||||||
|
} */
|
||||||
3
bron/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useMemo, Suspense } from "react";
|
import { useRef, useMemo, Suspense } from "react";
|
||||||
import { useConfiguratorStore } from "@/lib/store";
|
import { useConfiguratorStore, type GlassColor, type Finish } from "@/lib/store";
|
||||||
import { RoundedBox, useTexture } from "@react-three/drei";
|
import { RoundedBox, useTexture } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import {
|
import {
|
||||||
@@ -26,28 +26,58 @@ import {
|
|||||||
type PhysicalPart,
|
type PhysicalPart,
|
||||||
} from "@/lib/door-models";
|
} from "@/lib/door-models";
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// FRAME COLOR MAPPING
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
const FRAME_COLORS: Record<Finish, string> = {
|
||||||
|
zwart: "#1a1a1a",
|
||||||
|
brons: "#8B6F47",
|
||||||
|
grijs: "#525252",
|
||||||
|
goud: "#B8860B",
|
||||||
|
beige: "#C8B88A",
|
||||||
|
ral: "#4A6741",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FRAME_TEXTURE_PATHS: Record<Finish, string> = {
|
||||||
|
zwart: "/textures/proinn/proinn-metaalkleur-zwart.jpg",
|
||||||
|
brons: "/textures/proinn/proinn-metaalkleur-brons.jpg",
|
||||||
|
grijs: "/textures/proinn/proinn-metaalkleur-antraciet.jpg",
|
||||||
|
goud: "/textures/proinn/proinn-metaalkleur-goud.jpg",
|
||||||
|
beige: "/textures/proinn/proinn-metaalkleur-beige.jpg",
|
||||||
|
ral: "/textures/proinn/proinn-metaalkleur-ral-keuze.jpg",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// GLASS COLOR MAPPING
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
interface GlassColorProps {
|
||||||
|
color: string;
|
||||||
|
transmission: number;
|
||||||
|
roughness: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLASS_COLOR_MAP: Record<GlassColor, GlassColorProps> = {
|
||||||
|
helder: { color: "#eff6ff", transmission: 0.98, roughness: 0.05 },
|
||||||
|
grijs: { color: "#3a3a3a", transmission: 0.85, roughness: 0.1 },
|
||||||
|
brons: { color: "#8B6F47", transmission: 0.85, roughness: 0.1 },
|
||||||
|
"mat-blank": { color: "#e8e8e8", transmission: 0.7, roughness: 0.3 },
|
||||||
|
"mat-brons": { color: "#A0845C", transmission: 0.6, roughness: 0.35 },
|
||||||
|
"mat-zwart": { color: "#1a1a1a", transmission: 0.5, roughness: 0.4 },
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PHOTOREALISTIC MATERIALS
|
// PHOTOREALISTIC MATERIALS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
function SteelMaterialTextured({ color, finish }: { color: string; finish: Finish }) {
|
||||||
* Steel Material with Aluwdoors Texture
|
|
||||||
* Vertical steel grain for industrial look
|
|
||||||
*/
|
|
||||||
function SteelMaterialTextured({ color, finish }: { color: string; finish: string }) {
|
|
||||||
try {
|
try {
|
||||||
// Load texture based on finish
|
const texturePath = FRAME_TEXTURE_PATHS[finish];
|
||||||
const texturePath = {
|
|
||||||
zwart: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg",
|
|
||||||
brons: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-brons.jpg",
|
|
||||||
grijs: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-antraciet.jpg",
|
|
||||||
}[finish] || "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg";
|
|
||||||
|
|
||||||
const texture = useTexture(texturePath);
|
const texture = useTexture(texturePath);
|
||||||
|
|
||||||
// Configure texture for vertical steel grain
|
|
||||||
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
||||||
texture.repeat.set(0.5, 3); // Vertical grain
|
texture.repeat.set(0.5, 3);
|
||||||
texture.colorSpace = THREE.SRGBColorSpace;
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,14 +89,11 @@ function SteelMaterialTextured({ color, finish }: { color: string; finish: strin
|
|||||||
envMapIntensity={1.5}
|
envMapIntensity={1.5}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch {
|
||||||
return <SteelMaterialFallback color={color} />;
|
return <SteelMaterialFallback color={color} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback Steel Material (Solid Color)
|
|
||||||
*/
|
|
||||||
function SteelMaterialFallback({ color }: { color: string }) {
|
function SteelMaterialFallback({ color }: { color: string }) {
|
||||||
return (
|
return (
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
@@ -78,40 +105,37 @@ function SteelMaterialFallback({ color }: { color: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function GlassMaterial({ glassColor }: { glassColor: GlassColor }) {
|
||||||
* Photorealistic Glass Material
|
const props = GLASS_COLOR_MAP[glassColor];
|
||||||
* High transmission for realistic glass look
|
return (
|
||||||
*/
|
<meshPhysicalMaterial
|
||||||
const GlassMaterial = () => (
|
transmission={props.transmission}
|
||||||
<meshPhysicalMaterial
|
roughness={props.roughness}
|
||||||
transmission={0.98}
|
thickness={0.007}
|
||||||
roughness={0.05}
|
ior={1.5}
|
||||||
thickness={0.007}
|
color={props.color}
|
||||||
ior={1.5}
|
transparent
|
||||||
color="#eff6ff"
|
opacity={0.98}
|
||||||
transparent
|
envMapIntensity={1.0}
|
||||||
opacity={0.98}
|
/>
|
||||||
envMapIntensity={1.0}
|
);
|
||||||
/>
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PHYSICAL PART RENDERER
|
// PHYSICAL PART RENDERER
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a single physical part with correct geometry
|
|
||||||
*/
|
|
||||||
function PhysicalPartComponent({
|
function PhysicalPartComponent({
|
||||||
part,
|
part,
|
||||||
frameColor,
|
frameColor,
|
||||||
finish,
|
finish,
|
||||||
|
glassColor,
|
||||||
}: {
|
}: {
|
||||||
part: PhysicalPart;
|
part: PhysicalPart;
|
||||||
frameColor: string;
|
frameColor: string;
|
||||||
finish: string;
|
finish: Finish;
|
||||||
|
glassColor: GlassColor;
|
||||||
}) {
|
}) {
|
||||||
// Convert mm to meters
|
|
||||||
const x = mmToMeters(part.x);
|
const x = mmToMeters(part.x);
|
||||||
const y = mmToMeters(part.y);
|
const y = mmToMeters(part.y);
|
||||||
const z = mmToMeters(part.z);
|
const z = mmToMeters(part.z);
|
||||||
@@ -119,17 +143,15 @@ function PhysicalPartComponent({
|
|||||||
const height = mmToMeters(part.height);
|
const height = mmToMeters(part.height);
|
||||||
const depth = mmToMeters(part.depth);
|
const depth = mmToMeters(part.depth);
|
||||||
|
|
||||||
// Glass uses different material
|
|
||||||
if (part.isGlass) {
|
if (part.isGlass) {
|
||||||
return (
|
return (
|
||||||
<mesh position={[x, y, z]} castShadow receiveShadow>
|
<mesh position={[x, y, z]} castShadow receiveShadow>
|
||||||
<boxGeometry args={[width, height, depth]} />
|
<boxGeometry args={[width, height, depth]} />
|
||||||
<GlassMaterial />
|
<GlassMaterial glassColor={glassColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steel profiles use RoundedBox for realistic edges
|
|
||||||
const cornerRadius = mmToMeters(PROFILE_CORNER_RADIUS);
|
const cornerRadius = mmToMeters(PROFILE_CORNER_RADIUS);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -153,32 +175,21 @@ function PhysicalPartComponent({
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export function Door3DEnhanced() {
|
export function Door3DEnhanced() {
|
||||||
const { doorType, gridType, finish, handle, glassPattern, doorLeafWidth, height } =
|
const { doorType, gridType, finish, handle, glassPattern, glassColor, doorLeafWidth, height } =
|
||||||
useConfiguratorStore();
|
useConfiguratorStore();
|
||||||
const doorRef = useRef<THREE.Group>(null);
|
const doorRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
// Frame color based on finish
|
const frameColor = FRAME_COLORS[finish] || "#1a1a1a";
|
||||||
const frameColor = {
|
|
||||||
zwart: "#1a1a1a",
|
|
||||||
brons: "#8B6F47",
|
|
||||||
grijs: "#525252",
|
|
||||||
}[finish] || "#1a1a1a";
|
|
||||||
|
|
||||||
// Generate door assembly from manufacturing specs
|
|
||||||
const doorAssembly = useMemo(
|
const doorAssembly = useMemo(
|
||||||
() => generateDoorAssembly(doorType, gridType, doorLeafWidth, height),
|
() => generateDoorAssembly(doorType, gridType, doorLeafWidth, height),
|
||||||
[doorType, gridType, doorLeafWidth, height]
|
[doorType, gridType, doorLeafWidth, height]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert dimensions to meters
|
|
||||||
const doorWidth = mmToMeters(doorLeafWidth);
|
const doorWidth = mmToMeters(doorLeafWidth);
|
||||||
const doorHeight = mmToMeters(height);
|
const doorHeight = mmToMeters(height);
|
||||||
|
|
||||||
// Profile dimensions in meters (for handle positioning)
|
|
||||||
const stileWidth = mmToMeters(40);
|
const stileWidth = mmToMeters(40);
|
||||||
const railDepth = mmToMeters(40);
|
const railDepth = mmToMeters(40);
|
||||||
|
|
||||||
// Get divider positions for glass patterns (backward compatibility)
|
|
||||||
const dividerPositions = getDividerPositions(gridType, height);
|
const dividerPositions = getDividerPositions(gridType, height);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -190,6 +201,7 @@ export function Door3DEnhanced() {
|
|||||||
part={part}
|
part={part}
|
||||||
frameColor={frameColor}
|
frameColor={frameColor}
|
||||||
finish={finish}
|
finish={finish}
|
||||||
|
glassColor={glassColor}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -208,13 +220,12 @@ export function Door3DEnhanced() {
|
|||||||
{ depth: 0.01, bevelEnabled: false },
|
{ depth: 0.01, bevelEnabled: false },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<GlassMaterial />
|
<GlassMaterial glassColor={glassColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{glassPattern === "dt10-ushape" && dividerPositions.length > 0 && (
|
{glassPattern === "dt10-ushape" && dividerPositions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{/* Top section - Inverted U */}
|
|
||||||
<mesh
|
<mesh
|
||||||
position={[0, (doorHeight / 4 + dividerPositions[0]) / 2, 0]}
|
position={[0, (doorHeight / 4 + dividerPositions[0]) / 2, 0]}
|
||||||
castShadow
|
castShadow
|
||||||
@@ -229,10 +240,9 @@ export function Door3DEnhanced() {
|
|||||||
{ depth: 0.01, bevelEnabled: false },
|
{ depth: 0.01, bevelEnabled: false },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<GlassMaterial />
|
<GlassMaterial glassColor={glassColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Bottom section - Normal U */}
|
|
||||||
<mesh
|
<mesh
|
||||||
position={[
|
position={[
|
||||||
0,
|
0,
|
||||||
@@ -255,7 +265,7 @@ export function Door3DEnhanced() {
|
|||||||
{ depth: 0.01, bevelEnabled: false },
|
{ depth: 0.01, bevelEnabled: false },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<GlassMaterial />
|
<GlassMaterial glassColor={glassColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -264,58 +274,22 @@ export function Door3DEnhanced() {
|
|||||||
|
|
||||||
{/* PROFESSIONAL 3D HANDLES */}
|
{/* PROFESSIONAL 3D HANDLES */}
|
||||||
{handle === "beugelgreep" && (
|
{handle === "beugelgreep" && (
|
||||||
<Beugelgreep
|
<Beugelgreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
|
||||||
finish={finish}
|
|
||||||
doorWidth={doorWidth}
|
|
||||||
doorHeight={doorHeight}
|
|
||||||
railDepth={railDepth}
|
|
||||||
stileWidth={stileWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{handle === "hoekgreep" && (
|
{handle === "hoekgreep" && (
|
||||||
<Hoekgreep
|
<Hoekgreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
|
||||||
finish={finish}
|
|
||||||
doorWidth={doorWidth}
|
|
||||||
doorHeight={doorHeight}
|
|
||||||
railDepth={railDepth}
|
|
||||||
stileWidth={stileWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{handle === "maangreep" && (
|
{handle === "maangreep" && (
|
||||||
<Maangreep
|
<Maangreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
|
||||||
finish={finish}
|
|
||||||
doorWidth={doorWidth}
|
|
||||||
doorHeight={doorHeight}
|
|
||||||
railDepth={railDepth}
|
|
||||||
stileWidth={stileWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{handle === "ovaalgreep" && (
|
{handle === "ovaalgreep" && (
|
||||||
<Ovaalgreep
|
<Ovaalgreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
|
||||||
finish={finish}
|
|
||||||
doorWidth={doorWidth}
|
|
||||||
doorHeight={doorHeight}
|
|
||||||
railDepth={railDepth}
|
|
||||||
stileWidth={stileWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{handle === "klink" && (
|
{handle === "klink" && (
|
||||||
<Klink
|
<Klink finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
|
||||||
finish={finish}
|
|
||||||
doorWidth={doorWidth}
|
|
||||||
doorHeight={doorHeight}
|
|
||||||
railDepth={railDepth}
|
|
||||||
stileWidth={stileWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{handle === "u-greep" && (
|
{handle === "u-greep" && (
|
||||||
<UGreep
|
<UGreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
|
||||||
finish={finish}
|
|
||||||
doorWidth={doorWidth}
|
|
||||||
doorHeight={doorHeight}
|
|
||||||
railDepth={railDepth}
|
|
||||||
stileWidth={stileWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,11 +36,14 @@ export function Door3D() {
|
|||||||
const doorRef = useRef<THREE.Group>(null);
|
const doorRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
// Frame color based on finish
|
// Frame color based on finish
|
||||||
const frameColor = {
|
const frameColor = ({
|
||||||
zwart: "#1a1a1a",
|
zwart: "#1a1a1a",
|
||||||
brons: "#8B6F47",
|
brons: "#8B6F47",
|
||||||
grijs: "#525252",
|
grijs: "#525252",
|
||||||
}[finish];
|
goud: "#b8960c",
|
||||||
|
beige: "#c8b88a",
|
||||||
|
ral: "#2a2a2a",
|
||||||
|
} as Record<string, string>)[finish] ?? "#1a1a1a";
|
||||||
|
|
||||||
// Convert mm to meters for 3D scene
|
// Convert mm to meters for 3D scene
|
||||||
const doorWidth = doorLeafWidth / 1000; // Convert mm to m
|
const doorWidth = doorLeafWidth / 1000; // Convert mm to m
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Suspense } from "react";
|
import { Suspense, useCallback } from "react";
|
||||||
import { useConfiguratorStore } from "@/lib/store";
|
import { useConfiguratorStore } from "@/lib/store";
|
||||||
import { Scene3D } from "./scene";
|
import { Scene3D } from "./scene";
|
||||||
|
import { Camera } from "lucide-react";
|
||||||
|
|
||||||
function LoadingFallback() {
|
function LoadingFallback() {
|
||||||
return (
|
return (
|
||||||
@@ -15,8 +16,31 @@ function LoadingFallback() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPrice(amount: number): string {
|
||||||
|
return new Intl.NumberFormat("nl-NL", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
export function DoorVisualizer() {
|
export function DoorVisualizer() {
|
||||||
const { doorType, gridType, finish, handle } = useConfiguratorStore();
|
const { doorType, gridType, finish, handle, priceBreakdown, setScreenshotDataUrl } =
|
||||||
|
useConfiguratorStore();
|
||||||
|
|
||||||
|
const handleScreenshot = useCallback(() => {
|
||||||
|
const canvas = document.querySelector("canvas");
|
||||||
|
if (!canvas) return;
|
||||||
|
const dataUrl = canvas.toDataURL("image/png");
|
||||||
|
setScreenshotDataUrl(dataUrl);
|
||||||
|
|
||||||
|
// Also trigger download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "proinn-deur-configuratie.png";
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.click();
|
||||||
|
}, [setScreenshotDataUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full overflow-hidden rounded-[2.5rem]">
|
<div className="relative h-full w-full overflow-hidden rounded-[2.5rem]">
|
||||||
@@ -28,13 +52,36 @@ export function DoorVisualizer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Screenshot Button */}
|
||||||
|
<div className="absolute right-8 top-8 z-10">
|
||||||
|
<button
|
||||||
|
onClick={handleScreenshot}
|
||||||
|
className="flex items-center gap-2 rounded-full bg-[#1A2E2E]/80 px-3 py-2 text-xs font-medium text-white shadow-lg backdrop-blur-sm transition-all hover:bg-[#1A2E2E]"
|
||||||
|
>
|
||||||
|
<Camera className="size-3.5" />
|
||||||
|
Screenshot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 3D Scene */}
|
{/* 3D Scene */}
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Scene3D />
|
<Scene3D />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
{/* Live Price Badge */}
|
||||||
|
<div className="absolute right-8 bottom-24 z-10 lg:bottom-8">
|
||||||
|
<div className="rounded-2xl bg-[#1A2E2E] px-5 py-3 text-right shadow-lg">
|
||||||
|
<div className="text-[10px] font-medium uppercase tracking-wider text-gray-400">
|
||||||
|
Indicatieprijs
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-[#C4D668]">
|
||||||
|
{formatPrice(priceBreakdown.totalPrice)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Configuration Info Card */}
|
{/* Configuration Info Card */}
|
||||||
<div className="absolute bottom-8 left-8 right-8 z-10">
|
<div className="absolute bottom-8 left-8 z-10">
|
||||||
<div className="rounded-2xl bg-white/90 p-4 shadow-lg backdrop-blur-sm">
|
<div className="rounded-2xl bg-white/90 p-4 shadow-lg backdrop-blur-sm">
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
@@ -64,9 +111,9 @@ export function DoorVisualizer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls Hint */}
|
{/* Controls Hint */}
|
||||||
<div className="absolute bottom-8 right-8 z-10 hidden lg:block">
|
<div className="absolute bottom-8 right-8 z-10 hidden lg:hidden">
|
||||||
<div className="rounded-xl bg-[#1A2E2E]/80 px-3 py-2 text-xs text-white backdrop-blur-sm">
|
<div className="rounded-xl bg-[#1A2E2E]/80 px-3 py-2 text-xs text-white backdrop-blur-sm">
|
||||||
<p className="font-medium">🖱️ Drag to rotate • Scroll to zoom</p>
|
<p className="font-medium">Drag to rotate - Scroll to zoom</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,11 +37,14 @@ export interface HandleProps {
|
|||||||
*/
|
*/
|
||||||
function HandleMaterialTextured({ color, finish }: { color: string; finish: string }) {
|
function HandleMaterialTextured({ color, finish }: { color: string; finish: string }) {
|
||||||
try {
|
try {
|
||||||
const texturePath = {
|
const texturePath = ({
|
||||||
zwart: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg",
|
zwart: "/textures/proinn/proinn-metaalkleur-zwart.jpg",
|
||||||
brons: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-brons.jpg",
|
brons: "/textures/proinn/proinn-metaalkleur-brons.jpg",
|
||||||
grijs: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-antraciet.jpg",
|
grijs: "/textures/proinn/proinn-metaalkleur-antraciet.jpg",
|
||||||
}[finish] || "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg";
|
goud: "/textures/proinn/proinn-metaalkleur-goud.jpg",
|
||||||
|
beige: "/textures/proinn/proinn-metaalkleur-beige.jpg",
|
||||||
|
ral: "/textures/proinn/proinn-metaalkleur-ral-keuze.jpg",
|
||||||
|
} as Record<string, string>)[finish] || "/textures/proinn/proinn-metaalkleur-zwart.jpg";
|
||||||
|
|
||||||
const texture = useTexture(texturePath);
|
const texture = useTexture(texturePath);
|
||||||
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
||||||
@@ -83,7 +86,14 @@ function PowderCoatMaterial({ color, finish }: { color: string; finish: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getColor(finish: string): string {
|
function getColor(finish: string): string {
|
||||||
return { zwart: "#1a1a1a", brons: "#8B6F47", grijs: "#525252" }[finish] || "#1a1a1a";
|
return ({
|
||||||
|
zwart: "#1a1a1a",
|
||||||
|
brons: "#8B6F47",
|
||||||
|
grijs: "#525252",
|
||||||
|
goud: "#B8860B",
|
||||||
|
beige: "#C8B88A",
|
||||||
|
ral: "#4A6741",
|
||||||
|
} as Record<string, string>)[finish] || "#1a1a1a";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -122,14 +132,16 @@ function MountStandoff({
|
|||||||
/**
|
/**
|
||||||
* U-Greep: Proper U-shaped bar handle with two standoff mounts.
|
* U-Greep: Proper U-shaped bar handle with two standoff mounts.
|
||||||
* The grip sits 40mm off the door face, connected by two cylindrical pootjes.
|
* The grip sits 40mm off the door face, connected by two cylindrical pootjes.
|
||||||
|
* Mounted on the right stile (vertical frame profile).
|
||||||
*/
|
*/
|
||||||
export function UGreep({ finish, doorHeight }: HandleProps) {
|
export function UGreep({ finish, doorWidth, doorHeight, stileWidth }: HandleProps) {
|
||||||
const color = getColor(finish);
|
const color = getColor(finish);
|
||||||
const gripLength = Math.min(doorHeight * 0.25, 0.6); // Max 60cm, proportional
|
const gripLength = Math.min(doorHeight * 0.25, 0.6); // Max 60cm, proportional
|
||||||
const mountSpacing = gripLength - GRIP_BAR_SIZE; // Distance between mount centers
|
const mountSpacing = gripLength - GRIP_BAR_SIZE; // Distance between mount centers
|
||||||
|
const xPos = doorWidth / 2 - stileWidth / 2; // Center of right stile
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[0, 0, 0]}>
|
<group position={[xPos, 0, 0]}>
|
||||||
{/* Top mount standoff */}
|
{/* Top mount standoff */}
|
||||||
<MountStandoff
|
<MountStandoff
|
||||||
position={[0, mountSpacing / 2, MOUNT_CENTER_Z]}
|
position={[0, mountSpacing / 2, MOUNT_CENTER_Z]}
|
||||||
@@ -163,16 +175,18 @@ export function UGreep({ finish, doorHeight }: HandleProps) {
|
|||||||
* Beugelgreep: Vertical bar handle (round) with mounting blocks.
|
* Beugelgreep: Vertical bar handle (round) with mounting blocks.
|
||||||
* Two rectangular mounting blocks press against the door face,
|
* Two rectangular mounting blocks press against the door face,
|
||||||
* with a round bar connecting them.
|
* with a round bar connecting them.
|
||||||
|
* Mounted on the right stile (vertical frame profile).
|
||||||
*/
|
*/
|
||||||
export function Beugelgreep({ finish, doorHeight }: HandleProps) {
|
export function Beugelgreep({ finish, doorWidth, doorHeight, stileWidth }: HandleProps) {
|
||||||
const color = getColor(finish);
|
const color = getColor(finish);
|
||||||
const gripLength = Math.min(doorHeight * 0.35, 0.8); // Max 80cm
|
const gripLength = Math.min(doorHeight * 0.35, 0.8); // Max 80cm
|
||||||
const barDiameter = 0.025; // 25mm
|
const barDiameter = 0.025; // 25mm
|
||||||
const mountBlockSize: [number, number, number] = [0.04, 0.05, MOUNT_LENGTH];
|
const mountBlockSize: [number, number, number] = [0.04, 0.05, MOUNT_LENGTH];
|
||||||
const mountSpacing = gripLength * 0.85;
|
const mountSpacing = gripLength * 0.85;
|
||||||
|
const xPos = doorWidth / 2 - stileWidth / 2; // Center of right stile
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[0, 0, 0]}>
|
<group position={[xPos, 0, 0]}>
|
||||||
{/* Top mounting block (sits on door face, extends outward) */}
|
{/* Top mounting block (sits on door face, extends outward) */}
|
||||||
<RoundedBox
|
<RoundedBox
|
||||||
args={mountBlockSize}
|
args={mountBlockSize}
|
||||||
@@ -229,8 +243,8 @@ export function Hoekgreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
|||||||
const barThickness = 0.02;
|
const barThickness = 0.02;
|
||||||
const barWidth = 0.03;
|
const barWidth = 0.03;
|
||||||
|
|
||||||
// Position near right stile
|
// Position on right stile center
|
||||||
const xPos = doorWidth / 2 - stileWidth - 0.12;
|
const xPos = doorWidth / 2 - stileWidth / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[xPos, 0, 0]}>
|
<group position={[xPos, 0, 0]}>
|
||||||
@@ -289,7 +303,8 @@ export function Hoekgreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
|||||||
export function Maangreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
export function Maangreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
||||||
const color = getColor(finish);
|
const color = getColor(finish);
|
||||||
const curveRadius = 0.08;
|
const curveRadius = 0.08;
|
||||||
const xPos = doorWidth / 2 - stileWidth - 0.12;
|
// Position on right stile center
|
||||||
|
const xPos = doorWidth / 2 - stileWidth / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[xPos, 0, 0]}>
|
<group position={[xPos, 0, 0]}>
|
||||||
@@ -338,7 +353,8 @@ export function Maangreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
|||||||
*/
|
*/
|
||||||
export function Ovaalgreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
export function Ovaalgreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
||||||
const color = getColor(finish);
|
const color = getColor(finish);
|
||||||
const xPos = doorWidth / 2 - stileWidth - 0.12;
|
// Position on right stile center
|
||||||
|
const xPos = doorWidth / 2 - stileWidth / 2;
|
||||||
|
|
||||||
const shape = new THREE.Shape();
|
const shape = new THREE.Shape();
|
||||||
const rx = 0.06;
|
const rx = 0.06;
|
||||||
@@ -391,7 +407,8 @@ export function Ovaalgreep({ finish, doorWidth, stileWidth }: HandleProps) {
|
|||||||
export function Klink({ finish, doorWidth, stileWidth }: HandleProps) {
|
export function Klink({ finish, doorWidth, stileWidth }: HandleProps) {
|
||||||
const color = getColor(finish);
|
const color = getColor(finish);
|
||||||
const leverLength = 0.12;
|
const leverLength = 0.12;
|
||||||
const xPos = doorWidth / 2 - stileWidth - 0.1;
|
// Position on right stile center
|
||||||
|
const xPos = doorWidth / 2 - stileWidth / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[xPos, 0, 0]}>
|
<group position={[xPos, 0, 0]}>
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export function useMetalTexture(finish: string) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mapping: Record<string, string> = {
|
const mapping: Record<string, string> = {
|
||||||
zwart: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg",
|
zwart: "/textures/proinn/proinn-metaalkleur-zwart.jpg",
|
||||||
brons: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-brons.jpg",
|
brons: "/textures/proinn/proinn-metaalkleur-brons.jpg",
|
||||||
grijs: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-antraciet.jpg",
|
grijs: "/textures/proinn/proinn-metaalkleur-antraciet.jpg",
|
||||||
};
|
};
|
||||||
|
|
||||||
setTextureUrl(mapping[finish] || mapping.zwart);
|
setTextureUrl(mapping[finish] || mapping.zwart);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight, Phone } from "lucide-react";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
return (
|
return (
|
||||||
<section className="relative flex min-h-screen items-end overflow-hidden">
|
<section className="relative flex h-[70vh] min-h-[480px] max-h-[720px] items-end overflow-hidden">
|
||||||
{/* Background image */}
|
{/* Background image */}
|
||||||
<Image
|
<Image
|
||||||
src="/images/hero.jpg"
|
src="/images/hero.jpg"
|
||||||
@@ -15,45 +15,48 @@ export function Hero() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Gradient overlay */}
|
{/* Gradient overlay */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-[#1A2E2E]/90 via-[#1A2E2E]/30 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-[#1A2E2E]/90 via-[#1A2E2E]/40 to-[#1A2E2E]/10" />
|
||||||
|
|
||||||
{/* Content pinned to bottom */}
|
{/* Content pinned to bottom */}
|
||||||
<div className="relative w-full pb-20 pt-40">
|
<div className="relative w-full pb-12 pt-20">
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
{/* Label */}
|
<div className="max-w-2xl">
|
||||||
<div className="mb-6 flex items-center gap-2">
|
{/* Label */}
|
||||||
<div className="h-5 w-1 rounded-full bg-[#C4D668]" />
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<span className="text-sm font-medium tracking-wide text-[#C4D668]">
|
<div className="h-4 w-1 rounded-full bg-[#C4D668]" />
|
||||||
Staal · Vakmanschap · Maatwerk
|
<span className="text-xs font-medium tracking-widest uppercase text-[#C4D668]">
|
||||||
</span>
|
Handgemaakt in Roosendaal
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 className="max-w-3xl text-5xl font-light leading-[1.1] tracking-tight text-white md:text-7xl">
|
<h1 className="text-4xl font-light leading-[1.1] tracking-tight text-white sm:text-5xl lg:text-6xl">
|
||||||
Innovatieve
|
Stalen deuren
|
||||||
<br />
|
<br />
|
||||||
<span className="font-semibold">Stalen</span> Oplossingen
|
<span className="font-semibold">op maat</span> gemaakt
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="mt-6 max-w-md text-base font-light leading-relaxed text-white/60">
|
<p className="mt-4 max-w-md text-sm leading-relaxed text-white/60">
|
||||||
Maatwerk voor bedrijven en particulieren. Van stalen deuren tot
|
Van ontwerp tot montage. Wij maken stalen deuren, kozijnen en
|
||||||
industriële kozijnen — wij realiseren uw visie in staal.
|
wanden die perfect passen bij uw interieur.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-10 flex flex-col gap-4 sm:flex-row">
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
<Link
|
<Link
|
||||||
href="/offerte"
|
href="/offerte"
|
||||||
className="inline-flex items-center gap-2 rounded-md bg-[#C4D668] px-7 py-3 text-sm font-semibold text-black transition-colors hover:bg-[#b5c75a]"
|
className="inline-flex items-center gap-2 rounded-md bg-[#C4D668] px-6 py-3 text-sm font-semibold text-black transition-colors hover:bg-[#b5c75a]"
|
||||||
>
|
>
|
||||||
Zelf ontwerpen
|
Configureer uw deur
|
||||||
<ArrowRight className="size-4" />
|
<ArrowRight className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<a
|
||||||
href="/producten"
|
href="tel:0165311490"
|
||||||
className="inline-flex items-center gap-2 rounded-md border-2 border-white/30 px-7 py-3 text-sm font-semibold text-white transition-colors hover:bg-white/10"
|
className="inline-flex items-center gap-2 rounded-md border border-white/20 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Bekijk Producten
|
<Phone className="size-4" />
|
||||||
</Link>
|
0165 311 490
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,37 +3,26 @@ import { Mail, Phone, Star, Facebook, Instagram, Linkedin, Youtube } from "lucid
|
|||||||
|
|
||||||
const contactInfo = [
|
const contactInfo = [
|
||||||
{ icon: Mail, text: "info@proinn.nl", href: "mailto:info@proinn.nl" },
|
{ icon: Mail, text: "info@proinn.nl", href: "mailto:info@proinn.nl" },
|
||||||
{ icon: Phone, text: "085 - 1234 567", href: "tel:0851234567" },
|
{ icon: Phone, text: "0165 311 490", href: "tel:0165311490" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const companyInfo = [
|
const companyInfo = [
|
||||||
{ label: "KVK", value: "12345678" },
|
{ label: "KVK", value: "85086991" },
|
||||||
{ label: "BTW", value: "NL123456789B01" },
|
{ label: "BTW", value: "NL863503330.B01" },
|
||||||
{ label: "IBAN", value: "NL00 INGB 0000 0000 00" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const locations = [
|
|
||||||
"Nunspeet",
|
|
||||||
"Veghel",
|
|
||||||
"Amsterdam",
|
|
||||||
"Rotterdam",
|
|
||||||
"Utrecht",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const proinnLinks = [
|
const proinnLinks = [
|
||||||
{ label: "Projecten", href: "/projecten" },
|
{ label: "Producten", href: "/producten" },
|
||||||
{ label: "Configurator", href: "/offerte" },
|
{ label: "Configurator", href: "/offerte" },
|
||||||
{ label: "Over ons", href: "/over-ons" },
|
{ label: "Over ons", href: "/over-ons" },
|
||||||
{ label: "Vacatures", href: "/vacatures" },
|
{ label: "Contact", href: "/contact" },
|
||||||
{ label: "Showrooms", href: "/showrooms" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const serviceLinks = [
|
const serviceLinks = [
|
||||||
{ label: "Contact", href: "/contact" },
|
{ label: "Stalen binnendeuren", href: "/producten#binnendeuren" },
|
||||||
{ label: "Kennisbank", href: "/kennisbank" },
|
{ label: "Stalen buitendeuren", href: "/producten#buitendeuren" },
|
||||||
{ label: "Veelgestelde vragen", href: "/faq" },
|
{ label: "Stalen kantoorwanden", href: "/producten#kantoorwanden" },
|
||||||
{ label: "Garantie", href: "/garantie" },
|
{ label: "Maatwerk", href: "/producten#maatwerk" },
|
||||||
{ label: "Onderhoud", href: "/onderhoud" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
@@ -48,13 +37,16 @@ export function Footer() {
|
|||||||
<footer className="bg-[#1A2E2E]">
|
<footer className="bg-[#1A2E2E]">
|
||||||
{/* Main Footer */}
|
{/* Main Footer */}
|
||||||
<div className="mx-auto max-w-7xl px-4 pt-16 pb-12 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 pt-16 pb-12 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-5 lg:gap-8">
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-4 lg:gap-8">
|
||||||
{/* Col 1 - Logo & Contact */}
|
{/* Col 1 - Logo & Contact */}
|
||||||
<div className="lg:col-span-1">
|
<div>
|
||||||
<Link href="/" className="text-2xl font-extrabold tracking-tight text-white">
|
<Link href="/" className="text-2xl font-extrabold tracking-tight text-white">
|
||||||
PROINN
|
PROINN
|
||||||
</Link>
|
</Link>
|
||||||
<div className="mt-6 space-y-3">
|
<p className="mt-4 text-sm leading-relaxed text-gray-400">
|
||||||
|
Handgemaakte stalen deuren, op maat geleverd en geïnstalleerd vanuit Roosendaal.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 space-y-3">
|
||||||
{contactInfo.map((item) => (
|
{contactInfo.map((item) => (
|
||||||
<a
|
<a
|
||||||
key={item.text}
|
key={item.text}
|
||||||
@@ -66,7 +58,10 @@ export function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 space-y-2">
|
<p className="mt-3 text-xs text-gray-500">
|
||||||
|
Schotsbossenstraat 2, 4705AG Roosendaal
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 space-y-1.5">
|
||||||
{companyInfo.map((item) => (
|
{companyInfo.map((item) => (
|
||||||
<p key={item.label} className="text-xs text-gray-500">
|
<p key={item.label} className="text-xs text-gray-500">
|
||||||
<span className="text-gray-400">{item.label}:</span> {item.value}
|
<span className="text-gray-400">{item.label}:</span> {item.value}
|
||||||
@@ -75,24 +70,7 @@ export function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Col 2 - Locaties */}
|
{/* Col 2 - Proinn */}
|
||||||
<div>
|
|
||||||
<h4 className="mb-4 text-sm font-semibold text-white">Locaties</h4>
|
|
||||||
<ul className="space-y-2.5">
|
|
||||||
{locations.map((city) => (
|
|
||||||
<li key={city}>
|
|
||||||
<Link
|
|
||||||
href={`/showrooms/${city.toLowerCase()}`}
|
|
||||||
className="text-sm text-gray-400 transition-colors hover:text-white"
|
|
||||||
>
|
|
||||||
{city}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Col 3 - Proinn */}
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-4 text-sm font-semibold text-white">Proinn</h4>
|
<h4 className="mb-4 text-sm font-semibold text-white">Proinn</h4>
|
||||||
<ul className="space-y-2.5">
|
<ul className="space-y-2.5">
|
||||||
@@ -109,9 +87,9 @@ export function Footer() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Col 4 - Service */}
|
{/* Col 3 - Producten */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-4 text-sm font-semibold text-white">Service</h4>
|
<h4 className="mb-4 text-sm font-semibold text-white">Producten</h4>
|
||||||
<ul className="space-y-2.5">
|
<ul className="space-y-2.5">
|
||||||
{serviceLinks.map((link) => (
|
{serviceLinks.map((link) => (
|
||||||
<li key={link.label}>
|
<li key={link.label}>
|
||||||
@@ -126,7 +104,7 @@ export function Footer() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Col 5 - Trustpilot */}
|
{/* Col 4 - Trustpilot */}
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-2xl bg-[#243636] p-6">
|
<div className="rounded-2xl bg-[#243636] p-6">
|
||||||
<p className="mb-3 text-xs font-medium uppercase tracking-wider text-gray-400">
|
<p className="mb-3 text-xs font-medium uppercase tracking-wider text-gray-400">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ChevronDown, Phone, Mail } from "lucide-react";
|
import { Phone, Mail } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@@ -11,8 +11,7 @@ import {
|
|||||||
|
|
||||||
const menuLinks = [
|
const menuLinks = [
|
||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Home" },
|
||||||
{ href: "/producten", label: "Producten", hasSubmenu: true },
|
{ href: "/producten", label: "Producten" },
|
||||||
{ href: "/maatwerk", label: "Maatwerk", hasSubmenu: true },
|
|
||||||
{ href: "/over-ons", label: "Over Ons" },
|
{ href: "/over-ons", label: "Over Ons" },
|
||||||
{ href: "/contact", label: "Contact" },
|
{ href: "/contact", label: "Contact" },
|
||||||
];
|
];
|
||||||
@@ -43,9 +42,6 @@ export function MobileMenu({ open, onOpenChange }: MobileMenuProps) {
|
|||||||
className="flex items-center justify-between rounded-md px-3 py-3 text-base font-medium text-gray-800 transition-colors hover:bg-gray-50"
|
className="flex items-center justify-between rounded-md px-3 py-3 text-base font-medium text-gray-800 transition-colors hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
{link.hasSubmenu && (
|
|
||||||
<ChevronDown className="size-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -70,11 +66,11 @@ export function MobileMenu({ open, onOpenChange }: MobileMenuProps) {
|
|||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<a
|
<a
|
||||||
href="tel:0851234567"
|
href="tel:0165311490"
|
||||||
className="flex items-center gap-2 text-sm text-gray-300 transition-colors hover:text-white"
|
className="flex items-center gap-2 text-sm text-gray-300 transition-colors hover:text-white"
|
||||||
>
|
>
|
||||||
<Phone className="size-4" />
|
<Phone className="size-4" />
|
||||||
085 - 1234 567
|
0165 311 490
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="mailto:info@proinn.nl"
|
href="mailto:info@proinn.nl"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { cn } from "@/lib/utils";
|
|||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Home" },
|
||||||
{ href: "/producten", label: "Producten" },
|
{ href: "/producten", label: "Producten" },
|
||||||
{ href: "/maatwerk", label: "Maatwerk" },
|
{ href: "/over-ons", label: "Over Ons" },
|
||||||
{ href: "/contact", label: "Contact" },
|
{ href: "/contact", label: "Contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export function TopBar() {
|
|||||||
{/* Contact & Language */}
|
{/* Contact & Language */}
|
||||||
<div className="flex items-center gap-4 text-xs text-gray-600">
|
<div className="flex items-center gap-4 text-xs text-gray-600">
|
||||||
<a
|
<a
|
||||||
href="tel:0851234567"
|
href="tel:0165311490"
|
||||||
className="flex items-center gap-1.5 font-medium transition-colors hover:text-gray-900"
|
className="flex items-center gap-1.5 font-medium transition-colors hover:text-gray-900"
|
||||||
>
|
>
|
||||||
<Phone className="size-3.5" />
|
<Phone className="size-3.5" />
|
||||||
<span>085 - 1234 567</span>
|
<span>0165 311 490</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="h-3.5 w-px bg-gray-400" />
|
<div className="h-3.5 w-px bg-gray-400" />
|
||||||
<div className="flex items-center gap-1.5 font-medium">
|
<div className="flex items-center gap-1.5 font-medium">
|
||||||
|
|||||||
@@ -7,20 +7,15 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { QuoteData } from "@/lib/validators";
|
|
||||||
|
|
||||||
const TOTAL_STEPS = 5; // 4 form steps + 1 summary
|
const TOTAL_STEPS = 6; // Product, Dimensions, Options, Extras, Contact, Summary
|
||||||
|
|
||||||
type FormData = Partial<QuoteData>;
|
|
||||||
|
|
||||||
interface FormContextValue {
|
interface FormContextValue {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
formData: FormData;
|
|
||||||
totalSteps: number;
|
totalSteps: number;
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
goToStep: (step: number) => void;
|
goToStep: (step: number) => void;
|
||||||
updateData: (data: Partial<FormData>) => void;
|
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +23,6 @@ const FormContext = createContext<FormContextValue | null>(null);
|
|||||||
|
|
||||||
export function FormProvider({ children }: { children: ReactNode }) {
|
export function FormProvider({ children }: { children: ReactNode }) {
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [formData, setFormData] = useState<FormData>({});
|
|
||||||
|
|
||||||
const nextStep = useCallback(() => {
|
const nextStep = useCallback(() => {
|
||||||
setCurrentStep((s) => Math.min(s + 1, TOTAL_STEPS - 1));
|
setCurrentStep((s) => Math.min(s + 1, TOTAL_STEPS - 1));
|
||||||
@@ -42,25 +36,18 @@ export function FormProvider({ children }: { children: ReactNode }) {
|
|||||||
setCurrentStep(Math.max(0, Math.min(step, TOTAL_STEPS - 1)));
|
setCurrentStep(Math.max(0, Math.min(step, TOTAL_STEPS - 1)));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateData = useCallback((data: Partial<FormData>) => {
|
|
||||||
setFormData((prev) => ({ ...prev, ...data }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setCurrentStep(0);
|
setCurrentStep(0);
|
||||||
setFormData({});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContext.Provider
|
<FormContext.Provider
|
||||||
value={{
|
value={{
|
||||||
currentStep,
|
currentStep,
|
||||||
formData,
|
|
||||||
totalSteps: TOTAL_STEPS,
|
totalSteps: TOTAL_STEPS,
|
||||||
nextStep,
|
nextStep,
|
||||||
prevStep,
|
prevStep,
|
||||||
goToStep,
|
goToStep,
|
||||||
updateData,
|
|
||||||
reset,
|
reset,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useFormContext } from "@/components/offerte/form-context";
|
import { useConfiguratorStore } from "@/lib/store";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { User, Mail, Phone, MessageSquare } from "lucide-react";
|
import { User, Mail, Phone, MessageSquare } from "lucide-react";
|
||||||
|
|
||||||
export function StepContact() {
|
export function StepContact() {
|
||||||
const { formData, updateData } = useFormContext();
|
const { name, email, phone, note, setName, setEmail, setPhone, setNote } =
|
||||||
|
useConfiguratorStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -24,8 +25,8 @@ export function StepContact() {
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Uw volledige naam"
|
placeholder="Uw volledige naam"
|
||||||
value={formData.name ?? ""}
|
value={name}
|
||||||
onChange={(e) => updateData({ name: e.target.value })}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="h-11 focus-visible:ring-brand-orange"
|
className="h-11 focus-visible:ring-brand-orange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,8 +41,8 @@ export function StepContact() {
|
|||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="naam@bedrijf.nl"
|
placeholder="naam@bedrijf.nl"
|
||||||
value={formData.email ?? ""}
|
value={email}
|
||||||
onChange={(e) => updateData({ email: e.target.value })}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="h-11 focus-visible:ring-brand-orange"
|
className="h-11 focus-visible:ring-brand-orange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,8 +56,8 @@ export function StepContact() {
|
|||||||
id="phone"
|
id="phone"
|
||||||
type="tel"
|
type="tel"
|
||||||
placeholder="06 1234 5678"
|
placeholder="06 1234 5678"
|
||||||
value={formData.phone ?? ""}
|
value={phone}
|
||||||
onChange={(e) => updateData({ phone: e.target.value })}
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
className="h-11 focus-visible:ring-brand-orange"
|
className="h-11 focus-visible:ring-brand-orange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,8 +73,8 @@ export function StepContact() {
|
|||||||
id="note"
|
id="note"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Bijv. specifieke wensen, RAL-kleur, plaatsing..."
|
placeholder="Bijv. specifieke wensen, RAL-kleur, plaatsing..."
|
||||||
value={formData.note ?? ""}
|
value={note}
|
||||||
onChange={(e) => updateData({ note: e.target.value })}
|
onChange={(e) => setNote(e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
89
components/offerte/step-extras.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useConfiguratorStore } from "@/lib/store";
|
||||||
|
import { Check, Ruler, Wrench, MessageCircle, Truck } from "lucide-react";
|
||||||
|
|
||||||
|
const extraOptionsList = [
|
||||||
|
{
|
||||||
|
id: "Meetservice",
|
||||||
|
label: "Meetservice",
|
||||||
|
description: "Wij komen bij u langs om de exacte maten op te nemen.",
|
||||||
|
icon: Ruler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Montage",
|
||||||
|
label: "Montage",
|
||||||
|
description: "Professionele plaatsing door onze vakmensen.",
|
||||||
|
icon: Wrench,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Adviesgesprek",
|
||||||
|
label: "Adviesgesprek",
|
||||||
|
description: "Vrijblijvend advies over mogelijkheden en materialen.",
|
||||||
|
icon: MessageCircle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Bezorging",
|
||||||
|
label: "Bezorging",
|
||||||
|
description: "Bezorging aan huis, of afhalen op locatie.",
|
||||||
|
icon: Truck,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function StepExtras() {
|
||||||
|
const { extraOptions, toggleExtraOption } = useConfiguratorStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Extra opties</h2>
|
||||||
|
<p className="mb-6 text-sm text-gray-600">
|
||||||
|
Selecteer eventuele extra services bij uw stalen deur.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{extraOptionsList.map((option) => {
|
||||||
|
const selected = extraOptions.includes(option.id);
|
||||||
|
const Icon = option.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleExtraOption(option.id)}
|
||||||
|
className={`group relative rounded-xl border-2 p-4 text-left transition-all ${
|
||||||
|
selected
|
||||||
|
? "border-[#C4D668] bg-[#1A2E2E] text-white"
|
||||||
|
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
className={`flex size-10 shrink-0 items-center justify-center rounded-lg ${
|
||||||
|
selected ? "bg-[#C4D668]/20" : "bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`size-5 ${selected ? "text-[#C4D668]" : "text-[#1A2E2E]"}`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-bold">{option.label}</h3>
|
||||||
|
<p className={`mt-1 text-sm ${selected ? "text-white/80" : "text-gray-500"}`}>
|
||||||
|
{option.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex size-6 shrink-0 items-center justify-center rounded-md border-2 transition-all ${
|
||||||
|
selected
|
||||||
|
? "border-[#C4D668] bg-[#C4D668]"
|
||||||
|
: "border-gray-300 bg-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selected && <Check className="size-4 text-[#1A2E2E]" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +1,39 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useConfiguratorStore, type Finish, type Handle } from "@/lib/store";
|
import { useConfiguratorStore, type Finish, type GlassColor, type Handle, type FrameSize } from "@/lib/store";
|
||||||
import { glassPatternOptions, type GlassPattern } from "@/lib/glass-patterns";
|
import { glassPatternOptions, type GlassPattern } from "@/lib/glass-patterns";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// OPTIONS DATA
|
||||||
|
// ============================================
|
||||||
|
|
||||||
const finishOptions: Array<{
|
const finishOptions: Array<{
|
||||||
value: Finish;
|
value: Finish;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
swatch: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "zwart", label: "Mat Zwart", description: "Klassiek en tijdloos" },
|
{ value: "zwart", label: "Mat Zwart", description: "Klassiek en tijdloos", swatch: "#1A1A1A" },
|
||||||
{
|
{ value: "brons", label: "Brons", description: "Warm en industrieel", swatch: "#8B6F47" },
|
||||||
value: "brons",
|
{ value: "grijs", label: "Antraciet", description: "Modern en neutraal", swatch: "#525252" },
|
||||||
label: "Brons",
|
{ value: "goud", label: "Goud", description: "Luxe en opvallend", swatch: "#B8860B" },
|
||||||
description: "Warm en industrieel",
|
{ value: "beige", label: "Beige", description: "Zacht en natuurlijk", swatch: "#C8B88A" },
|
||||||
},
|
{ value: "ral", label: "RAL Kleur", description: "Op maat, +EUR 200", swatch: "#4A6741" },
|
||||||
{ value: "grijs", label: "Antraciet", description: "Modern en neutraal" },
|
];
|
||||||
|
|
||||||
|
const glassColorOptions: Array<{
|
||||||
|
value: GlassColor;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
swatch: string;
|
||||||
|
}> = [
|
||||||
|
{ value: "helder", label: "Helder", description: "Maximale transparantie", swatch: "#dbeafe" },
|
||||||
|
{ value: "grijs", label: "Rookglas", description: "Getint grijs glas", swatch: "#4B5563" },
|
||||||
|
{ value: "brons", label: "Bronsglas", description: "Warm getint glas", swatch: "#92764A" },
|
||||||
|
{ value: "mat-blank", label: "Mat Blank", description: "Zacht diffuus licht", swatch: "#e2e2e2" },
|
||||||
|
{ value: "mat-brons", label: "Mat Brons", description: "Warm en gedempd", swatch: "#A0845C" },
|
||||||
|
{ value: "mat-zwart", label: "Mat Zwart", description: "Privacy glas", swatch: "#2D2D2D" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleOptions: Array<{
|
const handleOptions: Array<{
|
||||||
@@ -23,46 +41,69 @@ const handleOptions: Array<{
|
|||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{ value: "beugelgreep", label: "Beugelgreep", description: "Verticale staaf met montageblokken" },
|
||||||
value: "beugelgreep",
|
{ value: "hoekgreep", label: "Hoekgreep", description: "L-vormige minimalistisch design" },
|
||||||
label: "Beugelgreep",
|
{ value: "maangreep", label: "Maangreep", description: "Gebogen half-maanvormige greep" },
|
||||||
description: "Verticale staaf met montageblokken",
|
{ value: "ovaalgreep", label: "Ovaalgreep", description: "Moderne ovale trekgreep" },
|
||||||
},
|
{ value: "klink", label: "Deurklink", description: "Klassieke deurklink met hendel" },
|
||||||
{
|
{ value: "u-greep", label: "U-Greep", description: "Eenvoudige rechte staaf" },
|
||||||
value: "hoekgreep",
|
{ value: "geen", label: "Geen greep", description: "Voor vaste panelen" },
|
||||||
label: "Hoekgreep",
|
|
||||||
description: "L-vormige minimalistisch design",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "maangreep",
|
|
||||||
label: "Maangreep",
|
|
||||||
description: "Gebogen half-maanvormige greep",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "ovaalgreep",
|
|
||||||
label: "Ovaalgreep",
|
|
||||||
description: "Moderne ovale trekgreep",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "klink",
|
|
||||||
label: "Deurklink",
|
|
||||||
description: "Klassieke deurklink met hendel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "u-greep",
|
|
||||||
label: "U-Greep",
|
|
||||||
description: "Eenvoudige rechte staaf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "geen",
|
|
||||||
label: "Geen greep",
|
|
||||||
description: "Voor vaste panelen",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const frameSizeOptions: Array<{
|
||||||
|
value: FrameSize;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{ value: 20, label: "Smal (20mm)", description: "Minimalistisch profiel" },
|
||||||
|
{ value: 30, label: "Standaard (30mm)", description: "Populairste keuze" },
|
||||||
|
{ value: 40, label: "Robuust (40mm)", description: "Industrieel karakter" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SHARED SELECTION COMPONENTS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function SelectionButton({
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
selected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={`group relative rounded-xl border-2 p-4 text-left transition-all ${
|
||||||
|
selected
|
||||||
|
? "border-[#C4D668] bg-[#1A2E2E] text-white"
|
||||||
|
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
{children}
|
||||||
|
{selected && (
|
||||||
|
<div className="ml-2 flex size-6 shrink-0 items-center justify-center rounded-full bg-[#C4D668]">
|
||||||
|
<Check className="size-4 text-[#1A2E2E]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// MAIN COMPONENT
|
||||||
|
// ============================================
|
||||||
|
|
||||||
export function StepOptions() {
|
export function StepOptions() {
|
||||||
const { finish, handle, glassPattern, setFinish, setHandle, setGlassPattern } =
|
const {
|
||||||
useConfiguratorStore();
|
finish, handle, glassPattern, glassColor, frameSize,
|
||||||
|
setFinish, setHandle, setGlassPattern, setGlassColor, setFrameSize,
|
||||||
|
} = useConfiguratorStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@@ -73,52 +114,113 @@ export function StepOptions() {
|
|||||||
Kies de kleur en afwerking van het staal.
|
Kies de kleur en afwerking van het staal.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{finishOptions.map((option) => {
|
{finishOptions.map((option) => {
|
||||||
const selected = finish === option.value;
|
const selected = finish === option.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFinish(option.value)}
|
onClick={() => setFinish(option.value)}
|
||||||
className={`group relative rounded-xl border-2 p-4 text-left transition-all ${
|
className={`group relative flex flex-col items-center rounded-xl border-2 p-3 transition-all ${
|
||||||
selected
|
selected
|
||||||
? "border-[#C4D668] bg-[#1A2E2E] text-white"
|
? "border-[#C4D668] bg-[#1A2E2E] text-white ring-2 ring-[#C4D668]"
|
||||||
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div
|
||||||
<div className="flex flex-1 items-center gap-4">
|
className="mb-2 size-10 rounded-lg border-2 border-white shadow-md"
|
||||||
{/* Color swatch */}
|
style={{ backgroundColor: option.swatch }}
|
||||||
<div
|
/>
|
||||||
className="size-10 rounded-lg border-2 border-white shadow-md"
|
<h3 className="text-sm font-bold">{option.label}</h3>
|
||||||
style={{
|
<p className={`mt-0.5 text-xs ${selected ? "text-white/70" : "text-gray-500"}`}>
|
||||||
backgroundColor:
|
{option.description}
|
||||||
option.value === "zwart"
|
</p>
|
||||||
? "#1A1A1A"
|
{selected && (
|
||||||
: option.value === "brons"
|
<div className="absolute right-2 top-2 flex size-5 items-center justify-center rounded-full bg-[#C4D668]">
|
||||||
? "#8B6F47"
|
<Check className="size-3 text-[#1A2E2E]" />
|
||||||
: "#4A5568",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-bold">{option.label}</h3>
|
|
||||||
<p
|
|
||||||
className={`mt-1 text-sm ${
|
|
||||||
selected ? "text-white/80" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{option.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{selected && (
|
)}
|
||||||
<div className="flex size-6 items-center justify-center rounded-full bg-[#C4D668]">
|
</button>
|
||||||
<Check className="size-4 text-[#1A2E2E]" />
|
);
|
||||||
</div>
|
})}
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Glass Color Selection */}
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Glaskleur</h2>
|
||||||
|
<p className="mb-4 text-sm text-gray-600">
|
||||||
|
Kies het type en de kleur van het glas.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{glassColorOptions.map((option) => {
|
||||||
|
const selected = glassColor === option.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGlassColor(option.value)}
|
||||||
|
className={`group relative flex flex-col items-center rounded-xl border-2 p-3 transition-all ${
|
||||||
|
selected
|
||||||
|
? "border-[#C4D668] bg-[#1A2E2E] text-white ring-2 ring-[#C4D668]"
|
||||||
|
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mb-2 size-8 rounded-full border-2 border-white shadow-md"
|
||||||
|
style={{ backgroundColor: option.swatch }}
|
||||||
|
/>
|
||||||
|
<h3 className="text-xs font-bold">{option.label}</h3>
|
||||||
|
{selected && (
|
||||||
|
<div className="absolute right-1.5 top-1.5 flex size-4 items-center justify-center rounded-full bg-[#C4D668]">
|
||||||
|
<Check className="size-2.5 text-[#1A2E2E]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Frame Size Selection */}
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Profielbreedte</h2>
|
||||||
|
<p className="mb-4 text-sm text-gray-600">
|
||||||
|
Kies de breedte van het stalen profiel.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{frameSizeOptions.map((option) => {
|
||||||
|
const selected = frameSize === option.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFrameSize(option.value)}
|
||||||
|
className={`group relative flex flex-col items-center rounded-xl border-2 px-2 py-3 transition-all ${
|
||||||
|
selected
|
||||||
|
? "border-[#C4D668] bg-[#1A2E2E] text-white ring-2 ring-[#C4D668]"
|
||||||
|
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Visual profile width indicator */}
|
||||||
|
<div className="mb-2 flex h-12 items-center justify-center">
|
||||||
|
<div
|
||||||
|
className={`rounded-sm ${selected ? "bg-[#C4D668]" : "bg-[#1A2E2E]"}`}
|
||||||
|
style={{ width: `${option.value * 0.4}px`, height: "40px" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 className="text-xs font-bold">{option.label}</h3>
|
||||||
|
<p className={`mt-0.5 text-[10px] ${selected ? "text-white/70" : "text-gray-500"}`}>
|
||||||
|
{option.description}
|
||||||
|
</p>
|
||||||
|
{selected && (
|
||||||
|
<div className="absolute right-1.5 top-1.5 flex size-4 items-center justify-center rounded-full bg-[#C4D668]">
|
||||||
|
<Check className="size-2.5 text-[#1A2E2E]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -135,36 +237,19 @@ export function StepOptions() {
|
|||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{glassPatternOptions.map((option) => {
|
{glassPatternOptions.map((option) => {
|
||||||
const selected = glassPattern === option.value;
|
const selected = glassPattern === option.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<SelectionButton
|
||||||
key={option.value}
|
key={option.value}
|
||||||
type="button"
|
selected={selected}
|
||||||
onClick={() => setGlassPattern(option.value)}
|
onClick={() => setGlassPattern(option.value)}
|
||||||
className={`group relative rounded-xl border-2 p-4 text-left transition-all ${
|
|
||||||
selected
|
|
||||||
? "border-[#C4D668] bg-[#1A2E2E] text-white"
|
|
||||||
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<h3 className="font-bold">{option.label}</h3>
|
||||||
<h3 className="font-bold">{option.label}</h3>
|
<p className={`mt-1 text-sm ${selected ? "text-white/80" : "text-gray-500"}`}>
|
||||||
<p
|
{option.description}
|
||||||
className={`mt-1 text-sm ${
|
</p>
|
||||||
selected ? "text-white/80" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{option.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{selected && (
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-full bg-[#C4D668]">
|
|
||||||
<Check className="size-4 text-[#1A2E2E]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</SelectionButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -180,36 +265,19 @@ export function StepOptions() {
|
|||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{handleOptions.map((option) => {
|
{handleOptions.map((option) => {
|
||||||
const selected = handle === option.value;
|
const selected = handle === option.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<SelectionButton
|
||||||
key={option.value}
|
key={option.value}
|
||||||
type="button"
|
selected={selected}
|
||||||
onClick={() => setHandle(option.value)}
|
onClick={() => setHandle(option.value)}
|
||||||
className={`group relative rounded-xl border-2 p-4 text-left transition-all ${
|
|
||||||
selected
|
|
||||||
? "border-[#C4D668] bg-[#1A2E2E] text-white"
|
|
||||||
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<h3 className="font-bold">{option.label}</h3>
|
||||||
<h3 className="font-bold">{option.label}</h3>
|
<p className={`mt-1 text-sm ${selected ? "text-white/80" : "text-gray-500"}`}>
|
||||||
<p
|
{option.description}
|
||||||
className={`mt-1 text-sm ${
|
</p>
|
||||||
selected ? "text-white/80" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{option.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{selected && (
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-full bg-[#C4D668]">
|
|
||||||
<Check className="size-4 text-[#1A2E2E]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</SelectionButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useConfiguratorStore, type DoorType } from "@/lib/store";
|
import { useConfiguratorStore, type DoorType, type GridType } from "@/lib/store";
|
||||||
import { useFormContext } from "@/components/offerte/form-context";
|
import { useFormContext } from "@/components/offerte/form-context";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
// Door type visual icons (inline SVGs)
|
// ============================================
|
||||||
|
// DOOR TYPE ICONS (SVG)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
function TaatsIcon({ selected }: { selected: boolean }) {
|
function TaatsIcon({ selected }: { selected: boolean }) {
|
||||||
const stroke = selected ? "#C4D668" : "#1A2E2E";
|
const stroke = selected ? "#C4D668" : "#1A2E2E";
|
||||||
const fill = selected ? "#C4D668" : "none";
|
const fill = selected ? "#C4D668" : "none";
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 64 80" className="h-20 w-16">
|
<svg viewBox="0 0 64 80" className="h-20 w-16">
|
||||||
{/* Door frame */}
|
|
||||||
<rect x="8" y="4" width="48" height="72" rx="2" fill="none" stroke={stroke} strokeWidth="2" />
|
<rect x="8" y="4" width="48" height="72" rx="2" fill="none" stroke={stroke} strokeWidth="2" />
|
||||||
{/* Glass */}
|
|
||||||
<rect x="14" y="10" width="36" height="60" rx="1" fill={selected ? "#C4D668" : "#e5e7eb"} opacity="0.2" />
|
<rect x="14" y="10" width="36" height="60" rx="1" fill={selected ? "#C4D668" : "#e5e7eb"} opacity="0.2" />
|
||||||
{/* Pivot point (center) */}
|
|
||||||
<circle cx="32" cy="40" r="3" fill={fill} stroke={stroke} strokeWidth="1.5" />
|
<circle cx="32" cy="40" r="3" fill={fill} stroke={stroke} strokeWidth="1.5" />
|
||||||
{/* Rotation arrow */}
|
|
||||||
<path d="M 44 20 A 16 16 0 0 1 44 60" fill="none" stroke={stroke} strokeWidth="1.5" strokeDasharray="3 2" />
|
<path d="M 44 20 A 16 16 0 0 1 44 60" fill="none" stroke={stroke} strokeWidth="1.5" strokeDasharray="3 2" />
|
||||||
<polygon points="44,58 48,54 40,54" fill={stroke} />
|
<polygon points="44,58 48,54 40,54" fill={stroke} />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -28,14 +27,10 @@ function ScharnierIcon({ selected }: { selected: boolean }) {
|
|||||||
const fill = selected ? "#C4D668" : "none";
|
const fill = selected ? "#C4D668" : "none";
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 64 80" className="h-20 w-16">
|
<svg viewBox="0 0 64 80" className="h-20 w-16">
|
||||||
{/* Door frame */}
|
|
||||||
<rect x="8" y="4" width="48" height="72" rx="2" fill="none" stroke={stroke} strokeWidth="2" />
|
<rect x="8" y="4" width="48" height="72" rx="2" fill="none" stroke={stroke} strokeWidth="2" />
|
||||||
{/* Glass */}
|
|
||||||
<rect x="14" y="10" width="36" height="60" rx="1" fill={selected ? "#C4D668" : "#e5e7eb"} opacity="0.2" />
|
<rect x="14" y="10" width="36" height="60" rx="1" fill={selected ? "#C4D668" : "#e5e7eb"} opacity="0.2" />
|
||||||
{/* Hinge dots on left side */}
|
|
||||||
<circle cx="10" cy="20" r="2.5" fill={fill} stroke={stroke} strokeWidth="1.5" />
|
<circle cx="10" cy="20" r="2.5" fill={fill} stroke={stroke} strokeWidth="1.5" />
|
||||||
<circle cx="10" cy="60" r="2.5" fill={fill} stroke={stroke} strokeWidth="1.5" />
|
<circle cx="10" cy="60" r="2.5" fill={fill} stroke={stroke} strokeWidth="1.5" />
|
||||||
{/* Rotation arrow from hinge side */}
|
|
||||||
<path d="M 56 18 A 24 24 0 0 1 56 62" fill="none" stroke={stroke} strokeWidth="1.5" strokeDasharray="3 2" />
|
<path d="M 56 18 A 24 24 0 0 1 56 62" fill="none" stroke={stroke} strokeWidth="1.5" strokeDasharray="3 2" />
|
||||||
<polygon points="56,60 60,56 52,56" fill={stroke} />
|
<polygon points="56,60 60,56 52,56" fill={stroke} />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -46,11 +41,8 @@ function PaneelIcon({ selected }: { selected: boolean }) {
|
|||||||
const stroke = selected ? "#C4D668" : "#1A2E2E";
|
const stroke = selected ? "#C4D668" : "#1A2E2E";
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 64 80" className="h-20 w-16">
|
<svg viewBox="0 0 64 80" className="h-20 w-16">
|
||||||
{/* Door frame */}
|
|
||||||
<rect x="8" y="4" width="48" height="72" rx="2" fill="none" stroke={stroke} strokeWidth="2" />
|
<rect x="8" y="4" width="48" height="72" rx="2" fill="none" stroke={stroke} strokeWidth="2" />
|
||||||
{/* Glass */}
|
|
||||||
<rect x="14" y="10" width="36" height="60" rx="1" fill={selected ? "#C4D668" : "#e5e7eb"} opacity="0.2" />
|
<rect x="14" y="10" width="36" height="60" rx="1" fill={selected ? "#C4D668" : "#e5e7eb"} opacity="0.2" />
|
||||||
{/* Fixed indicator - lock symbol */}
|
|
||||||
<rect x="26" y="34" width="12" height="12" rx="2" fill="none" stroke={stroke} strokeWidth="1.5" />
|
<rect x="26" y="34" width="12" height="12" rx="2" fill="none" stroke={stroke} strokeWidth="1.5" />
|
||||||
<circle cx="32" cy="34" r="5" fill="none" stroke={stroke} strokeWidth="1.5" />
|
<circle cx="32" cy="34" r="5" fill="none" stroke={stroke} strokeWidth="1.5" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -63,70 +55,149 @@ const doorTypeIcons: Record<DoorType, (props: { selected: boolean }) => React.Re
|
|||||||
paneel: PaneelIcon,
|
paneel: PaneelIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grid type visual illustrations (CSS-based rectangles with dividers)
|
// ============================================
|
||||||
function GridIllustration({ dividers, selected }: { dividers: number; selected: boolean }) {
|
// GRID PATTERN SVG ILLUSTRATIONS
|
||||||
const borderColor = selected ? "border-[#C4D668]" : "border-[#1A2E2E]/40";
|
// ============================================
|
||||||
const dividerBg = selected ? "bg-[#C4D668]" : "bg-[#1A2E2E]/30";
|
|
||||||
const glassBg = selected ? "bg-[#C4D668]/10" : "bg-gray-100";
|
|
||||||
|
|
||||||
return (
|
function GridSVG({ pattern, selected }: { pattern: GridType; selected: boolean }) {
|
||||||
<div className={`flex h-20 w-14 flex-col overflow-hidden rounded border-2 ${borderColor}`}>
|
const stroke = selected ? "#C4D668" : "#1A2E2E";
|
||||||
{dividers === 0 && (
|
const glass = selected ? "#C4D668" : "#e5e7eb";
|
||||||
<div className={`flex-1 ${glassBg}`} />
|
const opacity = selected ? 0.15 : 0.3;
|
||||||
)}
|
const sw = 1.5;
|
||||||
{dividers > 0 &&
|
|
||||||
Array.from({ length: dividers + 1 }).map((_, i) => (
|
// Frame: outer rect with inner glass area
|
||||||
<div key={i} className="flex flex-1 flex-col">
|
const frame = (children: React.ReactNode) => (
|
||||||
{i > 0 && <div className={`h-[2px] shrink-0 ${dividerBg}`} />}
|
<svg viewBox="0 0 40 60" className="h-14 w-10">
|
||||||
<div className={`flex-1 ${glassBg}`} />
|
<rect x="2" y="2" width="36" height="56" rx="1" fill="none" stroke={stroke} strokeWidth={sw} />
|
||||||
</div>
|
<rect x="5" y="5" width="30" height="50" fill={glass} opacity={opacity} />
|
||||||
))
|
{children}
|
||||||
}
|
</svg>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
switch (pattern) {
|
||||||
|
case "geen":
|
||||||
|
return frame(null);
|
||||||
|
|
||||||
|
case "2-vlak":
|
||||||
|
return frame(
|
||||||
|
<line x1="5" y1="30" x2="35" y2="30" stroke={stroke} strokeWidth={sw} />
|
||||||
|
);
|
||||||
|
|
||||||
|
case "3-vlak":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="22" x2="35" y2="22" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="38" x2="35" y2="38" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "4-vlak":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="18" x2="35" y2="18" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="30" x2="35" y2="30" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="42" x2="35" y2="42" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "6-vlak":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="22" x2="35" y2="22" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="38" x2="35" y2="38" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="20" y1="5" x2="20" y2="55" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "8-vlak":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="18" x2="35" y2="18" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="30" x2="35" y2="30" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="42" x2="35" y2="42" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="20" y1="5" x2="20" y2="55" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "kruis":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="30" x2="35" y2="30" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="20" y1="5" x2="20" y2="55" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "ongelijk-3":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="24" x2="35" y2="24" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="40" x2="35" y2="40" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "boerderij":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="20" x2="35" y2="20" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="20" y1="20" x2="20" y2="55" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "herenhuis":
|
||||||
|
return frame(
|
||||||
|
<>
|
||||||
|
<line x1="5" y1="18" x2="35" y2="18" stroke={stroke} strokeWidth={sw} />
|
||||||
|
<line x1="5" y1="40" x2="35" y2="40" stroke={stroke} strokeWidth={sw} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return frame(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DATA
|
||||||
|
// ============================================
|
||||||
|
|
||||||
const doorTypes: Array<{
|
const doorTypes: Array<{
|
||||||
value: DoorType;
|
value: DoorType;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{ value: "taats", label: "Taatsdeur", description: "Pivoterende deur" },
|
||||||
value: "taats",
|
{ value: "scharnier", label: "Scharnierdeur", description: "Zijscharnieren" },
|
||||||
label: "Taatsdeur",
|
{ value: "paneel", label: "Vast Paneel", description: "Geen beweging" },
|
||||||
description: "Pivoterende deur",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "scharnier",
|
|
||||||
label: "Scharnierdeur",
|
|
||||||
description: "Zijscharnieren",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "paneel",
|
|
||||||
label: "Vast Paneel",
|
|
||||||
description: "Geen beweging",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const gridTypes: Array<{
|
const gridTypes: Array<{
|
||||||
value: "3-vlak" | "4-vlak" | "geen";
|
value: GridType;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
dividers: number;
|
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "geen", label: "Geen", description: "Volledig vlak", dividers: 0 },
|
{ value: "geen", label: "Geen", description: "Volledig vlak" },
|
||||||
{ value: "3-vlak", label: "3-vlaks", description: "2 balken", dividers: 2 },
|
{ value: "2-vlak", label: "2-vlaks", description: "1 balk" },
|
||||||
{ value: "4-vlak", label: "4-vlaks", description: "3 balken", dividers: 3 },
|
{ value: "3-vlak", label: "3-vlaks", description: "2 balken" },
|
||||||
|
{ value: "4-vlak", label: "4-vlaks", description: "3 balken" },
|
||||||
|
{ value: "kruis", label: "Kruis", description: "1H + 1V" },
|
||||||
|
{ value: "6-vlak", label: "6-vlaks", description: "2H + 1V" },
|
||||||
|
{ value: "8-vlak", label: "8-vlaks", description: "3H + 1V" },
|
||||||
|
{ value: "ongelijk-3", label: "Ongelijk", description: "3 ongelijk" },
|
||||||
|
{ value: "boerderij", label: "Boerderij", description: "2+2 onder" },
|
||||||
|
{ value: "herenhuis", label: "Herenhuis", description: "3 horizontaal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// MAIN COMPONENT
|
||||||
|
// ============================================
|
||||||
|
|
||||||
export function StepProduct() {
|
export function StepProduct() {
|
||||||
const { nextStep } = useFormContext();
|
const { nextStep } = useFormContext();
|
||||||
const { doorType, gridType, setDoorType, setGridType } =
|
const { doorType, gridType, setDoorType, setGridType } = useConfiguratorStore();
|
||||||
useConfiguratorStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Door Type Selection - Visual Tiles */}
|
{/* Door Type Selection */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Kies uw deurtype</h2>
|
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Kies uw deurtype</h2>
|
||||||
<p className="mb-4 text-sm text-gray-600">
|
<p className="mb-4 text-sm text-gray-600">
|
||||||
@@ -153,11 +224,7 @@ export function StepProduct() {
|
|||||||
<IconComponent selected={selected} />
|
<IconComponent selected={selected} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-bold">{type.label}</h3>
|
<h3 className="text-sm font-bold">{type.label}</h3>
|
||||||
<p
|
<p className={`mt-1 text-xs ${selected ? "text-white/70" : "text-gray-500"}`}>
|
||||||
className={`mt-1 text-xs ${
|
|
||||||
selected ? "text-white/70" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{type.description}
|
{type.description}
|
||||||
</p>
|
</p>
|
||||||
{selected && (
|
{selected && (
|
||||||
@@ -171,14 +238,14 @@ export function StepProduct() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid Type Selection - Visual Tiles */}
|
{/* Grid Type Selection - 10 patterns */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Verdeling</h2>
|
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Verdeling</h2>
|
||||||
<p className="mb-4 text-sm text-gray-600">
|
<p className="mb-4 text-sm text-gray-600">
|
||||||
Kies het aantal horizontale vlakken.
|
Kies het patroon van de glasverdeling.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
{gridTypes.map((type) => {
|
{gridTypes.map((type) => {
|
||||||
const selected = gridType === type.value;
|
const selected = gridType === type.value;
|
||||||
|
|
||||||
@@ -187,26 +254,22 @@ export function StepProduct() {
|
|||||||
key={type.value}
|
key={type.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setGridType(type.value)}
|
onClick={() => setGridType(type.value)}
|
||||||
className={`group relative flex flex-col items-center rounded-xl border-2 px-2 py-4 transition-all ${
|
className={`group relative flex flex-col items-center rounded-xl border-2 px-1 py-3 transition-all ${
|
||||||
selected
|
selected
|
||||||
? "border-[#C4D668] bg-[#1A2E2E] text-white ring-2 ring-[#C4D668] shadow-lg shadow-[#C4D668]/20"
|
? "border-[#C4D668] bg-[#1A2E2E] text-white ring-2 ring-[#C4D668] shadow-lg shadow-[#C4D668]/20"
|
||||||
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/20 hover:shadow-md"
|
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/20 hover:shadow-md"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center justify-center">
|
<div className="mb-2 flex items-center justify-center">
|
||||||
<GridIllustration dividers={type.dividers} selected={selected} />
|
<GridSVG pattern={type.value} selected={selected} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-bold">{type.label}</h3>
|
<h3 className="text-[10px] font-bold leading-tight">{type.label}</h3>
|
||||||
<p
|
<p className={`text-[9px] leading-tight ${selected ? "text-white/70" : "text-gray-500"}`}>
|
||||||
className={`mt-1 text-xs ${
|
|
||||||
selected ? "text-white/70" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{type.description}
|
{type.description}
|
||||||
</p>
|
</p>
|
||||||
{selected && (
|
{selected && (
|
||||||
<div className="absolute right-2 top-2 flex size-5 items-center justify-center rounded-full bg-[#C4D668]">
|
<div className="absolute right-1 top-1 flex size-4 items-center justify-center rounded-full bg-[#C4D668]">
|
||||||
<Check className="size-3 text-[#1A2E2E]" />
|
<Check className="size-2.5 text-[#1A2E2E]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,83 +1,288 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useFormContext } from "@/components/offerte/form-context";
|
import { useState } from "react";
|
||||||
|
import { useConfiguratorStore } from "@/lib/store";
|
||||||
|
import { sendQuoteAction } from "@/actions/send-quote";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Send, Check } from "lucide-react";
|
import { Send, Check, Loader2, CheckCircle2, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
const fieldLabels: Record<string, string> = {
|
// ============================================
|
||||||
productType: "Product",
|
// LABEL MAPS
|
||||||
height: "Hoogte",
|
// ============================================
|
||||||
width: "Breedte",
|
|
||||||
glassType: "Glas Type",
|
const DOOR_TYPE_LABELS: Record<string, string> = {
|
||||||
finish: "Afwerking",
|
taats: "Taatsdeur",
|
||||||
name: "Naam",
|
scharnier: "Scharnierdeur",
|
||||||
email: "E-mail",
|
paneel: "Vast Paneel",
|
||||||
phone: "Telefoon",
|
|
||||||
note: "Opmerking",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldOrder = [
|
const CONFIG_LABELS: Record<string, string> = {
|
||||||
"productType",
|
enkele: "Enkele deur",
|
||||||
"height",
|
dubbele: "Dubbele deur",
|
||||||
"width",
|
};
|
||||||
"glassType",
|
|
||||||
"finish",
|
|
||||||
"name",
|
|
||||||
"email",
|
|
||||||
"phone",
|
|
||||||
"note",
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatValue(key: string, value: unknown): string {
|
const SIDE_PANEL_LABELS: Record<string, string> = {
|
||||||
if (value === undefined || value === null || value === "") return "—";
|
geen: "Geen",
|
||||||
if (key === "height" || key === "width") return `${value} mm`;
|
links: "Links",
|
||||||
return String(value);
|
rechts: "Rechts",
|
||||||
|
beide: "Beide zijden",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FINISH_LABELS: Record<string, string> = {
|
||||||
|
zwart: "Mat Zwart",
|
||||||
|
brons: "Brons",
|
||||||
|
grijs: "Antraciet",
|
||||||
|
goud: "Goud",
|
||||||
|
beige: "Beige",
|
||||||
|
ral: "RAL Kleur",
|
||||||
|
};
|
||||||
|
|
||||||
|
const GLASS_COLOR_LABELS: Record<string, string> = {
|
||||||
|
helder: "Helder",
|
||||||
|
grijs: "Rookglas",
|
||||||
|
brons: "Bronsglas",
|
||||||
|
"mat-blank": "Mat Blank",
|
||||||
|
"mat-brons": "Mat Brons",
|
||||||
|
"mat-zwart": "Mat Zwart",
|
||||||
|
};
|
||||||
|
|
||||||
|
const HANDLE_LABELS: Record<string, string> = {
|
||||||
|
beugelgreep: "Beugelgreep",
|
||||||
|
hoekgreep: "Hoekgreep",
|
||||||
|
maangreep: "Maangreep",
|
||||||
|
ovaalgreep: "Ovaalgreep",
|
||||||
|
klink: "Deurklink",
|
||||||
|
"u-greep": "U-Greep",
|
||||||
|
geen: "Geen greep",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATTERN_LABELS: Record<string, string> = {
|
||||||
|
standard: "Standaard",
|
||||||
|
"dt9-rounded": "DT9 Afgerond",
|
||||||
|
"dt10-ushape": "DT10 U-vorm",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatPrice(amount: number): string {
|
||||||
|
return new Intl.NumberFormat("nl-NL", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// COMPONENTS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function SummaryRow({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<span className="text-sm text-gray-500">{label}</span>
|
||||||
|
<span className={`text-sm font-medium ${highlight ? "text-[#C4D668]" : "text-[#1A2E2E]"}`}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummarySection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
||||||
|
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-gray-400">{title}</h3>
|
||||||
|
<div className="divide-y divide-gray-100">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PriceRow({ label, amount, bold }: { label: string; amount: number; bold?: boolean }) {
|
||||||
|
if (amount === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-between py-1.5 ${bold ? "text-base" : "text-sm"}`}>
|
||||||
|
<span className={bold ? "font-bold text-[#1A2E2E]" : "text-gray-500"}>{label}</span>
|
||||||
|
<span className={bold ? "font-bold text-[#1A2E2E]" : "text-gray-700"}>
|
||||||
|
{formatPrice(amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// MAIN COMPONENT
|
||||||
|
// ============================================
|
||||||
|
|
||||||
export function StepSummary() {
|
export function StepSummary() {
|
||||||
const { formData } = useFormContext();
|
const store = useConfiguratorStore();
|
||||||
|
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
doorType, gridType, doorConfig, sidePanel,
|
||||||
|
width, height, doorLeafWidth,
|
||||||
|
finish, glassColor, handle, frameSize, glassPattern,
|
||||||
|
extraOptions,
|
||||||
|
name, email, phone, note,
|
||||||
|
priceBreakdown, screenshotDataUrl,
|
||||||
|
} = store;
|
||||||
|
|
||||||
|
const canSubmit = name.length >= 2 && email.includes("@") && phone.length >= 10;
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!canSubmit) return;
|
||||||
|
setStatus("loading");
|
||||||
|
setErrorMsg("");
|
||||||
|
|
||||||
|
const result = await sendQuoteAction({
|
||||||
|
doorType, gridType, doorConfig, sidePanel,
|
||||||
|
width, height, doorLeafWidth,
|
||||||
|
finish, glassColor, handle, frameSize, glassPattern,
|
||||||
|
extraOptions,
|
||||||
|
name, email, phone, note,
|
||||||
|
totalPrice: priceBreakdown.totalPrice,
|
||||||
|
steelCost: priceBreakdown.steelCost,
|
||||||
|
glassCost: priceBreakdown.glassCost,
|
||||||
|
baseFee: priceBreakdown.baseFee,
|
||||||
|
mechanismSurcharge: priceBreakdown.mechanismSurcharge,
|
||||||
|
sidePanelSurcharge: priceBreakdown.sidePanelSurcharge,
|
||||||
|
handleCost: priceBreakdown.handleCost,
|
||||||
|
finishSurcharge: priceBreakdown.finishSurcharge,
|
||||||
|
screenshotDataUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setStatus("success");
|
||||||
|
} else {
|
||||||
|
setStatus("error");
|
||||||
|
setErrorMsg(result.error || "Onbekende fout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "success") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center py-8 text-center">
|
||||||
|
<div className="mb-4 flex size-16 items-center justify-center rounded-full bg-green-100">
|
||||||
|
<CheckCircle2 className="size-8 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-2 text-xl font-bold text-[#1A2E2E]">Aanvraag Verstuurd!</h2>
|
||||||
|
<p className="mb-1 text-sm text-gray-600">
|
||||||
|
Bedankt {name}, uw offerte aanvraag is ontvangen.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
We sturen een bevestiging naar {email}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<h2 className="mb-2 text-xl font-semibold">Overzicht</h2>
|
<div>
|
||||||
<p className="mb-6 text-sm text-muted-foreground">
|
<h2 className="mb-1 text-xl font-bold text-[#1A2E2E]">Overzicht</h2>
|
||||||
Controleer uw configuratie en verstuur de aanvraag.
|
<p className="text-sm text-gray-500">
|
||||||
</p>
|
Controleer uw configuratie en verstuur de aanvraag.
|
||||||
|
</p>
|
||||||
<div className="overflow-hidden rounded-lg border border-border">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<tbody>
|
|
||||||
{fieldOrder.map((key, i) => {
|
|
||||||
const value = formData[key as keyof typeof formData];
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={key}
|
|
||||||
className={i % 2 === 0 ? "bg-muted/30" : "bg-card"}
|
|
||||||
>
|
|
||||||
<td className="w-1/3 px-4 py-3 font-medium text-muted-foreground">
|
|
||||||
{fieldLabels[key]}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 font-medium">
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{value !== undefined && value !== "" && (
|
|
||||||
<Check className="size-3.5 text-green-600" />
|
|
||||||
)}
|
|
||||||
{formatValue(key, value)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Product Section */}
|
||||||
|
<SummarySection title="Product">
|
||||||
|
<SummaryRow label="Deurtype" value={DOOR_TYPE_LABELS[doorType] || doorType} />
|
||||||
|
<SummaryRow label="Verdeling" value={gridType} />
|
||||||
|
<SummaryRow label="Configuratie" value={CONFIG_LABELS[doorConfig] || doorConfig} />
|
||||||
|
<SummaryRow label="Zijpanelen" value={SIDE_PANEL_LABELS[sidePanel] || sidePanel} />
|
||||||
|
</SummarySection>
|
||||||
|
|
||||||
|
{/* Dimensions Section */}
|
||||||
|
<SummarySection title="Afmetingen">
|
||||||
|
<SummaryRow label="Wandopening (breedte)" value={`${width} mm`} />
|
||||||
|
<SummaryRow label="Hoogte" value={`${height} mm`} />
|
||||||
|
<SummaryRow label="Deurblad breedte" value={`${Math.round(doorLeafWidth)} mm`} />
|
||||||
|
</SummarySection>
|
||||||
|
|
||||||
|
{/* Style Section */}
|
||||||
|
<SummarySection title="Stijl">
|
||||||
|
<SummaryRow label="Afwerking" value={FINISH_LABELS[finish] || finish} />
|
||||||
|
<SummaryRow label="Glaskleur" value={GLASS_COLOR_LABELS[glassColor] || glassColor} />
|
||||||
|
<SummaryRow label="Greep" value={HANDLE_LABELS[handle] || handle} />
|
||||||
|
<SummaryRow label="Profielbreedte" value={`${frameSize} mm`} />
|
||||||
|
<SummaryRow label="Glaspatroon" value={PATTERN_LABELS[glassPattern] || glassPattern} />
|
||||||
|
</SummarySection>
|
||||||
|
|
||||||
|
{/* Extra Options */}
|
||||||
|
{extraOptions.length > 0 && (
|
||||||
|
<SummarySection title="Extra opties">
|
||||||
|
{extraOptions.map((opt) => (
|
||||||
|
<div key={opt} className="flex items-center gap-2 py-1.5">
|
||||||
|
<Check className="size-3.5 text-green-500" />
|
||||||
|
<span className="text-sm text-[#1A2E2E]">{opt}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</SummarySection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Price Breakdown */}
|
||||||
|
<div className="rounded-xl border-2 border-[#1A2E2E] bg-gradient-to-b from-white to-gray-50 p-4">
|
||||||
|
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-gray-400">
|
||||||
|
Indicatieprijs
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<PriceRow label="Staal" amount={priceBreakdown.steelCost} />
|
||||||
|
<PriceRow label="Glas" amount={priceBreakdown.glassCost} />
|
||||||
|
<PriceRow label="Basiskost" amount={priceBreakdown.baseFee} />
|
||||||
|
<PriceRow label="Mechanisme toeslag" amount={priceBreakdown.mechanismSurcharge} />
|
||||||
|
<PriceRow label="Zijpaneel toeslag" amount={priceBreakdown.sidePanelSurcharge} />
|
||||||
|
<PriceRow label="Greep" amount={priceBreakdown.handleCost} />
|
||||||
|
<PriceRow label="Kleur toeslag" amount={priceBreakdown.finishSurcharge} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 border-t-2 border-[#1A2E2E] pt-3">
|
||||||
|
<PriceRow label="Totaal (indicatie)" amount={priceBreakdown.totalPrice} bold />
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-[10px] text-gray-400">
|
||||||
|
* Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Summary */}
|
||||||
|
<SummarySection title="Contactgegevens">
|
||||||
|
<SummaryRow label="Naam" value={name || "—"} />
|
||||||
|
<SummaryRow label="E-mail" value={email || "—"} />
|
||||||
|
<SummaryRow label="Telefoon" value={phone || "—"} />
|
||||||
|
{note && <SummaryRow label="Opmerking" value={note} />}
|
||||||
|
</SummarySection>
|
||||||
|
|
||||||
|
{/* Validation Warning */}
|
||||||
|
{!canSubmit && (
|
||||||
|
<div className="flex items-start gap-2 rounded-lg bg-amber-50 p-3 text-sm text-amber-700">
|
||||||
|
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||||||
|
<span>Vul eerst uw contactgegevens in (stap 5) om de aanvraag te versturen.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{status === "error" && (
|
||||||
|
<div className="flex items-start gap-2 rounded-lg bg-red-50 p-3 text-sm text-red-700">
|
||||||
|
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||||||
|
<span>{errorMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="mt-8 w-full bg-brand-orange text-white hover:bg-brand-orange/90"
|
onClick={handleSubmit}
|
||||||
|
disabled={!canSubmit || status === "loading"}
|
||||||
|
className="w-full bg-[#C4D668] text-[#1A2E2E] hover:bg-[#b5c75a] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Send className="size-4" />
|
{status === "loading" ? (
|
||||||
Verzend Aanvraag
|
<>
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Verzenden...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="size-4" />
|
||||||
|
Verzend Offerte Aanvraag
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Asset mapping for Aluwdoors textures
|
* Asset mapping for Proinn textures
|
||||||
* Maps configurator state values to texture file paths
|
* Maps configurator state values to texture file paths
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -8,49 +8,49 @@ export type GlassTexture = 'blank' | 'brons-tint' | 'grijs-tint' | 'mat-blank' |
|
|||||||
export type HandleType = 'beugelgreep' | 'geen' | 'hoekgreep' | 'maangreep' | 'ovaalgreep';
|
export type HandleType = 'beugelgreep' | 'geen' | 'hoekgreep' | 'maangreep' | 'ovaalgreep';
|
||||||
export type DividerType = 'platte-roede' | 't-roede';
|
export type DividerType = 'platte-roede' | 't-roede';
|
||||||
|
|
||||||
const TEXTURE_BASE = '/textures/aluwdoors';
|
const TEXTURE_BASE = '/textures/proinn';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metal texture mapping
|
* Metal texture mapping
|
||||||
*/
|
*/
|
||||||
export const metalTextures: Record<MetalTexture, string> = {
|
export const metalTextures: Record<MetalTexture, string> = {
|
||||||
antraciet: `${TEXTURE_BASE}/aluwdoors-configurator-metaalkleur-antraciet.jpg`,
|
antraciet: `${TEXTURE_BASE}/proinn-metaalkleur-antraciet.jpg`,
|
||||||
beige: `${TEXTURE_BASE}/aluwdoors-configurator-metaalkleur-beige.jpg`,
|
beige: `${TEXTURE_BASE}/proinn-metaalkleur-beige.jpg`,
|
||||||
brons: `${TEXTURE_BASE}/aluwdoors-configurator-metaalkleur-brons.jpg`,
|
brons: `${TEXTURE_BASE}/proinn-metaalkleur-brons.jpg`,
|
||||||
goud: `${TEXTURE_BASE}/aluwdoors-configurator-metaalkleur-goud.jpg`,
|
goud: `${TEXTURE_BASE}/proinn-metaalkleur-goud.jpg`,
|
||||||
zwart: `${TEXTURE_BASE}/aluwdoors-configurator-metaalkleur-zwart.jpg`,
|
zwart: `${TEXTURE_BASE}/proinn-metaalkleur-zwart.jpg`,
|
||||||
ral: `${TEXTURE_BASE}/aluwdoors-configurator-metaalkleur-ral-keuze.jpg`,
|
ral: `${TEXTURE_BASE}/proinn-metaalkleur-ral-keuze.jpg`,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Glass texture mapping
|
* Glass texture mapping
|
||||||
*/
|
*/
|
||||||
export const glassTextures: Record<GlassTexture, string> = {
|
export const glassTextures: Record<GlassTexture, string> = {
|
||||||
'blank': `${TEXTURE_BASE}/aluwdoors-configurator-glaskleur-blank.jpg`,
|
'blank': `${TEXTURE_BASE}/proinn-glaskleur-blank.jpg`,
|
||||||
'brons-tint': `${TEXTURE_BASE}/aluwdoors-configurator-glaskleur-brons.jpg`,
|
'brons-tint': `${TEXTURE_BASE}/proinn-glaskleur-brons.jpg`,
|
||||||
'grijs-tint': `${TEXTURE_BASE}/aluwdoors-configurator-glaskleur-grijs.jpg`,
|
'grijs-tint': `${TEXTURE_BASE}/proinn-glaskleur-grijs.jpg`,
|
||||||
'mat-blank': `${TEXTURE_BASE}/aluwdoors-configurator-glaskleur-mat-blank.jpg`,
|
'mat-blank': `${TEXTURE_BASE}/proinn-glaskleur-mat-blank.jpg`,
|
||||||
'mat-brons': `${TEXTURE_BASE}/aluwdoors-configurator-glaskleur-mat-brons.jpg`,
|
'mat-brons': `${TEXTURE_BASE}/proinn-glaskleur-mat-brons.jpg`,
|
||||||
'mat-zwart': `${TEXTURE_BASE}/aluwdoors-configurator-glaskleur-mat-zwart.jpg`,
|
'mat-zwart': `${TEXTURE_BASE}/proinn-glaskleur-mat-zwart.jpg`,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle SVG mapping
|
* Handle SVG mapping
|
||||||
*/
|
*/
|
||||||
export const handleSVGs: Record<HandleType, string> = {
|
export const handleSVGs: Record<HandleType, string> = {
|
||||||
beugelgreep: `${TEXTURE_BASE}/aluwdoors-configurator-fineer-handgreep-beugelgreep.svg`,
|
beugelgreep: `${TEXTURE_BASE}/proinn-fineer-handgreep-beugelgreep.svg`,
|
||||||
geen: `${TEXTURE_BASE}/aluwdoors-configurator-fineer-handgreep-geen.svg`,
|
geen: `${TEXTURE_BASE}/proinn-fineer-handgreep-geen.svg`,
|
||||||
hoekgreep: `${TEXTURE_BASE}/aluwdoors-configurator-fineer-handgreep-hoekgreep.svg`,
|
hoekgreep: `${TEXTURE_BASE}/proinn-fineer-handgreep-hoekgreep.svg`,
|
||||||
maangreep: `${TEXTURE_BASE}/aluwdoors-configurator-fineer-handgreep-maangreep.svg`,
|
maangreep: `${TEXTURE_BASE}/proinn-fineer-handgreep-maangreep.svg`,
|
||||||
ovaalgreep: `${TEXTURE_BASE}/aluwdoors-configurator-fineer-handgreep-ovaalgreep.svg`,
|
ovaalgreep: `${TEXTURE_BASE}/proinn-fineer-handgreep-ovaalgreep.svg`,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Divider SVG mapping
|
* Divider SVG mapping
|
||||||
*/
|
*/
|
||||||
export const dividerSVGs: Record<DividerType, string> = {
|
export const dividerSVGs: Record<DividerType, string> = {
|
||||||
'platte-roede': `${TEXTURE_BASE}/aluwdoors-configurator-roedetype-platte-roede.svg`,
|
'platte-roede': `${TEXTURE_BASE}/proinn-roedetype-platte-roede.svg`,
|
||||||
't-roede': `${TEXTURE_BASE}/aluwdoors-configurator-roedetype-t-roede.svg`,
|
't-roede': `${TEXTURE_BASE}/proinn-roedetype-t-roede.svg`,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,26 +100,17 @@ export function getGlassMaterial(glassType: GlassTexture): GlassMaterialProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aluwdoors extracted color scheme
|
* Proinn color scheme
|
||||||
*/
|
*/
|
||||||
export const aluwColors = {
|
export const proinnColors = {
|
||||||
// Primary action color (from CSS analysis)
|
primary: '#C4D668',
|
||||||
primary: '#b1de6e', // Pistachio green
|
primaryDark: '#b5c75a',
|
||||||
primaryDark: '#9fcd5b',
|
darkest: '#1A2E2E',
|
||||||
|
|
||||||
// Dark backgrounds
|
|
||||||
darkest: '#1b2221',
|
|
||||||
dark: '#2b3937',
|
dark: '#2b3937',
|
||||||
darkMedium: '#3e4b49',
|
darkMedium: '#3e4b49',
|
||||||
|
|
||||||
// Light backgrounds
|
|
||||||
light: '#e0e5e5',
|
light: '#e0e5e5',
|
||||||
lightest: '#f0f3f3',
|
lightest: '#F5F5F3',
|
||||||
|
|
||||||
// Neutral
|
|
||||||
gray: '#868c8b',
|
gray: '#868c8b',
|
||||||
|
|
||||||
// Accent/Error
|
|
||||||
error: '#e74242',
|
error: '#e74242',
|
||||||
errorDark: '#c40c0c',
|
errorDark: '#c40c0c',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -8,55 +8,27 @@
|
|||||||
// MANUFACTURING CONSTANTS
|
// MANUFACTURING CONSTANTS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Steel Profile Dimensions (40x40mm Square Tube)
|
|
||||||
* Standard industrial steel door profile
|
|
||||||
*/
|
|
||||||
export const PROFILE_WIDTH = 40; // mm - Face width
|
export const PROFILE_WIDTH = 40; // mm - Face width
|
||||||
export const PROFILE_DEPTH = 40; // mm - Tube depth
|
export const PROFILE_DEPTH = 40; // mm - Tube depth
|
||||||
export const PROFILE_CORNER_RADIUS = 2; // mm - Rounded corners for welding
|
export const PROFILE_CORNER_RADIUS = 2; // mm - Rounded corners for welding
|
||||||
|
|
||||||
/**
|
export const STILE_WIDTH = 40; // mm - Vertical profiles
|
||||||
* Steel Profile Named Exports (aliases for pricing/manufacturing clarity)
|
|
||||||
*/
|
|
||||||
export const STILE_WIDTH = 40; // mm - Vertical profiles (same as PROFILE_WIDTH)
|
|
||||||
export const RAIL_WIDTH = 20; // mm - Horizontal slim-line profiles
|
export const RAIL_WIDTH = 20; // mm - Horizontal slim-line profiles
|
||||||
|
|
||||||
/**
|
|
||||||
* Glass Specifications - Standard 33.1 laminated safety glass (VSG 33.1)
|
|
||||||
*/
|
|
||||||
export const GLASS_THICKNESS = 7; // mm - Standard 33.1 Safety Glass
|
export const GLASS_THICKNESS = 7; // mm - Standard 33.1 Safety Glass
|
||||||
export const GLASS_OFFSET = 15; // mm - Center glass in 40mm profile: (40-7)/2 - 1.5mm clearance
|
export const GLASS_OFFSET = 15; // mm - Center glass in 40mm profile
|
||||||
|
|
||||||
/**
|
|
||||||
* Rail Height Variations
|
|
||||||
*/
|
|
||||||
export const RAIL_HEIGHT_SLIM = 20; // mm - Slim horizontal rails
|
export const RAIL_HEIGHT_SLIM = 20; // mm - Slim horizontal rails
|
||||||
export const RAIL_HEIGHT_ROBUST = 40; // mm - Standard robust rails (same as profile)
|
export const RAIL_HEIGHT_ROBUST = 40; // mm - Standard robust rails
|
||||||
|
|
||||||
/**
|
export const TAATS_PIVOT_OFFSET = 60; // mm
|
||||||
* Taats (Pivot) Door Mechanism
|
export const STELRUIMTE = 10; // mm
|
||||||
*/
|
export const HANGNAAD = 3; // mm
|
||||||
export const TAATS_PIVOT_OFFSET = 60; // mm - Pivot axis offset from wall for Taats doors
|
export const WALL_THICKNESS = 150; // mm
|
||||||
|
|
||||||
/**
|
|
||||||
* Wall Mounting Dimensions (Sparingsmaat / Deurmaat)
|
|
||||||
* Dutch building standard: Sparingsmaat = rough wall opening
|
|
||||||
*/
|
|
||||||
export const STELRUIMTE = 10; // mm - Total tolerance between wall and frame (5mm per side)
|
|
||||||
export const HANGNAAD = 3; // mm - Gap between frame and door leaf per side
|
|
||||||
export const WALL_THICKNESS = 150; // mm - Standard interior wall thickness
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate mounting dimensions from Sparingsmaat (wall opening).
|
|
||||||
*
|
|
||||||
* Sparingsmaat (input) -> Frame -> Door Leaf
|
|
||||||
* Frame = Sparingsmaat - STELRUIMTE (10mm tolerance)
|
|
||||||
* DoorLeaf = Frame - 2*PROFILE_WIDTH - 2*HANGNAAD (6mm gap)
|
|
||||||
*/
|
|
||||||
export function calculateMountingDimensions(sparingsmaatWidth: number, sparingsmaatHeight: number) {
|
export function calculateMountingDimensions(sparingsmaatWidth: number, sparingsmaatHeight: number) {
|
||||||
const frameOuterWidth = sparingsmaatWidth - STELRUIMTE;
|
const frameOuterWidth = sparingsmaatWidth - STELRUIMTE;
|
||||||
const frameOuterHeight = sparingsmaatHeight - STELRUIMTE / 2; // 5mm top tolerance only
|
const frameOuterHeight = sparingsmaatHeight - STELRUIMTE / 2;
|
||||||
const doorLeafWidth = frameOuterWidth - (2 * HANGNAAD);
|
const doorLeafWidth = frameOuterWidth - (2 * HANGNAAD);
|
||||||
const doorLeafHeight = frameOuterHeight - (2 * HANGNAAD);
|
const doorLeafHeight = frameOuterHeight - (2 * HANGNAAD);
|
||||||
|
|
||||||
@@ -67,8 +39,8 @@ export function calculateMountingDimensions(sparingsmaatWidth: number, sparingsm
|
|||||||
frameOuterHeight,
|
frameOuterHeight,
|
||||||
doorLeafWidth,
|
doorLeafWidth,
|
||||||
doorLeafHeight,
|
doorLeafHeight,
|
||||||
stelruimtePerSide: STELRUIMTE / 2, // 5mm gap visible on each side
|
stelruimtePerSide: STELRUIMTE / 2,
|
||||||
hangnaadPerSide: HANGNAAD, // 3mm gap between frame and leaf
|
hangnaadPerSide: HANGNAAD,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,51 +50,69 @@ export function calculateMountingDimensions(sparingsmaatWidth: number, sparingsm
|
|||||||
|
|
||||||
export type PartType = 'stile' | 'rail' | 'glass' | 'divider';
|
export type PartType = 'stile' | 'rail' | 'glass' | 'divider';
|
||||||
export type DoorModel = 'taats' | 'scharnier' | 'paneel';
|
export type DoorModel = 'taats' | 'scharnier' | 'paneel';
|
||||||
export type GridLayout = '3-vlak' | '4-vlak' | 'geen';
|
export type GridLayout =
|
||||||
|
| 'geen'
|
||||||
|
| '2-vlak'
|
||||||
|
| '3-vlak'
|
||||||
|
| '4-vlak'
|
||||||
|
| '6-vlak'
|
||||||
|
| '8-vlak'
|
||||||
|
| 'kruis'
|
||||||
|
| 'ongelijk-3'
|
||||||
|
| 'boerderij'
|
||||||
|
| 'herenhuis';
|
||||||
|
|
||||||
/**
|
|
||||||
* Physical Door Component
|
|
||||||
* Represents an actual steel part that will be manufactured
|
|
||||||
*/
|
|
||||||
export interface PhysicalPart {
|
export interface PhysicalPart {
|
||||||
type: PartType;
|
type: PartType;
|
||||||
// Position in 3D space (in mm, relative to door center)
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
// Dimensions in mm
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
depth: number;
|
depth: number;
|
||||||
// Metadata
|
|
||||||
label?: string;
|
label?: string;
|
||||||
isGlass?: boolean;
|
isGlass?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete Door Assembly
|
|
||||||
*/
|
|
||||||
export interface DoorAssembly {
|
export interface DoorAssembly {
|
||||||
modelId: DoorModel;
|
modelId: DoorModel;
|
||||||
gridLayout: GridLayout;
|
gridLayout: GridLayout;
|
||||||
doorWidth: number; // mm - Actual door leaf width
|
doorWidth: number;
|
||||||
doorHeight: number; // mm - Door height
|
doorHeight: number;
|
||||||
parts: PhysicalPart[];
|
parts: PhysicalPart[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// GRID PATTERN DEFINITIONS (DATA-DRIVEN)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid pattern definition as data.
|
||||||
|
* horizontalPositions: fractional Y positions (0 = bottom, 1 = top) for horizontal dividers
|
||||||
|
* verticalPositions: fractional X positions (0 = left, 1 = right) for vertical dividers
|
||||||
|
*/
|
||||||
|
interface GridPatternDef {
|
||||||
|
horizontalPositions: number[];
|
||||||
|
verticalPositions: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRID_PATTERNS: Record<GridLayout, GridPatternDef> = {
|
||||||
|
'geen': { horizontalPositions: [], verticalPositions: [] },
|
||||||
|
'2-vlak': { horizontalPositions: [0.5], verticalPositions: [] },
|
||||||
|
'3-vlak': { horizontalPositions: [1 / 3, 2 / 3], verticalPositions: [] },
|
||||||
|
'4-vlak': { horizontalPositions: [0.25, 0.5, 0.75], verticalPositions: [] },
|
||||||
|
'6-vlak': { horizontalPositions: [1 / 3, 2 / 3], verticalPositions: [0.5] },
|
||||||
|
'8-vlak': { horizontalPositions: [0.25, 0.5, 0.75], verticalPositions: [0.5] },
|
||||||
|
'kruis': { horizontalPositions: [0.5], verticalPositions: [0.5] },
|
||||||
|
'ongelijk-3': { horizontalPositions: [0.35, 0.65], verticalPositions: [] },
|
||||||
|
'boerderij': { horizontalPositions: [0.7], verticalPositions: [0.5] },
|
||||||
|
'herenhuis': { horizontalPositions: [0.3, 0.7], verticalPositions: [] },
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// LAYOUT GENERATION
|
// LAYOUT GENERATION
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate physical parts list for door manufacturing
|
|
||||||
*
|
|
||||||
* @param modelId - Door model (taats, scharnier, paneel)
|
|
||||||
* @param gridLayout - Grid division (3-vlak, 4-vlak, geen)
|
|
||||||
* @param doorWidth - Door leaf width in mm
|
|
||||||
* @param doorHeight - Door height in mm
|
|
||||||
* @returns Complete assembly with all physical parts
|
|
||||||
*/
|
|
||||||
export function generateDoorAssembly(
|
export function generateDoorAssembly(
|
||||||
modelId: DoorModel,
|
modelId: DoorModel,
|
||||||
gridLayout: GridLayout,
|
gridLayout: GridLayout,
|
||||||
@@ -130,12 +120,13 @@ export function generateDoorAssembly(
|
|||||||
doorHeight: number
|
doorHeight: number
|
||||||
): DoorAssembly {
|
): DoorAssembly {
|
||||||
const parts: PhysicalPart[] = [];
|
const parts: PhysicalPart[] = [];
|
||||||
|
const pattern = GRID_PATTERNS[gridLayout] || GRID_PATTERNS['geen'];
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PERIMETER FRAME (All door types)
|
// PERIMETER FRAME
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// LEFT STILE (Vertical)
|
// LEFT STILE
|
||||||
parts.push({
|
parts.push({
|
||||||
type: 'stile',
|
type: 'stile',
|
||||||
x: -doorWidth / 2 + PROFILE_WIDTH / 2,
|
x: -doorWidth / 2 + PROFILE_WIDTH / 2,
|
||||||
@@ -147,7 +138,7 @@ export function generateDoorAssembly(
|
|||||||
label: 'Left Stile',
|
label: 'Left Stile',
|
||||||
});
|
});
|
||||||
|
|
||||||
// RIGHT STILE (Vertical)
|
// RIGHT STILE
|
||||||
parts.push({
|
parts.push({
|
||||||
type: 'stile',
|
type: 'stile',
|
||||||
x: doorWidth / 2 - PROFILE_WIDTH / 2,
|
x: doorWidth / 2 - PROFILE_WIDTH / 2,
|
||||||
@@ -159,7 +150,7 @@ export function generateDoorAssembly(
|
|||||||
label: 'Right Stile',
|
label: 'Right Stile',
|
||||||
});
|
});
|
||||||
|
|
||||||
// TOP RAIL (Horizontal)
|
// TOP RAIL
|
||||||
const topRailWidth = doorWidth - PROFILE_WIDTH * 2;
|
const topRailWidth = doorWidth - PROFILE_WIDTH * 2;
|
||||||
parts.push({
|
parts.push({
|
||||||
type: 'rail',
|
type: 'rail',
|
||||||
@@ -172,7 +163,7 @@ export function generateDoorAssembly(
|
|||||||
label: 'Top Rail',
|
label: 'Top Rail',
|
||||||
});
|
});
|
||||||
|
|
||||||
// BOTTOM RAIL (Horizontal)
|
// BOTTOM RAIL
|
||||||
parts.push({
|
parts.push({
|
||||||
type: 'rail',
|
type: 'rail',
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -185,80 +176,57 @@ export function generateDoorAssembly(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// GRID DIVIDERS (Based on layout)
|
// HORIZONTAL DIVIDERS (from pattern data)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
if (gridLayout === '3-vlak') {
|
const innerHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
||||||
// Two horizontal dividers at 1/3 and 2/3 height
|
const innerBottom = -doorHeight / 2 + RAIL_HEIGHT_ROBUST;
|
||||||
const divider1Y = doorHeight / 2 - doorHeight / 3;
|
|
||||||
const divider2Y = doorHeight / 2 - (2 * doorHeight) / 3;
|
|
||||||
|
|
||||||
|
for (const fraction of pattern.horizontalPositions) {
|
||||||
|
const dividerY = innerBottom + innerHeight * fraction;
|
||||||
|
|
||||||
|
// Determine width: if there are vertical dividers, horizontal dividers span full width
|
||||||
|
// (vertical dividers will be handled separately)
|
||||||
parts.push({
|
parts.push({
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: divider1Y,
|
y: dividerY,
|
||||||
z: 0,
|
z: 0,
|
||||||
width: topRailWidth,
|
width: topRailWidth,
|
||||||
height: RAIL_HEIGHT_SLIM,
|
height: RAIL_HEIGHT_SLIM,
|
||||||
depth: PROFILE_DEPTH,
|
depth: PROFILE_DEPTH,
|
||||||
label: 'Divider 1/3',
|
label: `H-Divider ${Math.round(fraction * 100)}%`,
|
||||||
});
|
|
||||||
|
|
||||||
parts.push({
|
|
||||||
type: 'divider',
|
|
||||||
x: 0,
|
|
||||||
y: divider2Y,
|
|
||||||
z: 0,
|
|
||||||
width: topRailWidth,
|
|
||||||
height: RAIL_HEIGHT_SLIM,
|
|
||||||
depth: PROFILE_DEPTH,
|
|
||||||
label: 'Divider 2/3',
|
|
||||||
});
|
|
||||||
} else if (gridLayout === '4-vlak') {
|
|
||||||
// Three horizontal dividers at 1/4, 1/2, 3/4 height
|
|
||||||
const divider1Y = doorHeight / 2 - doorHeight / 4;
|
|
||||||
const divider2Y = 0;
|
|
||||||
const divider3Y = doorHeight / 2 - (3 * doorHeight) / 4;
|
|
||||||
|
|
||||||
parts.push({
|
|
||||||
type: 'divider',
|
|
||||||
x: 0,
|
|
||||||
y: divider1Y,
|
|
||||||
z: 0,
|
|
||||||
width: topRailWidth,
|
|
||||||
height: RAIL_HEIGHT_SLIM,
|
|
||||||
depth: PROFILE_DEPTH,
|
|
||||||
label: 'Divider 1/4',
|
|
||||||
});
|
|
||||||
|
|
||||||
parts.push({
|
|
||||||
type: 'divider',
|
|
||||||
x: 0,
|
|
||||||
y: divider2Y,
|
|
||||||
z: 0,
|
|
||||||
width: topRailWidth,
|
|
||||||
height: RAIL_HEIGHT_SLIM,
|
|
||||||
depth: PROFILE_DEPTH,
|
|
||||||
label: 'Divider 1/2',
|
|
||||||
});
|
|
||||||
|
|
||||||
parts.push({
|
|
||||||
type: 'divider',
|
|
||||||
x: 0,
|
|
||||||
y: divider3Y,
|
|
||||||
z: 0,
|
|
||||||
width: topRailWidth,
|
|
||||||
height: RAIL_HEIGHT_SLIM,
|
|
||||||
depth: PROFILE_DEPTH,
|
|
||||||
label: 'Divider 3/4',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// VERTICAL CENTER DIVIDER (Paneel type only)
|
// VERTICAL DIVIDERS (from pattern data)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
if (modelId === 'paneel') {
|
const innerWidth = doorWidth - PROFILE_WIDTH * 2;
|
||||||
|
const innerLeft = -doorWidth / 2 + PROFILE_WIDTH;
|
||||||
|
|
||||||
|
for (const fraction of pattern.verticalPositions) {
|
||||||
|
const dividerX = innerLeft + innerWidth * fraction;
|
||||||
|
const verticalDividerHeight = innerHeight;
|
||||||
|
|
||||||
|
parts.push({
|
||||||
|
type: 'divider',
|
||||||
|
x: dividerX,
|
||||||
|
y: 0,
|
||||||
|
z: 0,
|
||||||
|
width: PROFILE_WIDTH,
|
||||||
|
height: verticalDividerHeight,
|
||||||
|
depth: PROFILE_DEPTH,
|
||||||
|
label: `V-Divider ${Math.round(fraction * 100)}%`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// PANEEL TYPE CENTER VERTICAL DIVIDER
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
if (modelId === 'paneel' && !pattern.verticalPositions.includes(0.5)) {
|
||||||
const verticalDividerHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
const verticalDividerHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
||||||
|
|
||||||
parts.push({
|
parts.push({
|
||||||
@@ -277,7 +245,6 @@ export function generateDoorAssembly(
|
|||||||
// GLASS PANELS
|
// GLASS PANELS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Calculate glass dimensions (inside frame with offset)
|
|
||||||
const glassWidth = doorWidth - PROFILE_WIDTH * 2 - GLASS_OFFSET * 2;
|
const glassWidth = doorWidth - PROFILE_WIDTH * 2 - GLASS_OFFSET * 2;
|
||||||
const glassHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2 - GLASS_OFFSET * 2;
|
const glassHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2 - GLASS_OFFSET * 2;
|
||||||
|
|
||||||
@@ -302,41 +269,32 @@ export function generateDoorAssembly(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert mm to meters for Three.js 3D scene
|
|
||||||
*/
|
|
||||||
export function mmToMeters(mm: number): number {
|
export function mmToMeters(mm: number): number {
|
||||||
return mm / 1000;
|
return mm / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get divider positions in meters (for backward compatibility)
|
|
||||||
*/
|
|
||||||
export function getDividerPositions(
|
export function getDividerPositions(
|
||||||
gridLayout: GridLayout,
|
gridLayout: GridLayout,
|
||||||
doorHeight: number
|
doorHeight: number
|
||||||
): number[] {
|
): number[] {
|
||||||
|
const pattern = GRID_PATTERNS[gridLayout];
|
||||||
|
if (!pattern) return [];
|
||||||
|
|
||||||
const doorHeightMeters = mmToMeters(doorHeight);
|
const doorHeightMeters = mmToMeters(doorHeight);
|
||||||
|
const innerHeight = doorHeightMeters - mmToMeters(RAIL_HEIGHT_ROBUST * 2);
|
||||||
|
const innerBottom = -doorHeightMeters / 2 + mmToMeters(RAIL_HEIGHT_ROBUST);
|
||||||
|
|
||||||
if (gridLayout === '3-vlak') {
|
return pattern.horizontalPositions.map(
|
||||||
return [-doorHeightMeters / 3, doorHeightMeters / 3];
|
(fraction) => innerBottom + innerHeight * fraction
|
||||||
} else if (gridLayout === '4-vlak') {
|
);
|
||||||
return [-doorHeightMeters / 2, 0, doorHeightMeters / 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validation: Check if door dimensions are manufacturable
|
|
||||||
*/
|
|
||||||
export function validateDoorDimensions(
|
export function validateDoorDimensions(
|
||||||
doorWidth: number,
|
doorWidth: number,
|
||||||
doorHeight: number
|
doorHeight: number
|
||||||
): { valid: boolean; errors: string[] } {
|
): { valid: boolean; errors: string[] } {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Minimum dimensions check
|
|
||||||
if (doorWidth < PROFILE_WIDTH * 3) {
|
if (doorWidth < PROFILE_WIDTH * 3) {
|
||||||
errors.push(`Door width too small (min: ${PROFILE_WIDTH * 3}mm)`);
|
errors.push(`Door width too small (min: ${PROFILE_WIDTH * 3}mm)`);
|
||||||
}
|
}
|
||||||
@@ -345,7 +303,6 @@ export function validateDoorDimensions(
|
|||||||
errors.push(`Door height too small (min: ${RAIL_HEIGHT_ROBUST * 3}mm)`);
|
errors.push(`Door height too small (min: ${RAIL_HEIGHT_ROBUST * 3}mm)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum dimensions check (based on steel profile strength)
|
|
||||||
if (doorWidth > 1200) {
|
if (doorWidth > 1200) {
|
||||||
errors.push('Door width exceeds maximum (1200mm) - structural integrity');
|
errors.push('Door width exceeds maximum (1200mm) - structural integrity');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Pricing Engine for Proinn Configurator
|
* Pricing Engine for Proinn Configurator
|
||||||
* Based on Dutch market standard pricing (Metalworks/Aluwdoors reference)
|
* Based on Dutch market standard pricing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,22 +28,64 @@ const HANDLE_PRICES: Record<string, number> = {
|
|||||||
'geen': 0,
|
'geen': 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Premium finish surcharges
|
||||||
|
const FINISH_SURCHARGES: Record<string, number> = {
|
||||||
|
'zwart': 0,
|
||||||
|
'grijs': 0,
|
||||||
|
'brons': 0,
|
||||||
|
'goud': 150,
|
||||||
|
'beige': 75,
|
||||||
|
'ral': 200,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frame size multipliers (relative to standard 40mm)
|
||||||
|
const FRAME_SIZE_MULTIPLIERS: Record<number, number> = {
|
||||||
|
20: 0.7,
|
||||||
|
30: 0.85,
|
||||||
|
40: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHorizontalDividerCount(gridLayout: GridLayout): number {
|
||||||
|
switch (gridLayout) {
|
||||||
|
case '2-vlak': return 1;
|
||||||
|
case '3-vlak': return 2;
|
||||||
|
case '4-vlak': return 3;
|
||||||
|
case '6-vlak': return 2;
|
||||||
|
case '8-vlak': return 3;
|
||||||
|
case 'kruis': return 1;
|
||||||
|
case 'ongelijk-3': return 2;
|
||||||
|
case 'boerderij': return 1;
|
||||||
|
case 'herenhuis': return 2;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasVerticalDividers(gridLayout: GridLayout): boolean {
|
||||||
|
return ['6-vlak', '8-vlak', 'kruis', 'boerderij'].includes(gridLayout);
|
||||||
|
}
|
||||||
|
|
||||||
function calculateSteelLength(
|
function calculateSteelLength(
|
||||||
doorWidth: number,
|
doorWidth: number,
|
||||||
doorHeight: number,
|
doorHeight: number,
|
||||||
gridLayout: GridLayout,
|
gridLayout: GridLayout,
|
||||||
hasVerticalDivider: boolean
|
hasPaneelVerticalDivider: boolean
|
||||||
): number {
|
): number {
|
||||||
const innerWidth = doorWidth - PROFILE_WIDTH * 2;
|
const innerWidth = doorWidth - PROFILE_WIDTH * 2;
|
||||||
|
// Perimeter
|
||||||
let totalLength = doorHeight * 2 + innerWidth * 2;
|
let totalLength = doorHeight * 2 + innerWidth * 2;
|
||||||
|
|
||||||
if (gridLayout === '3-vlak') {
|
// Horizontal dividers
|
||||||
totalLength += innerWidth * 2;
|
const hDividers = getHorizontalDividerCount(gridLayout);
|
||||||
} else if (gridLayout === '4-vlak') {
|
totalLength += innerWidth * hDividers;
|
||||||
totalLength += innerWidth * 3;
|
|
||||||
|
// Vertical dividers from grid pattern
|
||||||
|
if (hasVerticalDividers(gridLayout)) {
|
||||||
|
const innerHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
||||||
|
totalLength += innerHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasVerticalDivider) {
|
// Paneel type adds center vertical divider
|
||||||
|
if (hasPaneelVerticalDivider) {
|
||||||
totalLength += doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
totalLength += doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,11 +100,12 @@ function calculateGlassArea(
|
|||||||
const glassWidth = doorWidth - PROFILE_WIDTH * 2;
|
const glassWidth = doorWidth - PROFILE_WIDTH * 2;
|
||||||
const glassHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
const glassHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
|
||||||
|
|
||||||
let dividerArea = 0;
|
const hDividers = getHorizontalDividerCount(gridLayout);
|
||||||
if (gridLayout === '3-vlak') {
|
let dividerArea = glassWidth * RAIL_HEIGHT_SLIM * hDividers;
|
||||||
dividerArea = glassWidth * RAIL_HEIGHT_SLIM * 2;
|
|
||||||
} else if (gridLayout === '4-vlak') {
|
// Vertical divider area
|
||||||
dividerArea = glassWidth * RAIL_HEIGHT_SLIM * 3;
|
if (hasVerticalDividers(gridLayout)) {
|
||||||
|
dividerArea += PROFILE_WIDTH * (glassHeight - RAIL_HEIGHT_SLIM * hDividers);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (glassWidth * glassHeight - dividerArea) / 1_000_000;
|
return (glassWidth * glassHeight - dividerArea) / 1_000_000;
|
||||||
@@ -75,6 +118,7 @@ export interface PriceBreakdown {
|
|||||||
mechanismSurcharge: number;
|
mechanismSurcharge: number;
|
||||||
sidePanelSurcharge: number;
|
sidePanelSurcharge: number;
|
||||||
handleCost: number;
|
handleCost: number;
|
||||||
|
finishSurcharge: number;
|
||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
steelLengthM: number;
|
steelLengthM: number;
|
||||||
glassAreaSqm: number;
|
glassAreaSqm: number;
|
||||||
@@ -87,12 +131,15 @@ export function calculatePrice(
|
|||||||
gridLayout: GridLayout,
|
gridLayout: GridLayout,
|
||||||
doorConfig: 'enkele' | 'dubbele',
|
doorConfig: 'enkele' | 'dubbele',
|
||||||
sidePanel: 'geen' | 'links' | 'rechts' | 'beide',
|
sidePanel: 'geen' | 'links' | 'rechts' | 'beide',
|
||||||
handle: string
|
handle: string,
|
||||||
|
finish: string = 'zwart',
|
||||||
|
frameSize: number = 40
|
||||||
): PriceBreakdown {
|
): PriceBreakdown {
|
||||||
const leafCount = doorConfig === 'dubbele' ? 2 : 1;
|
const leafCount = doorConfig === 'dubbele' ? 2 : 1;
|
||||||
const hasVerticalDivider = doorType === 'paneel';
|
const hasPaneelVerticalDivider = doorType === 'paneel';
|
||||||
|
const frameSizeMultiplier = FRAME_SIZE_MULTIPLIERS[frameSize] ?? 1.0;
|
||||||
|
|
||||||
const steelLengthPerLeaf = calculateSteelLength(doorWidth, doorHeight, gridLayout, hasVerticalDivider);
|
const steelLengthPerLeaf = calculateSteelLength(doorWidth, doorHeight, gridLayout, hasPaneelVerticalDivider);
|
||||||
const glassAreaPerLeaf = calculateGlassArea(doorWidth, doorHeight, gridLayout);
|
const glassAreaPerLeaf = calculateGlassArea(doorWidth, doorHeight, gridLayout);
|
||||||
|
|
||||||
const totalSteelLength = steelLengthPerLeaf * leafCount;
|
const totalSteelLength = steelLengthPerLeaf * leafCount;
|
||||||
@@ -100,7 +147,7 @@ export function calculatePrice(
|
|||||||
|
|
||||||
const sidePanelCount = sidePanel === 'beide' ? 2 : (sidePanel === 'geen' ? 0 : 1);
|
const sidePanelCount = sidePanel === 'beide' ? 2 : (sidePanel === 'geen' ? 0 : 1);
|
||||||
|
|
||||||
const steelCost = Math.round(totalSteelLength * STEEL_PRICE_PER_METER);
|
const steelCost = Math.round(totalSteelLength * STEEL_PRICE_PER_METER * frameSizeMultiplier);
|
||||||
const glassCost = Math.round(totalGlassArea * GLASS_PRICE_PER_SQM);
|
const glassCost = Math.round(totalGlassArea * GLASS_PRICE_PER_SQM);
|
||||||
|
|
||||||
let mechanismSurcharge = 0;
|
let mechanismSurcharge = 0;
|
||||||
@@ -109,8 +156,9 @@ export function calculatePrice(
|
|||||||
|
|
||||||
const handleCost = HANDLE_PRICES[handle] || 0;
|
const handleCost = HANDLE_PRICES[handle] || 0;
|
||||||
const sidePanelSurchrg = sidePanelCount * SIDE_PANEL_SURCHARGE;
|
const sidePanelSurchrg = sidePanelCount * SIDE_PANEL_SURCHARGE;
|
||||||
|
const finishSurcharge = FINISH_SURCHARGES[finish] ?? 0;
|
||||||
|
|
||||||
const totalPrice = steelCost + glassCost + BASE_FEE + mechanismSurcharge + sidePanelSurchrg + handleCost;
|
const totalPrice = steelCost + glassCost + BASE_FEE + mechanismSurcharge + sidePanelSurchrg + handleCost + finishSurcharge;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
steelCost,
|
steelCost,
|
||||||
@@ -119,6 +167,7 @@ export function calculatePrice(
|
|||||||
mechanismSurcharge,
|
mechanismSurcharge,
|
||||||
sidePanelSurcharge: sidePanelSurchrg,
|
sidePanelSurcharge: sidePanelSurchrg,
|
||||||
handleCost,
|
handleCost,
|
||||||
|
finishSurcharge,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
steelLengthM: Math.round(totalSteelLength * 100) / 100,
|
steelLengthM: Math.round(totalSteelLength * 100) / 100,
|
||||||
glassAreaSqm: Math.round(totalGlassArea * 100) / 100,
|
glassAreaSqm: Math.round(totalGlassArea * 100) / 100,
|
||||||
|
|||||||
88
lib/store.ts
@@ -12,8 +12,20 @@ import type { GlassPattern } from './glass-patterns';
|
|||||||
import { calculatePrice, type PriceBreakdown } from './pricing';
|
import { calculatePrice, type PriceBreakdown } from './pricing';
|
||||||
|
|
||||||
export type DoorType = 'taats' | 'scharnier' | 'paneel';
|
export type DoorType = 'taats' | 'scharnier' | 'paneel';
|
||||||
export type GridType = '3-vlak' | '4-vlak' | 'geen';
|
export type GridType =
|
||||||
export type Finish = 'zwart' | 'brons' | 'grijs';
|
| 'geen'
|
||||||
|
| '2-vlak'
|
||||||
|
| '3-vlak'
|
||||||
|
| '4-vlak'
|
||||||
|
| '6-vlak'
|
||||||
|
| '8-vlak'
|
||||||
|
| 'kruis'
|
||||||
|
| 'ongelijk-3'
|
||||||
|
| 'boerderij'
|
||||||
|
| 'herenhuis';
|
||||||
|
export type Finish = 'zwart' | 'brons' | 'grijs' | 'goud' | 'beige' | 'ral';
|
||||||
|
export type GlassColor = 'helder' | 'grijs' | 'brons' | 'mat-blank' | 'mat-brons' | 'mat-zwart';
|
||||||
|
export type FrameSize = 20 | 30 | 40;
|
||||||
export type Handle = 'beugelgreep' | 'hoekgreep' | 'maangreep' | 'ovaalgreep' | 'klink' | 'u-greep' | 'geen';
|
export type Handle = 'beugelgreep' | 'hoekgreep' | 'maangreep' | 'ovaalgreep' | 'klink' | 'u-greep' | 'geen';
|
||||||
|
|
||||||
interface ConfiguratorState {
|
interface ConfiguratorState {
|
||||||
@@ -25,8 +37,10 @@ interface ConfiguratorState {
|
|||||||
// Styling
|
// Styling
|
||||||
gridType: GridType;
|
gridType: GridType;
|
||||||
finish: Finish;
|
finish: Finish;
|
||||||
|
glassColor: GlassColor;
|
||||||
handle: Handle;
|
handle: Handle;
|
||||||
glassPattern: GlassPattern;
|
glassPattern: GlassPattern;
|
||||||
|
frameSize: FrameSize;
|
||||||
|
|
||||||
// Dimensions (in mm)
|
// Dimensions (in mm)
|
||||||
width: number;
|
width: number;
|
||||||
@@ -42,17 +56,37 @@ interface ConfiguratorState {
|
|||||||
// Pricing
|
// Pricing
|
||||||
priceBreakdown: PriceBreakdown;
|
priceBreakdown: PriceBreakdown;
|
||||||
|
|
||||||
|
// Contact info
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
note: string;
|
||||||
|
|
||||||
|
// Extra options
|
||||||
|
extraOptions: string[];
|
||||||
|
|
||||||
|
// Screenshot
|
||||||
|
screenshotDataUrl: string | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setDoorType: (type: DoorType) => void;
|
setDoorType: (type: DoorType) => void;
|
||||||
setDoorConfig: (config: DoorConfig) => void;
|
setDoorConfig: (config: DoorConfig) => void;
|
||||||
setSidePanel: (panel: SidePanel) => void;
|
setSidePanel: (panel: SidePanel) => void;
|
||||||
setGridType: (type: GridType) => void;
|
setGridType: (type: GridType) => void;
|
||||||
setFinish: (finish: Finish) => void;
|
setFinish: (finish: Finish) => void;
|
||||||
|
setGlassColor: (color: GlassColor) => void;
|
||||||
setHandle: (handle: Handle) => void;
|
setHandle: (handle: Handle) => void;
|
||||||
setGlassPattern: (pattern: GlassPattern) => void;
|
setGlassPattern: (pattern: GlassPattern) => void;
|
||||||
|
setFrameSize: (size: FrameSize) => void;
|
||||||
setWidth: (width: number) => void;
|
setWidth: (width: number) => void;
|
||||||
setHeight: (height: number) => void;
|
setHeight: (height: number) => void;
|
||||||
setDimensions: (width: number, height: number) => void;
|
setDimensions: (width: number, height: number) => void;
|
||||||
|
setName: (name: string) => void;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
setPhone: (phone: string) => void;
|
||||||
|
setNote: (note: string) => void;
|
||||||
|
toggleExtraOption: (option: string) => void;
|
||||||
|
setScreenshotDataUrl: (url: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for recalculation
|
// Helper function for recalculation
|
||||||
@@ -70,8 +104,8 @@ const recalculate = (get: () => ConfiguratorState, set: (state: Partial<Configur
|
|||||||
|
|
||||||
// Helper function for price recalculation
|
// Helper function for price recalculation
|
||||||
const recalculatePrice = (get: () => ConfiguratorState, set: (state: Partial<ConfiguratorState>) => void) => {
|
const recalculatePrice = (get: () => ConfiguratorState, set: (state: Partial<ConfiguratorState>) => void) => {
|
||||||
const { doorLeafWidth, height, doorType, gridType, doorConfig, sidePanel, handle } = get();
|
const { doorLeafWidth, height, doorType, gridType, doorConfig, sidePanel, handle, finish, frameSize } = get();
|
||||||
const priceBreakdown = calculatePrice(doorLeafWidth, height, doorType, gridType, doorConfig, sidePanel, handle);
|
const priceBreakdown = calculatePrice(doorLeafWidth, height, doorType, gridType, doorConfig, sidePanel, handle, finish, frameSize);
|
||||||
set({ priceBreakdown });
|
set({ priceBreakdown });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,8 +116,10 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
sidePanel: 'geen',
|
sidePanel: 'geen',
|
||||||
gridType: '3-vlak',
|
gridType: '3-vlak',
|
||||||
finish: 'zwart',
|
finish: 'zwart',
|
||||||
|
glassColor: 'helder',
|
||||||
handle: 'beugelgreep',
|
handle: 'beugelgreep',
|
||||||
glassPattern: 'standard',
|
glassPattern: 'standard',
|
||||||
|
frameSize: 40,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 2400,
|
height: 2400,
|
||||||
|
|
||||||
@@ -94,8 +130,20 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
minWidth: 860,
|
minWidth: 860,
|
||||||
maxWidth: 1360,
|
maxWidth: 1360,
|
||||||
|
|
||||||
// Initial price (computed with defaults: taats, 3-vlak, enkele, geen, beugelgreep, 1000x2400)
|
// Initial price
|
||||||
priceBreakdown: calculatePrice(1000, 2400, 'taats', '3-vlak', 'enkele', 'geen', 'beugelgreep'),
|
priceBreakdown: calculatePrice(1000, 2400, 'taats', '3-vlak', 'enkele', 'geen', 'beugelgreep', 'zwart', 40),
|
||||||
|
|
||||||
|
// Contact info
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
note: '',
|
||||||
|
|
||||||
|
// Extra options
|
||||||
|
extraOptions: [],
|
||||||
|
|
||||||
|
// Screenshot
|
||||||
|
screenshotDataUrl: null,
|
||||||
|
|
||||||
// Actions with automatic recalculation
|
// Actions with automatic recalculation
|
||||||
setDoorType: (doorType) => {
|
setDoorType: (doorType) => {
|
||||||
@@ -121,7 +169,12 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
recalculatePrice(get, set);
|
recalculatePrice(get, set);
|
||||||
},
|
},
|
||||||
|
|
||||||
setFinish: (finish) => set({ finish }),
|
setFinish: (finish) => {
|
||||||
|
set({ finish });
|
||||||
|
recalculatePrice(get, set);
|
||||||
|
},
|
||||||
|
|
||||||
|
setGlassColor: (glassColor) => set({ glassColor }),
|
||||||
|
|
||||||
setHandle: (handle) => {
|
setHandle: (handle) => {
|
||||||
set({ handle });
|
set({ handle });
|
||||||
@@ -130,6 +183,11 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
|
|
||||||
setGlassPattern: (glassPattern) => set({ glassPattern }),
|
setGlassPattern: (glassPattern) => set({ glassPattern }),
|
||||||
|
|
||||||
|
setFrameSize: (frameSize) => {
|
||||||
|
set({ frameSize });
|
||||||
|
recalculatePrice(get, set);
|
||||||
|
},
|
||||||
|
|
||||||
setWidth: (width) => {
|
setWidth: (width) => {
|
||||||
const { doorConfig, sidePanel } = get();
|
const { doorConfig, sidePanel } = get();
|
||||||
const minWidth = calculateHoleMinWidth(doorConfig, sidePanel);
|
const minWidth = calculateHoleMinWidth(doorConfig, sidePanel);
|
||||||
@@ -151,4 +209,20 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
get().setWidth(width);
|
get().setWidth(width);
|
||||||
get().setHeight(height);
|
get().setHeight(height);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setName: (name) => set({ name }),
|
||||||
|
setEmail: (email) => set({ email }),
|
||||||
|
setPhone: (phone) => set({ phone }),
|
||||||
|
setNote: (note) => set({ note }),
|
||||||
|
|
||||||
|
toggleExtraOption: (option) => {
|
||||||
|
const { extraOptions } = get();
|
||||||
|
if (extraOptions.includes(option)) {
|
||||||
|
set({ extraOptions: extraOptions.filter((o) => o !== option) });
|
||||||
|
} else {
|
||||||
|
set({ extraOptions: [...extraOptions, option] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setScreenshotDataUrl: (screenshotDataUrl) => set({ screenshotDataUrl }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
// ── Step 1: Product ──────────────────────────────────────────────
|
// ── Step 1: Product ──────────────────────────────────────────────
|
||||||
export const productTypes = ["Taatsdeur", "Scharnierdeur", "Vast Paneel"] as const;
|
export const doorTypes = ["taats", "scharnier", "paneel"] as const;
|
||||||
|
export const gridTypes = [
|
||||||
|
"geen", "2-vlak", "3-vlak", "4-vlak", "6-vlak", "8-vlak",
|
||||||
|
"kruis", "ongelijk-3", "boerderij", "herenhuis",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const productSchema = z.object({
|
export const productSchema = z.object({
|
||||||
productType: z.enum(productTypes),
|
doorType: z.enum(doorTypes),
|
||||||
|
gridType: z.enum(gridTypes),
|
||||||
|
doorConfig: z.enum(["enkele", "dubbele"]),
|
||||||
|
sidePanel: z.enum(["geen", "links", "rechts", "beide"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Step 2: Dimensions ──────────────────────────────────────────
|
// ── Step 2: Dimensions ──────────────────────────────────────────
|
||||||
export const dimensionsSchema = z.object({
|
export const dimensionsSchema = z.object({
|
||||||
height: z
|
height: z
|
||||||
.number({ error: "Vul een geldige hoogte in" })
|
.number({ error: "Vul een geldige hoogte in" })
|
||||||
.min(2000, "Minimaal 2000mm")
|
.min(1800, "Minimaal 1800mm")
|
||||||
.max(3000, "Maximaal 3000mm"),
|
.max(3000, "Maximaal 3000mm"),
|
||||||
width: z
|
width: z
|
||||||
.number({ error: "Vul een geldige breedte in" })
|
.number({ error: "Vul een geldige breedte in" })
|
||||||
@@ -20,15 +27,26 @@ export const dimensionsSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Step 3: Options ─────────────────────────────────────────────
|
// ── Step 3: Options ─────────────────────────────────────────────
|
||||||
export const glassTypes = ["Helder", "Rookglas", "Melkglas"] as const;
|
export const finishTypes = ["zwart", "brons", "grijs", "goud", "beige", "ral"] as const;
|
||||||
export const finishTypes = ["Poedercoat Zwart", "Goud", "Brons"] as const;
|
export const glassColorTypes = ["helder", "grijs", "brons", "mat-blank", "mat-brons", "mat-zwart"] as const;
|
||||||
|
export const handleTypes = [
|
||||||
|
"beugelgreep", "hoekgreep", "maangreep", "ovaalgreep", "klink", "u-greep", "geen",
|
||||||
|
] as const;
|
||||||
|
export const frameSizes = [20, 30, 40] as const;
|
||||||
|
|
||||||
export const optionsSchema = z.object({
|
export const optionsSchema = z.object({
|
||||||
glassType: z.enum(glassTypes),
|
|
||||||
finish: z.enum(finishTypes),
|
finish: z.enum(finishTypes),
|
||||||
|
glassColor: z.enum(glassColorTypes),
|
||||||
|
handle: z.enum(handleTypes),
|
||||||
|
frameSize: z.enum(["20", "30", "40"]).transform(Number),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Step 4: Contact ─────────────────────────────────────────────
|
// ── Step 4: Extras ──────────────────────────────────────────────
|
||||||
|
export const extrasSchema = z.object({
|
||||||
|
extraOptions: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Step 5: Contact ─────────────────────────────────────────────
|
||||||
export const contactSchema = z.object({
|
export const contactSchema = z.object({
|
||||||
name: z.string().min(2, "Vul uw naam in"),
|
name: z.string().min(2, "Vul uw naam in"),
|
||||||
email: z.string().email("Vul een geldig e-mailadres in"),
|
email: z.string().email("Vul een geldig e-mailadres in"),
|
||||||
@@ -40,6 +58,7 @@ export const contactSchema = z.object({
|
|||||||
export const quoteSchema = productSchema
|
export const quoteSchema = productSchema
|
||||||
.merge(dimensionsSchema)
|
.merge(dimensionsSchema)
|
||||||
.merge(optionsSchema)
|
.merge(optionsSchema)
|
||||||
|
.merge(extrasSchema)
|
||||||
.merge(contactSchema);
|
.merge(contactSchema);
|
||||||
|
|
||||||
export type QuoteData = z.infer<typeof quoteSchema>;
|
export type QuoteData = z.infer<typeof quoteSchema>;
|
||||||
@@ -49,5 +68,6 @@ export const stepSchemas = [
|
|||||||
productSchema,
|
productSchema,
|
||||||
dimensionsSchema,
|
dimensionsSchema,
|
||||||
optionsSchema,
|
optionsSchema,
|
||||||
|
extrasSchema,
|
||||||
contactSchema,
|
contactSchema,
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
73
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
|
"resend": "^6.9.2",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "^0.182.0",
|
"three": "^0.182.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
@@ -3641,6 +3642,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/utils": {
|
"node_modules/@standard-schema/utils": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
@@ -7125,6 +7132,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
@@ -10188,6 +10201,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postal-mime": {
|
||||||
|
"version": "2.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz",
|
||||||
|
"integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==",
|
||||||
|
"license": "MIT-0"
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -10707,6 +10726,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resend": {
|
||||||
|
"version": "6.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz",
|
||||||
|
"integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postal-mime": "2.7.3",
|
||||||
|
"svix": "1.84.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-email/render": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@react-email/render": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -11332,6 +11372,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stats-gl": {
|
"node_modules/stats-gl": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||||
@@ -11668,6 +11718,16 @@
|
|||||||
"react": ">=17.0"
|
"react": ">=17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svix": {
|
||||||
|
"version": "1.84.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
|
||||||
|
"integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"standardwebhooks": "1.0.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tagged-tag": {
|
"node_modules/tagged-tag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||||
@@ -12387,6 +12447,19 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/validate-npm-package-name": {
|
"node_modules/validate-npm-package-name": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
|
"resend": "^6.9.2",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "^0.182.0",
|
"three": "^0.182.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |