nomadweb/frontend/src/components/HomeClient.tsx
eric 4ab2d7e412 Add tools hub, changelog, ATM guide, and slim section nav
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 00:24:44 -05:00

189 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import { api } from "@/lib/api";
import type { Destination, BlogPost, FAQ, Testimonial, Tool, Visa, Stats, ChartData } from "@/lib/types";
import Loader from "./Loader";
import ParticleCanvas from "./ParticleCanvas";
import Navbar from "./Navbar";
import TickerBar from "./TickerBar";
import Hero from "./Hero";
import WorldMap from "./WorldMap";
import Destinations from "./Destinations";
import Calculator from "./Calculator";
import TripPlanner from "./TripPlanner";
import GlobalSearch from "./GlobalSearch";
import Lifestyle from "./Lifestyle";
import Charts from "./Charts";
import VisaSection from "./VisaSection";
import Tools from "./Tools";
import BlogSection from "./BlogSection";
import FAQSection from "./FAQSection";
import Community from "./Community";
import Footer from "./Footer";
import CompareModal from "./CompareModal";
import DestinationMatcher, { MatcherTrigger } from "./DestinationMatcher";
import TimezoneBoard from "./TimezoneBoard";
import ScrollProgress from "./ScrollProgress";
import SectionNav from "./SectionNav";
import CurrencyConverter from "./CurrencyConverter";
import NomadScore from "./NomadScore";
import CostCompare from "./CostCompare";
import CookieConsent from "./CookieConsent";
import CoworkingGuide from "./CoworkingGuide";
import PackingChecklist from "./PackingChecklist";
import SeasonGuide from "./SeasonGuide";
import NomadTips from "./NomadTips";
import SpinGlobe from "./SpinGlobe";
import FlightCost from "./FlightCost";
import NomadEvents from "./NomadEvents";
import WifiWorkScore from "./WifiWorkScore";
import SavingsGoal from "./SavingsGoal";
import DailyQuote from "./DailyQuote";
import JetLagEstimator from "./JetLagEstimator";
import HousingGuide from "./HousingGuide";
import Phrasebook from "./Phrasebook";
import FeedbackWidget from "./FeedbackWidget";
import InsuranceGuide from "./InsuranceGuide";
import ExpenseTracker from "./ExpenseTracker";
import SimGuide from "./SimGuide";
import TaxDayCounter from "./TaxDayCounter";
import ArrivalChecklist from "./ArrivalChecklist";
import CafeGuide from "./CafeGuide";
import OnboardingTour from "./OnboardingTour";
import PowerPlugGuide from "./PowerPlugGuide";
import EmergencyContacts from "./EmergencyContacts";
import CityNotes from "./CityNotes";
import FocusTimer from "./FocusTimer";
import AtmCashGuide from "./AtmCashGuide";
import KeyboardShortcuts from "./KeyboardShortcuts";
import BackToTop from "./BackToTop";
interface Props {
stats: Stats;
ticker: string[];
destinations: Destination[];
visas: Visa[];
faqs: FAQ[];
testimonials: Testimonial[];
tools: Tool[];
blog: BlogPost[];
charts: { cost: ChartData; speed: ChartData; growth: ChartData; radar: ChartData };
}
export default function HomeClient(props: Props) {
const [compareList, setCompareList] = useState<string[]>([]);
const [compareResults, setCompareResults] = useState<Destination[] | null>(null);
const [compareOpen, setCompareOpen] = useState(false);
const [matcherOpen, setMatcherOpen] = useState(false);
useEffect(() => {
const onMatcher = () => setMatcherOpen(true);
window.addEventListener("open-matcher", onMatcher);
return () => window.removeEventListener("open-matcher", onMatcher);
}, []);
useEffect(() => {
const reveals = document.querySelectorAll(".reveal");
const observer = new IntersectionObserver(
(entries) => entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("visible"); observer.unobserve(e.target); } }),
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" }
);
reveals.forEach((el, i) => {
(el as HTMLElement).style.transitionDelay = `${(i % 6) * 0.1}s`;
observer.observe(el);
});
return () => observer.disconnect();
}, []);
const toggleCompare = (slug: string) => {
setCompareList((prev) => {
if (prev.includes(slug)) return prev.filter((s) => s !== slug);
if (prev.length >= 4) return prev;
return [...prev, slug];
});
setCompareResults(null);
};
const runCompare = async () => {
if (compareList.length < 2) return;
try {
const data = await api.compareDestinations(compareList);
setCompareResults(data);
setCompareOpen(true);
} catch { /* ignore */ }
};
return (
<>
<ScrollProgress />
<Loader />
<ParticleCanvas />
<Navbar />
<GlobalSearch />
<TickerBar messages={props.ticker} />
<Hero stats={props.stats} />
<WorldMap destinations={props.destinations} />
<Destinations destinations={props.destinations} onCompare={toggleCompare} compareList={compareList} onOpenMatcher={() => setMatcherOpen(true)} />
<SpinGlobe destinations={props.destinations} />
<DailyQuote />
<TimezoneBoard destinations={props.destinations} />
<JetLagEstimator destinations={props.destinations} />
<TripPlanner destinations={props.destinations} />
<Calculator destinations={props.destinations} />
<CostCompare destinations={props.destinations} />
<SavingsGoal destinations={props.destinations} />
<ExpenseTracker />
<TaxDayCounter />
<FlightCost destinations={props.destinations} />
<CurrencyConverter />
<WifiWorkScore destinations={props.destinations} />
<FocusTimer />
<NomadScore />
<Lifestyle />
<Charts {...props.charts} />
<VisaSection visas={props.visas} />
<InsuranceGuide />
<EmergencyContacts destinations={props.destinations} />
<AtmCashGuide destinations={props.destinations} />
<CoworkingGuide />
<HousingGuide destinations={props.destinations} />
<SimGuide destinations={props.destinations} />
<PowerPlugGuide destinations={props.destinations} />
<CafeGuide destinations={props.destinations} />
<SeasonGuide destinations={props.destinations} />
<PackingChecklist />
<ArrivalChecklist destinations={props.destinations} />
<Phrasebook destinations={props.destinations} />
<CityNotes destinations={props.destinations} />
<NomadEvents />
<Tools tools={props.tools} />
<BlogSection posts={props.blog} />
<FAQSection faqs={props.faqs} />
<Community testimonials={props.testimonials} />
<Footer />
<BackToTop />
<SectionNav />
<MatcherTrigger onClick={() => setMatcherOpen(true)} />
<DestinationMatcher destinations={props.destinations} open={matcherOpen} onClose={() => setMatcherOpen(false)} />
<KeyboardShortcuts onOpenMatcher={() => setMatcherOpen(true)} />
<CookieConsent />
<NomadTips />
<FeedbackWidget />
<OnboardingTour />
{compareList.length > 0 && (
<div className="compare-bar">
<span>⚖️ 已选 {compareList.length}/4</span>
<button className="btn btn-primary" onClick={runCompare} disabled={compareList.length < 2}>对比城市</button>
<button className="btn btn-ghost" onClick={() => { setCompareList([]); setCompareResults(null); }}>清空</button>
</div>
)}
{compareOpen && compareResults && (
<CompareModal destinations={compareResults} onClose={() => setCompareOpen(false)} />
)}
</>
);
}