import { useMemo, useState } from 'react'; import { PosBadge } from './pos-helpers'; import { fullReading } from '../../lib/reading-utils'; import type { VocabularyEntry } from '../../types/stats'; interface CrossAnimeWordsTableProps { words: VocabularyEntry[]; knownWords: Set; onSelectWord?: (word: VocabularyEntry) => void; } const PAGE_SIZE = 25; export function CrossAnimeWordsTable({ words, knownWords, onSelectWord, }: CrossAnimeWordsTableProps) { const [page, setPage] = useState(0); const [hideKnown, setHideKnown] = useState(true); const [collapsed, setCollapsed] = useState(false); const hasKnownData = knownWords.size > 0; const ranked = useMemo(() => { let filtered = words.filter((w) => w.animeCount >= 2); if (hideKnown && hasKnownData) { filtered = filtered.filter((w) => !knownWords.has(w.headword) && !knownWords.has(w.word)); } const byHeadword = new Map(); for (const w of filtered) { const existing = byHeadword.get(w.headword); if (!existing) { byHeadword.set(w.headword, { ...w }); } else { existing.frequency += w.frequency; existing.animeCount = Math.max(existing.animeCount, w.animeCount); if ( w.frequencyRank != null && (existing.frequencyRank == null || w.frequencyRank < existing.frequencyRank) ) { existing.frequencyRank = w.frequencyRank; } if (!existing.reading && w.reading) existing.reading = w.reading; if (!existing.partOfSpeech && w.partOfSpeech) existing.partOfSpeech = w.partOfSpeech; } } return [...byHeadword.values()].sort((a, b) => { if (b.animeCount !== a.animeCount) return b.animeCount - a.animeCount; return b.frequency - a.frequency; }); }, [words, knownWords, hideKnown, hasKnownData]); const hasMultiAnimeWords = words.some((w) => w.animeCount >= 2); if (!hasMultiAnimeWords) return null; const totalPages = Math.ceil(ranked.length / PAGE_SIZE); const paged = ranked.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); return (
{hasKnownData && ( )} {ranked.length} words
{collapsed ? null : ranked.length === 0 ? (
{hideKnown ? 'All multi-anime words are already known!' : 'No words found across multiple anime.'}
) : ( <>
{paged.map((w) => ( onSelectWord?.(w)} className="border-b border-ctp-surface1 last:border-0 cursor-pointer hover:bg-ctp-surface1/50 transition-colors" > ))}
Word Reading POS Anime Seen
{w.headword} {fullReading(w.headword, w.reading) || w.headword} {w.partOfSpeech && } {w.animeCount} {w.frequency}x
{totalPages > 1 && (
{page + 1} / {totalPages}
)} )}
); }