-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathprofile.js
More file actions
68 lines (68 loc) · 7.98 KB
/
Copy pathprofile.js
File metadata and controls
68 lines (68 loc) · 7.98 KB
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
(() => {
'use strict';
const labels = { pong: 'Pong', sudoku: 'Sudoku', minesweeper: 'Minesweeper', tictactoe: 'Tic-tac-toe', battletanks: 'Battle Tanks', tetris: 'Tetris' };
const profileForm = document.querySelector('#profile-form');
const currentPasscode = document.createElement('input');
currentPasscode.name = 'currentPasscode'; currentPasscode.type = 'password'; currentPasscode.minLength = 4; currentPasscode.maxLength = 128; currentPasscode.required = true; currentPasscode.placeholder = 'Current passcode'; currentPasscode.autocomplete = 'current-password'; currentPasscode.setAttribute('aria-label', 'Current passcode');
profileForm.insertBefore(currentPasscode, profileForm.elements.passcode);
profileForm.elements.passcode.autocomplete = 'new-password';
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' })[character]);
const formatDuration = seconds => {
const total = Number(seconds), hours = Math.floor(total / 3600), minutes = Math.floor(total % 3600 / 60), remainder = total % 60;
return hours ? `${hours}:${String(minutes).padStart(2, '0')}:${String(remainder).padStart(2, '0')}` : `${String(minutes).padStart(2, '0')}:${String(remainder).padStart(2, '0')}`;
};
const detailLabels = { difficulty: 'Difficulty', mode: 'Mode', score: 'Result', mistakes: 'Mistakes', hintsUsed: 'Hints', seconds: 'Time', winner: 'Winner', turns: 'Turns', shots: 'Shots', hits: 'Hits', accuracy: 'Accuracy %', damageTaken: 'Damage taken', lines: 'Lines', level: 'Level', pieces: 'Pieces', tetrises: 'Four-line clears' };
const duration = row => Number.isFinite(Number(row.details?.seconds)) ? formatDuration(row.details.seconds) : '—';
const details = row => Object.entries(row.details || {}).filter(([key]) => key !== 'seconds' && (row.game !== 'tetris' || ['lines','level','pieces','tetrises'].includes(key))).map(([key, value]) => `${detailLabels[key] || key}: ${value}`).join(' · ') || '—';
async function shareAchievement(item) {
const text = `${item.icon} I unlocked “${item.title}” in JavaScript Playground — ${item.condition}`;
const url = location.href.split('#')[0] + '#achievements';
const image = window.ResultShare?.achievement(item);
if (image) return window.ResultShare.share({ image, filename: `achievement-${item.id}.png`, title: item.title, text, url });
if (navigator.share) return navigator.share({ title: item.title, text, url });
await navigator.clipboard.writeText(`${text} ${url}`);
}
function renderAchievements(items) {
const panel = document.querySelector('#achievements'); panel.hidden = false;
const unlocked = items.filter(item => item.unlocked).length;
document.querySelector('#achievement-count').textContent = `${unlocked} / ${items.length}`;
const container = document.querySelector('#profile-achievements');
container.innerHTML = items.map(item => `<article class="profile-achievement ${item.unlocked ? '' : 'is-locked'}"><span class="icon" aria-hidden="true">${item.icon}</span><div><small>${labels[item.game]} · ${item.unlocked ? `Unlocked ${new Date(item.unlockedAt + 'Z').toLocaleDateString()}` : 'Locked'}</small><strong>${item.title}</strong><p>${item.condition}</p>${item.target > 1 ? `<progress value="${item.progress}" max="${item.target}"></progress><small>${item.progress} / ${item.target}</small>` : ''}</div>${item.unlocked ? `<button type="button" data-share="${item.id}">Share</button>` : ''}</article>`).join('');
container.querySelectorAll('[data-share]').forEach(button => button.addEventListener('click', () => shareAchievement(items.find(item => item.id === button.dataset.share)).catch(() => {})));
}
async function loadProfile(user, page = 1) {
document.querySelector('#account-panel').hidden = !user; document.querySelector('#stats-panel').hidden = !user;
if (!user) return;
const profile = await Arcade.api(`/api/profile?page=${page}&pageSize=10`);
document.querySelector('#profile-title').textContent = profile.user.gamertag;
document.querySelector('#profile-note').textContent = `Member since ${new Date(profile.user.createdAt + 'Z').toLocaleDateString()}`;
document.querySelector('#profile-form').gamertag.value = profile.user.gamertag;
const byGame = Object.fromEntries(profile.totals.map(item => [item.game, item]));
renderAchievements(profile.achievements || []);
document.querySelector('#stats').innerHTML = Object.keys(labels).map(game => { const row = byGame[game] || {}; const middle = game === 'tetris' ? `<strong>${row.total_lines || 0}</strong><small>Lines</small>` : `<strong>${row.wins || 0}</strong><small>Wins</small>`; return `<div class="stat"><span class="stat-game">${labels[game]}</span><div class="stat-metrics"><div><strong>${row.games_played || 0}</strong><small>Played</small></div><div>${middle}</div><div><strong>${row.best_score ?? '—'}</strong><small>Best</small></div></div></div>`; }).join('');
document.querySelector('#history').innerHTML = profile.recent.length ? profile.recent.map(row => `<tr><td>${labels[row.game]}</td><td>${row.won ? 'Win' : row.game === 'battletanks' ? 'Loss' : 'Played'}</td><td>${row.score}</td><td>${duration(row)}</td><td>${new Date(row.played_at + 'Z').toLocaleString()}</td></tr>`).join('') : '<tr><td colspan="5" class="empty">Play a game to begin your history.</td></tr>';
const pagination = document.querySelector('#history-pagination');
pagination.hidden = profile.pagination.totalPages <= 1;
pagination.dataset.page = profile.pagination.page;
document.querySelector('#history-page').textContent = `Page ${profile.pagination.page} of ${profile.pagination.totalPages}`;
pagination.querySelector('[data-page-action="previous"]').disabled = profile.pagination.page === 1;
pagination.querySelector('[data-page-action="next"]').disabled = profile.pagination.page === profile.pagination.totalPages;
}
async function loadLeaders(game) {
const result = await Arcade.api(`/api/leaderboards/${game}`);
document.querySelector('#leaders').innerHTML = result.entries.length ? result.entries.map((row, index) => `<tr><td>${index + 1}</td><td>${escape(row.gamertag)}</td><td>${row.score}</td><td>${duration(row)}</td><td>${escape(details(row))}</td></tr>`).join('') : '<tr><td colspan="5" class="empty">No scores yet. Be the first.</td></tr>';
}
window.ArcadeEvents.on('account:user-changed', event => loadProfile(event.detail.user).catch(() => {}));
document.querySelector('#game-tabs').addEventListener('click', event => { if (!event.target.dataset.game) return; document.querySelectorAll('#game-tabs button').forEach(button => button.setAttribute('aria-pressed', button === event.target)); loadLeaders(event.target.dataset.game); });
document.querySelector('#history-pagination').addEventListener('click', event => {
const action = event.target.dataset.pageAction;
if (!action) return;
const currentPage = Number(event.currentTarget.dataset.page);
loadProfile(true, currentPage + (action === 'next' ? 1 : -1));
});
profileForm.addEventListener('submit', async event => { event.preventDefault(); const message = document.querySelector('#profile-message'); try { await Arcade.api('/api/profile', { method:'PATCH', body:JSON.stringify(Object.fromEntries(new FormData(event.target))) }); message.textContent = 'Profile updated. Refreshing…'; location.reload(); } catch (error) { message.textContent = error.message; } });
const linkedGame = new URLSearchParams(location.search).get('game');
const initialTab = [...document.querySelectorAll('#game-tabs button')].find(button => button.dataset.game === linkedGame) || document.querySelector('#game-tabs button[data-game="pong"]');
document.querySelectorAll('#game-tabs button').forEach(button => button.setAttribute('aria-pressed', button === initialTab));
loadLeaders(initialTab.dataset.game);
})();