Compare commits

...

11 Commits

Author SHA1 Message Date
Ubuntu
3d788740cb 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>
2026-03-01 14:50:31 +00:00
Ubuntu
748a5814e7 feat: Wall mounting system with Sparingsmaat logic and reveal surfaces
Phase 1 (Logic): Add Dutch mounting constants to door-models.ts
- STELRUIMTE=10mm (tolerance), HANGNAAD=3mm (gap per side)
- WALL_THICKNESS=150mm (standard interior wall)
- calculateMountingDimensions() derives frame/leaf from sparingsmaat

Phase 2 (Visual): Replace LivingRoom with WallContainer in scene.tsx
- 4-box wall construction with precise rectangular hole
- Hole = doorLeafWidth + STELRUIMTE (visible 5mm gap per side)
- Door sits INSIDE the wall, not in front of it

Phase 3 (Detail): Reveal surfaces and door-type positioning
- Plaster/stucco material on reveal edges (inner hole surfaces)
- Taats: door centered in wall depth (pivot at center)
- Scharnier/Paneel: offset toward front face
- Dedicated fill light illuminating reveal depth
- Baseboard (plint) on both sides of opening

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 01:23:18 +00:00
Ubuntu
fbc9fefeea fix: Physically mounted handles with proper standoffs and powder-coat material
- All 6 handle types now have cylindrical mount standoffs (pootjes)
  connecting grip to door face: r=6mm, length=40mm
- Z-positioning: grip sits at PROFILE_DEPTH/2 + MOUNT_LENGTH (60mm from
  center), no more floating handles inside the door
- Material: replaced chrome HandleMaterial (metalness=0.95) with
  PowderCoatMaterial matching door frame texture (roughness=0.7, metalness=0.6)
- UGreep fully redesigned: proper U-shape with 2 standoffs + vertical bar
- All handles cast shadows onto the door frame for depth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 01:19:54 +00:00
Ubuntu
87be70e78b feat: Production-ready configurator with Dutch standards, pricing & visual UI
- Update door-models.ts: 7mm VSG 33.1 safety glass, 15mm offset, Taats pivot 60mm
- Add pricing engine (lib/pricing.ts): steel €45/m + glass €140/m² + €650 base
- Wire reactive pricing into Zustand store on every config change
- Fix 3D materials: glass thickness 0.007m, corrected roughness/metalness
- Upgrade scene: apartment environment, wider contact shadows
- Add Dutch height presets: Renovatie 2015mm, Nieuwbouw 2315mm, Plafondhoog 2500mm
- Replace text buttons with visual SVG tiles for door type & grid selection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 01:11:55 +00:00
Ubuntu
9319750912 feat: Complete production deployment with SSL 2026-02-10 19:23:26 +00:00
Ubuntu
e192f19e5f feat: Manufacturing-grade door geometry + photorealistic materials
🏗️ Architecture (@Logic-Architect):
- Created lib/door-models.ts with exact manufacturing specs
- PROFILE_WIDTH = 40mm, PROFILE_DEPTH = 40mm (real steel tubes)
- GLASS_OFFSET = 18mm for proper centering
- Physical parts system (stiles, rails, dividers, glass)
- generateDoorAssembly() returns manufacturable parts list
- Validation for structural integrity limits

🎨 Visuals (@3D-Visual-Lead):
- Aluwdoors texture loading with vertical steel grain
- MeshStandardMaterial: roughness 0.6, metalness 0.7
- Photorealistic glass: transmission 0.98, IOR 1.5
- RoundedBox with 2mm radius for all profiles
- Suspense boundaries for progressive texture loading
- Studio environment preset + enhanced contact shadows

🔧 Technical:
- UseMemo for door assembly generation
- mmToMeters() conversion utility
- PhysicalPartComponent renderer
- Backward compatibility with glass patterns
- Fallback materials when textures fail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:15:51 +00:00
Ubuntu
32ed02c1f3 fix: TypeScript build errors and add production deployment
- Fix handle color type safety
- Fix RoundedBox args tuple type
- Remove invalid ellipseGeometry
- Add PM2 ecosystem config
- Add SSL setup automation script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 18:37:42 +00:00
Ubuntu
b30e8d18d4 feat: Add professional 3D handles, glass patterns, and living room scene
 New Features:
- 6 procedural 3D handles (Beugelgreep, Hoekgreep, Maangreep, Ovaalgreep, Klink, U-greep)
- Glass pattern generator (Standard, DT9 rounded corners, DT10 U-shapes)
- Dynamic living room scene with adaptive doorway
- Enhanced camera controls (zoomed out, more freedom)
- Texture loading system (prepared for future enhancement)

🎨 Visual Improvements:
- Professional handle details (screws, mounting blocks, rosettes)
- Realistic materials (metalness 0.95, proper roughness)
- Living room context (wood floor, white walls, baseboards)
- Better lighting (sunlight simulation, fill lights)
- Apartment environment preset

🏗️ Technical:
- Parametric glass shapes with THREE.Shape
- Dynamic doorway sizing based on door dimensions
- Store updates for handle and glass pattern types
- UI components for all new options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 18:23:52 +00:00
Ubuntu
bd9c6545da Add comprehensive README documentation 2026-02-10 17:31:46 +00:00
Ubuntu
0de3893b30 Improve 3D view to match reference drawings
Based on reference images in afbeeldingen/modellen/:
- dt9.png, dt10.png, door_type_4.jpg, samenstelling_beide.png

Changes for technical drawing aesthetic:

**Camera Improvements:**
- Position: [0, 1.2, 3.5] - More frontal, less perspective
- FOV: 35° (was 45°) - Less distortion
- Limited rotation: ±15° azimuth, near-horizontal polar
- Damping enabled for smooth movement
- Result: Flatter, more schematic view

**Profile Thickness (Match Reference Lines):**
- Stiles: 60mm (was 40mm) - Thicker vertical frames
- Rails: 40mm (was 20mm) - Thicker horizontal frames
- Depth: 60mm uniform - More prominent profiles
- Radius: 2mm (was 1mm) - Slightly more visible edges
- Result: Bold, visible frame lines like references

**Lighting (High Contrast):**
- Ambient: 0.8 (was 0.5) - Brighter overall
- Front key light: Straight on from [0,5,10]
- Intensity: 2.0 - Strong, even illumination
- Subtle side lights for minimal depth
- Result: Flat, technical drawing appearance

**Glass Material (White/Opaque):**
- Color: #f8f9fa (bright white)
- Transmission: 0.3 (was 1.0) - Much less transparent
- Opacity: 0.95 - Nearly opaque
- Result: White glass areas like reference drawings

