diff --git a/app/buying-guide/finder/page.tsx b/app/buying-guide/finder/page.tsx index d3724f85..66b29b94 100644 --- a/app/buying-guide/finder/page.tsx +++ b/app/buying-guide/finder/page.tsx @@ -1,480 +1,536 @@ -"use client"; +'use client'; -import React, { useMemo, useState } from "react"; +import { useMemo, useState } from 'react'; -/** - * Buying Guide → Laser Finder - * Path: /buying-guide/finder - * - * - Lightweight scoring based on answers - * - Nice, readable "Cautions" chips - * - Clear callouts and reset / refine controls - * - Pure Tailwind, no external deps - */ +/* ----------------------------------------------------------- + * Data: laser types (copy tweaks welcome) + * ----------------------------------------------------------- */ +type LaserKey = 'co2_gantry' | 'co2_galvo' | 'fiber' | 'uv'; -type LaserKind = "fiber" | "co2-galvo" | "co2-gantry" | "uv"; - -type Answers = { - material?: "metal" | "organic" | "mixed"; - task?: "mark" | "engrave" | "cut"; - detail?: "fine" | "medium" | "coarse"; - size?: "small" | "medium" | "large"; - colorChangeOk?: boolean; // okay with color/anneal vs. deep mark - budget?: "tight" | "moderate" | "flexible"; -}; - -type Profile = { - id: LaserKind; +const LASER_TYPES: Record = { + strengths: string[]; + typical: string[]; +}> = { + co2_gantry: { + title: 'CO₂ Gantry', + blurb: + 'Large work areas, great for cutting/engraving organics (wood, leather, paper) and many plastics. Best for sheet work and signage.', + cautions: [ + 'Requires robust ventilation & fire safety for cutting organics.', + 'Not suitable for bare metals without coatings/pastes.', + ], + strengths: [ + 'Big beds (e.g. 600×400mm+)', + 'Cuts thicker organics cleanly', + 'Lower cost per watt than galvos', + ], + typical: ['signs', 'boxes', 'inlays', 'production sheet work'], + }, + co2_galvo: { + title: 'CO₂ Galvo', + blurb: + 'High-speed galvo for organics/plastics in smaller fields (e.g. 110×110mm). Excellent for rapid engraving and serialization.', + cautions: [ + 'Smaller field; “stitching” required for large designs.', + 'Requires smoke extraction for organics/plastics.', + ], + strengths: [ + 'Very fast engraving', + 'Sharp detail in small fields', + 'Great for coated/anodized items', + ], + typical: ['batch marking', 'brand marks', 'rapid engraving'], + }, fiber: { - id: "fiber", - title: "Fiber (1064 nm • Galvo)", - wavelength: "1064 nm", - bestFor: [ - "Marking/engraving bare metals", - "High-detail logos & serials", - "Fast batch work on small parts", - ], + title: 'Fiber (1064nm)', + blurb: + 'The go-to for metals. Deep engraving, anneal/black marking, and serial/UID marking. Small F-theta fields; superb precision.', cautions: [ - "Small scan field vs gantry machines", - "Not for cutting non-metals", - "Ventilation needed for some plastics", + 'Not suited for clear/white acrylic; limited plastics.', + 'Eye safety is critical; use certified eyewear/enclosure.', ], - notes: - "Ideal for stainless, aluminum, tool steels. Deep marks possible with multiple passes.", - }, - "co2-galvo": { - id: "co2-galvo", - title: "CO₂ Galvo (10.6 µm)", - wavelength: "10.6 µm", - bestFor: [ - "Rapid engraving on wood, leather, glass, acrylic", - "Small items in trays/fixtures", - "Branding & raster graphics", + strengths: [ + 'Marks/engraves steel, aluminum, brass, Ti, etc.', + 'High precision in small fields', + 'Supports color marking on some alloys (with tuning)', ], - cautions: [ - "Poor on bare metals (needs marking spray)", - "Smaller working area than gantry CO₂", - "Not ideal for thick cutting", - ], - notes: - "When you need speed on organic materials at smaller sizes, CO₂ galvo is excellent.", - }, - "co2-gantry": { - id: "co2-gantry", - title: "CO₂ Gantry (10.6 µm)", - wavelength: "10.6 µm", - bestFor: [ - "Cutting acrylic & organics", - "Larger panels/signage", - "Prototyping jigs and fixtures", - ], - cautions: [ - "Not for bare metals without compound", - "Finer micro-detail is slower than galvos", - ], - notes: - "Your general-purpose cutter/engraver for wood, acrylic, cardboard, leather, and more.", + typical: ['metal tags', 'tools', 'knives', 'jewelry', 'UIDs'], }, uv: { - id: "uv", - title: "UV (355 nm • Galvo)", - wavelength: "355 nm", - bestFor: [ - "Heat-sensitive plastics & films", - "Glass, ceramics, and clear/translucent parts", - "High-contrast codes on consumer packaging", - ], + title: 'UV (355nm)', + blurb: + 'Best for fine marking of plastics, PCBs, some glass/ceramics with minimal heat. Micro features and delicate substrates.', cautions: [ - "Higher cost per watt than fiber/CO₂", - "Slower for deep material removal", - "Keep optics clean; sensitive to contamination", + 'Generally slower and pricier per watt.', + 'UV eye/skin safety needs strict controls.', ], - notes: - "When you need crisp marks with minimal heat on delicate or clear materials, UV is the tool.", + strengths: [ + 'Minimal heat-affected zone', + 'Excellent for delicate plastics & micro text', + 'Can mark glass/ceramics with good contrast', + ], + typical: ['medical devices', 'plastics, PCB legends', 'micro text'], }, }; -/** ──────────────────────────────────────────────────────────── - * Scoring rules - * - Very simple heuristic = good enough for a first pass. - * - Feel free to tweak weights as you gather user feedback. - * ──────────────────────────────────────────────────────────── */ -function scoreProfiles(a: Answers): Array<{ id: LaserKind; score: number; why: string[] }> { - const why: Record = { - fiber: [], - "co2-galvo": [], - "co2-gantry": [], - uv: [], - }; - const s: Record = { fiber: 0, "co2-galvo": 0, "co2-gantry": 0, uv: 0 }; +/* ----------------------------------------------------------- + * Styling-only Cautions component (contrast fix) + * ----------------------------------------------------------- */ +function CautionBox({ items }: { items?: string[] }) { + if (!items || items.length === 0) return null; - // Material - if (a.material === "metal") { - s.fiber += 6; why.fiber.push("Best for bare metals"); - s.uv += 1; why.uv.push("Can mark some coated metals"); - } - if (a.material === "organic") { - s["co2-gantry"] += 4; why["co2-gantry"].push("Great on wood & acrylic"); - s["co2-galvo"] += 4; why["co2-galvo"].push("Super fast on organics"); - } - if (a.material === "mixed") { - s.fiber += 2; why.fiber.push("Covers metals"); - s["co2-gantry"] += 2; why["co2-gantry"].push("Cuts & engraves organics"); - s["co2-galvo"] += 2; why["co2-galvo"].push("Fast organic engraving"); - s.uv += 1; why.uv.push("Excellent for delicate/clear materials"); - } - - // Task - if (a.task === "mark" || a.task === "engrave") { - s.fiber += 3; why.fiber.push("High-contrast metal marking"); - s["co2-galvo"] += 2; why["co2-galvo"].push("Fast raster engraving"); - s.uv += 2; why.uv.push("Clean marks on delicate substrates"); - } - if (a.task === "cut") { - if (a.material !== "metal") { - s["co2-gantry"] += 6; why["co2-gantry"].push("Best for cutting non-metals"); - } - } - - // Detail - if (a.detail === "fine") { - s.fiber += 2; why.fiber.push("Very fine vector detail"); - s.uv += 2; why.uv.push("Minimal heat, crisp small features"); - s["co2-galvo"] += 1; why["co2-galvo"].push("Fast raster detail (smaller fields)"); - } - if (a.detail === "coarse") { - s["co2-gantry"] += 1; why["co2-gantry"].push("Large letters & cuts ok"); - } - - // Part size - if (a.size === "large") { - s["co2-gantry"] += 3; why["co2-gantry"].push("Large work area"); - } - if (a.size === "small") { - s.fiber += 1; why.fiber.push("Small parts throughput"); - s["co2-galvo"] += 1; why["co2-galvo"].push("Tray/fixture friendly"); - } - - // Color change acceptable? - if (a.colorChangeOk === true) { - s.fiber += 1; why.fiber.push("Black/anneal OK"); - s.uv += 1; why.uv.push("High contrast on plastics/glass"); - } - - // Budget (soft nudge) - if (a.budget === "tight") { - s["co2-gantry"] += 1; why["co2-gantry"].push("Often best $/area for organics"); - } - if (a.budget === "flexible") { - s.uv += 1; why.uv.push("Premium for delicate materials"); - } - - return (Object.keys(s) as LaserKind[]) - .map((id) => ({ id, score: s[id], why: why[id] })) - .sort((a, b) => b.score - a.score); -} - -/** ──────────────────────────────────────────────────────────── - * UI Bits - * ──────────────────────────────────────────────────────────── */ -function Section({ title, children }: { title: string; children: React.ReactNode }) { return ( -
-

{title}

- {children} -
- ); -} +
- {items.map((it) => { - const active = value === it.value; - return ( - - ); - })} + dark:bg-amber-950/40 dark:text-amber-100 + dark:border-amber-400/40 dark:ring-amber-400/20 + " + > +
+ + ! + + +
+
+ Cautions
- ); -} -function YesNo({ value, onChange }: { value?: boolean; onChange: (v: boolean) => void }) { - return ( - onChange(v === "yes")} - items={[ - { value: "yes", label: "Yes" }, - { value: "no", label: "No" }, - ]} - /> - ); -} - -function Pill({ children }: { children: React.ReactNode }) { - return ( -
  • - {children} -
  • - ); -} - -/** High-contrast, accessible caution chips */ -function Cautions({ items }: { items: string[] }) { - return ( -
    -
    - ⚠️ -

    Cautions

    -
    -
      - {items.map((c, i) => ( -
    • - {c} +
        + {items.map((it, i) => ( +
      • + {it}
      • ))}
    +
    +
    ); } -function ResultCard({ - pick, - why, - rank, +/* ----------------------------------------------------------- + * Questionnaire model + * (Focused on use-cases; no product ids) + * ----------------------------------------------------------- */ +type Answers = { + materials: 'organics' | 'metals' | 'both' | 'plastics_glass'; + workSize: 'small' | 'medium' | 'large' | 'micro'; + detail: 'low' | 'medium' | 'high' | 'micro'; + throughput: 'normal' | 'high'; + colorOnMetal: 'needed' | 'nice' | 'no'; + budget: 'low' | 'mid' | 'high'; + reflectivity: 'low' | 'mixed' | 'high'; + enclosure: 'enclosed' | 'open_ok'; +}; + +const DEFAULT_ANSWERS: Answers = { + materials: 'both', + workSize: 'medium', + detail: 'medium', + throughput: 'normal', + colorOnMetal: 'no', + budget: 'mid', + reflectivity: 'mixed', + enclosure: 'enclosed', +}; + +/* ----------------------------------------------------------- + * Scoring rules (easy to tweak) + * ----------------------------------------------------------- */ +type ScoreMap = Record; + +const BASE_WEIGHTS: ScoreMap = { + co2_gantry: 0, + co2_galvo: 0, + fiber: 0, + uv: 0, +}; + +function score(answers: Answers): ScoreMap { + const s: ScoreMap = { ...BASE_WEIGHTS }; + + // Materials + if (answers.materials === 'organics') { + s.co2_gantry += 4; + s.co2_galvo += 3; + } else if (answers.materials === 'metals') { + s.fiber += 5; + } else if (answers.materials === 'plastics_glass') { + s.uv += 5; + s.co2_galvo += 2; + } else if (answers.materials === 'both') { + s.co2_gantry += 2; + s.co2_galvo += 2; + s.fiber += 2; + } + + // Work size + if (answers.workSize === 'large') s.co2_gantry += 5; + if (answers.workSize === 'medium') s.co2_gantry += 2; + if (answers.workSize === 'small') s.co2_galvo += 2; + if (answers.workSize === 'micro') { + s.fiber += 3; + s.uv += 3; + } + + // Detail + if (answers.detail === 'low') s.co2_gantry += 1; + if (answers.detail === 'medium') { + s.co2_gantry += 1; + s.co2_galvo += 1; + } + if (answers.detail === 'high') { + s.co2_galvo += 2; + s.fiber += 2; + } + if (answers.detail === 'micro') { + s.fiber += 4; + s.uv += 4; + } + + // Throughput + if (answers.throughput === 'high') { + s.co2_galvo += 3; + s.fiber += 3; + } + + // Color on metal + if (answers.colorOnMetal === 'needed') s.fiber += 3; + if (answers.colorOnMetal === 'nice') s.fiber += 1; + + // Budget + if (answers.budget === 'low') s.co2_gantry += 2; + if (answers.budget === 'mid') { + s.co2_gantry += 1; + s.co2_galvo += 1; + s.fiber += 1; + } + if (answers.budget === 'high') { + s.fiber += 1; + s.uv += 1; + s.co2_galvo += 1; + } + + // Reflectivity exposure + if (answers.reflectivity === 'high') s.fiber += 1; // designed for metals + if (answers.reflectivity === 'low') s.co2_gantry += 1; + + // Enclosure requirement + if (answers.enclosure === 'enclosed') { + s.co2_gantry += 1; // many enclosed units exist + s.fiber += 1; // many enclosed stations exist + s.uv += 1; // usually enclosed + } else { + s.co2_galvo += 1; // many open-table galvos + } + + return s; +} + +/* ----------------------------------------------------------- + * Small UI helpers + * ----------------------------------------------------------- */ +function Card({ + children, + className = '', }: { - pick: Profile; - why: string[]; - rank: number; + children: React.ReactNode; + className?: string; }) { return (
    -
    -

    - {rank === 0 ? "Recommended" : "Alternative"} · {pick.title} -

    - {pick.wavelength} -
    - - {pick.notes &&

    {pick.notes}

    } - -
    -

    Best For

    -
      - {pick.bestFor.map((b, i) => ( - {b} - ))} -
    -
    - - - - {why.length > 0 && ( -
    - Why you’re seeing this:{" "} - {why.join(" · ")} -
    - )} + {children}
    ); } -/** ──────────────────────────────────────────────────────────── - * Page - * ──────────────────────────────────────────────────────────── */ +function SectionTitle({ children }: { children: React.ReactNode }) { + return ( +

    + {children} +

    + ); +} + +/* ----------------------------------------------------------- + * The Page + * ----------------------------------------------------------- */ export default function FinderPage() { - const [a, setA] = useState({}); + const [answers, setAnswers] = useState({ ...DEFAULT_ANSWERS }); + const [showResults, setShowResults] = useState(false); - const results = useMemo(() => scoreProfiles(a), [a]); + const results = useMemo(() => { + const scores = score(answers); + return (Object.keys(scores) as LaserKey[]) + .map((k) => ({ key: k, score: scores[k] })) + .sort((a, b) => b.score - a.score); + }, [answers]); - const complete = - a.material && a.task && a.detail && a.size && a.colorChangeOk !== undefined; - - const top = results[0]; - const alt = results.slice(1, 3); - - function reset() { - setA({}); - window.scrollTo({ top: 0, behavior: "smooth" }); - } + const top = results.slice(0, 2); // show top 1–2 return ( -
    -
    -

    Laser Finder

    -

    - Answer a few quick questions and we’ll suggest the laser family that - fits your work best. +

    +
    +

    + Laser Type Finder +

    +

    + Answer a few questions about your work. We’ll suggest the laser{' '} + type (CO₂ gantry, CO₂ galvo, Fiber, UV) that fits best. No + product ads—just use-case guidance.

    -
    +
    -
    - setA((p) => ({ ...p, material: v as Answers["material"] }))} - items={[ - { value: "metal", label: "Metals" }, - { value: "organic", label: "Organics / Acrylic" }, - { value: "mixed", label: "Mixed" }, - ]} - /> -
    + + What are you working with? +
    + -
    - setA((p) => ({ ...p, task: v as Answers["task"] }))} - items={[ - { value: "mark", label: "Mark" }, - { value: "engrave", label: "Engrave" }, - { value: "cut", label: "Cut" }, - ]} - /> -
    + -
    - setA((p) => ({ ...p, detail: v as Answers["detail"] }))} - items={[ - { value: "fine", label: "Fine" }, - { value: "medium", label: "Medium" }, - { value: "coarse", label: "Coarse" }, - ]} - /> -
    + -
    - setA((p) => ({ ...p, size: v as Answers["size"] }))} - items={[ - { value: "small", label: "Small" }, - { value: "medium", label: "Medium" }, - { value: "large", label: "Large" }, - ]} - /> -
    + +
    +
    -
    - setA((p) => ({ ...p, colorChangeOk: v }))} - /> -
    + + Constraints & preferences +
    + -
    - setA((p) => ({ ...p, budget: v as Answers["budget"] }))} - items={[ - { value: "tight", label: "Tight" }, - { value: "moderate", label: "Moderate" }, - { value: "flexible", label: "Flexible" }, - ]} - /> -
    + + + + + +
    +
    - {/* Results */} -
    - {!complete ? ( -
    - Fill out the questions above to see recommendations. -
    - ) : ( - <> - {top && ( - - )} - {alt.map((r, i) => ( - - ))} - - )} -
    - - {/* Actions */} -
    +
    - - {/* Quick links back into your existing app sections */} - { + setAnswers({ ...DEFAULT_ANSWERS }); + setShowResults(false); + }} > - See Fiber Examples - - - See CO₂ Gantry - - - See CO₂ Galvo - - - See UV - + Reset +
    + + {showResults && ( +
    + {top.map(({ key, score }) => { + const t = LASER_TYPES[key]; + return ( + +
    +
    +

    {t.title}

    +

    + {t.blurb} +

    +
    + + Score {score} + +
    + +
    +
    +
    + Strengths +
    +
      + {t.strengths.map((s, i) => ( +
    • + {s} +
    • + ))} +
    +
    + +
    +
    + Typical jobs +
    +
      + {t.typical.map((s, i) => ( +
    • + {s} +
    • + ))} +
    +
    +
    + + {/* High-contrast cautions */} + +
    + ); + })} +
    + )}
    ); }