srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/screens/statsScreen.js
blob: b15bf514b22fa98c8a4aa3f46a6c95f30e9596b3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { CLOSE_ICON } from './icons.js';
import { logoLockup } from '../logo.js';
import { getProfileStats } from '../profileStats.js';
import { getAllPersonalBests } from '../pb.js';
import { getWeakCharDetails } from '../keyStats.js';
import { escapeHtml } from '../util.js';
import { drawProgressChart } from '../chart.js';
import { renderCornerRail } from '../cornerRail.js';
import { attachTooltips } from '../tooltip.js';
import { renderTopRail } from '../topRail.js';

// modeKey format is set in app.js: "words-25", "time-30", "quote-<category>".
function modeLabel(modeKey) {
    const [kind, arg] = modeKey.split('-');
    if (kind === 'words') return `Words · ${arg}`;
    if (kind === 'time') return `Time · ${arg}s`;
    if (kind === 'quote') return arg === 'random' ? 'Random' : arg.charAt(0).toUpperCase() + arg.slice(1);
    return modeKey;
}

function formatDate(ts) {
    if (!ts) return '';
    return new Date(ts).toLocaleDateString();
}

export function renderStatsScreen(root, { onBack, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore }) {
    const profile = getProfileStats();
    const bests = getAllPersonalBests();
    const weakKeys = getWeakCharDetails(8);

    // Derived from the stored history rather than kept as separate counters,
    // so these stay correct for anyone who already has a history recorded.
    const history = profile.history || [];
    const recent = history.slice(-10);
    const recentAvg = recent.length ? Math.round(recent.reduce((a, h) => a + h.wpm, 0) / recent.length) : 0;
    const bestAcc = history.length ? Math.round(Math.max(...history.map(h => h.accuracy))) : 0;
    const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
    const thisWeek = history.filter(h => h.date && new Date(h.date).getTime() >= weekAgo).length;
    // Recent form against the lifetime average: the single most useful thing
    // a stats page can tell you is whether you are getting better.
    const trend = recentAvg && profile.averageWpm ? recentAvg - profile.averageWpm : 0;

    root.innerHTML = `
        <div class="stats-screen">
            <button class="screen-close" data-action="menu" aria-label="Close" data-tooltip="Close (Esc)">${CLOSE_ICON}</button>
                ${logoLockup()}
            <h2>Stats</h2>

            ${profile.testsCompleted > 0 ? `
            <div class="stats-summary">
                <div class="menu-stat"><div class="stat-label">Tests</div><div class="stat-value">${profile.testsCompleted}</div></div>
                <div class="menu-stat"><div class="stat-label">Avg WPM</div><div class="stat-value">${profile.averageWpm}</div></div>
                <div class="menu-stat"><div class="stat-label">Best WPM</div><div class="stat-value">${Math.round(profile.bestWpm)}</div></div>
                <div class="menu-stat"><div class="stat-label">Avg Acc</div><div class="stat-value">${profile.averageAccuracy}%</div></div>
                <div class="menu-stat"><div class="stat-label">Streak</div><div class="stat-value">${profile.streakDays}d</div></div>
                <div class="menu-stat"><div class="stat-label">Time Typed</div><div class="stat-value">${Math.round(profile.totalTimeSeconds / 60)}m</div></div>
                <div class="menu-stat" data-tooltip="Average of your last 10 tests, and how that compares with your lifetime average"><div class="stat-label">Last 10</div><div class="stat-value">${recentAvg}${trend ? `<span class="stat-trend ${trend > 0 ? 'up' : 'down'}">${trend > 0 ? '+' : ''}${trend}</span>` : ''}</div></div>
                <div class="menu-stat"><div class="stat-label">Best Acc</div><div class="stat-value">${bestAcc}%</div></div>
                <div class="menu-stat"><div class="stat-label">This Week</div><div class="stat-value">${thisWeek}</div></div>
            </div>
            ` : `<div class="stats-empty">No tests completed yet - finish a test to start tracking stats.</div>`}

            <h3>Personal bests</h3>
            ${bests.length > 0 ? `
            <div class="stats-pb-list">
                ${bests.map(b => `
                    <div class="stats-pb-row">
                        <div class="stats-pb-mode">${escapeHtml(modeLabel(b.modeKey))}</div>
                        <div class="stats-pb-wpm">${Math.round(b.wpm)} wpm</div>
                        <div class="stats-pb-date">${formatDate(b.date)}</div>
                    </div>
                `).join('')}
            </div>
            ` : `<div class="stats-empty">No personal bests recorded yet.</div>`}

            ${weakKeys.length ? `
            <h3>Weakest keys</h3>
            <div class="settings-hint">What Practice mode targets. Ranked by how often you mistype a key and how long you pause before it.</div>
            <div class="weak-key-list">
                ${weakKeys.map(k => `
                    <div class="weak-key" data-tooltip="${k.errors} wrong out of ${k.attempts} attempts">
                        <span class="weak-key-char">${escapeHtml(k.char)}</span>
                        <span class="weak-key-err">${Math.round(k.errorRate * 100)}%</span>
                        <span class="weak-key-lat">${Math.round(k.avgLatency)}ms</span>
                    </div>
                `).join('')}
            </div>` : ''}

            <h3>Progress</h3>
            ${profile.history.length > 1
                ? `<div class="stats-chart"><canvas></canvas></div>`
                : `<div class="stats-placeholder">Complete a few more tests to see a progress chart.</div>`}

        </div>
    `;

    root.querySelectorAll('[data-action="menu"]').forEach(el => el.addEventListener('click', onBack));
    attachTooltips(root);
    const cleanupTheme = renderTopRail(root, { onShowAccount, onShowFriends });

    const canvas = root.querySelector('.stats-chart canvas');
    let cleanupResize = () => {};
    if (canvas) {
        const redraw = () => drawProgressChart(canvas, profile.history);
        redraw();
        window.addEventListener('resize', redraw);
        cleanupResize = () => window.removeEventListener('resize', redraw);
    }

    const cleanupRail = renderCornerRail(root, { onShowStats: null, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore });

    return () => {
        cleanupResize();
        cleanupTheme();
        cleanupRail();
    };
}