Public Access
feat(ui): make stat numbers clickable to open games
StatDrill context holds one dialog for a page. StatCount wraps a number: total and status counts link to the library filter, other numbers open StatGamesDialog. DrillRow and DrillButton support drill-down in tag bars (genres, platforms, moods) and other stat displays. Dialog shows 40 games per page with bottom-sheet layout on phones, handles pagination and errors (403 not shared, 401 expired, 400 bad argument).
This commit is contained in:
1 parent
ed5df8c40d
commit
777ba87fd6
31 files changed
+1026
-261
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import {request} from './api.js';
|
||||
|
||||
const CORE = () => import.meta.env.VITE_CORE_API_URL;
|
||||
|
||||
const query = (params) => params
|
||||
.filter(([, value]) => value != null && value !== '')
|
||||
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
|
||||
/**
|
||||
* The URL of one page of the games behind a number.
|
||||
*
|
||||
* @param source own, profile, community or points
|
||||
* @param track points: the unlock track of a locked bar
|
||||
* @param kind points: the history kind (MONTH or STREAK) with its `ref`
|
||||
*/
|
||||
export function statGamesUrl({source, key, arg, page = 0, track, kind, ref}) {
|
||||
const base = `${CORE()}/api`;
|
||||
if (source === 'points') {
|
||||
const path = track
|
||||
? `progress/${encodeURIComponent(track)}`
|
||||
: `history/${encodeURIComponent(kind)}/${encodeURIComponent(ref)}`;
|
||||
return `${base}/rewards/${path}/games?${query([['page', page]])}`;
|
||||
}
|
||||
const path = {
|
||||
own: 'library/stats/games',
|
||||
profile: 'public/profiles/library/stats/games',
|
||||
community: 'stats/public/games',
|
||||
}[source];
|
||||
return `${base}/${path}?${query([['key', key], ['arg', arg], ['page', page]])}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* `{ok, status, data}` like `request`. The profile and the community list are public: no login, no refresh.
|
||||
*/
|
||||
export async function fetchStatGames(source, params, ticket) {
|
||||
const url = statGamesUrl({source, ...params});
|
||||
if (source !== 'profile' && source !== 'community') return request(url);
|
||||
try {
|
||||
const response = await fetch(url, source === 'profile' ? {headers: {'X-Profile-Ticket': ticket}} : undefined);
|
||||
return {ok: response.ok, status: response.status, data: response.ok ? await response.json() : null};
|
||||
} catch (err) {
|
||||
console.error(`Request to ${url} failed:`, err);
|
||||
return {ok: false, status: 0, data: null};
|
||||
}
|
||||
}
|
||||
|
||||
export const libraryLink = (status) => `/library?hideDuplicates=true${status ? `&status=${status}` : ''}`;
|
||||
|
||||
// A visitor has no game page of the owner; the Steam store is the only public one. Manual games have no appid there.
|
||||
export const gameHref = (appid, own) => {
|
||||
if (own) return `/library/${appid}`;
|
||||
return appid > 0 ? `https://store.steampowered.com/app/${appid}` : null;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {gameHref, libraryLink, statGamesUrl} from './statGames.js';
|
||||
|
||||
describe('statGamesUrl', () => {
|
||||
it('own_EncodesTheArgAndPage', () => {
|
||||
expect(statGamesUrl({source: 'own', key: 'GENRE', arg: 'Role-Playing & Co', page: 2}))
|
||||
.toMatch(/\/api\/library\/stats\/games\?key=GENRE&arg=Role-Playing%20%26%20Co&page=2$/);
|
||||
});
|
||||
|
||||
it('profile_And_Community_UseTheirPaths', () => {
|
||||
expect(statGamesUrl({source: 'profile', key: 'PILE'})).toMatch(/\/api\/public\/profiles\/library\/stats\/games\?key=PILE&page=0$/);
|
||||
expect(statGamesUrl({source: 'community', key: 'OWNED'})).toMatch(/\/api\/stats\/public\/games\?key=OWNED&page=0$/);
|
||||
});
|
||||
|
||||
it('points_TrackAndHistory', () => {
|
||||
expect(statGamesUrl({source: 'points', track: 'GAMES_FINISHED'})).toMatch(/\/api\/rewards\/progress\/GAMES_FINISHED\/games\?page=0$/);
|
||||
expect(statGamesUrl({source: 'points', kind: 'MONTH', ref: '2026-09', page: 1}))
|
||||
.toMatch(/\/api\/rewards\/history\/MONTH\/2026-09\/games\?page=1$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('libraryLink', () => {
|
||||
it('addsTheStatusOnlyWhenGiven', () => {
|
||||
expect(libraryLink()).toBe('/library?hideDuplicates=true');
|
||||
expect(libraryLink('BACKLOG')).toBe('/library?hideDuplicates=true&status=BACKLOG');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gameHref', () => {
|
||||
it('own_LinksTheLibraryPage', () => expect(gameHref(5, true)).toBe('/library/5'));
|
||||
it('visitor_LinksTheStoreOnlyForSteamApps', () => {
|
||||
expect(gameHref(5, false)).toBe('https://store.steampowered.com/app/5');
|
||||
expect(gameHref(-3, false)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user