Add premium configurator with split-screen layout

- Redesigned configurator page with split-screen interface
- Left: Large visual preview with sticky positioning
- Right: Premium white controls container with form steps
- Added complete configurator wizard (5 steps)
- Updated hero CTA to "Zelf ontwerpen"
- Configured Shadcn UI with Slate theme
- Added layout components (Navbar, Footer)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ubuntu
2026-02-10 15:59:37 +00:00
parent c283d7193a
commit 9cf5cea3ba
55 changed files with 8411 additions and 99 deletions

View File

@@ -1,26 +1,136 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
:root {
--background: #ffffff;
--foreground: #171717;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-brand-orange: var(--brand-orange);
--color-brand-blue: var(--brand-blue);
--color-brand-pistachio: var(--brand-pistachio);
--color-topbar-bg: var(--topbar-bg);
--color-menu-footer-bg: var(--menu-footer-bg);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
:root {
--radius: 0.625rem;
--brand-orange: #F97316;
--brand-blue: #0ea5e9;
--brand-pistachio: #C4D668;
--topbar-bg: #E8E8E6;
--menu-footer-bg: #2F3B3B;
--background: oklch(0.985 0.002 247);
--foreground: oklch(0.1 0.02 265);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1 0.02 265);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1 0.02 265);
--primary: oklch(0.145 0.02 265);
--primary-foreground: oklch(0.985 0.003 247);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.023 264.364);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.554 0.023 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.554 0.023 264.364);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,20 +1,18 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import "./globals.css";
const geistSans = Geist({
const inter = Inter({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "PROINN | Stalen Deuren & Maatwerk",
description:
"Industriële stalen deuren, kozijnen en maatwerk van PROINN. Vraag direct een offerte aan via onze configurator.",
};
export default function RootLayout({
@@ -23,11 +21,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang="nl">
<body className={`${inter.variable} font-sans antialiased`}>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);

149
app/offerte/page.tsx Normal file
View File

@@ -0,0 +1,149 @@
"use client";
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 { 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";
const stepLabels = ["Product", "Afmetingen", "Opties", "Contact", "Overzicht"];
const stepComponents = [
StepProduct,
StepDimensions,
StepOptions,
StepContact,
StepSummary,
];
function StepIndicator() {
const { currentStep, totalSteps } = useFormContext();
return (
<div className="mb-8 flex items-center gap-2">
{stepLabels.map((label, i) => (
<div key={label} className="flex items-center gap-2">
<div className="flex flex-col items-center gap-1">
<div
className={`flex size-8 items-center justify-center rounded-full text-sm font-semibold transition-colors ${
i <= currentStep
? "bg-brand-orange text-white"
: "bg-muted text-muted-foreground"
}`}
>
{i + 1}
</div>
<span
className={`hidden text-xs font-medium lg:inline ${
i === currentStep ? "text-foreground" : "text-muted-foreground"
}`}
>
{label}
</span>
</div>
{i < totalSteps - 1 && (
<div
className={`h-px w-4 transition-colors lg:w-6 ${
i < currentStep ? "bg-brand-orange" : "bg-border"
}`}
/>
)}
</div>
))}
</div>
);
}
function WizardContent() {
const { currentStep, nextStep, prevStep, totalSteps } = useFormContext();
const CurrentStepComponent = stepComponents[currentStep];
const isLastStep = currentStep === totalSteps - 1;
const isFirstStep = currentStep === 0;
return (
<div>
<div className="rounded-2xl border border-border/50 bg-card p-6">
<CurrentStepComponent />
</div>
{/* 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 variant="outline" onClick={prevStep} className="flex-1 sm:flex-none">
<ChevronLeft className="size-4" />
Vorige
</Button>
<Button
onClick={nextStep}
className="flex-1 bg-brand-orange text-white hover:bg-brand-orange/90 sm:flex-none"
>
Volgende
<ChevronRight className="size-4" />
</Button>
</div>
)}
{/* Summary: only show back button */}
{isLastStep && (
<div className="mt-6">
<Button variant="outline" onClick={prevStep} className="w-full sm:w-auto">
<ChevronLeft className="size-4" />
Terug naar Contact
</Button>
</div>
)}
</div>
);
}
export default function OffertePage() {
return (
<FormProvider>
{/* Mobile Header */}
<div className="px-4 py-6 lg:hidden">
<h1 className="mb-1 text-2xl font-bold tracking-tight">
Offerte Aanvragen
</h1>
<p className="text-sm text-muted-foreground">
Configureer uw product in een paar stappen.
</p>
</div>
{/* Split Screen Layout */}
<div className="grid grid-cols-1 gap-8 px-4 py-8 lg:grid-cols-12 lg:px-8 lg:py-12">
{/* Left Column: Visual Preview (Desktop Only) */}
<div className="relative hidden overflow-hidden rounded-[2.5rem] bg-gradient-to-br from-slate-100 to-slate-200 lg:col-span-8 lg:block lg:h-[calc(100vh-150px)]">
<div className="sticky top-24 flex h-full items-center justify-center">
<div className="relative h-full w-full overflow-hidden rounded-[2.5rem]">
<img
src="/images/hero.jpg"
alt="Product preview"
className="h-full w-full object-cover"
/>
<div className="absolute left-8 top-8">
<div className="rounded-full bg-black/60 px-4 py-2 text-sm font-medium text-white backdrop-blur-sm">
Live Voorbeeld
</div>
</div>
</div>
</div>
</div>
{/* Right Column: Controls */}
<div className="lg:col-span-4">
<div className="rounded-3xl bg-white p-6 shadow-xl lg:p-8">
<h2 className="mb-6 text-2xl font-bold text-brand-dark-green">
Configureer jouw product
</h2>
<StepIndicator />
<WizardContent />
</div>
</div>
</div>
</FormProvider>
);
}

View File

@@ -1,65 +1,17 @@
import Image from "next/image";
import { Hero } from "@/components/home/hero";
import { OfferSection } from "@/components/home/offer-section";
import { ProcessSection } from "@/components/home/process-section";
import { AboutSection } from "@/components/home/about-section";
import { CraftsmanshipSection } from "@/components/home/craftsmanship-section";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
<>
<Hero />
<OfferSection />
<ProcessSection />
<AboutSection />
<CraftsmanshipSection />
</>
);
}