mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-21 00:11:27 -07:00
feat: optimize stats dashboard data and components
This commit is contained in:
@@ -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), []);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user