Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,16 @@
"rules.time": "60 seconds time limit",
"rules.tap": "Tap lines with bugs to report them",
"rules.combo": "Consecutive correct answers for combo bonus",
"rules.skip": "Skip difficult problems"
"rules.skip": "Skip difficult problems",
"ranking.title": "Ranking",
"ranking.period": "Period",
"ranking.period.week": "Last 7 Days",
"ranking.rank": "Rank",
"ranking.player": "Player",
"ranking.score": "Score",
"ranking.accuracy": "Accuracy",
"ranking.date": "Date",
"ranking.codeLanguage": "Code Language",
"ranking.noData": "No data available",
"ranking.anonymous": "Anonymous"
}
13 changes: 12 additions & 1 deletion app/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,16 @@
"rules.time": "制限時間は60秒",
"rules.tap": "バグがある行をタップして指摘",
"rules.combo": "連続正解でコンボボーナス",
"rules.skip": "難しい問題はスキップ可能"
"rules.skip": "難しい問題はスキップ可能",
"ranking.title": "ランキング",
"ranking.period": "期間",
"ranking.period.week": "過去7日間",
"ranking.rank": "順位",
"ranking.player": "プレイヤー",
"ranking.score": "スコア",
"ranking.accuracy": "正確性",
"ranking.date": "日付",
"ranking.codeLanguage": "コード言語",
"ranking.noData": "データがありません",
"ranking.anonymous": "匿名"
}
1 change: 1 addition & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default [
index('routes/_index.tsx'),
route(':lang', 'routes/$lang.tsx'),
route(':lang/:codeLanguage/play', 'routes/$lang.$codeLanguage.play.tsx'),
route(':lang/ranking', 'routes/$lang.ranking.tsx'),
route('result/create', 'routes/result.create.tsx'),
route('result/:id', 'routes/result.$id.tsx'),
] satisfies RouteConfig;
238 changes: 238 additions & 0 deletions app/routes/$lang.ranking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import type { Route } from './+types/$lang.ranking';
import { redirect } from 'react-router';
import { useLoaderData } from 'react-router';
import { t, type SupportedLanguage } from '~/locales';
import { Header } from '~/components/Header';

type ScoreRecord = {
id: string;
score: number;
issues_found: number;
total_issues: number;
accuracy: number;
ui_language: string;
code_language: string;
player_name: string | null;
created_at: string;
};

export function meta({ params }: Route.MetaArgs) {
const lang = params.lang as SupportedLanguage;
const title = t('ranking.title', lang);
const period = t('ranking.period.week', lang);

return [{ title: `${title} (${period}) | Bug Sniper` }];
}

export async function loader({ params, request, context }: Route.LoaderArgs) {
const lang = params.lang;

// Validate language parameter
if (lang !== 'ja' && lang !== 'en') {
throw redirect('/');
}

const url = new URL(request.url);
const codeLanguage = url.searchParams.get('code') || 'all';

const db = context.cloudflare.env.DB;

if (!db) {
throw new Response('Database not configured', { status: 500 });
}

// Get top 50 scores from the last 7 days
let query: string;
let queryParams: string[];

if (codeLanguage === 'all') {
query = `
SELECT * FROM scores
WHERE created_at >= datetime('now', '-7 days')
ORDER BY score DESC
LIMIT 50
`;
queryParams = [];
} else {
query = `
SELECT * FROM scores
WHERE code_language = ?
AND created_at >= datetime('now', '-7 days')
ORDER BY score DESC
LIMIT 50
`;
queryParams = [codeLanguage];
}

const result = await db.prepare(query).bind(...queryParams).all();

return {
scores: (result.results || []) as ScoreRecord[],
codeLanguage,
lang: lang as SupportedLanguage,
};
}

function getCodeLanguageDisplay(code: string): string {
const map: Record<string, string> = {
all: 'All',
javascript: 'JavaScript',
python: 'Python',
php: 'PHP',
ruby: 'Ruby',
java: 'Java',
dart: 'Dart',
};
return map[code] || code;
}
Comment on lines +76 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace hardcoded English strings with i18n for consistency.

The getCodeLanguageDisplay function returns hardcoded English strings, which bypasses the app's i18n system and will display English language names even when the UI is in Japanese. The app already has language.* keys in the locale files for this purpose.

Apply this diff to use the i18n system:

-function getCodeLanguageDisplay(code: string): string {
-  const map: Record<string, string> = {
-    all: 'All',
-    javascript: 'JavaScript',
-    python: 'Python',
-    php: 'PHP',
-    ruby: 'Ruby',
-    java: 'Java',
-    dart: 'Dart',
-  };
-  return map[code] || code;
-}
+function getCodeLanguageDisplay(code: string, lang: SupportedLanguage): string {
+  return t(`language.${code}`, lang);
+}

Then update the call site at line 205:

