Restore guest city panels while keeping Pro data protected
This commit is contained in:
@@ -131,6 +131,17 @@ export function DetailPanel() {
|
|||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const detail = store.selectedDetail;
|
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 = store.selectedCity
|
||||||
|
? store.citySummariesByName[store.selectedCity] || null
|
||||||
|
: null;
|
||||||
|
const isBasicGuestPanel = !detail && Boolean(store.selectedCity && selectedCityItem && selectedSummary);
|
||||||
const isPro = store.proAccess.subscriptionActive;
|
const isPro = store.proAccess.subscriptionActive;
|
||||||
const panelRef = useRef<HTMLElement | null>(null);
|
const panelRef = useRef<HTMLElement | null>(null);
|
||||||
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
||||||
@@ -279,7 +290,7 @@ export function DetailPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
{!detail ? (
|
{!detail && !isBasicGuestPanel ? (
|
||||||
<section>
|
<section>
|
||||||
<div style={{ color: "var(--text-muted)", fontSize: "13px" }}>
|
<div style={{ color: "var(--text-muted)", fontSize: "13px" }}>
|
||||||
{store.loadingState.cityDetail
|
{store.loadingState.cityDetail
|
||||||
@@ -289,18 +300,78 @@ export function DetailPanel() {
|
|||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{isBasicGuestPanel && selectedCityItem && selectedSummary ? (
|
||||||
|
<>
|
||||||
|
<section className="detail-section">
|
||||||
|
<h3>{t("detail.profile")}</h3>
|
||||||
|
<div className="detail-grid">
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "City" : "城市"}</span>
|
||||||
|
<span className="detail-value">{selectedCityItem.display_name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Current" : "当前温度"}</span>
|
||||||
|
<span className="detail-value">
|
||||||
|
{selectedSummary.current?.temp != null
|
||||||
|
? `${selectedSummary.current.temp}${selectedSummary.temp_symbol || "°C"}`
|
||||||
|
: t("common.na")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Observation" : "观测时间"}</span>
|
||||||
|
<span className="detail-value">{selectedSummary.current?.obs_time || t("common.na")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "DEB" : "DEB 预测"}</span>
|
||||||
|
<span className="detail-value">
|
||||||
|
{selectedSummary.deb?.prediction != null
|
||||||
|
? `${selectedSummary.deb.prediction}${selectedSummary.temp_symbol || "°C"}`
|
||||||
|
: t("common.na")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Settlement" : "结算口径"}</span>
|
||||||
|
<span className="detail-value">
|
||||||
|
{selectedSummary.current?.settlement_source_label ||
|
||||||
|
selectedCityItem.settlement_source_label ||
|
||||||
|
t("common.na")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">{t("section.airport")}</span>
|
||||||
|
<span className="detail-value">{selectedCityItem.airport || t("common.na")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="detail-section">
|
||||||
|
<div className="detail-card">
|
||||||
|
<span className="detail-label">
|
||||||
|
{locale === "en-US" ? "Pro required for intraday analysis and history" : "今日日内分析与历史对账需开通 Pro"}
|
||||||
|
</span>
|
||||||
|
<span className="detail-source-note" style={{ marginTop: 8 }}>
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Guests can browse the city overview here. Sign in and subscribe to unlock the full intraday model, history reconciliation, and market-linked weather analysis."
|
||||||
|
: "游客可先浏览城市概览。登录并开通 Pro 后,可查看完整的今日日内分析、历史对账和市场联动天气解读。"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isBasicGuestPanel ? (
|
||||||
|
<>
|
||||||
<section className="detail-scenery-card">
|
<section className="detail-scenery-card">
|
||||||
{scenery ? (
|
{scenery ? (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
className="detail-scenery-image"
|
className="detail-scenery-image"
|
||||||
src={scenery.imageUrl}
|
src={scenery.imageUrl}
|
||||||
alt={t("detail.sceneryAlt", { city: detail.display_name })}
|
alt={t("detail.sceneryAlt", { city: detail?.display_name || "" })}
|
||||||
/>
|
/>
|
||||||
<div className="detail-scenery-overlay">
|
<div className="detail-scenery-overlay">
|
||||||
<div className="detail-scenery-copy">
|
<div className="detail-scenery-copy">
|
||||||
<span className="detail-scenery-kicker">
|
<span className="detail-scenery-kicker">
|
||||||
{detail.display_name}
|
{detail?.display_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -316,7 +387,7 @@ export function DetailPanel() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="detail-scenery-fallback">
|
<div className="detail-scenery-fallback">
|
||||||
<span className="detail-scenery-kicker">
|
<span className="detail-scenery-kicker">
|
||||||
{detail.display_name}
|
{detail?.display_name}
|
||||||
</span>
|
</span>
|
||||||
<strong className="detail-scenery-title">
|
<strong className="detail-scenery-title">
|
||||||
{t("detail.sceneryTitle")}
|
{t("detail.sceneryTitle")}
|
||||||
@@ -370,13 +441,15 @@ export function DetailPanel() {
|
|||||||
<section className="detail-section rounded-2xl">
|
<section className="detail-section rounded-2xl">
|
||||||
<h3>{t("detail.todayMiniTrend")}</h3>
|
<h3>{t("detail.todayMiniTrend")}</h3>
|
||||||
{heavyContentReady ? (
|
{heavyContentReady ? (
|
||||||
<DetailMiniTemperatureChart detail={detail} />
|
<DetailMiniTemperatureChart detail={detail!} />
|
||||||
) : (
|
) : (
|
||||||
<div className="detail-mini-meta">{t("detail.loading")}</div>
|
<div className="detail-mini-meta">{t("detail.loading")}</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{heavyContentReady ? <ForecastTable /> : null}
|
{heavyContentReady ? <ForecastTable /> : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ export function DashboardStoreProvider({
|
|||||||
const [proAccess, setProAccess] = useState<ProAccessState>(
|
const [proAccess, setProAccess] = useState<ProAccessState>(
|
||||||
getInitialProAccessState,
|
getInitialProAccessState,
|
||||||
);
|
);
|
||||||
|
const proAccessRef = useRef<ProAccessState>(getInitialProAccessState());
|
||||||
|
|
||||||
const mapStopMotionRef = useRef<() => void>(() => {});
|
const mapStopMotionRef = useRef<() => void>(() => {});
|
||||||
const hydratedSelectionRef = useRef(false);
|
const hydratedSelectionRef = useRef(false);
|
||||||
@@ -254,6 +255,10 @@ export function DashboardStoreProvider({
|
|||||||
citySummariesRef.current = citySummariesByName;
|
citySummariesRef.current = citySummariesByName;
|
||||||
}, [citySummariesByName]);
|
}, [citySummariesByName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
proAccessRef.current = proAccess;
|
||||||
|
}, [proAccess]);
|
||||||
|
|
||||||
const scheduleBackgroundDetailRefresh = (
|
const scheduleBackgroundDetailRefresh = (
|
||||||
cityName: string,
|
cityName: string,
|
||||||
cached: CityDetail,
|
cached: CityDetail,
|
||||||
@@ -481,6 +486,14 @@ export function DashboardStoreProvider({
|
|||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
setSelectedForecastDate(null);
|
setSelectedForecastDate(null);
|
||||||
setFutureModalDate(null);
|
setFutureModalDate(null);
|
||||||
|
|
||||||
|
if (proAccessRef.current.loading) {
|
||||||
|
await refreshProAccess();
|
||||||
|
}
|
||||||
|
const access = proAccessRef.current;
|
||||||
|
if (!access.authenticated || !access.subscriptionActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
||||||
try {
|
try {
|
||||||
const detail = await ensureCityDetail(cityName);
|
const detail = await ensureCityDetail(cityName);
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ export interface CitySummary {
|
|||||||
current?: {
|
current?: {
|
||||||
temp?: number | null;
|
temp?: number | null;
|
||||||
obs_time?: string | null;
|
obs_time?: string | null;
|
||||||
|
settlement_source?: string | null;
|
||||||
|
settlement_source_label?: string | null;
|
||||||
};
|
};
|
||||||
deb?: {
|
deb?: {
|
||||||
prediction?: number | null;
|
prediction?: number | null;
|
||||||
|
|||||||
@@ -37,12 +37,19 @@ function isStaticAsset(pathname: string) {
|
|||||||
|
|
||||||
function isPublicPage(pathname: string) {
|
function isPublicPage(pathname: string) {
|
||||||
return (
|
return (
|
||||||
|
pathname === "/" ||
|
||||||
|
pathname.startsWith("/docs") ||
|
||||||
|
pathname.startsWith("/subscription-help") ||
|
||||||
pathname === "/entitlement-required" ||
|
pathname === "/entitlement-required" ||
|
||||||
pathname.startsWith("/auth/login") ||
|
pathname.startsWith("/auth/login") ||
|
||||||
pathname.startsWith("/auth/callback")
|
pathname.startsWith("/auth/callback")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPublicApi(pathname: string) {
|
||||||
|
return pathname === "/api/cities" || /^\/api\/city\/[^/]+\/summary$/i.test(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
function handleLegacyTokenGate(request: NextRequest) {
|
function handleLegacyTokenGate(request: NextRequest) {
|
||||||
const requiredToken = process.env.POLYWEATHER_DASHBOARD_ACCESS_TOKEN?.trim();
|
const requiredToken = process.env.POLYWEATHER_DASHBOARD_ACCESS_TOKEN?.trim();
|
||||||
if (!requiredToken) {
|
if (!requiredToken) {
|
||||||
@@ -50,7 +57,7 @@ function handleLegacyTokenGate(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { pathname, searchParams } = request.nextUrl;
|
const { pathname, searchParams } = request.nextUrl;
|
||||||
if (isStaticAsset(pathname) || isPublicPage(pathname)) {
|
if (isStaticAsset(pathname) || isPublicPage(pathname) || isPublicApi(pathname)) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +98,7 @@ function handleLegacyTokenGate(request: NextRequest) {
|
|||||||
|
|
||||||
async function handleSupabaseAuthGate(request: NextRequest) {
|
async function handleSupabaseAuthGate(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
if (isPublicPage(pathname)) {
|
if (isPublicPage(pathname) || isPublicApi(pathname)) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
if (pathname.startsWith("/api/")) {
|
if (pathname.startsWith("/api/")) {
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ async def metrics():
|
|||||||
|
|
||||||
@router.get("/api/cities")
|
@router.get("/api/cities")
|
||||||
async def list_cities(request: Request):
|
async def list_cities(request: Request):
|
||||||
_assert_entitlement(request)
|
|
||||||
try:
|
try:
|
||||||
out = []
|
out = []
|
||||||
for name, info in CITIES.items():
|
for name, info in CITIES.items():
|
||||||
@@ -630,7 +629,6 @@ async def payment_reconcile_latest(request: Request):
|
|||||||
|
|
||||||
@router.get("/api/city/{name}/summary")
|
@router.get("/api/city/{name}/summary")
|
||||||
async def city_summary(request: Request, name: str, force_refresh: bool = False):
|
async def city_summary(request: Request, name: str, force_refresh: bool = False):
|
||||||
_assert_entitlement(request)
|
|
||||||
data = _analyze(_normalize_city_or_404(name), force_refresh=force_refresh)
|
data = _analyze(_normalize_city_or_404(name), force_refresh=force_refresh)
|
||||||
return _build_city_summary_payload(data)
|
return _build_city_summary_payload(data)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user