"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useToast } from "@/lib/toast"; import { loadTrip } from "@/lib/tripStorage"; import { loadPlanMeta, stayHint } from "@/lib/planMeta"; import { mergeDestinationsIntoTrip } from "@/lib/tripActions"; import { trackRecentDestination } from "@/lib/recentDestinations"; import { api, type CityContentItem, type CityDetailPayload } from "@/lib/api"; import type { Destination, Discussion, Meetup, TripItem } from "@/lib/types"; import RingNext from "@/components/RingNext"; import { useRingSteps } from "@/lib/rings"; import { useI18n } from "@/lib/i18n"; import { meetupCityHref, meetupEventHref } from "@/lib/meetupLinks"; const TZ_LABELS: Record = { bali: "UTC+8", lisbon: "UTC+0", chiangmai: "UTC+7", mexico: "UTC-6", barcelona: "UTC+1", tokyo: "UTC+9", }; interface Props { dest: Destination; allDestinations: Destination[]; } export default function DestinationDetailClient({ dest, allDestinations }: Props) { const { toast } = useToast(); const { t } = useI18n(); const rings = useRingSteps(); const [added, setAdded] = useState(false); const [months, setMonths] = useState(1); const [detail, setDetail] = useState(null); const [content, setContent] = useState([]); const [meetups, setMeetups] = useState([]); const [discussions, setDiscussions] = useState([]); const [depthLoading, setDepthLoading] = useState(true); const scores = useMemo(() => { const nomads = parseInt(dest.nomads_count.replace(/[^0-9]/g, ""), 10) || 0; return [ { label: t.destDetail.scoreValue, value: Math.round((12000 - dest.cost) / 120), emoji: "💰" }, { label: t.destDetail.scoreSpeed, value: Math.round(dest.speed / 2), emoji: "📶" }, { label: t.destDetail.scoreClimate, value: Math.round(100 - Math.abs(dest.temperature - 24) * 4), emoji: "🌡️" }, { label: t.destDetail.scoreRating, value: Math.round(dest.rating * 10), emoji: "⭐" }, { label: t.destDetail.scoreCommunity, value: Math.min(Math.round(nomads / 150), 100), emoji: "🤝" }, ]; }, [dest, t]); const visa = useMemo(() => stayHint(dest.country, months), [dest.country, months]); const budget = useMemo(() => loadPlanMeta().monthlyBudget, []); const overBudget = budget > 0 && dest.cost > budget; const related = allDestinations .filter((d) => d.region === dest.region && d.slug !== dest.slug) .slice(0, 3); useEffect(() => { trackRecentDestination({ slug: dest.slug, name: dest.name, country: dest.country, emoji: dest.emoji, cost: dest.cost, rating: dest.rating, }); }, [dest.slug, dest.name, dest.country, dest.emoji, dest.cost, dest.rating]); useEffect(() => { setDepthLoading(true); api .getDestinationFull(dest.slug) .then((full) => { setDetail(full.detail); setContent(full.content || []); setMeetups(full.meetups || []); setDiscussions(full.discussions || []); }) .catch(() => { setDetail(null); setContent([]); setMeetups([]); setDiscussions([]); }) .finally(() => setDepthLoading(false)); }, [dest.slug]); const addToTrip = () => { const trip = loadTrip(); if (trip.some((item) => item.slug === dest.slug)) { toast(t.common.alreadyInTrip, "info", { href: "/plan", label: t.strip.openPlan }); setAdded(true); return; } const { added: n } = mergeDestinationsIntoTrip([dest], months); setAdded(true); toast( n > 0 ? t.common.addedToPlanMonths .replace("{emoji}", dest.emoji) .replace("{city}", dest.name) .replace("{months}", String(months)) : t.common.alreadyInPlan, n > 0 ? "success" : "info", { href: "/plan", label: t.strip.openPlan } ); }; const share = async () => { const url = window.location.href; if (navigator.share) { await navigator.share({ title: `${dest.name} - nomadro`, url }); } else { await navigator.clipboard.writeText(url); toast(t.common.linkCopied); } }; const copySnapshot = async () => { const url = typeof window !== "undefined" ? window.location.href : `/destinations/${dest.slug}`; const lines = [ `${dest.emoji} ${dest.name}, ${dest.country}`, t.destDetail.snapCost .replace("{cost}", dest.cost.toLocaleString()) .replace("{speed}", String(dest.speed)) .replace("{rating}", String(dest.rating)), t.destDetail.snapClimate .replace("{temp}", String(dest.temperature)) .replace("{nomads}", dest.nomads_count), visa ? t.destDetail.snapVisa.replace("{visa}", visa.text) : "", t.destDetail.snapDetail.replace("{url}", url), "— via nomadro", ].filter(Boolean); try { await navigator.clipboard.writeText(lines.join("\n")); toast(t.profile.snapshotOk); } catch { toast(t.profile.snapshotOk, "info"); } }; useEffect(() => { const trip = loadTrip(); setAdded(trip.some((item) => item.slug === dest.slug)); }, [dest.slug]); const costBreak = detail?.cost?.breakdown || []; const guide = detail?.guide; const pros = detail?.pros; return ( <>
{added ? ( {t.common.inPlanOpen} ) : ( <> )} {t.common.goCompare} {t.common.findNext} {t.common.cityMeetups}
{depthLoading &&

{t.common.loadingDepth}

} {(visa || overBudget) && (
{visa && (

🛂 {visa.text}

)} {overBudget && (

{t.common.cityOverBudget .replace("{cost}", dest.cost.toLocaleString()) .replace("{budget}", budget.toLocaleString())}

)}
)} {guide?.summary && (

{t.destDetail.guide}

{guide.summary}

{guide.workSetup &&

{guide.workSetup}

} {!!guide.bestFor?.length && (

{t.destDetail.bestFor}{guide.bestFor.join(" · ")}

)} {!!guide.arrivalChecklist?.length && (
    {guide.arrivalChecklist.map((item) =>
  • {item}
  • )}
)}
)}