-                        {getCodeLanguageDisplay(score.code_language)}
+                        {getCodeLanguageDisplay(score.code_language, lang)}

Committable suggestion skipped: line range outside the PR's diff.


export default function Ranking() {
const { scores, codeLanguage, lang } = useLoaderData<typeof loader>();

const codeLanguages = ['all', 'javascript', 'python', 'php', 'ruby', 'java', 'dart'];

const formatDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
return date.toLocaleDateString(lang === 'ja' ? 'ja-JP' : 'en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return dateStr;
}
};

return (
<div className="min-h-screen bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">
<Header currentLang={lang} />

<main className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Title */}
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold tracking-tight">
{t('ranking.title', lang)}
</h1>
<p className="text-sm text-slate-600 dark:text-slate-400">
{t('ranking.period.week', lang)}
</p>
</div>

{/* Language Filter Tabs */}
<div className="flex overflow-x-auto gap-2 pb-2">
{codeLanguages.map((code) => {
const isActive = codeLanguage === code;
const href = `/${lang}/ranking?code=${code}`;

return (
<a
key={code}
href={href}
className={`
px-4 py-2 rounded-md font-medium text-sm whitespace-nowrap transition
${
isActive
? 'bg-sky-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700'
}
`}
>
{t(`language.${code}`, lang)}
</a>
);
})}
</div>

{/* Ranking Table */}
{scores.length === 0 ? (
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
{t('ranking.noData', lang)}
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b border-slate-200 dark:border-slate-700">
<th className="py-3 px-2 text-left text-sm font-semibold">
{t('ranking.rank', lang)}
</th>
<th className="py-3 px-4 text-left text-sm font-semibold">
{t('ranking.player', lang)}
</th>
<th className="py-3 px-4 text-left text-sm font-semibold">
{t('ranking.codeLanguage', lang)}
</th>
<th className="py-3 px-4 text-right text-sm font-semibold">
{t('ranking.score', lang)}
</th>
<th className="py-3 px-4 text-right text-sm font-semibold">
{t('ranking.accuracy', lang)}
</th>
<th className="py-3 px-4 text-right text-sm font-semibold">
{t('ranking.date', lang)}
</th>
</tr>
</thead>
<tbody>
{scores.map((score: ScoreRecord, index: number) => {
const rank = index + 1;
const rankClass =
rank === 1
? 'text-yellow-600 dark:text-yellow-400 font-bold'
: rank === 2
? 'text-slate-400 dark:text-slate-500 font-bold'
: rank === 3
? 'text-orange-600 dark:text-orange-400 font-bold'
: '';

return (
<tr
key={score.id}
className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition"
>
<td className={`py-3 px-2 text-sm ${rankClass}`}>
{rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : rank}
</td>
<td className="py-3 px-4">
<a
href={`/result/${score.id}`}
className="text-sky-600 dark:text-sky-400 hover:underline"
>
{score.player_name || t('ranking.anonymous', lang)}
</a>
</td>
<td className="py-3 px-4 text-sm">
{getCodeLanguageDisplay(score.code_language)}
</td>
<td className="py-3 px-4 text-right font-semibold">
{score.score}
</td>
<td className="py-3 px-4 text-right text-sm text-slate-600 dark:text-slate-400">
{(score.accuracy * 100).toFixed(1)}%
</td>
<td className="py-3 px-4 text-right text-sm text-slate-600 dark:text-slate-400">
{formatDate(score.created_at)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}

{/* Back to Home Button */}
<div className="flex justify-center pt-6">
<a
href={`/${lang}`}
className="px-6 py-3 rounded-md bg-sky-500 text-white hover:bg-sky-600 active:bg-sky-700 transition font-medium"
>
{t('nav.home', lang)}
</a>
</div>
</main>
</div>
);
}
2 changes: 1 addition & 1 deletion app/routes/result.$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export default function Result({ loaderData }: Route.ComponentProps) {
{lang === 'ja' ? 'もう一度プレイ' : 'Play Again'}
</a>
<a
href="/ranking"
href={`/${lang}/ranking`}
className="flex-1 py-3 text-center rounded-md bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 transition font-medium"
>
{lang === 'ja' ? 'ランキングを見る' : 'View Ranking'}
Expand Down
7 changes: 7 additions & 0 deletions app/welcome/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export function Welcome({ lang }: { lang: SupportedLanguage }) {
</ul>
</div>

<Link
to={`/${lang}/ranking`}
className="block w-full py-3 text-center rounded-md border-2 border-sky-500 text-sky-600 dark:text-sky-400 hover:bg-sky-50 dark:hover:bg-sky-900/30 transition font-medium"
>
{t('nav.ranking', lang)}
</Link>

<div>
<label className="block text-sm font-medium mb-2">{t('label.codeLanguage', lang)}</label>
<div className="grid grid-cols-3 gap-3">
Expand Down