**Visual Result:**
- Clear black frame lines on white glass
- Frontal view with minimal perspective
- Technical drawing aesthetic
- Matches dt9.png, door_type_4.jpg style
- User can see door design clearly

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 17:22:10 +00:00
Ubuntu
d2119eaa16 Fix 3D scene loading issue
Problem: Scene stuck on loading screen
Cause: Texture loading blocking render + missing font file

Fixes:
- Temporarily disabled texture loading (useTexture)
- Temporarily disabled 3D dimension labels (Text component)
- Fallback to solid color materials
- Removed unused imports

Result: Scene loads immediately with procedural door geometry

Note: Textures and dimension labels can be re-enabled once:
1. Font file is added to public/fonts/
2. Texture preloading strategy is implemented
2026-02-10 17:13:48 +00:00
151 changed files with 164829 additions and 12935 deletions

View File

@@ -0,0 +1,174 @@
---
name: 3d-visual-lead
description: "Use this agent when working on 3D rendering, materials, lighting, textures, or visual quality improvements for the Proinn door configurator. This includes any changes to the R3F scene, PBR materials, environment lighting, or texture application. The agent should be invoked proactively whenever visual/rendering files are being created or modified.\\n\\nExamples:\\n\\n- User: \"The door model looks flat and unrealistic, can you improve it?\"\\n Assistant: \"I'm going to use the Task tool to launch the 3d-visual-lead agent to enhance the door's materials and lighting for photorealism.\"\\n\\n- User: \"Add the powder-coated texture to the door frame.\"\\n Assistant: \"Let me use the Task tool to launch the 3d-visual-lead agent to apply the PBR texture from the textures directory to the door frame geometry.\"\\n\\n- User: \"The scene lighting doesn't look premium enough.\"\\n Assistant: \"I'll use the Task tool to launch the 3d-visual-lead agent to redesign the studio lighting setup for a high-end aesthetic.\"\\n\\n- Context: A new door component or material variant has just been added to the configurator.\\n Assistant: \"Since a new visual component was added, let me use the Task tool to launch the 3d-visual-lead agent to ensure it matches the photorealistic Anti-Gravity aesthetic standard.\"\\n\\n- User: \"Can you make the steel look more realistic with reflections?\"\\n Assistant: \"I'm going to use the Task tool to launch the 3d-visual-lead agent to configure the MeshStandardMaterial with proper metalness, roughness, and environment reflections.\""
model: sonnet
color: purple
memory: project
---
You are the **3D Visual Lead** for the Proinn Configurator project — an elite specialist in real-time 3D rendering, photorealistic materials, and premium product visualization using React Three Fiber. You have deep expertise in achieving showroom-quality renders in the browser.
## Your Identity & Expertise
You are a senior 3D technical artist who has shipped production product configurators for luxury brands. Your specializations:
1. **React Three Fiber (R3F) & Drei**: You know every prop of `<Canvas>`, `<MeshStandardMaterial>`, `<MeshPhysicalMaterial>`, `<Environment>`, `<ContactShadows>`, `<AccumulativeShadows>`, `<Lightformer>`, `<Float>`, `<RoundedBox>`, `<Center>`, and all Drei helpers. You write idiomatic R3F JSX, not imperative Three.js.
2. **PBR Textures & Materials**: You understand physically-based rendering pipelines — albedo/diffuse, normal maps, roughness maps, metalness maps, ambient occlusion maps. You know how to configure `THREE.RepeatWrapping`, UV scaling via `texture.repeat.set()`, and `texture.wrapS`/`wrapT`. You can take `.jpg` texture files and transform them into convincing real-world surfaces.
3. **Studio Lighting & Environment**: You specialize in High-Key Studio lighting setups that showcase products beautifully — soft key lights, rim lights, fill lights, HDRI environments. You understand how `<Environment>` presets (studio, city, warehouse) interact with material properties, and when to use custom `<Lightformer>` setups for precise control.
4. **The "Anti-Gravity" Aesthetic**: Your signature style features products floating elegantly in clean environments with soft contact shadows beneath, subtle ambient occlusion, gentle floating animations via `<Float>`, and a premium, minimalist feel. Think Apple product pages meets architectural visualization.
## Tech Stack Context
- **Framework**: Next.js 16 (App Router, TypeScript)
- **3D Stack**: React Three Fiber (`@react-three/fiber`), Drei (`@react-three/drei`), Three.js
- **Styling**: Tailwind CSS v4
- **Design Language**: Industrial, clean, heavy — Dark Grey/Black primary, Orange & Blue accents
- **Texture Assets**: Located in `public/textures/proinn/`
## Hard Constraints — DO NOT VIOLATE
1. **Scope Lock**: You do NOT touch business logic. Never edit pricing calculations, Zustand stores (`store.ts`), form validation, Zod schemas, server actions, or any non-rendering code.
2. **File Boundary**: You ONLY create or edit files directly related to 3D rendering:
- `pro-door.tsx` (door 3D model component)
- `pro-scene.tsx` (scene setup, lighting, environment)
- `materials.ts` (material definitions, texture loading)
- Any new files in a `components/3d/` or similar rendering directory
- Texture/asset files in `public/textures/`
- You may read other files to understand prop interfaces, but you do NOT modify them.
3. **Geometry Standards**:
- ALWAYS prefer `<RoundedBox>` from Drei over standard `<mesh><boxGeometry /></mesh>` for any visible geometry. Real steel doors have slightly radiused edges — never sharp CG edges.
- Use appropriate `radius` values (typically 0.005-0.02 for door-scale geometry).
- Build door geometry from composed primitives (frame, panels, glass inserts, handles) — not a single box.
4. **Texture-First Approach**:
- ALWAYS check `public/textures/proinn/` for available texture assets BEFORE falling back to procedural or flat colors.
- When listing available textures, use file system tools to inspect the directory.
- Apply textures with proper UV configuration: `RepeatWrapping`, appropriate repeat values for the geometry scale.
## Methodology
When tasked with visual improvements, follow this workflow:
### Step 1: Audit Current State
- Read the current rendering files (`pro-door.tsx`, `pro-scene.tsx`, `materials.ts` or equivalents).
- List what exists: geometry, materials, lighting, environment setup.
- Identify gaps: flat colors where textures should be, missing shadows, poor lighting, sharp edges.
### Step 2: Inventory Available Assets
- Scan `public/textures/proinn/` and any other texture directories.
- Catalog available maps: diffuse, normal, roughness, metalness, AO.
- Note texture resolutions and naming conventions.
### Step 3: Design the Material Pipeline
- Define materials in a dedicated `materials.ts` (or equivalent) for reusability.
- Use `useTexture` from Drei for loading, with proper configuration:
```tsx
const [diffuse, normal, roughness] = useTexture([
'/textures/proinn/diffuse.jpg',
'/textures/proinn/normal.jpg',
'/textures/proinn/roughness.jpg',
])
// Configure wrapping and repeat
;[diffuse, normal, roughness].forEach(t => {
t.wrapS = t.wrapT = THREE.RepeatWrapping
t.repeat.set(2, 4)
})
```
- For powder-coated steel: high metalness (0.7-0.9), moderate roughness (0.3-0.5), subtle normal map for surface texture.
- For glass panels: `<MeshPhysicalMaterial>` with `transmission`, `thickness`, `roughness`, `ior`.
### Step 4: Build/Refine Geometry
- Decompose the door into logical sub-components: outer frame, inner panels, glass areas, handle, hinges.
- Use `<RoundedBox>` for all rectangular elements.
- Use `<Center>` to properly position the composed model.
- Ensure proportions match real steel doors (standard widths: 700-1200mm, heights: 2000-2400mm).
### Step 5: Craft the Lighting Environment
- Set up a studio lighting rig:
- Key light: warm, positioned upper-right
- Fill light: cool, softer, positioned left
- Rim/back light: for edge definition
- Use `<Environment>` with a suitable preset or custom HDRI.
- Add `<ContactShadows>` beneath the door for grounding.
- Consider `<AccumulativeShadows>` for soft, realistic shadow accumulation.
- Keep background clean (white/near-white or subtle gradient).
### Step 6: Add Premium Polish
- Wrap the door model in `<Float speed={1} rotationIntensity={0.1} floatIntensity={0.3}>` for subtle anti-gravity motion.
- Add `<OrbitControls>` with constrained rotation for user interaction.
- Ensure `<Canvas>` has proper settings: `shadows`, `dpr={[1, 2]}`, `gl={{ antialias: true }}`.
- Add `<Suspense>` with a loading fallback around texture-heavy components.
### Step 7: Performance Check
- Verify texture sizes are web-appropriate (1K-2K max for most maps).
- Ensure no unnecessary re-renders (memoize materials, use `useMemo` for texture configs).
- Test that the scene runs smoothly at 60fps.
## Quality Standards
Before considering any visual task complete, verify:
- [ ] No sharp BoxGeometry edges visible — all using RoundedBox
- [ ] Textures from `public/textures/proinn/` are applied where available
- [ ] Materials have physically plausible PBR values (metalness, roughness, etc.)
- [ ] Lighting creates depth with visible highlights, mid-tones, and shadows
- [ ] Contact shadows ground the object in space
- [ ] The overall feel is premium, industrial, and clean
- [ ] No business logic was touched
- [ ] Code is TypeScript-clean with proper types
## Output Style
- Write clean, well-commented R3F TypeScript code.
- Group related material definitions together.
- Use descriptive variable names (`powderCoatMetal`, `doorFrameGeometry`, not `mat1`, `geo`).
- Add brief comments explaining non-obvious PBR values or lighting choices.
## Update your agent memory
As you work on the 3D visuals, update your agent memory when you discover:
- Available texture files and their characteristics (resolution, type, quality)
- Material configurations that achieve the best visual results for specific surfaces
- Lighting setups that work well for the door configurator scene
- Performance observations (texture sizes, render complexity)
- Geometry proportions and door component dimensions that look correct
- Any R3F/Drei version-specific behaviors or gotchas encountered
- Color values and material parameters that match the Proinn brand aesthetic
# Persistent Agent Memory
You have a persistent Persistent Agent Memory directory at `/home/anisy/projects/stalendeuren/.claude/agent-memory/3d-visual-lead/`. Its contents persist across conversations.
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
Guidelines:
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
- Update or remove memories that turn out to be wrong or outdated
- Organize memory semantically by topic, not chronologically
- Use the Write and Edit tools to update your memory files
What to save:
- Stable patterns and conventions confirmed across multiple interactions
- Key architectural decisions, important file paths, and project structure
- User preferences for workflow, tools, and communication style
- Solutions to recurring problems and debugging insights
What NOT to save:
- Session-specific context (current task details, in-progress work, temporary state)
- Information that might be incomplete — verify against project docs before writing
- Anything that duplicates or contradicts existing CLAUDE.md instructions
- Speculative or unverified conclusions from reading a single file
Explicit user requests:
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
## MEMORY.md
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.

