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>
This commit is contained in:
@@ -1,83 +1,288 @@
|
||||
"use client";
|
||||
|
||||
import { useFormContext } from "@/components/offerte/form-context";
|
||||
import { useState } from "react";
|
||||
import { useConfiguratorStore } from "@/lib/store";
|
||||
import { sendQuoteAction } from "@/actions/send-quote";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Send, Check } from "lucide-react";
|
||||
import { Send, Check, Loader2, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
productType: "Product",
|
||||
height: "Hoogte",
|
||||
width: "Breedte",
|
||||
glassType: "Glas Type",
|
||||
finish: "Afwerking",
|
||||
name: "Naam",
|
||||
email: "E-mail",
|
||||
phone: "Telefoon",
|
||||
note: "Opmerking",
|
||||
// ============================================
|
||||
// LABEL MAPS
|
||||
// ============================================
|
||||
|
||||
const DOOR_TYPE_LABELS: Record<string, string> = {
|
||||
taats: "Taatsdeur",
|
||||
scharnier: "Scharnierdeur",
|
||||
paneel: "Vast Paneel",
|
||||
};
|
||||
|
||||
const fieldOrder = [
|
||||
"productType",
|
||||
"height",
|
||||
"width",
|
||||
"glassType",
|
||||
"finish",
|
||||
"name",
|
||||
"email",
|
||||
"phone",
|
||||
"note",
|
||||
];
|
||||
const CONFIG_LABELS: Record<string, string> = {
|
||||
enkele: "Enkele deur",
|
||||
dubbele: "Dubbele deur",
|
||||
};
|
||||
|
||||
function formatValue(key: string, value: unknown): string {
|
||||
if (value === undefined || value === null || value === "") return "—";
|
||||
if (key === "height" || key === "width") return `${value} mm`;
|
||||
return String(value);
|
||||
const SIDE_PANEL_LABELS: Record<string, string> = {
|
||||
geen: "Geen",
|
||||
links: "Links",
|
||||
rechts: "Rechts",
|
||||
beide: "Beide zijden",
|
||||
};
|
||||
|
||||
const FINISH_LABELS: Record<string, string> = {
|
||||
zwart: "Mat Zwart",
|
||||
brons: "Brons",
|
||||
grijs: "Antraciet",
|
||||
goud: "Goud",
|
||||
beige: "Beige",
|
||||
ral: "RAL Kleur",
|
||||
};
|
||||
|
||||
const GLASS_COLOR_LABELS: Record<string, string> = {
|
||||
helder: "Helder",
|
||||
grijs: "Rookglas",
|
||||
brons: "Bronsglas",
|
||||
"mat-blank": "Mat Blank",
|
||||
"mat-brons": "Mat Brons",
|
||||
"mat-zwart": "Mat Zwart",
|
||||
};
|
||||
|
||||
const HANDLE_LABELS: Record<string, string> = {
|
||||
beugelgreep: "Beugelgreep",
|
||||
hoekgreep: "Hoekgreep",
|
||||
maangreep: "Maangreep",
|
||||
ovaalgreep: "Ovaalgreep",
|
||||
klink: "Deurklink",
|
||||
"u-greep": "U-Greep",
|
||||
geen: "Geen greep",
|
||||
};
|
||||
|
||||
const PATTERN_LABELS: Record<string, string> = {
|
||||
standard: "Standaard",
|
||||
"dt9-rounded": "DT9 Afgerond",
|
||||
"dt10-ushape": "DT10 U-vorm",
|
||||
};
|
||||
|
||||
function formatPrice(amount: number): string {
|
||||
return new Intl.NumberFormat("nl-NL", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// COMPONENTS
|
||||
// ============================================
|
||||
|
||||
function SummaryRow({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-gray-500">{label}</span>
|
||||
<span className={`text-sm font-medium ${highlight ? "text-[#C4D668]" : "text-[#1A2E2E]"}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SummarySection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-gray-400">{title}</h3>
|
||||
<div className="divide-y divide-gray-100">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PriceRow({ label, amount, bold }: { label: string; amount: number; bold?: boolean }) {
|
||||
if (amount === 0) return null;
|
||||
return (
|
||||
<div className={`flex items-center justify-between py-1.5 ${bold ? "text-base" : "text-sm"}`}>
|
||||
<span className={bold ? "font-bold text-[#1A2E2E]" : "text-gray-500"}>{label}</span>
|
||||
<span className={bold ? "font-bold text-[#1A2E2E]" : "text-gray-700"}>
|
||||
{formatPrice(amount)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MAIN COMPONENT
|
||||
// ============================================
|
||||
|
||||
export function StepSummary() {
|
||||
const { formData } = useFormContext();
|
||||
const store = useConfiguratorStore();
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
const {
|
||||
doorType, gridType, doorConfig, sidePanel,
|
||||
width, height, doorLeafWidth,
|
||||
finish, glassColor, handle, frameSize, glassPattern,
|
||||
extraOptions,
|
||||
name, email, phone, note,
|
||||
priceBreakdown, screenshotDataUrl,
|
||||
} = store;
|
||||
|
||||
const canSubmit = name.length >= 2 && email.includes("@") && phone.length >= 10;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!canSubmit) return;
|
||||
setStatus("loading");
|
||||
setErrorMsg("");
|
||||
|
||||
const result = await sendQuoteAction({
|
||||
doorType, gridType, doorConfig, sidePanel,
|
||||
width, height, doorLeafWidth,
|
||||
finish, glassColor, handle, frameSize, glassPattern,
|
||||
extraOptions,
|
||||
name, email, phone, note,
|
||||
totalPrice: priceBreakdown.totalPrice,
|
||||
steelCost: priceBreakdown.steelCost,
|
||||
glassCost: priceBreakdown.glassCost,
|
||||
baseFee: priceBreakdown.baseFee,
|
||||
mechanismSurcharge: priceBreakdown.mechanismSurcharge,
|
||||
sidePanelSurcharge: priceBreakdown.sidePanelSurcharge,
|
||||
handleCost: priceBreakdown.handleCost,
|
||||
finishSurcharge: priceBreakdown.finishSurcharge,
|
||||
screenshotDataUrl,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setStatus("success");
|
||||
} else {
|
||||
setStatus("error");
|
||||
setErrorMsg(result.error || "Onbekende fout");
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "success") {
|
||||
return (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="mb-4 flex size-16 items-center justify-center rounded-full bg-green-100">
|
||||
<CheckCircle2 className="size-8 text-green-600" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-bold text-[#1A2E2E]">Aanvraag Verstuurd!</h2>
|
||||
<p className="mb-1 text-sm text-gray-600">
|
||||
Bedankt {name}, uw offerte aanvraag is ontvangen.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
We sturen een bevestiging naar {email}.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-2 text-xl font-semibold">Overzicht</h2>
|
||||
<p className="mb-6 text-sm text-muted-foreground">
|
||||
Controleer uw configuratie en verstuur de aanvraag.
|
||||
</p>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{fieldOrder.map((key, i) => {
|
||||
const value = formData[key as keyof typeof formData];
|
||||
return (
|
||||
<tr
|
||||
key={key}
|
||||
className={i % 2 === 0 ? "bg-muted/30" : "bg-card"}
|
||||
>
|
||||
<td className="w-1/3 px-4 py-3 font-medium text-muted-foreground">
|
||||
{fieldLabels[key]}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium">
|
||||
<span className="flex items-center gap-2">
|
||||
{value !== undefined && value !== "" && (
|
||||
<Check className="size-3.5 text-green-600" />
|
||||
)}
|
||||
{formatValue(key, value)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="mb-1 text-xl font-bold text-[#1A2E2E]">Overzicht</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Controleer uw configuratie en verstuur de aanvraag.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Product Section */}
|
||||
<SummarySection title="Product">
|
||||
<SummaryRow label="Deurtype" value={DOOR_TYPE_LABELS[doorType] || doorType} />
|
||||
<SummaryRow label="Verdeling" value={gridType} />
|
||||
<SummaryRow label="Configuratie" value={CONFIG_LABELS[doorConfig] || doorConfig} />
|
||||
<SummaryRow label="Zijpanelen" value={SIDE_PANEL_LABELS[sidePanel] || sidePanel} />
|
||||
</SummarySection>
|
||||
|
||||
{/* Dimensions Section */}
|
||||
<SummarySection title="Afmetingen">
|
||||
<SummaryRow label="Wandopening (breedte)" value={`${width} mm`} />
|
||||
<SummaryRow label="Hoogte" value={`${height} mm`} />
|
||||
<SummaryRow label="Deurblad breedte" value={`${Math.round(doorLeafWidth)} mm`} />
|
||||
</SummarySection>
|
||||
|
||||
{/* Style Section */}
|
||||
<SummarySection title="Stijl">
|
||||
<SummaryRow label="Afwerking" value={FINISH_LABELS[finish] || finish} />
|
||||
<SummaryRow label="Glaskleur" value={GLASS_COLOR_LABELS[glassColor] || glassColor} />
|
||||
<SummaryRow label="Greep" value={HANDLE_LABELS[handle] || handle} />
|
||||
<SummaryRow label="Profielbreedte" value={`${frameSize} mm`} />
|
||||
<SummaryRow label="Glaspatroon" value={PATTERN_LABELS[glassPattern] || glassPattern} />
|
||||
</SummarySection>
|
||||
|
||||
{/* Extra Options */}
|
||||
{extraOptions.length > 0 && (
|
||||
<SummarySection title="Extra opties">
|
||||
{extraOptions.map((opt) => (
|
||||
<div key={opt} className="flex items-center gap-2 py-1.5">
|
||||
<Check className="size-3.5 text-green-500" />
|
||||
<span className="text-sm text-[#1A2E2E]">{opt}</span>
|
||||
</div>
|
||||
))}
|
||||
</SummarySection>
|
||||
)}
|
||||
|
||||
{/* Price Breakdown */}
|
||||
<div className="rounded-xl border-2 border-[#1A2E2E] bg-gradient-to-b from-white to-gray-50 p-4">
|
||||
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-gray-400">
|
||||
Indicatieprijs
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
<PriceRow label="Staal" amount={priceBreakdown.steelCost} />
|
||||
<PriceRow label="Glas" amount={priceBreakdown.glassCost} />
|
||||
<PriceRow label="Basiskost" amount={priceBreakdown.baseFee} />
|
||||
<PriceRow label="Mechanisme toeslag" amount={priceBreakdown.mechanismSurcharge} />
|
||||
<PriceRow label="Zijpaneel toeslag" amount={priceBreakdown.sidePanelSurcharge} />
|
||||
<PriceRow label="Greep" amount={priceBreakdown.handleCost} />
|
||||
<PriceRow label="Kleur toeslag" amount={priceBreakdown.finishSurcharge} />
|
||||
</div>
|
||||
<div className="mt-3 border-t-2 border-[#1A2E2E] pt-3">
|
||||
<PriceRow label="Totaal (indicatie)" amount={priceBreakdown.totalPrice} bold />
|
||||
</div>
|
||||
<p className="mt-2 text-[10px] text-gray-400">
|
||||
* Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Summary */}
|
||||
<SummarySection title="Contactgegevens">
|
||||
<SummaryRow label="Naam" value={name || "—"} />
|
||||
<SummaryRow label="E-mail" value={email || "—"} />
|
||||
<SummaryRow label="Telefoon" value={phone || "—"} />
|
||||
{note && <SummaryRow label="Opmerking" value={note} />}
|
||||
</SummarySection>
|
||||
|
||||
{/* Validation Warning */}
|
||||
{!canSubmit && (
|
||||
<div className="flex items-start gap-2 rounded-lg bg-amber-50 p-3 text-sm text-amber-700">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||||
<span>Vul eerst uw contactgegevens in (stap 5) om de aanvraag te versturen.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{status === "error" && (
|
||||
<div className="flex items-start gap-2 rounded-lg bg-red-50 p-3 text-sm text-red-700">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
size="lg"
|
||||
className="mt-8 w-full bg-brand-orange text-white hover:bg-brand-orange/90"
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit || status === "loading"}
|
||||
className="w-full bg-[#C4D668] text-[#1A2E2E] hover:bg-[#b5c75a] disabled:opacity-50"
|
||||
>
|
||||
<Send className="size-4" />
|
||||
Verzend Aanvraag
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Verzenden...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="size-4" />
|
||||
Verzend Offerte Aanvraag
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user