94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
|
|
// Fetches the daily market aggregate series for a given listing-type +
|
||
|
|
// bedroom band. Re-fetches when the inputs change. Returns the raw array
|
||
|
|
// of points plus a derived "now vs N days ago" delta the strip renders.
|
||
|
|
|
||
|
|
import { useEffect, useState } from 'react';
|
||
|
|
import type { AuthUser } from '@/auth/types';
|
||
|
|
import type { MarketTrendPoint } from '@/types';
|
||
|
|
import { apiRequest } from '@/services/apiClient';
|
||
|
|
|
||
|
|
export interface MarketTrendDelta {
|
||
|
|
metric: 'median_total_price' | 'median_qmprice' | 'listing_count';
|
||
|
|
current: number | null;
|
||
|
|
previous: number | null;
|
||
|
|
changePct: number | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface UseMarketTrendResult {
|
||
|
|
series: MarketTrendPoint[];
|
||
|
|
isLoading: boolean;
|
||
|
|
error: string | null;
|
||
|
|
// Convenience: today's value vs the oldest in-window value.
|
||
|
|
deltas: Record<MarketTrendDelta['metric'], MarketTrendDelta>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildDelta(
|
||
|
|
metric: MarketTrendDelta['metric'],
|
||
|
|
series: MarketTrendPoint[],
|
||
|
|
): MarketTrendDelta {
|
||
|
|
if (series.length < 2) {
|
||
|
|
const only = series[0];
|
||
|
|
return {
|
||
|
|
metric,
|
||
|
|
current: only ? (only[metric] as number | null) : null,
|
||
|
|
previous: null,
|
||
|
|
changePct: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const current = series[series.length - 1][metric] as number | null;
|
||
|
|
const previous = series[0][metric] as number | null;
|
||
|
|
if (current === null || previous === null || previous === 0) {
|
||
|
|
return { metric, current, previous, changePct: null };
|
||
|
|
}
|
||
|
|
const changePct = Math.round(((current - previous) / previous) * 1000) / 10;
|
||
|
|
return { metric, current, previous, changePct };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useMarketTrend(
|
||
|
|
user: AuthUser | null,
|
||
|
|
listingType: 'RENT' | 'BUY',
|
||
|
|
minBedrooms: number,
|
||
|
|
maxBedrooms: number,
|
||
|
|
days: number = 30,
|
||
|
|
): UseMarketTrendResult {
|
||
|
|
const [series, setSeries] = useState<MarketTrendPoint[]>([]);
|
||
|
|
const [isLoading, setIsLoading] = useState(false);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!user) return;
|
||
|
|
let cancelled = false;
|
||
|
|
setIsLoading(true);
|
||
|
|
setError(null);
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
listing_type: listingType,
|
||
|
|
min_bedrooms: String(minBedrooms),
|
||
|
|
max_bedrooms: String(maxBedrooms),
|
||
|
|
days: String(days),
|
||
|
|
});
|
||
|
|
apiRequest<MarketTrendPoint[]>(user, `/api/market_trend?${params}`)
|
||
|
|
.then((data) => {
|
||
|
|
if (cancelled) return;
|
||
|
|
setSeries(data);
|
||
|
|
})
|
||
|
|
.catch((err: Error) => {
|
||
|
|
if (cancelled) return;
|
||
|
|
setError(err.message);
|
||
|
|
})
|
||
|
|
.finally(() => {
|
||
|
|
if (!cancelled) setIsLoading(false);
|
||
|
|
});
|
||
|
|
return () => {
|
||
|
|
cancelled = true;
|
||
|
|
};
|
||
|
|
}, [user, listingType, minBedrooms, maxBedrooms, days]);
|
||
|
|
|
||
|
|
const deltas: UseMarketTrendResult['deltas'] = {
|
||
|
|
median_total_price: buildDelta('median_total_price', series),
|
||
|
|
median_qmprice: buildDelta('median_qmprice', series),
|
||
|
|
listing_count: buildDelta('listing_count', series),
|
||
|
|
};
|
||
|
|
|
||
|
|
return { series, isLoading, error, deltas };
|
||
|
|
}
|