-
{detail.canonicalTitle}
+
+ {detail.canonicalTitle}
+
{onDeleteEpisode != null ? (
) : null}
diff --git a/stats/src/components/overview/WatchTimeChart.tsx b/stats/src/components/overview/WatchTimeChart.tsx
index 7ab1e6c6..7a5d1d92 100644
--- a/stats/src/components/overview/WatchTimeChart.tsx
+++ b/stats/src/components/overview/WatchTimeChart.tsx
@@ -1,13 +1,5 @@
import { useState } from 'react';
-import {
- BarChart,
- Bar,
- CartesianGrid,
- XAxis,
- YAxis,
- Tooltip,
- ResponsiveContainer,
-} from 'recharts';
+import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import { epochDayToDate } from '../../lib/formatters';
import { CHART_DEFAULTS, CHART_THEME, TOOLTIP_CONTENT_STYLE } from '../../lib/chart-theme';
import type { DailyRollup } from '../../types/stats';
diff --git a/stats/src/components/sessions/SessionsTab.test.tsx b/stats/src/components/sessions/SessionsTab.test.tsx
index a91020e8..ebf17334 100644
--- a/stats/src/components/sessions/SessionsTab.test.tsx
+++ b/stats/src/components/sessions/SessionsTab.test.tsx
@@ -75,10 +75,7 @@ test('buildBucketDeleteHandler is a no-op when confirm returns false', async ()
let deleteCalled = false;
let successCalled = false;
- const bucket = makeBucket([
- makeSession({ sessionId: 1 }),
- makeSession({ sessionId: 2 }),
- ]);
+ const bucket = makeBucket([makeSession({ sessionId: 1 }), makeSession({ sessionId: 2 })]);
const handler = buildBucketDeleteHandler({
bucket,
@@ -104,10 +101,7 @@ test('buildBucketDeleteHandler reports errors via onError without calling onSucc
let errorMessage: string | null = null;
let successCalled = false;
- const bucket = makeBucket([
- makeSession({ sessionId: 1 }),
- makeSession({ sessionId: 2 }),
- ]);
+ const bucket = makeBucket([makeSession({ sessionId: 1 }), makeSession({ sessionId: 2 })]);
const handler = buildBucketDeleteHandler({
bucket,
diff --git a/stats/src/components/sessions/SessionsTab.tsx b/stats/src/components/sessions/SessionsTab.tsx
index 560c0d4d..c35e8c98 100644
--- a/stats/src/components/sessions/SessionsTab.tsx
+++ b/stats/src/components/sessions/SessionsTab.tsx
@@ -269,9 +269,7 @@ export function SessionsTab({
isExpanded={expandedId === s.sessionId}
detailsId={detailsId}
onToggle={() =>
- setExpandedId(
- expandedId === s.sessionId ? null : s.sessionId,
- )
+ setExpandedId(expandedId === s.sessionId ? null : s.sessionId)
}
onDelete={() => void handleDeleteSession(s)}
deleteDisabled={deletingSessionId === s.sessionId}
diff --git a/stats/src/components/vocabulary/FrequencyRankTable.test.tsx b/stats/src/components/vocabulary/FrequencyRankTable.test.tsx
index 50f0d113..37f64d2e 100644
--- a/stats/src/components/vocabulary/FrequencyRankTable.test.tsx
+++ b/stats/src/components/vocabulary/FrequencyRankTable.test.tsx
@@ -36,5 +36,8 @@ test('omits reading when reading equals headword', () => {
,
);
assert.ok(markup.includes('カレー'), 'should include the headword');
- assert.ok(!markup.includes('【カレー】'), 'should not render reading in brackets when equal to headword');
+ assert.ok(
+ !markup.includes('【'),
+ 'should not render any bracketed reading when equal to headword',
+ );
});
diff --git a/stats/src/components/vocabulary/FrequencyRankTable.tsx b/stats/src/components/vocabulary/FrequencyRankTable.tsx
index c6488908..470a60c9 100644
--- a/stats/src/components/vocabulary/FrequencyRankTable.tsx
+++ b/stats/src/components/vocabulary/FrequencyRankTable.tsx
@@ -131,11 +131,13 @@ export function FrequencyRankTable({ words, knownWords, onSelectWord }: Frequenc
{w.headword}
{(() => {
const reading = fullReading(w.headword, w.reading);
- if (!reading || reading === w.headword) return null;
+ // `fullReading` normalizes katakana to hiragana, so we normalize the
+ // headword the same way before comparing — otherwise katakana-only
+ // entries like `カレー` would render `【かれー】`.
+ const normalizedHeadword = fullReading(w.headword, w.headword);
+ if (!reading || reading === normalizedHeadword) return null;
return (
-
- 【{reading}】
-
+
【{reading}】
);
})()}
diff --git a/stats/src/lib/delete-confirm.test.ts b/stats/src/lib/delete-confirm.test.ts
index 044de2b7..585d19db 100644
--- a/stats/src/lib/delete-confirm.test.ts
+++ b/stats/src/lib/delete-confirm.test.ts
@@ -65,16 +65,15 @@ test('confirmBucketDelete asks about merging multiple sessions of the same episo
try {
assert.equal(confirmBucketDelete('My Episode', 3), true);
- assert.equal(calls.length, 1);
- assert.match(calls[0]!, /3/);
- assert.match(calls[0]!, /My Episode/);
- assert.match(calls[0]!, /sessions/);
+ assert.deepEqual(calls, [
+ 'Delete all 3 sessions of "My Episode" from this day and all associated data?',
+ ]);
} finally {
globalThis.confirm = originalConfirm;
}
});
-test('confirmBucketDelete uses singular for one session', () => {
+test('confirmBucketDelete uses a clean singular form for one session', () => {
const calls: string[] = [];
const originalConfirm = globalThis.confirm;
globalThis.confirm = ((message?: string) => {
@@ -84,7 +83,9 @@ test('confirmBucketDelete uses singular for one session', () => {
try {
assert.equal(confirmBucketDelete('Solo Episode', 1), false);
- assert.match(calls[0]!, /1 session of/);
+ assert.deepEqual(calls, [
+ 'Delete this session of "Solo Episode" from this day and all associated data?',
+ ]);
} finally {
globalThis.confirm = originalConfirm;
}
diff --git a/stats/src/lib/delete-confirm.ts b/stats/src/lib/delete-confirm.ts
index 14f7abf3..137e3996 100644
--- a/stats/src/lib/delete-confirm.ts
+++ b/stats/src/lib/delete-confirm.ts
@@ -19,7 +19,12 @@ export function confirmEpisodeDelete(title: string): boolean {
}
export function confirmBucketDelete(title: string, count: number): boolean {
+ if (count === 1) {
+ return globalThis.confirm(
+ `Delete this session of "${title}" from this day and all associated data?`,
+ );
+ }
return globalThis.confirm(
- `Delete all ${count} session${count === 1 ? '' : 's'} of "${title}" from this day?`,
+ `Delete all ${count} sessions of "${title}" from this day and all associated data?`,
);
}
diff --git a/stats/src/lib/session-grouping.test.ts b/stats/src/lib/session-grouping.test.ts
index feabd927..3215a447 100644
--- a/stats/src/lib/session-grouping.test.ts
+++ b/stats/src/lib/session-grouping.test.ts
@@ -32,8 +32,20 @@ test('empty input returns empty array', () => {
test('two unique videoIds produce 2 singleton buckets', () => {
const sessions = [
- makeSession({ sessionId: 1, videoId: 10, startedAtMs: 1000, activeWatchedMs: 100, cardsMined: 2 }),
- makeSession({ sessionId: 2, videoId: 20, startedAtMs: 2000, activeWatchedMs: 200, cardsMined: 3 }),
+ makeSession({
+ sessionId: 1,
+ videoId: 10,
+ startedAtMs: 1000,
+ activeWatchedMs: 100,
+ cardsMined: 2,
+ }),
+ makeSession({
+ sessionId: 2,
+ videoId: 20,
+ startedAtMs: 2000,
+ activeWatchedMs: 200,
+ cardsMined: 3,
+ }),
];
const buckets = groupSessionsByVideo(sessions);
assert.equal(buckets.length, 2);
@@ -45,8 +57,20 @@ test('two unique videoIds produce 2 singleton buckets', () => {
});
test('two sessions sharing a videoId collapse into 1 bucket with summed totals and most-recent representative', () => {
- const older = makeSession({ sessionId: 1, videoId: 42, startedAtMs: 1000, activeWatchedMs: 300, cardsMined: 5 });
- const newer = makeSession({ sessionId: 2, videoId: 42, startedAtMs: 9000, activeWatchedMs: 500, cardsMined: 7 });
+ const older = makeSession({
+ sessionId: 1,
+ videoId: 42,
+ startedAtMs: 1000,
+ activeWatchedMs: 300,
+ cardsMined: 5,
+ });
+ const newer = makeSession({
+ sessionId: 2,
+ videoId: 42,
+ startedAtMs: 9000,
+ activeWatchedMs: 500,
+ cardsMined: 7,
+ });
const buckets = groupSessionsByVideo([older, newer]);
assert.equal(buckets.length, 1);
const [bucket] = buckets;