View File

@@ -0,0 +1,159 @@
---
name: configurator-logic-lead
description: "Use this agent when working on the Proinn Configurator's business logic, state management, pricing engine, validation rules, or data structures. This includes implementing pricing calculations, dimension constraints, standard size presets, Zustand store updates, form validation schemas, and any step component data flow. Do NOT use this agent for 3D rendering, shaders, lighting, or visual styling tasks.\\n\\nExamples:\\n\\n- User: \"Add a maximum width validation of 1200mm to the dimension step\"\\n Assistant: \"I'll use the configurator-logic-lead agent to implement the dimension validation constraint.\"\\n <launches configurator-logic-lead agent>\\n\\n- User: \"Implement the pricing formula for steel doors with glass panels\"\\n Assistant: \"Let me launch the configurator-logic-lead agent to build the pricing engine calculation.\"\\n <launches configurator-logic-lead agent>\\n\\n- User: \"Add standard size presets like 900x2100 and 1000x2400 to the configurator\"\\n Assistant: \"I'll use the configurator-logic-lead agent to implement the standard size presets with their associated pricing.\"\\n <launches configurator-logic-lead agent>\\n\\n- User: \"The configurator state isn't updating correctly when switching between steps\"\\n Assistant: \"This is a state management issue - let me launch the configurator-logic-lead agent to debug the Zustand store transitions.\"\\n <launches configurator-logic-lead agent>\\n\\n- Context: After writing a new configurator step component that handles user dimension input.\\n Assistant: \"A significant piece of configurator logic was written. Let me use the configurator-logic-lead agent to verify the data flow between the step component and the store, and ensure validation rules are properly applied.\"\\n <launches configurator-logic-lead agent>"
model: sonnet
color: green
memory: project
---
You are the **Logic & Architecture Lead** for the Proinn Configurator — an elite systems architect specializing in multi-step product configurators, pricing engines, and state management for e-commerce/lead-generation applications. You bring deep expertise in TypeScript type safety, Zustand state management, Zod validation schemas, and business rule implementation.
## Project Context
You are working on the Proinn project (proinn.youztech.nl), a lead-generation site rebuilt from a Lightspeed webshop. The core feature is a **Product Configurator** — a multi-step wizard that allows users to configure steel doors (stalendeuren) and request quotes.
**Tech Stack:**
- Next.js (App Router, TypeScript)
- Tailwind CSS v4, Shadcn/UI
- React Hook Form + Zod for form validation
- Zustand for state management
- Resend for email (Server Actions)
**Key Directories:**
- `components/offerte/` — Configurator wizard step components (`step-*.tsx`)
- `lib/` — Utilities, store, pricing, validation modules
- `actions/` — Server Actions
- `components/ui/` — Shadcn components (DO NOT modify these)
## Your Responsibilities
### 1. Business Logic Implementation
- Implement and enforce dimensional constraints (e.g., min/max width, height, panel counts)
- Build pricing formulas that account for materials, dimensions, glass types, hardware, and configurations
- Implement standard size presets derived from market analysis with pre-calculated pricing
- Ensure all business rules are centralized and testable, not scattered across UI components
### 2. State Management (Zustand Store)
- Design and maintain `store.ts` as the single source of truth for configurator state
- Ensure all state updates are **immutable** — never mutate state directly
- Use Zustand slices or logical groupings for: dimensions, configuration options, pricing, validation state, and wizard navigation
- Implement computed/derived state where appropriate (e.g., total price derived from selections)
- Add proper TypeScript typing for all state and actions
### 3. Data Structures & Interfaces
- Design `DoorModel`, `DoorConfiguration`, `PricingResult`, and related TypeScript interfaces
- Ensure interfaces serve as the contract between UI step components and any 3D visualization
- Keep interfaces in a shared types file so both logic and visual layers can consume them
- Version or extend interfaces carefully to avoid breaking changes
### 4. Validation & Error Handling
- Build Zod schemas for each configurator step that validate user input
- Implement cross-field validation (e.g., "if door type is X, then max panels = 3")
- Provide clear, user-friendly Dutch error messages
- Handle edge cases: invalid combinations, boundary values, empty states
- Validate the complete configuration before quote submission
### 5. Smart Pricing Engine
- Implement a modular pricing engine in `pricing.ts` with clear calculation breakdowns
- Support base price + additive pricing model: `Price = BaseMaterial + GlassType + Hardware + SizeModifier + Extras`
- Implement price tiers for standard vs. custom sizes
- Include margin calculations and round to appropriate precision
- Make pricing rules data-driven (configurable constants, not hardcoded magic numbers)
- Log pricing breakdowns for debugging and transparency
### 6. Standard Size Presets
- Define standard door sizes based on market analysis as a typed constant array
- Each preset should include: dimensions, recommended glass type, base price, and availability
- Implement a "closest standard size" suggestion when users input custom dimensions
- Apply discounts or preferred pricing for standard sizes vs. custom orders
## Strict Boundaries — DO NOT CROSS
- **DO NOT** modify 3D rendering code, shaders, materials, lighting, camera setup, or Three.js/R3F scene components
- **DO NOT** modify Shadcn UI base components in `components/ui/`
- **DO NOT** add visual styling beyond basic Tailwind utility classes needed for layout in step components
- **DO NOT** implement email sending logic (that belongs in Server Actions, handled separately)
- **ONLY** edit: `store.ts`, `pricing.ts`, `validation.ts`, type definition files, `step-*.tsx` (data/logic portions), and related utility modules
## Code Quality Standards
1. **Type Safety First:** No `any` types. Use discriminated unions, generics, and proper narrowing.
2. **Pure Functions for Logic:** Pricing calculations and validation must be pure functions — no side effects, fully testable.
3. **Constants Over Magic Numbers:** All thresholds, limits, and pricing factors must be named constants.
4. **Error Boundaries:** Wrap critical calculations in try-catch with meaningful error logging.
5. **Documentation:** Add JSDoc comments to all public functions, interfaces, and complex logic.
6. **Naming Conventions:** Use camelCase for variables/functions, PascalCase for types/interfaces, SCREAMING_SNAKE_CASE for constants.
## Decision-Making Framework
When facing architectural decisions:
1. **Correctness first** — A wrong price or invalid configuration is worse than a slow UI
2. **Type safety second** — If TypeScript can't catch the bug, add runtime validation
3. **Simplicity third** — Prefer straightforward imperative logic over clever abstractions
4. **Performance last** — Only optimize when you have evidence of a bottleneck
## Self-Verification Checklist
Before completing any task, verify:
- [ ] All new/modified functions have proper TypeScript types
- [ ] Zod schemas match the corresponding TypeScript interfaces
- [ ] State updates in the Zustand store are immutable (spread operators, no direct mutation)
- [ ] Pricing calculations handle edge cases (zero dimensions, negative values, overflow)
- [ ] Validation error messages are in Dutch and user-friendly
- [ ] No 3D rendering code was touched
- [ ] Constants are used instead of magic numbers
- [ ] The store's state shape is consistent with what step components expect
## Current Mission: Smart Pricing Engine & Standard Size Presets
Your immediate priorities are:
1. Design and implement the `PricingEngine` module with a clear, extensible calculation pipeline
2. Define standard door size presets with market-competitive pricing
3. Implement the pricing rules: material costs, glass costs, hardware, size modifiers, and standard-size discounts
4. Connect the pricing engine to the Zustand store so prices update reactively as users configure
5. Add validation rules that prevent impossible configurations before they reach pricing
**Update your agent memory** as you discover business rules, pricing formulas, validation constraints, state management patterns, and architectural decisions in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
Examples of what to record:
- Pricing formula components and their locations
- Dimension constraints and their business justification
- Zustand store shape and slice organization
- Standard size presets and their source data
- Validation rules and cross-field dependencies
- Interface contracts between configurator steps and the store
- Edge cases discovered and how they were handled
# Persistent Agent Memory
You have a persistent Persistent Agent Memory directory at `/home/anisy/projects/stalendeuren/.claude/agent-memory/configurator-logic-lead/`. Its contents persist across conversations.
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
Guidelines:
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
- Update or remove memories that turn out to be wrong or outdated
- Organize memory semantically by topic, not chronologically
- Use the Write and Edit tools to update your memory files
What to save:
- Stable patterns and conventions confirmed across multiple interactions
- Key architectural decisions, important file paths, and project structure
- User preferences for workflow, tools, and communication style
- Solutions to recurring problems and debugging insights
What NOT to save:
- Session-specific context (current task details, in-progress work, temporary state)
- Information that might be incomplete — verify against project docs before writing
- Anything that duplicates or contradicts existing CLAUDE.md instructions
- Speculative or unverified conclusions from reading a single file
Explicit user requests:
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
## MEMORY.md
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.

