mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 07:21:33 -07:00
feat(stats): dashboard updates (#50)
This commit is contained in:
@@ -72,7 +72,7 @@ export function CrossAnimeWordsTable({
|
||||
>
|
||||
{'\u25B6'}
|
||||
</span>
|
||||
Words In Multiple Anime
|
||||
Words Across Multiple Titles
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{hasKnownData && (
|
||||
@@ -97,8 +97,8 @@ export function CrossAnimeWordsTable({
|
||||
{collapsed ? null : ranked.length === 0 ? (
|
||||
<div className="text-xs text-ctp-overlay2 mt-3">
|
||||
{hideKnown
|
||||
? 'All multi-anime words are already known!'
|
||||
: 'No words found across multiple anime.'}
|
||||
? 'All words that span multiple titles are already known!'
|
||||
: 'No words found across multiple titles.'}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -109,7 +109,7 @@ export function CrossAnimeWordsTable({
|
||||
<th className="text-left py-2 pr-3 font-medium">Word</th>
|
||||
<th className="text-left py-2 pr-3 font-medium">Reading</th>
|
||||
<th className="text-left py-2 pr-3 font-medium w-20">POS</th>
|
||||
<th className="text-right py-2 pr-3 font-medium w-16">Anime</th>
|
||||
<th className="text-right py-2 pr-3 font-medium w-16">Titles</th>
|
||||
<th className="text-right py-2 font-medium w-16">Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { FrequencyRankTable } from './FrequencyRankTable';
|
||||
import type { VocabularyEntry } from '../../types/stats';
|
||||
|
||||
function makeEntry(over: Partial<VocabularyEntry>): VocabularyEntry {
|
||||
return {
|
||||
wordId: 1,
|
||||
headword: '日本語',
|
||||
word: '日本語',
|
||||
reading: 'にほんご',
|
||||
frequency: 5,
|
||||
frequencyRank: 100,
|
||||
animeCount: 1,
|
||||
partOfSpeech: null,
|
||||
firstSeen: 0,
|
||||
lastSeen: 0,
|
||||
...over,
|
||||
} as VocabularyEntry;
|
||||
}
|
||||
|
||||
test('renders headword and reading inline in a single column (no separate Reading header)', () => {
|
||||
const entry = makeEntry({});
|
||||
const markup = renderToStaticMarkup(
|
||||
<FrequencyRankTable words={[entry]} knownWords={new Set()} />,
|
||||
);
|
||||
assert.ok(!markup.includes('>Reading<'), 'should not have a Reading column header');
|
||||
assert.ok(markup.includes('日本語'), 'should include the headword');
|
||||
assert.ok(markup.includes('にほんご'), 'should include the reading inline');
|
||||
});
|
||||
|
||||
test('omits reading when reading equals headword', () => {
|
||||
const entry = makeEntry({ headword: 'カレー', word: 'カレー', reading: 'カレー' });
|
||||
const markup = renderToStaticMarkup(
|
||||
<FrequencyRankTable words={[entry]} knownWords={new Set()} />,
|
||||
);
|
||||
assert.ok(markup.includes('カレー'), 'should include the headword');
|
||||
assert.ok(
|
||||
!markup.includes('【'),
|
||||
'should not render any bracketed reading when equal to headword',
|
||||
);
|
||||
});
|
||||
@@ -113,7 +113,6 @@ export function FrequencyRankTable({ words, knownWords, onSelectWord }: Frequenc
|
||||
<tr className="text-xs text-ctp-overlay2 border-b border-ctp-surface1">
|
||||
<th className="text-left py-2 pr-3 font-medium w-16">Rank</th>
|
||||
<th className="text-left py-2 pr-3 font-medium">Word</th>
|
||||
<th className="text-left py-2 pr-3 font-medium">Reading</th>
|
||||
<th className="text-left py-2 pr-3 font-medium w-20">POS</th>
|
||||
<th className="text-right py-2 font-medium w-20">Seen</th>
|
||||
</tr>
|
||||
@@ -128,9 +127,19 @@ export function FrequencyRankTable({ words, knownWords, onSelectWord }: Frequenc
|
||||
<td className="py-1.5 pr-3 font-mono tabular-nums text-ctp-peach text-xs">
|
||||
#{w.frequencyRank!.toLocaleString()}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 text-ctp-text font-medium">{w.headword}</td>
|
||||
<td className="py-1.5 pr-3 text-ctp-subtext0">
|
||||
{fullReading(w.headword, w.reading) || w.headword}
|
||||
<td className="py-1.5 pr-3">
|
||||
<span className="text-ctp-text font-medium">{w.headword}</span>
|
||||
{(() => {
|
||||
const reading = fullReading(w.headword, w.reading);
|
||||
// `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 (
|
||||
<span className="text-ctp-subtext0 text-xs ml-1.5">【{reading}】</span>
|
||||
);
|
||||
})()}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3">
|
||||
{w.partOfSpeech && <PosBadge pos={w.partOfSpeech} />}
|
||||
|
||||
Reference in New Issue
Block a user