feat: add ProbabilityHubPage component for monitoring city risk and market scan data
This commit is contained in:
@@ -64,13 +64,40 @@ function getPositiveEdge(detail?: CityDetail | null) {
|
|||||||
return Math.max(yesEdge, noEdge, Number.NEGATIVE_INFINITY);
|
return Math.max(yesEdge, noEdge, Number.NEGATIVE_INFINITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isActiveMarketScan(detail?: CityDetail | null) {
|
||||||
|
const scan = detail?.market_scan;
|
||||||
|
if (!scan?.available) return false;
|
||||||
|
const primary = scan.primary_market as
|
||||||
|
| ({
|
||||||
|
active?: boolean | null;
|
||||||
|
closed?: boolean | null;
|
||||||
|
accepting_orders?: boolean | null;
|
||||||
|
tradable?: boolean | null;
|
||||||
|
ended_at_utc?: string | null;
|
||||||
|
} & Record<string, unknown>)
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
if (!primary) return false;
|
||||||
|
if (primary.closed === true) return false;
|
||||||
|
if (primary.active === false) return false;
|
||||||
|
if (primary.accepting_orders === false) return false;
|
||||||
|
if (primary.tradable === false) return false;
|
||||||
|
if (primary.ended_at_utc) {
|
||||||
|
const endedAt = new Date(String(primary.ended_at_utc)).getTime();
|
||||||
|
if (Number.isFinite(endedAt) && endedAt <= Date.now()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function hasMarket(detail?: CityDetail | null) {
|
function hasMarket(detail?: CityDetail | null) {
|
||||||
|
const scan = detail?.market_scan;
|
||||||
return Boolean(
|
return Boolean(
|
||||||
detail?.market_scan?.available &&
|
isActiveMarketScan(detail) &&
|
||||||
((Array.isArray(detail.market_scan.all_buckets) &&
|
scan &&
|
||||||
detail.market_scan.all_buckets.length > 0) ||
|
((Array.isArray(scan.all_buckets) && scan.all_buckets.length > 0) ||
|
||||||
(Array.isArray(detail.market_scan.top_buckets) &&
|
(Array.isArray(scan.top_buckets) && scan.top_buckets.length > 0)),
|
||||||
detail.market_scan.top_buckets.length > 0)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +165,7 @@ function ProbabilityHubScreen() {
|
|||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
batch.map((city) =>
|
batch.map((city) =>
|
||||||
dashboardClient.getCityMarketScan(city.name, {
|
dashboardClient.getCityMarketScan(city.name, {
|
||||||
force: true,
|
force: false,
|
||||||
targetDate: detailMap[city.name]?.local_date || null,
|
targetDate: detailMap[city.name]?.local_date || null,
|
||||||
marketSlug: detailMap[city.name]?.market_scan?.selected_slug || null,
|
marketSlug: detailMap[city.name]?.market_scan?.selected_slug || null,
|
||||||
}),
|
}),
|
||||||
@@ -187,18 +214,22 @@ function ProbabilityHubScreen() {
|
|||||||
setLastUpdatedAt(new Date().toISOString());
|
setLastUpdatedAt(new Date().toISOString());
|
||||||
await refreshMarketScans(fetched, cityList);
|
await refreshMarketScans(fetched, cityList);
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
setError(
|
if (!Object.keys(detailsByName).length && !cities.length) {
|
||||||
loadError instanceof Error
|
setError(
|
||||||
? loadError.message
|
loadError instanceof Error
|
||||||
: locale === "en-US"
|
? loadError.message
|
||||||
? "Failed to load probability hub"
|
: locale === "en-US"
|
||||||
: "加载概率页失败",
|
? "Failed to load probability hub"
|
||||||
);
|
: "加载概率页失败",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn("probability hub refresh failed", loadError);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
}, [cities.length, fetchCityDetails, locale, refreshMarketScans]);
|
}, [cities.length, detailsByName, fetchCityDetails, locale, refreshMarketScans]);
|
||||||
|
|
||||||
const retryMissingCities = useCallback(async () => {
|
const retryMissingCities = useCallback(async () => {
|
||||||
if (!cities.length) return;
|
if (!cities.length) return;
|
||||||
@@ -230,7 +261,7 @@ function ProbabilityHubScreen() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
if (document.visibilityState === "hidden") return;
|
if (document.visibilityState === "hidden") return;
|
||||||
void loadAll(true);
|
void loadAll(false);
|
||||||
}, FULL_REFRESH_INTERVAL_MS);
|
}, FULL_REFRESH_INTERVAL_MS);
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
@@ -508,11 +539,18 @@ function ProbabilityHubScreen() {
|
|||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ProbabilityDistribution
|
{(() => {
|
||||||
detail={detail}
|
const activeMarketScan = isActiveMarketScan(detail)
|
||||||
hideTitle
|
? detail.market_scan
|
||||||
marketScan={detail.market_scan}
|
: null;
|
||||||
/>
|
return (
|
||||||
|
<ProbabilityDistribution
|
||||||
|
detail={detail}
|
||||||
|
hideTitle
|
||||||
|
marketScan={activeMarketScan}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user