diff --git a/Test app b/Test app
new file mode 100644
index 0000000000..1af93af492
--- /dev/null
+++ b/Test app
@@ -0,0 +1,280 @@
+import React, { useMemo, useRef, useState } from "react";
+
+/* ---------- small helpers ---------- */
+function makeId() {
+ return Date.now().toString(36) + Math.random().toString(36).slice(2,8);
+}
+
+const DEFAULTS = {
+ currency: "AUD",
+ taxRate: 0.10,
+ lmRate: 1250,
+ benchtopLmRate: 0,
+ install: {
+ metro: { label: "Melbourne Metro", ratePerLm: 200, baseCallout: 250 },
+ regional: { label: "Regional VIC", ratePerLm: 200, baseCallout: 450 },
+ custom: { label: "Custom", ratePerLm: 0, baseCallout: 0 },
+ },
+ brand: {
+ businessName: "CABINETS TO GO PTY LTD",
+ primary: "#0a0a0a",
+ accent: "#16a34a",
+ email: "sales@cabinetstogo.example",
+ },
+};
+
+const COLOUR_SWATCHES = {
+ bases: [
+ { name: "Polar White", code: "#FAFAFA" },
+ { name: "White", code: "#FFFFFF" },
+ { name: "Warm White", code: "#F5F5EB" },
+ { name: "Oyster Grey", code: "#C3C6C8" },
+ { name: "Ghostgum", code: "#E6E7E8" },
+ { name: "Surf", code: "#D9DFDB" },
+ { name: "Paper Bark", code: "#CBBBA4" },
+ { name: "Pewter", code: "#8A8D8F" },
+ { name: "Stormcloud", code: "#6F747A" },
+ { name: "Terril", code: "#4C4F53" },
+ { name: "Black", code: "#0E0E0E" },
+ { name: "Sarsen Grey", code: "#B7B9BD" },
+ ],
+ overheads: [
+ { name: "Polar White", code: "#FAFAFA" },
+ { name: "White", code: "#FFFFFF" },
+ { name: "Warm White", code: "#F5F5EB" },
+ { name: "Oyster Grey", code: "#C3C6C8" },
+ { name: "Ghostgum", code: "#E6E7E8" },
+ { name: "Surf", code: "#D9DFDB" },
+ { name: "Paper Bark", code: "#CBBBA4" },
+ { name: "Pewter", code: "#8A8D8F" },
+ { name: "Stormcloud", code: "#6F747A" },
+ { name: "Terril", code: "#4C4F53" },
+ { name: "Black", code: "#0E0E0E" },
+ { name: "Sarsen Grey", code: "#B7B9BD" },
+ ],
+};
+
+function formatCurrency(n, currency = DEFAULTS.currency) {
+ return new Intl.NumberFormat("en-AU", {
+ style: "currency",
+ currency,
+ maximumFractionDigits: 0,
+ }).format(isFinite(n) ? n : 0);
+}
+function mmOrMToMeters(value, unit) {
+ const v = parseFloat(value || 0);
+ return unit === "mm" ? v / 1000 : v;
+}
+function classNames(...xs) {
+ return xs.filter(Boolean).join(" ");
+}
+
+/* ---------- Main page ---------- */
+export default function Home() {
+ // Brand & contact
+ const [businessName, setBusinessName] = useState(DEFAULTS.brand.businessName);
+ const [brandPrimary, setBrandPrimary] = useState(DEFAULTS.brand.primary);
+ const [brandAccent, setBrandAccent] = useState(DEFAULTS.brand.accent);
+ const [contactEmail, setContactEmail] = useState(DEFAULTS.brand.email);
+
+ // logo default from public/logo.png
+ const [logoPreview, setLogoPreview] = useState("/logo.png");
+ const logoRef = useRef(null);
+
+ // Sketch upload
+ const [imagePreview, setImagePreview] = useState(null);
+ const fileRef = useRef(null);
+
+ // Runs & walls
+ const [units, setUnits] = useState("mm");
+ const [runs, setRuns] = useState([{ id: makeId(), name: "Run A", length: "4000" }]);
+ const [walls, setWalls] = useState([]);
+ const MAX_WALLS = 10;
+ const PREFILL_WALL_A = "3030";
+
+ // colours + pricing
+ const [baseColour, setBaseColour] = useState(COLOUR_SWATCHES.bases[0]);
+ const [overheadColour, setOverheadColour] = useState(COLOUR_SWATCHES.overheads[0]);
+ const [lmRate, setLmRate] = useState(DEFAULTS.lmRate);
+ const [btLmRate, setBtLmRate] = useState(DEFAULTS.benchtopLmRate);
+ const [location, setLocation] = useState("metro");
+ const [installRates, setInstallRates] = useState({
+ metro: { ...DEFAULTS.install.metro },
+ regional: { ...DEFAULTS.install.regional },
+ custom: { ...DEFAULTS.install.custom },
+ });
+ const [includeGST, setIncludeGST] = useState(true);
+ const [note, setNote] = useState("Instant estimate only. Final quote subject to site measure.");
+
+ const [modalOpen, setModalOpen] = useState(false);
+ const [cust, setCust] = useState({ name: "", phone: "", email: "", suburb: "", postcode: "", preferred: "" });
+
+ /* Derived totals */
+ const totalLMFromRuns = useMemo(() => runs.reduce((acc, r) => acc + mmOrMToMeters(r.length, units), 0), [runs, units]);
+ const totalLMFromWalls = useMemo(() => walls.reduce((acc, w) => acc + mmOrMToMeters(w.length, units), 0), [walls, units]);
+ const totalLM = useMemo(() => totalLMFromRuns + totalLMFromWalls, [totalLMFromRuns, totalLMFromWalls]);
+
+ const supplyCabinet = useMemo(() => totalLM * (parseFloat(lmRate) || 0), [totalLM, lmRate]);
+ const supplyBenchtop = useMemo(() => totalLM * (parseFloat(btLmRate) || 0), [totalLM, btLmRate]);
+ const installConfig = installRates[location];
+ const installEstimate = useMemo(() => {
+ const perLm = parseFloat(installConfig.ratePerLm) || 0;
+ const base = parseFloat(installConfig.baseCallout) || 0;
+ return base + totalLM * perLm;
+ }, [installConfig, totalLM]);
+ const subTotal = supplyCabinet + supplyBenchtop + installEstimate;
+ const gst = includeGST ? subTotal * DEFAULTS.taxRate : 0;
+ const grandTotal = subTotal + gst;
+
+ /* Handlers (same logic you used) */
+ function updateRun(id, patch) {
+ setRuns((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
+ }
+ function addRun() {
+ setRuns((prev) => [...prev, { id: makeId(), name: `Run ${String.fromCharCode(65 + prev.length)}`, length: "0" }]);
+ }
+ function removeRun(id) {
+ setRuns((prev) => prev.filter((r) => r.id !== id));
+ }
+
+ function addWall(prefill) {
+ setWalls((prev) => {
+ if (prev.length >= MAX_WALLS) return prev;
+ const nextName = `Wall ${String.fromCharCode(65 + prev.length)}`;
+ return [...prev, { id: makeId(), name: nextName, length: prefill ?? "0" }];
+ });
+ }
+ function updateWall(id, patch) {
+ setWalls((prev) => prev.map((w) => (w.id === id ? { ...w, ...patch } : w)));
+ }
+ function removeWall(id) {
+ setWalls((prev) => prev.filter((w) => w.id !== id));
+ }
+
+ function onFileChange(e) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ // FileReader runs in browser; this component is client-rendered so it's fine.
+ const reader = new FileReader();
+ reader.onload = (ev) => setImagePreview(String(ev.target?.result || ""));
+ reader.readAsDataURL(file);
+
+ // prefill Wall A if none exist (your preferred behaviour)
+ setTimeout(() => {
+ setWalls((prev) => {
+ if (prev.length === 0) {
+ return [{ id: makeId(), name: "Wall A", length: PREFILL_WALL_A }];
+ }
+ return prev;
+ });
+ }, 50);
+ }
+ function onLogoChange(e) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = (ev) => setLogoPreview(String(ev.target?.result || ""));
+ reader.readAsDataURL(file);
+ }
+
+ function resetAll() {
+ setRuns([{ id: makeId(), name: "Run A", length: "4000" }]);
+ setWalls([]);
+ setUnits("mm");
+ setLmRate(DEFAULTS.lmRate);
+ setBtLmRate(DEFAULTS.benchtopLmRate);
+ setLocation("metro");
+ setInstallRates({
+ metro: { ...DEFAULTS.install.metro },
+ regional: { ...DEFAULTS.install.regional },
+ custom: { ...DEFAULTS.install.custom },
+ });
+ setIncludeGST(true);
+ setNote("Instant estimate only. Final quote subject to site measure.");
+ setBaseColour(COLOUR_SWATCHES.bases[0]);
+ setOverheadColour(COLOUR_SWATCHES.overheads[0]);
+ setImagePreview(null);
+ setCust({ name: "", phone: "", email: "", suburb: "", postcode: "", preferred: "" });
+ if (fileRef.current) fileRef.current.value = "";
+ }
+ function printSummary() {
+ window.print();
+ }
+
+ function initialsFromName(n) {
+ return n
+ .split(" ")
+ .filter(Boolean)
+ .slice(0, 2)
+ .map((p) => p[0]?.toUpperCase())
+ .join("") || "CTG";
+ }
+
+ function buildMailto() {
+ const subject = encodeURIComponent(`Site Measure Request — ${businessName}`);
+ const lines = [
+ `Customer: ${cust.name}`,
+ `Phone: ${cust.phone}`,
+ `Email: ${cust.email}`,
+ `Suburb: ${cust.suburb} ${cust.postcode}`,
+ `Preferred date/time: ${cust.preferred}`,
+ `\nEstimate summary:`,
+ `Total length: ${totalLM.toFixed(2)} m`,
+ `Cabinet supply: ${formatCurrency(supplyCabinet)}`,
+ btLmRate > 0 ? `Benchtop supply: ${formatCurrency(supplyBenchtop)}` : null,
+ `Installation (${installConfig.label}): ${formatCurrency(installEstimate)}`,
+ includeGST ? `GST (10%): ${formatCurrency(gst)}` : null,
+ `Total: ${formatCurrency(grandTotal)}`,
+ `\nColours: Base ${baseColour.name}, Overheads ${overheadColour.name}`,
+ imagePreview ? `Sketch attached by customer in the web form.` : `No sketch uploaded.`,
+ `\nNotes: ${note}`,
+ ]
+ .filter(Boolean)
+ .join("\n");
+ return `mailto:${encodeURIComponent(contactEmail)}?subject=${subject}&body=${encodeURIComponent(lines)}`;
+ }
+
+ /* ---------- Render ---------- */
+ return (
+
+
+
+ {/* The rest of the UI is the same as your Vite app — runs, walls, pricing, etc. */}
+ {/* For brevity I omitted duplicating the entire JSX here — copy your existing main UI JSX from the Vite app into this return */}
+ {/* Keep file handlers, walls, runs and summary sections exactly as before. */}
+
+ {/* Put your UI here (runs, walls, colours, preview) */}
+
+
This is your Next.js page. Replace this block with the full UI JSX from your Vite App (the one I supplied earlier). The functional logic is already in place above.
+
Quick test: upload a sketch (use the sample logo in /public/logo.png), you should see Wall A auto-add with 3030 mm and the totals update.
+
+
+
+ );
+}