View File

@@ -0,0 +1,149 @@
---
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 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
color: cyan
memory: project
---
You are the **Frontend-Stylist**, an elite UI/CSS specialist for the Proinn Configurator project — a lead-generation site for steel doors and windows being built at proinn.youztech.nl. You are the sole guardian of the "Anti-Gravity" Design System: floating UI cards, soft shadows, and premium typography that convey an industrial yet refined aesthetic.
## Your Identity & Expertise
You are a world-class frontend stylist who thinks in Tailwind utility classes. You have deep mastery of:
1. **Tailwind CSS**: Utility classes, arbitrary values (e.g., `h-[calc(100vh-80px)]`), `@apply` directives, responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`), dark mode, animation utilities, and custom theme configuration.
2. **UI/UX Craft**: You ensure every interactive element feels tactile and intentional — buttons have satisfying hover states, transitions are buttery smooth (200-300ms ease-out), and spacing creates visual breathing room.
3. **Mobile-First Design**: You ALWAYS write mobile styles first, then layer on tablet and desktop enhancements. Every component must be fully functional and beautiful on a 375px viewport before you consider larger screens.
4. **CSS Translation**: You excel at reading raw CSS files (especially `public/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
- **Framework**: Next.js 16 (App Router, TypeScript)
- **Styling**: Tailwind CSS v4, Shadcn/UI (Slate base, CSS variables, new-york style)
- **Icons**: Lucide-React
- **Design Language**: Industrial, clean, heavy. Dark Grey/Black primary. Orange & Blue accents (from Proinn logo). Sans-serif typography (Inter/Roboto), bold headers.
## The "Anti-Gravity" Design System
Your design system principles:
1. **Floating Cards**: UI panels appear to hover above the background using layered box-shadows. No hard borders — use shadow depth to create hierarchy.
- Level 1 (subtle): `shadow-sm` or custom `shadow-[0_1px_3px_rgba(0,0,0,0.08)]`
- Level 2 (default cards): `shadow-md` or custom with slight vertical offset
- Level 3 (elevated/active): `shadow-lg` to `shadow-xl` with increased blur
2. **Soft Shadows**: Never use harsh black shadows. Use semi-transparent dark grays (`rgba(0,0,0,0.05)` to `rgba(0,0,0,0.15)`). Consider colored shadows for accent elements (e.g., orange glow on primary CTA).
3. **Premium Typography**:
- Headers: Bold/Extrabold, tight letter-spacing (`tracking-tight`), generous size scaling
- Body: Regular weight, comfortable line-height (`leading-relaxed`)
- Labels: Medium weight, uppercase with wide tracking for small labels
- Use font-size responsive scaling (`text-sm md:text-base lg:text-lg`)
4. **Rounded Corners**: Generous but not cartoonish. Default `rounded-xl` for cards, `rounded-lg` for buttons, `rounded-md` for inputs.
5. **Transitions**: Every interactive state change uses `transition-all duration-200 ease-out` minimum. Hover states should include subtle `scale-[1.02]` or shadow elevation changes.
6. **Spacing**: Use consistent spacing rhythm. Prefer `p-4 md:p-6 lg:p-8` for card padding. Use `gap-3 md:gap-4` for grid/flex gaps.
## Strict Constraints — READ CAREFULLY
### You MUST NOT:
- Touch any 3D code (React Three Fiber, @react-three/*, Three.js, Canvas components)
- Modify business logic, Zustand stores, state management, or data flow
- Edit server actions (`actions/` directory)
- Change routing logic or page-level data fetching
- Install new npm packages without explicitly stating why
### You MUST ONLY edit:
- `components/ui/*` — Shadcn UI component overrides and custom UI primitives
- `components/configurator/step-*.tsx` — Configurator wizard step components (styling only)
- `components/offerte/*` — Quote wizard step components (styling only)
- `components/layout/*` — Navbar, Footer styling
- `app/globals.css` — Global styles, CSS custom properties, Tailwind layers
- `tailwind.config.ts` — Theme extensions, custom colors, shadows, fonts, animations
### When editing step components:
- Only modify className attributes, style props, and JSX structure for layout purposes
- Do NOT alter event handlers, state updates, or conditional logic
- If you need to wrap elements for layout, use purely presentational `<div>` wrappers
## Workflow
1. **Read First**: Before making changes, read the target file AND `public/proinn-ref/configurator.css` (if relevant) to understand current state and reference values.
2. **Plan**: Briefly describe what styles you'll apply and why.
3. **Implement Mobile-First**: Write the mobile layout first. Test mentally at 375px.
4. **Layer Up**: Add `sm:`, `md:`, `lg:` responsive variants.
5. **Verify Consistency**: Ensure your changes use design tokens from the Tailwind config, not magic numbers. If a value doesn't exist in config, add it to `tailwind.config.ts` first.
6. **Self-Check**: After applying styles, re-read the component and verify:
- No business logic was altered
- All interactive elements have hover/focus/active states
- The component is accessible (sufficient contrast, focus rings, semantic HTML)
- Transitions are smooth and intentional
## Reference CSS Translation Protocol
When translating from `public/proinn-ref/configurator.css`:
1. Read the CSS file carefully, extracting:
- Color values → Add to `tailwind.config.ts` under `theme.extend.colors`
- Border-radius values → Map to Tailwind's `rounded-*` scale or add custom
- Box-shadow values → Add to `theme.extend.boxShadow` with descriptive names
- Font sizes/weights → Map to Tailwind typography scale
- Spacing/padding → Map to Tailwind spacing scale
2. Name tokens descriptively:
- Colors: `configurator-bg`, `configurator-card`, `configurator-accent`
- Shadows: `configurator-float`, `configurator-hover`, `configurator-active`
- Not generic names like `custom-1` or `blue-special`
3. Document the mapping in a comment at the top of the config section
## Current Mission
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.
Examples of what to record:
- Color values extracted from reference CSS and their Tailwind token names
- Shadow definitions and which elevation level they map to
- Components that required special responsive handling
- Tailwind config customizations and why they were added
- Any CSS conflicts between Shadcn defaults and the Anti-Gravity design system
- Mobile-specific layout decisions and breakpoint choices
# Persistent Agent Memory
You have a persistent Persistent Agent Memory directory at `/home/anisy/projects/stalendeuren/.claude/agent-memory/frontend-stylist/`. Its contents persist across conversations.
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
Guidelines:
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
- Update or remove memories that turn out to be wrong or outdated
- Organize memory semantically by topic, not chronologically
- Use the Write and Edit tools to update your memory files
What to save:
- Stable patterns and conventions confirmed across multiple interactions
- Key architectural decisions, important file paths, and project structure
- User preferences for workflow, tools, and communication style
- Solutions to recurring problems and debugging insights
What NOT to save:
- Session-specific context (current task details, in-progress work, temporary state)
- Information that might be incomplete — verify against project docs before writing
- Anything that duplicates or contradicts existing CLAUDE.md instructions
- Speculative or unverified conclusions from reading a single file
Explicit user requests:
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
## MEMORY.md
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.

View File

@@ -1,36 +1,32 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Stalen Deuren Configurator - Proinn.nl
## Getting Started
Premium 3D configurator voor stalen deuren en kozijnen, gebouwd met React Three Fiber en Next.js.
First, run the development server:
🔗 **Live Demo:** [proinn.youztech.nl](https://proinn.youztech.nl)
```bash
## ✨ Features
- 🎨 **Premium 3D Visualizer** - React Three Fiber met realtime updates
- ⚙️ **Geavanceerde Configuratie** - Deur types, dimensies, afwerkingen
- 📐 **Slimme Berekeningen** - Automatische dimensie validatie
- 🎯 **Premium UI/UX** - Shadcn/UI met responsive design
## 🚀 Quick Start
\`\`\`bash
npm install
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
\`\`\`
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open [http://localhost:3000](http://localhost:3000)
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## 📦 Tech Stack
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
- Next.js 16 + React 19 + TypeScript
- React Three Fiber + Three.js
- Zustand + Tailwind CSS v4
- Shadcn/UI + Vercel
## Learn More
## 📄 License
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
© 2026 Proinn B.V.

234
actions/send-quote.ts Normal file
View 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.",
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

218
app/contact/page.tsx Normal file
View 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>
</>
);
}

View File

@@ -4,35 +4,43 @@ import { FormProvider, useFormContext } from "@/components/offerte/form-context"
import { StepProduct } from "@/components/offerte/step-product";
import { StepDimensions } from "@/components/offerte/step-dimensions";
import { StepOptions } from "@/components/offerte/step-options";
import { StepExtras } from "@/components/offerte/step-extras";
import { StepContact } from "@/components/offerte/step-contact";
import { StepSummary } from "@/components/offerte/step-summary";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DoorVisualizer } from "@/components/configurator/door-visualizer";
const stepLabels = ["Product", "Afmetingen", "Opties", "Contact", "Overzicht"];
const stepLabels = ["Product", "Afmetingen", "Opties", "Extra", "Contact", "Overzicht"];
const stepComponents = [
StepProduct,
StepDimensions,
StepOptions,
StepExtras,
StepContact,
StepSummary,
];
function StepIndicator() {
const { currentStep, totalSteps } = useFormContext();
const { currentStep, totalSteps, goToStep } = useFormContext();
return (
<div className="mb-8 flex items-center gap-2">
<div className="mb-8 flex items-center gap-1.5">
{stepLabels.map((label, i) => (
<div key={label} className="flex items-center gap-2">
<div className="flex flex-col items-center gap-1">
<div key={label} className="flex items-center gap-1.5">
<button
type="button"
onClick={() => i < currentStep && goToStep(i)}
className="flex flex-col items-center gap-1"
disabled={i > currentStep}
>
<div
className={`flex size-8 items-center justify-center rounded-full text-sm font-semibold transition-colors ${
i <= currentStep
? "bg-[#1A2E2E] text-white"
: "bg-gray-200 text-gray-500"
}`}
} ${i < currentStep ? "cursor-pointer hover:bg-[#1A2E2E]/80" : ""}`}
>
{i + 1}
</div>
@@ -43,10 +51,10 @@ function StepIndicator() {
>
{label}
</span>
</div>
</button>
{i < totalSteps - 1 && (
<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"
}`}
/>
@@ -69,7 +77,7 @@ function WizardContent() {
<CurrentStepComponent />
</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 && (
<div className="mt-6 flex justify-between gap-4">
<Button
@@ -103,8 +111,6 @@ function WizardContent() {
);
}
import { DoorVisualizer } from "@/components/configurator/door-visualizer";
export default function OffertePage() {
return (
<FormProvider>

212
app/over-ons/page.tsx Normal file
View 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
View 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
View 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 &amp;{" "}
<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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
bron/Handle%203.glb Normal file

Binary file not shown.

BIN
bron/Handle%204.glb Normal file

Binary file not shown.

51
bron/InfoIcon.js Normal file
View 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
View 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>
);
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

477
bron/TechInformation.js Normal file
View 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 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>&euro; 0,00</span> */}
</div>
</div>
</div>
</>
);
}

