Files
goodgo-platform/apps/web/lib/hooks/use-analytics.ts
Ho Ngoc Hai b9a1a24f65
Some checks failed
Security Scanning / Security Gate (push) Failing after 2s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Blocked by required conditions
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 6s
CI / AI Services (Python) — Smoke (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 5s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 5s
Security Scanning / Trivy Scan — Web Image (push) Failing after 37s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 43s
Deploy / Build API Image (push) Failing after 7s
Deploy / Build Web Image (push) Failing after 5s
E2E Tests / Playwright E2E (push) Failing after 8s
Security Scanning / Trivy Scan — API Image (push) Failing after 37s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 30s
Security Scanning / Trivy Filesystem Scan (push) Failing after 30s
fix(web): homepage analytics — auth gate, district dedup, district name normalize
Three issues found while auditing the homepage:

1. Analytics queries never fired for authed visitors. The
   `useAuthedAnalytics()` gate required `isInitialized && isAuthenticated`
   but the React subscription to the auth store occasionally lagged behind
   the cookie-based `initialize()` flow, leaving every panel stuck on
   "Đang tải..." even though the cookie + profile API responded fine.
   Drop the `isAuthenticated` requirement — anon users now fire one query
   that returns 401 and the components fall back to empty states (cheaper
   UX cost than a perpetually empty homepage for authed users).

2. "Top khu vực" table had React duplicate-key warnings + showed Q1
   three times etc. The backend returns one row per (district ×
   propertyType) — 24 rows for 8 districts. Aggregate to one row per
   district with listing-count-weighted averages for price/yoy/days.

3. Seed used "Thủ Đức" in some properties and "Thành phố Thủ Đức" in
   others, causing the same physical district to appear twice everywhere.
   Normalize seed.ts to always use "Thành phố Thủ Đức" (matches the
   admin Vn districts canonical form).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:39:18 +07:00

111 lines
3.8 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { analyticsApi } from '@/lib/analytics-api';
import { useAuthStore } from '@/lib/auth-store';
export const analyticsKeys = {
all: ['analytics'] as const,
marketReport: (city: string, period: string) =>
['analytics', 'market-report', city, period] as const,
heatmap: (city: string, period: string) =>
['analytics', 'heatmap', city, period] as const,
districtStats: (city: string, period: string) =>
['analytics', 'district-stats', city, period] as const,
priceTrend: (district: string, city: string, propertyType: string, periods: string[]) =>
['analytics', 'price-trend', district, city, propertyType, periods] as const,
marketSnapshot: (city: string) =>
['analytics', 'market-snapshot', city] as const,
priceMovers: (direction: 'up' | 'down', period: string) =>
['analytics', 'price-movers', direction, period] as const,
trendingAreas: (period: number) =>
['analytics', 'trending-areas', period] as const,
};
/**
* Analytics endpoints currently require authentication on the backend. We
* gate React Query hooks on `isInitialized` (not `isAuthenticated`) so that:
*
* - Authenticated visitors fire queries the moment `initialize()` finishes,
* even if the React subscription to `isAuthenticated` lags a tick behind
* (we previously saw the homepage stay stuck on "Đang tải..." because the
* gate stayed `false` after the first render and React-Query never refetched).
* - Anonymous visitors fire one request that returns 401 — react-query
* handles this gracefully (silent toast-less rejection in api-client) and
* the components fall back to empty states.
*
* The 401 cost for anon users is preferable to a perpetually empty homepage
* for authed users.
*/
function useAuthedAnalytics() {
const isInitialized = useAuthStore((s) => s.isInitialized);
return isInitialized;
}
export function useMarketReport(city: string, period: string) {
const enabled = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.marketReport(city, period),
queryFn: () => analyticsApi.getMarketReport(city, period),
enabled,
});
}
export function useHeatmap(city: string, period: string) {
const enabled = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.heatmap(city, period),
queryFn: () => analyticsApi.getHeatmap(city, period),
enabled,
});
}
export function useDistrictStats(city: string, period: string) {
const enabled = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.districtStats(city, period),
queryFn: () => analyticsApi.getDistrictStats(city, period),
enabled,
});
}
export function usePriceTrend(
district: string,
city: string,
propertyType: string,
periods: string[],
) {
const authed = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.priceTrend(district, city, propertyType, periods),
queryFn: () => analyticsApi.getPriceTrend(district, city, propertyType, periods),
enabled: authed && !!district && !!city,
});
}
export function useMarketSnapshot(city: string) {
const enabled = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.marketSnapshot(city),
queryFn: () => analyticsApi.getMarketSnapshot(city),
refetchInterval: 5 * 60 * 1000,
enabled,
});
}
export function usePriceMovers(direction: 'up' | 'down', period = '7d', limit = 5) {
const enabled = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.priceMovers(direction, period),
queryFn: () => analyticsApi.getPriceMovers(direction, period, limit),
enabled,
});
}
export function useTrendingAreas(period = 7, limit = 10) {
const enabled = useAuthedAnalytics();
return useQuery({
queryKey: analyticsKeys.trendingAreas(period),
queryFn: () => analyticsApi.getTrendingAreas(period, limit),
enabled,
});
}