import React from "react"; import SHA1 from "crypto-js/sha1"; import { Link } from "react-router"; import { globalSkipKeys } from "../../types/constants"; import notClearImg from "../../assets/games/taiko/not_clear.webp"; import clearImg from "../../assets/games/taiko/clear.webp"; import donderfulImg from "../../assets/games/taiko/donderful_combo.webp"; import easyImg from "../../assets/games/taiko/easy.webp"; import normalImg from "../../assets/games/taiko/normal.webp"; import full_comboImg from "../../assets/games/taiko/full_combo.webp"; import hardImg from "../../assets/games/taiko/hard.webp"; import iki_1 from "../../assets/games/taiko/iki_1.webp"; import iki_2 from "../../assets/games/taiko/iki_2.webp"; import iki_3 from "../../assets/games/taiko/iki_3.webp"; import kiwami from "../../assets/games/taiko/kiwami.webp"; import miyabi_1 from "../../assets/games/taiko/miyabi_1.webp"; import miyabi_2 from "../../assets/games/taiko/miyabi_2.webp"; import miyabi_3 from "../../assets/games/taiko/miyabi_3.webp"; import oni from "../../assets/games/taiko/oni.webp"; import ura_oni from "../../assets/games/taiko/ura_oni.webp"; import type {Score, ScoreDisplayProps} from "../../types/game"; const TaikoScoreDisplay: React.FC = ({ scores, viewMode, sortField, sortDirection, onSort, onDelete, showUsername = false, hideTitleArtist = false, }) => { // Key mappings for better display names. Hit or miss const keyDisplayNames: Record = { title: "Title", artist: "Artist", score: "Score", difficulty: "Difficulty", level: "Level", score_rank: "Score Rank", crown_rank: "Crown Rank", timestamp: "Date", judgements: "Judgements", good: "Good/良", ok: "Ok/可", bad: "Bad/不可 ", max_combo: "Combo", pound: "Drumrolls", date: "Date", username: "Username", }; const mainStatKeys = [ "score", "difficulty", "lamp", "score_rank", "crown_rank", "diff_lamp", "percent", "rating", "grade", ]; const expandableKeys = ["judgements", "optional"]; const gameParam = new URLSearchParams(window.location.search).get("game") || "taiko"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const formatValue = (value: any, key: string): string => { if (value === null || value === undefined) return "N/A"; // Handle timestamps if (key === "timestamp" || key === "date") { if(value === 0) return "N/A"; const date = new Date(typeof value === "number" ? value : value); return date.toLocaleDateString(); } if (typeof value === "number") { if (key === "score" || key === "maxCombo" || key === "combo") { return value.toLocaleString(); } return value.toString(); } if (typeof value === "boolean") { return value ? "Yes" : "No"; } if (Array.isArray(value)) { return value.join(", "); } return String(value); }; const getDisplayName = (key: string): string => { return keyDisplayNames[key] || key.charAt(0).toUpperCase() + key.slice(1); }; const renderValue = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, key: string, compact: boolean = false, ): React.ReactElement => { if (value === null || value === undefined) return N/A; if(key === "difficulty"){ let imgSrc = null; switch (value) { case "EASY": imgSrc = easyImg; break; case "NORMAL": imgSrc = normalImg; break; case "HARD": imgSrc = hardImg; break; case "ONI": imgSrc = oni; break; case "URA_ONI": imgSrc = ura_oni; break; default: imgSrc = easyImg; break; } return {value} ; } if(key === "score_rank"){ let imgSrc = null; switch (value) { case "IKI 1": imgSrc = iki_1; break; case "IKI 2": imgSrc = iki_2; break; case "IKI 3": imgSrc = iki_3; break; case "MIYABI 1": imgSrc = miyabi_1; break; case "MIYABI 2": imgSrc = miyabi_2; break; case "MIYABI 3": imgSrc = miyabi_3; break; case "KIWAMI": imgSrc = kiwami; break; default: imgSrc = easyImg; break; } return {value} ; } if(key === "crown_rank"){ let imgSrc = null; switch (value) { case "CLEAR": imgSrc = clearImg; break; case "FULL COMBO": imgSrc = full_comboImg; break; case "DONDERFUL COMBO": imgSrc = donderfulImg; break; default: imgSrc = notClearImg; break; } return {value} ; } // Handle judgements specially if (key === "judgements" && typeof value === "object") { const judgementEntries = Object.entries(value); if (compact) { return (
{judgementEntries.map(([jKey, jValue]) => (
{getDisplayName(jKey)}: {formatValue(jValue, jKey)}
))}
); } return (
{judgementEntries.map(([jKey, jValue]) => ( {getDisplayName(jKey)}:{" "} {formatValue(jValue, jKey)} ))}
); } if (typeof value === "object" && !Array.isArray(value)) { return (
{Object.entries(value).map(([subKey, subValue]) => (
{getDisplayName(subKey)}: {formatValue(subValue, subKey)}
))}
); } return {formatValue(value, key)}; }; const getScoreEntries = (score: Score) => { const entries = Object.entries(score).filter( ([key]) => !globalSkipKeys.includes(key), ); const mainStats = entries.filter(([key]) => mainStatKeys.includes(key)); const expandable = entries.filter(([key]) => expandableKeys.includes(key)); return { mainStats, expandable, timestamp: score.timestamp, }; }; const SortIcon = ({ field }: { field: string }) => { if (sortField !== field) { return ; } return sortDirection === "asc" ? ( ) : ( ); }; const sortedScores = [...scores].sort((a, b) => { const aVal = a[sortField]; const bVal = b[sortField]; if (aVal === undefined || aVal === null) return 1; if (bVal === undefined || bVal === null) return -1; let comparison = 0; if (typeof aVal === "string" && typeof bVal === "string") { comparison = aVal.localeCompare(bVal); } else if (typeof aVal === "number" && typeof bVal === "number") { comparison = aVal - bVal; } else if (aVal instanceof Date && bVal instanceof Date) { comparison = aVal.getTime() - bVal.getTime(); } else { comparison = String(aVal).localeCompare(String(bVal)); } return sortDirection === "asc" ? comparison : -comparison; }); // Get all possible keys for table headers const allKeys = Array.from( new Set(scores.flatMap((score) => Object.keys(score))), ).filter((key) => !globalSkipKeys.includes(key)); // Prioritize important keys for table display const tableKeys = [ ...(hideTitleArtist ? [] : ["title", "song", "artist"]), ...(showUsername ? ["username"] : []), "score", "difficulty", "lamp", "diff_lamp", "rating", "percent", "grade", "score_rank", "crown_rank", "level", "judgements", "combo", "timestamp", ].filter((key) => allKeys.includes(key)); // Add actions column if delete function is provided const showActions = onDelete && viewMode === "table"; if (scores.length === 0) { return (
🎵