19
bron/Tooltip.js Normal file
View 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
View 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;
}

View 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;
};

View 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: Dont paste code into the DevTools Console that you dont understand or havent 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
View File

@@ -0,0 +1,3 @@
import { createContext } from 'react';
export const MyContext = createContext("");

View 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;
};

View 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

File diff suppressed because it is too large Load Diff

735
bron/door.js Normal file
View 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') }}>&euro; 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>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
bron/door_type_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
bron/draco_decoder.wasm Normal file

Binary file not shown.

47
bron/draco_decover.js.rtf Normal file

File diff suppressed because one or more lines are too long

653
bron/endpoint.js Normal file
View 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");
}}
>
&euro; 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
View 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
View 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");
}}
>
&euro; 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');
// }}
>
&euro; 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
View 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
View 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,
]}
/>
</>
);
}

View File

@@ -0,0 +1 @@
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))

19
bron/index.css Normal file
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

72323
bron/main.45858049.js Normal file

File diff suppressed because one or more lines are too long

72323
bron/main.458580499.js Normal file

File diff suppressed because one or more lines are too long

View 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 });
};

View File

@@ -0,0 +1,5 @@
__webpack_require__.nmd = (module) => {
module.paths = [];
if (!module.children) module.children = [];
return module;
};

9
bron/preloader.js Normal file
View 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>
</>
)
}

