import React, { useMemo, useState } from "react"; import { motion } from "framer-motion"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Calculator, Mail, Phone, Ruler, Send, Shapes, User } from "lucide-react"; const LETTERS = "ABCDEFG"; const DEFAULT_NM_PRICE = 24990; function sideLabels(n) { return Array.from({ length: n }, (_, i) => `${LETTERS[i]}${LETTERS[(i + 1) % n]}`); } function diagLabels(n) { return Array.from({ length: Math.max(0, n - 3) }, (_, i) => `A${LETTERS[i + 2]}`); } function parseNum(v) { if (typeof v === "number") return v; const x = parseFloat(String(v).replace(",", ".")); return Number.isFinite(x) ? x : 0; } function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); } function circleIntersections(c0, r0, c1, r1) { const dx = c1.x - c0.x; const dy = c1.y - c0.y; const d = Math.hypot(dx, dy); if (d === 0) return []; if (d > r0 + r1) return []; if (d < Math.abs(r0 - r1)) return []; const a = (r0 * r0 - r1 * r1 + d * d) / (2 * d); const h2 = r0 * r0 - a * a; if (h2 < 0) return []; const h = Math.sqrt(Math.max(0, h2)); const xm = c0.x + (a * dx) / d; const ym = c0.y + (a * dy) / d; const rx = (-dy * h) / d; const ry = (dx * h) / d; return [ { x: xm + rx, y: ym + ry }, { x: xm - rx, y: ym - ry }, ]; } function polygonArea(points) { let s = 0; for (let i = 0; i < points.length; i++) { const p1 = points[i]; const p2 = points[(i + 1) % points.length]; s += p1.x * p2.y - p2.x * p1.y; } return s / 2; } function centroid(points) { return { x: points.reduce((a, p) => a + p.x, 0) / points.length, y: points.reduce((a, p) => a + p.y, 0) / points.length, }; } function normalize(v) { const len = Math.hypot(v.x, v.y); if (!len) return { x: 0, y: 0 }; return { x: v.x / len, y: v.y / len }; } function add(a, b) { return { x: a.x + b.x, y: a.y + b.y }; } function sub(a, b) { return { x: a.x - b.x, y: a.y - b.y }; } function mul(a, s) { return { x: a.x * s, y: a.y * s }; } function dot(a, b) { return a.x * b.x + a.y * b.y; } function buildPolygon(n, sides, diags) { const pts = [{ x: 0, y: 0 }, { x: sides[0], y: 0 }]; const A = pts[0]; for (let k = 2; k < n; k++) { const prev = pts[k - 1]; const rPrev = sides[k - 1]; const rA = k === n - 1 ? sides[n - 1] : diags[k - 2]; const cands = circleIntersections(prev, rPrev, A, rA); if (!cands.length) throw new Error(`Nem szerkeszthető a(z) ${LETTERS[k]} pont.`); pts.push(cands.sort((p, q) => q.y - p.y || q.x - p.x)[0]); } for (let i = 0; i < n; i++) { const got = dist(pts[i], pts[(i + 1) % n]); if (Math.abs(got - sides[i]) > 0.5) { throw new Error(`Élhossz hiba: ${sideLabels(n)[i]}`); } } if (polygonArea(pts) < 0) { return [pts[0], ...pts.slice(1).reverse()]; } return pts; } function insetPolygon(points, allowances) { const c = centroid(points); const out = points.map((pt, i) => { const prev = points[(i - 1 + points.length) % points.length]; const next = points[(i + 1) % points.length]; let b = add(normalize(sub(prev, pt)), normalize(sub(next, pt))); b = normalize(b); if (dot(b, sub(c, pt)) < 0) b = mul(b, -1); return add(pt, mul(b, allowances[i])); }); if (Math.abs(polygonArea(out)) < 1e-6) throw new Error("A csökkentett alak túl kicsi."); return out; } function toSvgPath(points, width, height, pad = 24) { if (!points.length) return ""; const xs = points.map((p) => p.x); const ys = points.map((p) => p.y); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); const dx = Math.max(maxX - minX, 1); const dy = Math.max(maxY - minY, 1); const scale = Math.min((width - pad * 2) / dx, (height - pad * 2) / dy); const map = (p) => ({ x: pad + (p.x - minX) * scale, y: height - pad - (p.y - minY) * scale, }); const mapped = points.map(map); const d = mapped .map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`) .join(" "); return { path: `${d} Z`, mapped, }; } function formatFt(v) { return new Intl.NumberFormat("hu-HU", { style: "currency", currency: "HUF", maximumFractionDigits: 0 }).format(v || 0); } function formatM2(v) { return `${(v || 0).toFixed(2)} m²`; } function approxAreaM2(reducedCmPoints) { return Math.abs(polygonArea(reducedCmPoints)) / 10000; } function initialState(n) { return { customerName: "", email: "", phone: "", material: "HDPE 320", color: "homok", sailName: "egyedi napvitorla", note: "", n, curvaturePercent: 4, nmPrice: DEFAULT_NM_PRICE, sides: sideLabels(n).map(() => "500"), diags: diagLabels(n).map(() => "700"), allowances: Array.from({ length: n }, () => "25"), gdpr: false, terms: false, }; } export default function NapvitorlaMVP() { const [form, setForm] = useState(initialState(4)); const [submitting, setSubmitting] = useState(false); const [submitMsg, setSubmitMsg] = useState(""); const calc = useMemo(() => { try { const n = Number(form.n); const sides = form.sides.map(parseNum); const diags = form.diags.map(parseNum); const allowances = form.allowances.map(parseNum); const original = buildPolygon(n, sides, diags); const reduced = insetPolygon(original, allowances); const areaM2 = approxAreaM2(reduced); const total = areaM2 * parseNum(form.nmPrice); const outerSvg = toSvgPath(original, 520, 420); const innerSvg = toSvgPath(reduced, 520, 420); return { ok: true, original, reduced, areaM2, total, outerSvg, innerSvg, }; } catch (e) { return { ok: false, error: e.message || "Hibás geometria", }; } }, [form]); function updateShape(nextN) { setForm((prev) => { const n = Number(nextN); return { ...prev, n, sides: sideLabels(n).map((_, i) => prev.sides[i] ?? ""), diags: diagLabels(n).map((_, i) => prev.diags[i] ?? ""), allowances: Array.from({ length: n }, (_, i) => prev.allowances[i] ?? "25"), }; }); } function updateArray(field, index, value) { setForm((prev) => ({ ...prev, [field]: prev[field].map((v, i) => (i === index ? value : v)), })); } async function handleOrder() { setSubmitMsg(""); if (!form.gdpr || !form.terms) { setSubmitMsg("A megrendeléshez fogadd el az adatkezelést és a feltételeket."); return; } if (!calc.ok) { setSubmitMsg("Előbb javítani kell a méreteket."); return; } const payload = { customer: { name: form.customerName, email: form.email, phone: form.phone, }, product: { sailName: form.sailName, material: form.material, color: form.color, n: form.n, curvaturePercent: parseNum(form.curvaturePercent), nmPrice: parseNum(form.nmPrice), sides: form.sides.map(parseNum), diags: form.diags.map(parseNum), allowances: form.allowances.map(parseNum), note: form.note, }, calculation: { areaM2: calc.areaM2, total: calc.total, }, }; try { setSubmitting(true); const res = await fetch("/api/order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.message || "Sikertelen küldés"); setSubmitMsg("A megrendelés elküldve. Hamarosan jelentkeztek nálatok."); } catch (err) { setSubmitMsg(`Demó módban fut: a backend még nincs bekötve. Várható végpont: POST /api/order. (${err.message})`); } finally { setSubmitting(false); } } return (
Napvitorla webes MVP Élő kalkuláció
Egyedi napvitorla tervező és megrendelő A vevő megadja a méreteket, az oldal kirajzolja a formát, kiszámolja a területet és az árat, majd egy gombbal elküldi a rendelést emailben.
Vevő adatai
setForm({ ...form, customerName: e.target.value })} placeholder="Kiss Péter" />
setForm({ ...form, email: e.target.value })} placeholder="vevo@email.hu" />
setForm({ ...form, phone: e.target.value })} placeholder="+36 30 123 4567" />
setForm({ ...form, sailName: e.target.value })} placeholder="Terasz fölé" />
Geometria és anyag
setForm({ ...form, curvaturePercent: e.target.value })} />
setForm({ ...form, material: e.target.value })} />
setForm({ ...form, color: e.target.value })} />
Élek (cm)
{sideLabels(form.n).map((label, i) => (
updateArray("sides", i, e.target.value)} />
))}
Átlók (cm)
{diagLabels(form.n).length ? diagLabels(form.n).map((label, i) => (
updateArray("diags", i, e.target.value)} />
)) :
Háromszögnél nincs szükség átlóra.
}
Sarokhely a szereléknek (cm)
{Array.from({ length: form.n }, (_, i) => (
updateArray("allowances", i, e.target.value)} />
))}