322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
// app/buying-guide/finder/page.tsx
|
||
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import { useForm } from "react-hook-form";
|
||
import type { Answers, LaserType } from "@/lib/laser-finder";
|
||
import { scoreAnswers, LASER_LABEL, TYPE_INFO } from "@/lib/laser-finder";
|
||
import Link from "next/link";
|
||
|
||
export default function LaserFinderPage() {
|
||
const [result, setResult] = useState<{
|
||
top: LaserType[];
|
||
score: Record<LaserType, number>;
|
||
why: Record<LaserType, string[]>;
|
||
} | null>(null);
|
||
|
||
const { register, handleSubmit, reset, watch } = useForm<Answers>({
|
||
defaultValues: {
|
||
materials: [],
|
||
operations: [],
|
||
part_size: "medium",
|
||
detail: "medium",
|
||
throughput: "medium",
|
||
budget: "mid",
|
||
},
|
||
});
|
||
|
||
const onSubmit = (vals: Answers) => {
|
||
const { ranked, score, why } = scoreAnswers(vals);
|
||
setResult({ top: ranked.slice(0, 2), score, why });
|
||
// (Optional) later: POST vals to Directus for analytics
|
||
};
|
||
|
||
const selectedMaterials = watch("materials");
|
||
const selectedOps = watch("operations");
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto py-8 space-y-6">
|
||
<h1 className="text-2xl font-semibold">Laser Type Finder</h1>
|
||
<p className="text-sm text-muted-foreground">
|
||
Answer a few questions and we’ll suggest the best laser <em>types</em> for your work
|
||
with clear use-cases, materials, and cautions. No product pitches—just guidance.
|
||
</p>
|
||
|
||
{!result && (
|
||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
{/* Materials */}
|
||
<fieldset className="border rounded p-4">
|
||
<legend className="font-medium">Materials (select all that apply)</legend>
|
||
<div className="grid sm:grid-cols-2 gap-2 mt-2">
|
||
{[
|
||
["metals_bare", "Bare metals"],
|
||
["metals_coated", "Coated/painted metals"],
|
||
["plastics", "Plastics"],
|
||
["wood_paper_leather", "Wood, paper, leather"],
|
||
["glass_ceramic", "Glass / ceramic"],
|
||
["stone", "Stone"],
|
||
["textiles", "Textiles"],
|
||
].map(([val, label]) => (
|
||
<label key={val} className="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" value={val} {...register("materials")} /> {label}
|
||
</label>
|
||
))}
|
||
</div>
|
||
{selectedMaterials?.length === 0 && (
|
||
<p className="text-xs text-amber-600 mt-2">Tip: choose at least one material for a better match.</p>
|
||
)}
|
||
</fieldset>
|
||
|
||
{/* Operations */}
|
||
<fieldset className="border rounded p-4">
|
||
<legend className="font-medium">Typical operations (select all that apply)</legend>
|
||
<div className="grid sm:grid-cols-2 gap-2 mt-2">
|
||
{[
|
||
["deep_mark_metal", "Deep mark on metal"],
|
||
["color_mark_stainless", "Color mark stainless"],
|
||
["fine_engraving", "Fine engraving (small features)"],
|
||
["photo_engrave", "Photo engraving"],
|
||
["cut_nonmetals_thick", "Cut thick non-metals (e.g., 6+ mm acrylic/wood)"],
|
||
["cut_nonmetals_thin", "Cut thin non-metals"],
|
||
["mark_coated", "Mark coated items"],
|
||
].map(([val, label]) => (
|
||
<label key={val} className="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" value={val} {...register("operations")} /> {label}
|
||
</label>
|
||
))}
|
||
</div>
|
||
{selectedOps?.length === 0 && (
|
||
<p className="text-xs text-amber-600 mt-2">Tip: pick one or more to sharpen the recommendation.</p>
|
||
)}
|
||
</fieldset>
|
||
|
||
{/* Size / Detail / Speed / Budget */}
|
||
<div className="grid sm:grid-cols-4 gap-4">
|
||
<div>
|
||
<label className="block text-sm mb-1">Part size</label>
|
||
<select className="w-full border rounded px-2 py-1" {...register("part_size")}>
|
||
<option value="small">Small (≤ 200 mm)</option>
|
||
<option value="medium">Medium (≤ 600 mm)</option>
|
||
<option value="large">Large (> 600 mm)</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm mb-1">Detail</label>
|
||
<select className="w-full border rounded px-2 py-1" {...register("detail")}>
|
||
<option value="low">Low</option>
|
||
<option value="medium">Medium</option>
|
||
<option value="high">High</option>
|
||
<option value="micro">Micro</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm mb-1">Throughput</label>
|
||
<select className="w-full border rounded px-2 py-1" {...register("throughput")}>
|
||
<option value="low">Low</option>
|
||
<option value="medium">Medium</option>
|
||
<option value="high">High</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm mb-1">Budget</label>
|
||
<select className="w-full border rounded px-2 py-1" {...register("budget")}>
|
||
<option value="low">Lower</option>
|
||
<option value="mid">Mid</option>
|
||
<option value="high">Higher</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<button className="px-3 py-2 border rounded bg-accent text-background hover:opacity-90" type="submit">
|
||
Get recommendations
|
||
</button>
|
||
<Link className="text-sm underline hover:no-underline" href="/buying-guide">
|
||
Back to Buying Guide
|
||
</Link>
|
||
</div>
|
||
</form>
|
||
)}
|
||
|
||
{!!result && (
|
||
<div className="space-y-6">
|
||
<ResultCard
|
||
title="Top recommendation"
|
||
type={result.top[0]}
|
||
why={result.why[result.top[0]]}
|
||
/>
|
||
|
||
{result.top[1] && (
|
||
<ResultCard
|
||
title="Alternative to consider"
|
||
type={result.top[1]}
|
||
why={result.why[result.top[1]]}
|
||
secondary
|
||
/>
|
||
)}
|
||
|
||
<CompareMatrix />
|
||
|
||
<div className="flex items-center gap-3">
|
||
<button className="px-3 py-2 border rounded hover:bg-muted" onClick={() => setResult(null)}>
|
||
Start over
|
||
</button>
|
||
<button
|
||
className="px-3 py-2 border rounded hover:bg-muted"
|
||
onClick={() => { reset(); setResult(null); }}
|
||
>
|
||
New answers
|
||
</button>
|
||
<Link className="text-sm underline hover:no-underline" href="/buying-guide">
|
||
Back to Buying Guide
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ResultCard({
|
||
title,
|
||
type,
|
||
why,
|
||
secondary,
|
||
}: {
|
||
title: string;
|
||
type: LaserType;
|
||
why?: string[];
|
||
secondary?: boolean;
|
||
}) {
|
||
const info = TYPE_INFO[type];
|
||
|
||
return (
|
||
<div className="rounded border p-4 space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-lg font-semibold">{title}</h2>
|
||
{!secondary && <span className="text-xs rounded bg-muted px-2 py-0.5">Best match</span>}
|
||
</div>
|
||
<div className="text-base font-medium">{LASER_LABEL[type]}</div>
|
||
<p className="text-sm text-muted-foreground">{info.summary}</p>
|
||
|
||
{!!why?.length && (
|
||
<div>
|
||
<div className="text-sm font-medium mb-1">Why this fits</div>
|
||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||
{why.slice(0, 5).map((w, i) => <li key={i}>{w}</li>)}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid sm:grid-cols-3 gap-3 text-sm">
|
||
<TagList label="Best for" items={info.bestFor} />
|
||
<TagList label="Materials" items={info.materials} />
|
||
<TagList label="Cautions" items={info.cautions} tone="warn" />
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 pt-1">
|
||
<Link className="text-sm underline hover:no-underline" href={info.learnLink}>
|
||
See community settings
|
||
</Link>
|
||
<Link className="text-sm underline hover:no-underline" href="/submit/settings?target=settings_fiber">
|
||
Suggest new settings
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TagList({
|
||
label,
|
||
items,
|
||
tone,
|
||
}: {
|
||
label: string;
|
||
items: string[];
|
||
tone?: "warn";
|
||
}) {
|
||
return (
|
||
<div>
|
||
<div className="text-sm font-medium mb-1">{label}</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{items.map((t, i) => (
|
||
<span
|
||
key={i}
|
||
className={`text-xs px-2 py-1 rounded border ${
|
||
tone === "warn" ? "bg-amber-50 border-amber-200" : "bg-muted"
|
||
}`}
|
||
>
|
||
{t}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CompareMatrix() {
|
||
const rows: Array<{
|
||
k: LaserType;
|
||
label: string;
|
||
best: string[];
|
||
ok: string[];
|
||
avoid: string[];
|
||
}> = [
|
||
{
|
||
k: "fiber",
|
||
label: LASER_LABEL.fiber,
|
||
best: ["Bare metals", "Deep metal engrave", "Color marking stainless"],
|
||
ok: ["Some coated items", "Some plastics w/ additives"],
|
||
avoid: ["Thick organics cutting"],
|
||
},
|
||
{
|
||
k: "co2_gantry",
|
||
label: LASER_LABEL.co2_gantry,
|
||
best: ["Acrylic cutting", "Wood cutting/engraving", "Large panels"],
|
||
ok: ["Leathers, textiles, rubber"],
|
||
avoid: ["Bare metals (no coat)"],
|
||
},
|
||
{
|
||
k: "co2_galvo",
|
||
label: LASER_LABEL.co2_galvo,
|
||
best: ["Fast marking organics", "Photo engraving organics"],
|
||
ok: ["Coated metals/non-metals"],
|
||
avoid: ["Thick sheet cutting", "Large panels"],
|
||
},
|
||
{
|
||
k: "uv",
|
||
label: LASER_LABEL.uv,
|
||
best: ["Micro features", "Glass/ceramic/plastics marking"],
|
||
ok: ["Fine logos on coated metals"],
|
||
avoid: ["Thick cutting"],
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="rounded border p-4">
|
||
<div className="text-sm font-medium mb-2">Compare laser types</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="text-left border-b">
|
||
<th className="py-2 pr-3">Type</th>
|
||
<th className="py-2 pr-3">Best for</th>
|
||
<th className="py-2 pr-3">Okay for</th>
|
||
<th className="py-2">Not ideal</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((r) => (
|
||
<tr key={r.k} className="border-b last:border-0 align-top">
|
||
<td className="py-2 pr-3 font-medium">{r.label}</td>
|
||
<td className="py-2 pr-3">{r.best.join(", ")}</td>
|
||
<td className="py-2 pr-3">{r.ok.join(", ")}</td>
|
||
<td className="py-2">{r.avoid.join(", ")}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|