feat: optimize stats dashboard data and components

This commit is contained in:
2026-03-17 00:48:56 -07:00
parent 11710f20db
commit 390ae1b2f2
24 changed files with 837 additions and 174 deletions

View File

@@ -9,14 +9,34 @@ export function useAnimeDetail(animeId: number | null) {
const [reloadKey, setReloadKey] = useState(0);
useEffect(() => {
if (animeId === null) return;
let cancelled = false;
if (animeId === null) {
setData(null);
setLoading(false);
setError(null);
return () => {
cancelled = true;
};
}
setLoading(true);
setError(null);
getStatsClient()
.getAnimeDetail(animeId)
.then(setData)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
.then((next) => {
if (cancelled) return;
setData(next);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err.message);
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, [animeId, reloadKey]);
const reload = useCallback(() => setReloadKey((k) => k + 1), []);

View File

@@ -8,14 +8,34 @@ export function useKanjiDetail(kanjiId: number | null) {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (kanjiId === null) return;
let cancelled = false;
if (kanjiId === null) {
setData(null);
setLoading(false);
setError(null);
return () => {
cancelled = true;
};
}
setLoading(true);
setError(null);
getStatsClient()
.getKanjiDetail(kanjiId)
.then(setData)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
.then((next) => {
if (cancelled) return;
setData(next);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err.message);
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, [kanjiId]);
return { data, loading, error };

View File

@@ -8,14 +8,34 @@ export function useMediaDetail(videoId: number | null) {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (videoId === null) return;
let cancelled = false;
if (videoId === null) {
setData(null);
setLoading(false);
setError(null);
return () => {
cancelled = true;
};
}
setLoading(true);
setError(null);
getStatsClient()
.getMediaDetail(videoId)
.then(setData)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
.then((next) => {
if (cancelled) return;
setData(next);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err.message);
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, [videoId]);
return { data, loading, error };

View File

@@ -8,11 +8,26 @@ export function useMediaLibrary() {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
getStatsClient()
.getMediaLibrary()
.then(setMedia)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
.then((rows) => {
if (cancelled) return;
setMedia(rows);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err.message);
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return { media, loading, error };

View File

@@ -9,14 +9,27 @@ export function useOverview() {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
const client = getStatsClient();
Promise.all([client.getOverview(), client.getSessions(50)])
.then(([overview, allSessions]) => {
if (cancelled) return;
setData(overview);
setSessions(allSessions);
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
.catch((err) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return { data, sessions, loading, error };

View File

@@ -35,12 +35,19 @@ export function useTrends(range: TimeRange, groupBy: GroupBy) {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
const client = getStatsClient();
const limitMap: Record<TimeRange, number> = { '7d': 7, '30d': 30, '90d': 90, all: 365 };
const limit = limitMap[range];
const monthlyLimit = Math.max(1, Math.ceil(limit / 30));
const sessionsLimitMap: Record<TimeRange, number> = {
'7d': 200,
'30d': 500,
'90d': 500,
all: 500,
};
const rollupFetcher =
groupBy === 'month' ? client.getMonthlyRollups(monthlyLimit) : client.getDailyRollups(limit);
@@ -50,23 +57,43 @@ export function useTrends(range: TimeRange, groupBy: GroupBy) {
client.getEpisodesPerDay(limit),
client.getNewAnimePerDay(limit),
client.getWatchTimePerAnime(limit),
client.getSessions(500),
client.getSessions(sessionsLimitMap[range]),
client.getAnimeLibrary(),
])
.then(
([rollups, episodesPerDay, newAnimePerDay, watchTimePerAnime, sessions, animeLibrary]) => {
if (cancelled) return;
const now = new Date();
const localMidnight = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
).getTime();
const cutoffMs =
range === 'all' ? null : localMidnight - (limitMap[range] - 1) * 86_400_000;
const filteredSessions =
cutoffMs == null ? sessions : sessions.filter((s) => s.startedAtMs >= cutoffMs);
setData({
rollups,
episodesPerDay,
newAnimePerDay,
watchTimePerAnime,
sessions,
sessions: filteredSessions,
animeLibrary,
});
},
)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
.catch((err) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, [range, groupBy]);
return { data, loading, error };

View File

@@ -10,11 +10,13 @@ export function useVocabulary() {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
const client = getStatsClient();
Promise.allSettled([client.getVocabulary(500), client.getKanji(200), client.getKnownWords()])
.then(([wordsResult, kanjiResult, knownResult]) => {
if (cancelled) return;
const errors: string[] = [];
if (wordsResult.status === 'fulfilled') {
@@ -37,7 +39,13 @@ export function useVocabulary() {
setError(errors.join('; '));
}
})
.finally(() => setLoading(false));
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return { words, kanji, knownWords, loading, error };

View File

@@ -8,14 +8,34 @@ export function useWordDetail(wordId: number | null) {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (wordId === null) return;
let cancelled = false;
if (wordId === null) {
setData(null);
setLoading(false);
setError(null);
return () => {
cancelled = true;
};
}
setLoading(true);
setError(null);
getStatsClient()
.getWordDetail(wordId)
.then(setData)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
.then((next) => {
if (cancelled) return;
setData(next);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err.message);
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, [wordId]);
return { data, loading, error };