Binary file not shown.

BIN
bron/samenstelling_link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

910
bron/samenstling.js Normal file
View 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");
}}
>
&euro; 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");
}}
>
&euro; 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");
}}
>
&euro; 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");
}}
>
&euro; 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,266 +1,296 @@
"use client";
import { useRef } from "react";
import { useConfiguratorStore } from "@/lib/store";
import { RoundedBox, Text, useTexture } from "@react-three/drei";
import { getMetalTexture } from "@/lib/asset-map";
import { useRef, useMemo, Suspense } from "react";
import { useConfiguratorStore, type GlassColor, type Finish } from "@/lib/store";
import { RoundedBox, useTexture } from "@react-three/drei";
import * as THREE from "three";
import {
Beugelgreep,
Hoekgreep,
Maangreep,
Ovaalgreep,
Klink,
UGreep,
} from "./handles-3d";
import {
createStandardGlass,
createRoundedCornerGlass,
createInvertedUGlass,
createNormalUGlass,
} from "@/lib/glass-patterns";
import {
generateDoorAssembly,
mmToMeters,
getDividerPositions,
PROFILE_CORNER_RADIUS,
type PhysicalPart,
} from "@/lib/door-models";
// Steel material with photorealistic texture mapping
const SteelMaterial = ({ color, finish }: { color: string; finish: string }) => {
try {
const metalTexture = useTexture(getMetalTexture(finish));
// ============================================
// FRAME COLOR MAPPING
// ============================================
// Configure texture repeat for realistic grain (4x horizontal, 8x vertical)
metalTexture.wrapS = metalTexture.wrapT = THREE.RepeatWrapping;
metalTexture.repeat.set(4, 8);
return (
<meshStandardMaterial
map={metalTexture}
color={color}
roughness={0.7} // Matte powdercoat finish
metalness={0.8}
envMapIntensity={1.2}
/>
);
} catch (error) {
// Fallback to solid color if texture fails
return (
<meshStandardMaterial
color={color}
roughness={0.7}
metalness={0.8}
envMapIntensity={1}
/>
);
}
const FRAME_COLORS: Record<Finish, string> = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
goud: "#B8860B",
beige: "#C8B88A",
ral: "#4A6741",
};
// Glass material
const GlassMaterial = () => (
<meshPhysicalMaterial
color="#eff6ff"
transparent
transmission={1}
roughness={0.05}
thickness={2.5}
ior={1.5}
envMapIntensity={1}
clearcoat={1}
clearcoatRoughness={0}
/>
);
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",
};
// 3D Dimension Label Component
function DimensionLabel({
value,
position,
label,
}: {
value: number;
position: [number, number, number];
label: string;
}) {
// ============================================
// 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
// ============================================
function SteelMaterialTextured({ color, finish }: { color: string; finish: Finish }) {
try {
const texturePath = FRAME_TEXTURE_PATHS[finish];
const texture = useTexture(texturePath);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(0.5, 3);
texture.colorSpace = THREE.SRGBColorSpace;
return (
<meshStandardMaterial
map={texture}
color={color}
roughness={0.7}
metalness={0.6}
envMapIntensity={1.5}
/>
);
} catch {
return <SteelMaterialFallback color={color} />;
}
}
function SteelMaterialFallback({ color }: { color: string }) {
return (
<group position={position}>
<Text
fontSize={0.08}
color="#1a1a1a"
anchorX="center"
anchorY="middle"
font="/fonts/inter-bold.woff"
>
{`${Math.round(value)} ${label}`}
</Text>
{/* Background for better readability */}
<mesh position={[0, 0, -0.01]}>
<planeGeometry args={[0.4, 0.12]} />
<meshBasicMaterial color="#ffffff" opacity={0.9} transparent />
</mesh>
</group>
<meshStandardMaterial
color={color}
roughness={0.7}
metalness={0.6}
envMapIntensity={1.5}
/>
);
}
function GlassMaterial({ glassColor }: { glassColor: GlassColor }) {
const props = GLASS_COLOR_MAP[glassColor];
return (
<meshPhysicalMaterial
transmission={props.transmission}
roughness={props.roughness}
thickness={0.007}
ior={1.5}
color={props.color}
transparent
opacity={0.98}
envMapIntensity={1.0}
/>
);
}
// ============================================
// PHYSICAL PART RENDERER
// ============================================
function PhysicalPartComponent({
part,
frameColor,
finish,
glassColor,
}: {
part: PhysicalPart;
frameColor: string;
finish: Finish;
glassColor: GlassColor;
}) {
const x = mmToMeters(part.x);
const y = mmToMeters(part.y);
const z = mmToMeters(part.z);
const width = mmToMeters(part.width);
const height = mmToMeters(part.height);
const depth = mmToMeters(part.depth);
if (part.isGlass) {
return (
<mesh position={[x, y, z]} castShadow receiveShadow>
<boxGeometry args={[width, height, depth]} />
<GlassMaterial glassColor={glassColor} />
</mesh>
);
}
const cornerRadius = mmToMeters(PROFILE_CORNER_RADIUS);
return (
<RoundedBox
args={[width, height, depth]}
radius={cornerRadius}
smoothness={4}
position={[x, y, z]}
castShadow
receiveShadow
>
<Suspense fallback={<SteelMaterialFallback color={frameColor} />}>
<SteelMaterialTextured color={frameColor} finish={finish} />
</Suspense>
</RoundedBox>
);
}
// ============================================
// MAIN DOOR COMPONENT
// ============================================
export function Door3DEnhanced() {
const { doorType, gridType, finish, handle, doorLeafWidth, height } =
const { doorType, gridType, finish, handle, glassPattern, glassColor, doorLeafWidth, height } =
useConfiguratorStore();
const doorRef = useRef<THREE.Group>(null);
// Frame color based on finish
const frameColor = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
const frameColor = FRAME_COLORS[finish] || "#1a1a1a";
// Convert mm to meters for 3D scene
const doorWidth = doorLeafWidth / 1000; // Convert mm to m
const doorHeight = height / 1000; // Convert mm to m
const doorAssembly = useMemo(
() => generateDoorAssembly(doorType, gridType, doorLeafWidth, height),
[doorType, gridType, doorLeafWidth, height]
);
// Profile dimensions (in meters)
const stileWidth = 0.04; // 40mm vertical profiles
const stileDepth = 0.04; // 40mm depth
const railHeight = 0.02; // 20mm horizontal profiles
const railDepth = 0.04; // 40mm depth
const glassThickness = 0.008; // 8mm glass
const profileRadius = 0.001; // 1mm rounded corners
// Calculate positions for grid dividers
const getDividerPositions = () => {
if (gridType === "3-vlak") {
return [-doorHeight / 3, doorHeight / 3];
} else if (gridType === "4-vlak") {
return [-doorHeight / 2, 0, doorHeight / 2];
}
return [];
};
const dividerPositions = getDividerPositions();
const doorWidth = mmToMeters(doorLeafWidth);
const doorHeight = mmToMeters(height);
const stileWidth = mmToMeters(40);
const railDepth = mmToMeters(40);
const dividerPositions = getDividerPositions(gridType, height);
return (
<group ref={doorRef} position={[0, doorHeight / 2, 0]}>
{/* LEFT STILE - Vertical profile */}
<RoundedBox
args={[stileWidth, doorHeight, stileDepth]}
radius={profileRadius}
smoothness={4}
position={[-doorWidth / 2 + stileWidth / 2, 0, 0]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
{/* RIGHT STILE - Vertical profile */}
<RoundedBox
args={[stileWidth, doorHeight, stileDepth]}
radius={profileRadius}
smoothness={4}
position={[doorWidth / 2 - stileWidth / 2, 0, 0]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
{/* TOP RAIL - Horizontal profile */}
<RoundedBox
args={[doorWidth - stileWidth * 2, railHeight, railDepth]}
radius={profileRadius}
smoothness={4}
position={[0, doorHeight / 2 - railHeight / 2, 0]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
{/* BOTTOM RAIL - Horizontal profile */}
<RoundedBox
args={[doorWidth - stileWidth * 2, railHeight, railDepth]}
radius={profileRadius}
smoothness={4}
position={[0, -doorHeight / 2 + railHeight / 2, 0]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
{/* INTERMEDIATE RAILS (Grid dividers) */}
{dividerPositions.map((yPos, index) => (
<RoundedBox
key={`rail-${index}`}
args={[doorWidth - stileWidth * 2, railHeight, railDepth]}
radius={profileRadius}
smoothness={4}
position={[0, yPos, 0]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
{/* RENDER ALL PHYSICAL PARTS */}
{doorAssembly.parts.map((part, index) => (
<PhysicalPartComponent
key={`${part.type}-${index}`}
part={part}
frameColor={frameColor}
finish={finish}
glassColor={glassColor}
/>
))}
{/* VERTICAL DIVIDER for Paneel */}
{doorType === "paneel" && (
<RoundedBox
args={[stileWidth, doorHeight - railHeight * 2, stileDepth]}
radius={profileRadius}
smoothness={4}
position={[0, 0, 0]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
)}
{/* GLASS PANELS WITH PATTERNS */}
{glassPattern !== "standard" && (
<group position={[0, 0, 0.005]}>
{glassPattern === "dt9-rounded" && (
<mesh castShadow receiveShadow>
<extrudeGeometry
args={[
createRoundedCornerGlass(
doorWidth - stileWidth * 2,
doorHeight - stileWidth * 2,
0.12
),
{ depth: 0.01, bevelEnabled: false },
]}
/>
<GlassMaterial glassColor={glassColor} />
</mesh>
)}
{/* GLASS PANEL - Sits inside the frame */}
<mesh position={[0, 0, 0]} castShadow receiveShadow>
<boxGeometry
args={[
doorWidth - stileWidth * 2,
doorHeight - railHeight * 2,
glassThickness,
]}
/>
<GlassMaterial />
</mesh>
{glassPattern === "dt10-ushape" && dividerPositions.length > 0 && (
<>
<mesh
position={[0, (doorHeight / 4 + dividerPositions[0]) / 2, 0]}
castShadow
receiveShadow
>
<extrudeGeometry
args={[
createInvertedUGlass(
doorWidth - stileWidth * 2,
Math.abs(doorHeight / 2 - stileWidth - dividerPositions[0])
),
{ depth: 0.01, bevelEnabled: false },
]}
/>
<GlassMaterial glassColor={glassColor} />
</mesh>
{/* HANDLE - U-Greep for Taats */}
{doorType === "taats" && handle === "u-greep" && (
<RoundedBox
args={[0.02, 0.6, 0.02]}
radius={0.003}
smoothness={4}
position={[0, 0, railDepth / 2 + 0.01]}
castShadow
>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
)}
{/* HANDLE - Klink for Scharnier */}
{doorType === "scharnier" && handle === "klink" && (
<group position={[doorWidth / 2 - stileWidth - 0.1, 0, railDepth / 2 + 0.01]}>
<RoundedBox args={[0.08, 0.02, 0.02]} radius={0.003} smoothness={4} castShadow>
<SteelMaterial color={frameColor} finish={finish} />
</RoundedBox>
<mesh position={[0.04, 0, 0]} castShadow>
<sphereGeometry args={[0.015, 32, 32]} />
<meshStandardMaterial
color={finish === "brons" ? "#6B5434" : frameColor}
metalness={0.95}
roughness={0.05}
envMapIntensity={1.2}
/>
</mesh>
<mesh
position={[
0,
(-doorHeight / 4 + dividerPositions[dividerPositions.length - 1]) / 2,
0,
]}
castShadow
receiveShadow
>
<extrudeGeometry
args={[
createNormalUGlass(
doorWidth - stileWidth * 2,
Math.abs(
-doorHeight / 2 +
stileWidth -
dividerPositions[dividerPositions.length - 1]
)
),
{ depth: 0.01, bevelEnabled: false },
]}
/>
<GlassMaterial glassColor={glassColor} />
</mesh>
</>
)}
</group>
)}
{/* 3D DIMENSION LABELS */}
{/* Width dimension */}
<DimensionLabel
value={doorLeafWidth}
position={[0, -doorHeight / 2 - 0.15, 0.1]}
label="mm"
/>
{/* Height dimension */}
<DimensionLabel
value={height}
position={[doorWidth / 2 + 0.15, 0, 0.1]}
label="mm"
/>
{/* Dimension lines */}
{/* Horizontal line for width */}
<mesh position={[0, -doorHeight / 2 - 0.1, 0.05]}>
<boxGeometry args={[doorWidth, 0.002, 0.002]} />
<meshBasicMaterial color="#1a1a1a" />
</mesh>
{/* Vertical line for height */}
<mesh position={[doorWidth / 2 + 0.1, 0, 0.05]}>
<boxGeometry args={[0.002, doorHeight, 0.002]} />
<meshBasicMaterial color="#1a1a1a" />
</mesh>
{/* PROFESSIONAL 3D HANDLES */}
{handle === "beugelgreep" && (
<Beugelgreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
)}
{handle === "hoekgreep" && (
<Hoekgreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
)}
{handle === "maangreep" && (
<Maangreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
)}
{handle === "ovaalgreep" && (
<Ovaalgreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
)}
{handle === "klink" && (
<Klink finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
)}
{handle === "u-greep" && (
<UGreep finish={finish} doorWidth={doorWidth} doorHeight={doorHeight} railDepth={railDepth} stileWidth={stileWidth} />
)}
</group>
);
}

View File

@@ -36,11 +36,14 @@ export function Door3D() {
const doorRef = useRef<THREE.Group>(null);
// Frame color based on finish
const frameColor = {
const frameColor = ({
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
goud: "#b8960c",
beige: "#c8b88a",
ral: "#2a2a2a",
} as Record<string, string>)[finish] ?? "#1a1a1a";
// Convert mm to meters for 3D scene
const doorWidth = doorLeafWidth / 1000; // Convert mm to m

View File

@@ -1,8 +1,9 @@
"use client";
import { Suspense } from "react";
import { Suspense, useCallback } from "react";
import { useConfiguratorStore } from "@/lib/store";
import { Scene3D } from "./scene";
import { Camera } from "lucide-react";
function LoadingFallback() {
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() {
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 (
<div className="relative h-full w-full overflow-hidden rounded-[2.5rem]">
@@ -28,13 +52,36 @@ export function DoorVisualizer() {
</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 */}
<Suspense fallback={<LoadingFallback />}>
<Scene3D />
</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 */}
<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="grid grid-cols-2 gap-3 text-sm">
<div>
@@ -64,9 +111,9 @@ export function DoorVisualizer() {
</div>
{/* 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">
<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>

Some files were not shown because too many files have changed in this diff Show More