189 lines
7.1 KiB
TypeScript
189 lines
7.1 KiB
TypeScript
"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)} />
|
||
)}
|
||
</>
|
||
);
|
||
}
|