import { escapeHtml } from '../util.js';
import { logoLockup } from '../logo.js';
import { renderCornerRail } from '../cornerRail.js';
import { attachTooltips } from '../tooltip.js';
import { renderTopRail } from '../topRail.js';
import { api, ApiError } from '../api.js';
import { FLAIR_ICONS, CLOSE_ICON } from './icons.js';
const WORD_COUNTS = [10, 25, 50, 100];
const TIME_DURATIONS = [15, 30, 60, 120];
// Scoped to Words/Time only, not the open-ended quote categories - those
// grow with the text pack and don't make a stable, comparable leaderboard
// mode the way a fixed word count or time duration does.
const MODES = [
...TIME_DURATIONS.map(d => ({ key: `time-${d}`, label: `Time ${d}s` })),
...WORD_COUNTS.map(w => ({ key: `words-${w}`, label: `Words ${w}` })),
];
function formatDate(iso) {
const t = Date.parse(iso);
return Number.isNaN(t) ? '' : new Date(t).toLocaleDateString();
}
export function renderLeaderboardScreen(root, { onBack, onShowPublicProfile, onShowStats, onShowPlaceholder, onShowAccount, onShowFriends, onShowMultiplayer, onShowStore }) {
let selectedMode = MODES[1].key; // time-30, matches the app's own default mode
let desktopOnly = false;
function rowsMarkup(state) {
if (state.status === 'loading') return `
Loading...
`;
if (state.status === 'error') return `Could not load the leaderboard - ${escapeHtml(state.message)}
`;
if (state.entries.length === 0) return `No results yet for this mode - be the first.
`;
return `
${state.entries.map((e, i) => `
#${i + 1}
${e.is_bot ? `
bot` : ''}
${e.device_type === 'mobile' ? `
mobile` : ''}
${Math.round(e.wpm)} wpm
${Math.round(e.accuracy)}%
${formatDate(e.date)}
`).join('')}
`;
}
root.innerHTML = `
${logoLockup()}
Leaderboard
${MODES.map(m => ``).join('')}
`;
root.querySelectorAll('[data-action="menu"]').forEach(el => el.addEventListener('click', onBack));
attachTooltips(root);
const cleanupTheme = renderTopRail(root, { onShowAccount, onShowFriends });
const resultsEl = root.querySelector('.leaderboard-results');
let requestToken = 0;
async function load() {
const token = ++requestToken;
resultsEl.innerHTML = rowsMarkup({ status: 'loading' });
try {
const params = new URLSearchParams({ mode: selectedMode });
if (desktopOnly) params.set('device', 'desktop');
const entries = await api.get(`/api/leaderboard?${params.toString()}`);
if (token !== requestToken) return; // a newer mode pick already superseded this request
resultsEl.innerHTML = rowsMarkup({ status: 'ok', entries });
// Scoped to resultsEl, not the whole root - attachTooltips
// already ran once for the rest of the screen at render time,
// and re-running it there would double-bind those elements'
// listeners on every reload.
attachTooltips(resultsEl);
} catch (err) {
if (token !== requestToken) return;
const message = err instanceof ApiError ? err.message : 'network error';
resultsEl.innerHTML = rowsMarkup({ status: 'error', message });
}
}
root.querySelectorAll('.leaderboard-modes [data-mode]').forEach(btn => {
btn.addEventListener('click', () => {
selectedMode = btn.dataset.mode;
root.querySelectorAll('.leaderboard-modes [data-mode]').forEach(b => b.classList.toggle('active', b === btn));
load();
});
});
const deviceFilterBtn = root.querySelector('[data-action="toggle-device-filter"]');
deviceFilterBtn.addEventListener('click', () => {
desktopOnly = !desktopOnly;
deviceFilterBtn.textContent = `Devices: ${desktopOnly ? 'Desktop only' : 'All'}`;
// A filter that is doing something should look like it is.
deviceFilterBtn.classList.toggle('active', desktopOnly);
load();
});
// Delegated rather than attached per-row - resultsEl's rows get
// replaced wholesale on every load(), so a listener attached to a
// specific row would be gone the next time the mode changes.
resultsEl.addEventListener('click', e => {
const btn = e.target.closest('[data-username]');
if (btn) onShowPublicProfile(btn.dataset.username);
});
load();
const cleanupRail = renderCornerRail(root, { onShowStats, onShowPlaceholder, onShowAccount, onShowFriends, onShowMultiplayer, onShowStore });
return () => {
cleanupTheme();
cleanupRail();
};
}