"use client"; import clsx from "clsx"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { ForecastTable } from "@/components/dashboard/PanelSections"; import { useDashboardModal, useDashboardStore, useProAccess, } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; import { useRelativeTime } from "@/hooks/useRelativeTime"; import { getOfficialSourceLinks } from "@/lib/dashboard-official-sources"; import { trackAppEvent } from "@/lib/app-analytics"; import { getTodayPolymarketUrl } from "@/lib/polymarket-market-links"; import { getCityProfileStats } from "@/lib/dashboard-utils"; import { normalizeObservationSourceLabel } from "@/lib/source-labels"; import { getRiskBadgeLabel } from "@/lib/weather-summary-utils"; const DetailMiniTemperatureChart = dynamic( () => import("@/components/dashboard/DetailMiniTemperatureChart").then( (module) => module.DetailMiniTemperatureChart, ), { loading: () =>
, ssr: false, }, ); export function DetailPanel({ variant = "overlay", }: { variant?: "overlay" | "rail"; } = {}) { const store = useDashboardStore(); const modal = useDashboardModal(); const { proAccess } = useProAccess(); const { locale, t } = useI18n(); const router = useRouter(); const isRail = variant === "rail"; const detail = store.selectedDetail; const selectedCityItem = useMemo( () => store.selectedCity ? store.cities.find((city) => city.name === store.selectedCity) || null : null, [store.cities, store.selectedCity], ); const selectedSummary = useMemo( () => store.selectedCity ? store.citySummariesByName[store.selectedCity] || null : null, [store.citySummariesByName, store.selectedCity], ); const isPro = proAccess.subscriptionActive; const isAuthenticated = proAccess.authenticated; const panelRef = useRef(null); const lastDetailFetchedAtRef = useRef(0); useEffect(() => { if (detail?.name) { lastDetailFetchedAtRef.current = Date.now(); } }, [detail?.name]); const detailAgeText = useRelativeTime( lastDetailFetchedAtRef.current ? new Date(lastDetailFetchedAtRef.current).toISOString() : null, ); const [heavyContentReady, setHeavyContentReady] = useState(false); const isOverlayOpen = Boolean(modal.futureModalDate); const isVisible = isRail ? Boolean(store.selectedCity) && !isOverlayOpen : store.isPanelOpen && Boolean(store.selectedCity) && !isOverlayOpen; const hasBasicPanelContent = Boolean( detail || selectedSummary || selectedCityItem, ); const panelDisplayName = detail?.display_name || selectedSummary?.display_name || selectedCityItem?.display_name || store.selectedCity || "..."; const panelRiskLevel = detail?.risk?.level || selectedSummary?.risk?.level || selectedCityItem?.risk_level || "low"; const profileStats = useMemo( () => (detail ? getCityProfileStats(detail, locale) : []), [detail, locale], ); const officialLinks = useMemo( () => (detail ? getOfficialSourceLinks(detail) : []), [detail], ); const marketUrl = useMemo( () => getTodayPolymarketUrl(detail, locale), [detail, locale], ); const basicSettlementLabel = normalizeObservationSourceLabel( selectedSummary?.current?.settlement_source_label || selectedCityItem?.settlement_source_label || selectedCityItem?.settlement_source, locale === "en-US" ? "Settlement source pending" : "结算口径待确认", ); const basicAirportLabel = selectedCityItem?.airport || selectedSummary?.icao || (locale === "en-US" ? "Airport pending" : "机场待确认"); const heroSettlementLabel = normalizeObservationSourceLabel( detail?.current?.settlement_source_label, basicSettlementLabel, ); const heroAirportLabel = detail?.risk?.airport || basicAirportLabel; const isSparsePanelDetail = Boolean( detail && (detail.detail_depth !== "full" || (detail.forecast?.daily?.length ?? 0) <= 1), ); const isPanelSyncing = store.loadingState.cityDetail; const [panelSyncTimedOut, setPanelSyncTimedOut] = useState(false); const showPanelSyncing = isPanelSyncing && !panelSyncTimedOut; const isShowingCachedDetailDuringSync = false; const blurActiveElement = () => { if (typeof document === "undefined") return; const active = document.activeElement; if (active instanceof HTMLElement) { active.blur(); } }; const handleTodayAccess = () => { blurActiveElement(); if (isPro) { void modal.openTodayModal(); return; } trackAppEvent("paywall_feature_clicked", { entry: "detail_panel", feature: "today", city: store.selectedCity, user_state: isAuthenticated ? "logged_in" : "guest", }); router.push("/account"); }; useEffect(() => { if (!isPanelSyncing || !store.selectedCity) { setPanelSyncTimedOut(false); return; } setPanelSyncTimedOut(false); const timeoutId = window.setTimeout(() => { setPanelSyncTimedOut(true); }, 12_000); return () => { window.clearTimeout(timeoutId); }; }, [isPanelSyncing, store.selectedCity]); useEffect(() => { const panel = panelRef.current; if (!panel) return; if (!isVisible) { panel.setAttribute("inert", ""); if ( typeof document !== "undefined" && panel.contains(document.activeElement) ) { const active = document.activeElement; if (active instanceof HTMLElement) { active.blur(); } } return; } panel.removeAttribute("inert"); }, [isVisible]); useEffect(() => { if (!isVisible || !detail) { setHeavyContentReady(false); return; } let canceled = false; let timeoutId: number | null = null; let idleId: number | null = null; const win = typeof window !== "undefined" ? (window as any) : null; const markReady = () => { if (!canceled) { setHeavyContentReady(true); } }; if (win && typeof win.requestIdleCallback === "function") { idleId = win.requestIdleCallback(markReady, { timeout: 180 }); } else if (typeof window !== "undefined") { timeoutId = window.setTimeout(markReady, 80); } else { setHeavyContentReady(true); } return () => { canceled = true; if ( win && idleId != null && typeof win.cancelIdleCallback === "function" ) { win.cancelIdleCallback(idleId); } if (timeoutId != null && typeof window !== "undefined") { window.clearTimeout(timeoutId); } }; }, [detail, isVisible]); return ( ); }