No scores found

Import some score data to get started!

); } if (viewMode === "cards") { return (
{sortedScores.map((score, index) => { const chartIdHash = SHA1(`${gameParam}${score.title}${score.artist}`).toString(); const { mainStats, expandable, timestamp } = getScoreEntries(score); return (
{/* Primary Info */}
{!hideTitleArtist && (

{score.title || score.song || "Unknown Title"}

{score.artist && (

{score.artist}

)} )} {showUsername && score.username && (

by {score.username}

)}
{/* Main Stats */} {mainStats.length > 0 && (
{mainStats.slice(0, 4).map(([key, value]) => (

{getDisplayName(key)}

{renderValue(value, key)}

))}
)} {/* Level */} {score.level && (

Level

{score.level}

)} {/* Expandable sections (judgements, optional) */} {expandable.map(([key, value]) => (

{getDisplayName(key)}

{renderValue(value, key)}
))} {/* Timestamp */}

{timestamp === 0 ? "N/A" : ( <> {new Date( typeof timestamp === "number" ? timestamp : timestamp, ).toLocaleDateString()}{" "} •{" "} {new Date( typeof timestamp === "number" ? timestamp : timestamp, ).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })} )}

); })}
); } return (
{tableKeys.map((key) => ( ))} {showActions && ( )} {sortedScores.map((score, index) => ( {tableKeys.map((key) => ( ))} {showActions && ( )} ))}
{key === "judgements" ? ( {getDisplayName(key)} ) : ( )} Actions
{key === "lamp" || key === "diff_lamp" ? (
{score[key] || "No Clear"}
) : key === "judgements" ? (
{renderValue(score[key], key, true)}
) : key === "timestamp" ? ( {score[key] === 0 ? "N/A" : new Date( typeof score[key] === "number" ? score[key] : score[key], ).toLocaleDateString()} ) : key === "username" ? ( {score[key] || "Unknown"} ) : key === "level" || key === "crown_rank" || key === "score_rank" ? (
{renderValue(score[key], key)}
) : ( {renderValue(score[key], key)} )}
); }; export default TaikoScoreDisplay;