"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useToast } from "@/lib/toast"; import { useI18n } from "@/lib/i18n"; import { compareUrl } from "@/lib/compareScore"; import { EMPTY_MATCHER_PREFS, loadMatcherPrefs, matcherPrefsComplete, saveMatcherPrefs, clearMatcherPrefs, type MatcherPrefs, type MatcherBudget, type MatcherClimate, type MatcherPriority, type MatcherRegion, } from "@/lib/matcherPrefs"; import { mergeDestinationsIntoTrip } from "@/lib/tripActions"; import type { Destination } from "@/lib/types"; interface Props { destinations: Destination[]; open: boolean; onClose: () => void; } const STEP_KEYS = ["budget", "climate", "priority", "region"] as const; function parseNomads(n: string): number { return parseInt(n.replace(/[^0-9]/g, ""), 10) || 0; } function scoreDestination( d: Destination, prefs: Required> ): number { let score = 0; if (prefs.budget === "low") score += d.cost <= 5000 ? 35 : Math.max(0, 35 - (d.cost - 5000) / 200); else if (prefs.budget === "medium") score += d.cost >= 4500 && d.cost <= 9500 ? 35 : 35 - Math.abs(d.cost - 7000) / 300; else score += d.cost >= 9000 ? 35 : Math.max(0, 35 - (9000 - d.cost) / 200); if (prefs.climate === "warm") score += d.temperature >= 25 ? 25 : Math.max(0, 25 - (25 - d.temperature) * 3); else if (prefs.climate === "mild") score += d.temperature >= 18 && d.temperature <= 25 ? 25 : 25 - Math.abs(d.temperature - 21) * 2; else score += d.temperature <= 18 ? 25 : Math.max(0, 25 - (d.temperature - 18) * 3); if (prefs.priority === "cost") score += (12000 - d.cost) / 80; else if (prefs.priority === "speed") score += d.speed / 4; else if (prefs.priority === "community") score += parseNomads(d.nomads_count) / 500; else score += d.rating * 3; if (prefs.region === "any" || d.region === prefs.region) score += 20; else score -= 10; score += d.rating * 2; return Math.round(score); } export default function DestinationMatcher({ destinations, open, onClose }: Props) { const router = useRouter(); const { toast } = useToast(); const { t } = useI18n(); const [step, setStep] = useState(0); const [prefs, setPrefs] = useState(EMPTY_MATCHER_PREFS); const [direction, setDirection] = useState<"forward" | "back">("forward"); const [hydrated, setHydrated] = useState(false); const steps = useMemo( () => [ { title: t.matcher.budgetTitle, subtitle: t.matcher.budgetSub, key: "budget" as const }, { title: t.matcher.climateTitle, subtitle: t.matcher.climateSub, key: "climate" as const }, { title: t.matcher.priorityTitle, subtitle: t.matcher.prioritySub, key: "priority" as const }, { title: t.matcher.regionTitle, subtitle: t.matcher.regionSub, key: "region" as const }, ], [t] ); const options = useMemo( () => ({ budget: [ { value: "low" as MatcherBudget, emoji: "๐Ÿ’ฐ", label: t.matcher.budgetLow, desc: t.matcher.budgetLowDesc }, { value: "medium" as MatcherBudget, emoji: "๐Ÿ’ณ", label: t.matcher.budgetMed, desc: t.matcher.budgetMedDesc }, { value: "high" as MatcherBudget, emoji: "๐Ÿ’Ž", label: t.matcher.budgetHigh, desc: t.matcher.budgetHighDesc }, ], climate: [ { value: "warm" as MatcherClimate, emoji: "โ˜€๏ธ", label: t.matcher.climateWarm, desc: t.matcher.climateWarmDesc }, { value: "mild" as MatcherClimate, emoji: "๐ŸŒค๏ธ", label: t.matcher.climateMild, desc: t.matcher.climateMildDesc }, { value: "cool" as MatcherClimate, emoji: "๐Ÿ‚", label: t.matcher.climateCool, desc: t.matcher.climateCoolDesc }, ], priority: [ { value: "cost" as MatcherPriority, emoji: "๐Ÿ’ฐ", label: t.matcher.priCost, desc: t.matcher.priCostDesc }, { value: "speed" as MatcherPriority, emoji: "๐Ÿ“ถ", label: t.matcher.priSpeed, desc: t.matcher.priSpeedDesc }, { value: "community" as MatcherPriority, emoji: "๐Ÿค", label: t.matcher.priCommunity, desc: t.matcher.priCommunityDesc }, { value: "lifestyle" as MatcherPriority, emoji: "๐ŸŽจ", label: t.matcher.priLifestyle, desc: t.matcher.priLifestyleDesc }, ], region: [ { value: "any" as MatcherRegion, emoji: "๐ŸŒ", label: t.matcher.regionAny, desc: t.matcher.regionAnyDesc }, { value: "sea" as MatcherRegion, emoji: "๐ŸŒด", label: t.matcher.regionSea, desc: t.matcher.regionSeaDesc }, { value: "europe" as MatcherRegion, emoji: "๐Ÿฐ", label: t.matcher.regionEurope, desc: t.matcher.regionEuropeDesc }, { value: "latam" as MatcherRegion, emoji: "๐ŸŒฎ", label: t.matcher.regionLatam, desc: t.matcher.regionLatamDesc }, { value: "asia" as MatcherRegion, emoji: "๐Ÿฏ", label: t.matcher.regionAsia, desc: t.matcher.regionAsiaDesc }, ], }), [t] ); useEffect(() => { if (!open) { setHydrated(false); return; } const saved = loadMatcherPrefs(); setPrefs(saved); if (matcherPrefsComplete(saved)) setStep(STEP_KEYS.length); else { const filled = STEP_KEYS.findIndex((k) => !saved[k]); setStep(filled === -1 ? 0 : filled); } setHydrated(true); }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); const results = useMemo(() => { if (!matcherPrefsComplete(prefs)) return []; return [...destinations] .map((d) => ({ dest: d, score: scoreDestination(d, prefs) })) .sort((a, b) => b.score - a.score); }, [destinations, prefs]); const top3 = results.slice(0, 3).map((r) => r.dest); const currentKey = steps[step]?.key; const isComplete = step >= STEP_KEYS.length; const progress = isComplete ? 100 : ((step + 1) / STEP_KEYS.length) * 100; const select = (value: string) => { if (!currentKey) return; setDirection("forward"); const next = { ...prefs, [currentKey]: value }; setPrefs(next); saveMatcherPrefs(next); setTimeout(() => setStep((s) => s + 1), 280); }; const goBack = () => { if (step === 0) { onClose(); return; } setDirection("back"); setStep((s) => s - 1); }; const reset = () => { setStep(0); setPrefs(EMPTY_MATCHER_PREFS); clearMatcherPrefs(); setDirection("forward"); }; const buildPlanFromTop = () => { if (top3.length === 0) return; const { added, skipped } = mergeDestinationsIntoTrip(top3, 1); onClose(); if (added === 0) { toast(skipped ? t.matcher.alreadyAll : t.matcher.addFail, "info", { href: "/plan", label: t.strip.openPlan, }); router.push("/plan"); return; } toast( t.matcher.addedTop.replace("{n}", String(added)) + (skipped ? t.matcher.skipped.replace("{n}", String(skipped)) : ""), "success", { href: "/plan", label: t.strip.openPlan } ); router.push("/plan"); }; const addOne = (dest: Destination, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const { added } = mergeDestinationsIntoTrip([dest], 1); toast( added ? `${dest.emoji} ${dest.name} ${t.plan.addedToPlan}` : t.common.alreadyInPlan, added ? "success" : "info", { href: "/plan", label: t.strip.openPlan } ); }; if (!open) return null; return (
e.target === e.currentTarget && onClose()} > {!hydrated ? (

{t.common.loading}

) : (
{!isComplete && currentKey ? (
{t.matcher.tag .replace("{n}", String(step + 1)) .replace("{total}", String(STEP_KEYS.length))}

{steps[step].title}

{steps[step].subtitle}

{options[currentKey].map((opt) => ( ))}
) : (
{t.matcher.resultsTag}

{t.matcher.resultsTitle}

{t.matcher.resultsSub}

{results.length === 0 ? (

{t.matcher.empty}

{t.matcher.goNextStop}
) : ( <>
{results.slice(0, 8).map(({ dest, score }, i) => (
{i === 0 ? "๐Ÿฅ‡" : i === 1 ? "๐Ÿฅˆ" : i === 2 ? "๐Ÿฅ‰" : `#${i + 1}`}
{dest.emoji}
{dest.name} {dest.country} ยท {dest.tag}
{t.matcher.matchPct.replace( "{n}", String(Math.min(Math.round((score / (results[0]?.score || 1)) * 100), 100)) )}
๐Ÿ’ฐ ยฅ{dest.cost.toLocaleString()} ๐Ÿ“ถ {dest.speed}Mbps โญ {dest.rating}
))}
{top3.length >= 2 && ( d.slug))} className="btn btn-ghost" onClick={onClose} > {t.matcher.compareTop.replace("{n}", String(top3.length))} )}
)}
)}
)}
); }