feat(stats): expose 365d trends range in dashboard UI

Add '365d' to the client TrendRange union, the TimeRange hook type, and
the DateRangeSelector segmented control so users can select a 365-day
window in the trends dashboard.
This commit is contained in:
2026-04-09 00:44:31 -07:00
parent 76547bb96e
commit 364f7aacb7
4 changed files with 52 additions and 3 deletions

View File

@@ -53,7 +53,7 @@ export function DateRangeSelector({
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-4 text-sm">
<SegmentedControl <SegmentedControl
label="Range" label="Range"
options={['7d', '30d', '90d', 'all'] as TimeRange[]} options={['7d', '30d', '90d', '365d', 'all'] as TimeRange[]}
value={range} value={range}
onChange={onRangeChange} onChange={onRangeChange}
formatLabel={(r) => (r === 'all' ? 'All' : r)} formatLabel={(r) => (r === 'all' ? 'All' : r)}

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { getStatsClient } from './useStatsApi'; import { getStatsClient } from './useStatsApi';
import type { TrendsDashboardData } from '../types/stats'; import type { TrendsDashboardData } from '../types/stats';
export type TimeRange = '7d' | '30d' | '90d' | 'all'; export type TimeRange = '7d' | '30d' | '90d' | '365d' | 'all';
export type GroupBy = 'day' | 'month'; export type GroupBy = 'day' | 'month';
export function useTrends(range: TimeRange, groupBy: GroupBy) { export function useTrends(range: TimeRange, groupBy: GroupBy) {

View File

@@ -115,6 +115,55 @@ test('getTrendsDashboard requests the chart-ready trends endpoint with range and
} }
}); });
test('getTrendsDashboard accepts 365d range and builds correct URL', async () => {
const originalFetch = globalThis.fetch;
let seenUrl = '';
globalThis.fetch = (async (input: RequestInfo | URL) => {
seenUrl = String(input);
return new Response(
JSON.stringify({
activity: { watchTime: [], cards: [], words: [], sessions: [] },
progress: {
watchTime: [],
sessions: [],
words: [],
newWords: [],
cards: [],
episodes: [],
lookups: [],
},
ratios: { lookupsPerHundred: [] },
animePerDay: {
episodes: [],
watchTime: [],
cards: [],
words: [],
lookups: [],
lookupsPerHundred: [],
},
animeCumulative: {
watchTime: [],
episodes: [],
cards: [],
words: [],
},
patterns: {
watchTimeByDayOfWeek: [],
watchTimeByHour: [],
},
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
}) as typeof globalThis.fetch;
try {
await apiClient.getTrendsDashboard('365d', 'day');
assert.equal(seenUrl, `${BASE_URL}/api/stats/trends/dashboard?range=365d&groupBy=day`);
} finally {
globalThis.fetch = originalFetch;
}
});
test('getSessionEvents can request only specific event types', async () => { test('getSessionEvents can request only specific event types', async () => {
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
let seenUrl = ''; let seenUrl = '';

View File

@@ -116,7 +116,7 @@ export const apiClient = {
fetchJson<NewAnimePerDay[]>(`/api/stats/trends/new-anime-per-day?limit=${limit}`), fetchJson<NewAnimePerDay[]>(`/api/stats/trends/new-anime-per-day?limit=${limit}`),
getWatchTimePerAnime: (limit = 90) => getWatchTimePerAnime: (limit = 90) =>
fetchJson<WatchTimePerAnime[]>(`/api/stats/trends/watch-time-per-anime?limit=${limit}`), fetchJson<WatchTimePerAnime[]>(`/api/stats/trends/watch-time-per-anime?limit=${limit}`),
getTrendsDashboard: (range: '7d' | '30d' | '90d' | 'all', groupBy: 'day' | 'month') => getTrendsDashboard: (range: '7d' | '30d' | '90d' | '365d' | 'all', groupBy: 'day' | 'month') =>
fetchJson<TrendsDashboardData>( fetchJson<TrendsDashboardData>(
`/api/stats/trends/dashboard?range=${encodeURIComponent(range)}&groupBy=${encodeURIComponent(groupBy)}`, `/api/stats/trends/dashboard?range=${encodeURIComponent(range)}&groupBy=${encodeURIComponent(groupBy)}`,
), ),