{t.destDetail.radar}

{scores.map((s) => (
{s.emoji} {s.label}
{Math.min(s.value, 100)}
))}
🕐
{t.destDetail.timezone} {TZ_LABELS[dest.slug] || t.common.viewTimezone}
👥
{t.destDetail.nomadCommunity} {detail?.people?.nomadsNow?.toLocaleString() || dest.nomads_count} {t.destDetail.active}
🌡️
{t.destDetail.climate} {detail?.weather?.temperature ?? dest.temperature}°C {detail?.weather?.bestMonths ? ` · ${t.destDetail.livable} ${detail.weather.bestMonths.join("/")}` : ""}
{!!costBreak.length && (

{t.destDetail.costBreak}

{costBreak.map((row) => (
{row.label} ¥{row.amount.toLocaleString()}
))}
{detail?.cost?.tip &&

{detail.cost.tip}

}
)} {(pros?.pros || pros?.cons) && (

{t.destDetail.pros}

    {(pros?.pros || []).map((p) =>
  • {p}
  • )}

{t.destDetail.cons}

    {(pros?.cons || []).map((p) =>
  • {p}
  • )}
)} {!!detail?.reviews?.items?.length && (

{t.destDetail.reviews} · {detail.reviews.rating}/10

{detail.reviews.items.map((r) => (

{r.text}

{r.author} · {r.role} · {"★".repeat(r.score)}
))}
)} {!!detail?.chat?.channels?.length && (

{t.destDetail.localChat}

    {detail.chat.channels.map((c) => (
  • {c.name} · {c.members} {t.destDetail.peopleUnit} · {c.status}
  • ))}
{!!detail.chat.latestTopics?.length && (

{t.destDetail.recentTopics}{detail.chat.latestTopics.join(" · ")}

)}
)} {!!meetups.length && (

{t.destDetail.relatedMeetups}

{meetups.slice(0, 4).map((m) => ( {m.emoji || "🎉"}
{m.title} {m.date} · {m.city}
))}
)} {!!discussions.length && (

{t.destDetail.relatedDiscussions}

    {discussions.slice(0, 5).map((d) => (
  • {d.title}
  • ))}
)} {!!content.length && (

{t.destDetail.relatedContent}

{content.map((c) => ( 📄
{c.title} {c.subtitle || c.ctaLabel || t.destDetail.viewFallback}
))}
)} {related.length > 0 && (

{t.destDetail.relatedRegion}

{related.map((d) => ( {d.emoji}
{d.name} ¥{d.cost.toLocaleString()}{t.destDetail.perMonth} · ⭐ {d.rating}
))}
)} ); }