Skip to content

Commit 074c8b0

Browse files
fix: migrate item_loader from jQuery to fetch (#266)
1 parent ccd883a commit 074c8b0

4 files changed

Lines changed: 161 additions & 121 deletions

File tree

eslint.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export default [
1515
globals: {
1616
...globals.browser,
1717
...globals.node,
18-
...globals.jquery,
1918
// cross-file globals injected by item_detail.js into the browser scope
2019
"base_url": "readonly",
2120
"base_path": "readonly",

gh-pages-template/assets/js/item_loader.js

Lines changed: 116 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -400,108 +400,126 @@ function createPlatformCardElement(platform, platform_region_flag_map, metadata_
400400
return column;
401401
}
402402

403-
$(document).ready(function(){
404-
// Set cache = false for all jquery ajax requests.
405-
$.ajaxSetup({
406-
cache: false,
407-
})
403+
/**
404+
* Map platform release regions to display flags and Bootstrap font sizes.
405+
*
406+
* @type {Object.<string, {code: string, size: string}>}
407+
*/
408+
const platform_region_flag_map = {
409+
"europe": {
410+
"code": String.fromCodePoint(0x1F1EA, 0x1F1FA),
411+
"size": "fs-2",
412+
},
413+
"north_america": {
414+
"code": String.fromCodePoint(0x1F1FA, 0x1F1F8),
415+
"size": "fs-2",
416+
},
417+
"australia": {
418+
"code": String.fromCodePoint(0x1F1E6, 0x1F1FA),
419+
"size": "fs-2",
420+
},
421+
"new_zealand": {
422+
"code": String.fromCodePoint(0x1F1F3, 0x1F1FF),
423+
"size": "fs-2",
424+
},
425+
"japan": {
426+
"code": String.fromCodePoint(0x1F1EF, 0x1F1F5),
427+
"size": "fs-2",
428+
},
429+
"china": {
430+
"code": String.fromCodePoint(0x1F1E8, 0x1F1F3),
431+
"size": "fs-2",
432+
},
433+
"asia": {
434+
"code": String.fromCodePoint(0x1F30F),
435+
"size": "fs-4",
436+
},
437+
"worldwide": {
438+
"code": String.fromCodePoint(0x1F30E),
439+
"size": "fs-4",
440+
},
441+
"korea": {
442+
"code": String.fromCodePoint(0x1F1F0, 0x1F1F7),
443+
"size": "fs-2",
444+
},
445+
"brazil": {
446+
"code": String.fromCodePoint(0x1F1E7, 0x1F1F7),
447+
"size": "fs-2",
448+
},
449+
}
408450

409-
// get platform cross-reference from json
410-
let platform_xref
411-
let get_platform_xref = function() {
412-
$.ajax({
413-
url: `${base_url}/platforms/cross-reference.json`,
414-
type: "GET",
415-
dataType: "json",
416-
async: false, // this is false, so we can set the platform_xref variable
417-
success: function (result) {
418-
platform_xref = result
419-
}
420-
})
421-
}
451+
/**
452+
* Map platform version metadata keys to Material Symbols icon names.
453+
*
454+
* Keys with null values are rendered by specialized handlers.
455+
*
456+
* @type {Object.<string, string|null>}
457+
*/
458+
const metadata_key_icon_map = {
459+
// material icons
460+
'os': 'code_blocks',
461+
'cpu': 'memory',
462+
'graphics': 'developer_board',
463+
'memory': 'memory_alt',
464+
'storage': 'storage',
465+
'media': 'save',
466+
'connectivity': 'cable',
467+
'output': 'settings_input_component',
468+
'resolutions': 'aspect_ratio',
469+
'sound': 'volume_up',
470+
// these will be processed slightly differently
471+
'platform_version_release_dates': null,
472+
'summary': null,
473+
}
422474

423-
let platform_region_flag_map = {
424-
"europe": {
425-
"code": String.fromCodePoint(0x1F1EA, 0x1F1FA),
426-
"size": "fs-2",
427-
},
428-
"north_america": {
429-
"code": String.fromCodePoint(0x1F1FA, 0x1F1F8),
430-
"size": "fs-2",
431-
},
432-
"australia": {
433-
"code": String.fromCodePoint(0x1F1E6, 0x1F1FA),
434-
"size": "fs-2",
435-
},
436-
"new_zealand": {
437-
"code": String.fromCodePoint(0x1F1F3, 0x1F1FF),
438-
"size": "fs-2",
439-
},
440-
"japan": {
441-
"code": String.fromCodePoint(0x1F1EF, 0x1F1F5),
442-
"size": "fs-2",
443-
},
444-
"china": {
445-
"code": String.fromCodePoint(0x1F1E8, 0x1F1F3),
446-
"size": "fs-2",
447-
},
448-
"asia": {
449-
"code": String.fromCodePoint(0x1F30F),
450-
"size": "fs-4",
451-
},
452-
"worldwide": {
453-
"code": String.fromCodePoint(0x1F30E),
454-
"size": "fs-4",
455-
},
456-
"korea": {
457-
"code": String.fromCodePoint(0x1F1F0, 0x1F1F7),
458-
"size": "fs-2",
459-
},
460-
"brazil": {
461-
"code": String.fromCodePoint(0x1F1E7, 0x1F1F7),
462-
"size": "fs-2",
463-
},
464-
}
475+
/**
476+
* Fetch a JSON resource without using the browser HTTP cache.
477+
*
478+
* @param {string} url - URL to load.
479+
* @returns {Promise<object>} Parsed JSON response.
480+
*/
481+
function fetchJson(url) {
482+
return fetch(url, { cache: "no-store" }).then(response => response.json());
483+
}
465484

466-
let metadata_key_icon_map = {
467-
// material icons
468-
'os': 'code_blocks',
469-
'cpu': 'memory',
470-
'graphics': 'developer_board',
471-
'memory': 'memory_alt',
472-
'storage': 'storage',
473-
'media': 'save',
474-
'connectivity': 'cable',
475-
'output': 'settings_input_component',
476-
'resolutions': 'aspect_ratio',
477-
'sound': 'volume_up',
478-
// these will be processed slightly differently
479-
'platform_version_release_dates': null,
480-
'summary': null,
481-
}
485+
/**
486+
* Render platform cards after enriching IGDB platform data with cross-reference metadata.
487+
*
488+
* @param {Object.<string, object>} result - Platform records keyed by source identifier.
489+
* @param {Object.<string, object>} platform_xref - Platform cross-reference records.
490+
* @returns {void}
491+
*/
492+
function renderPlatformCards(result, platform_xref) {
493+
const platforms = processPlatformsData(result, platform_xref);
494+
const sorted = platforms.toSorted(globalThis.rankingSorter("name", "id")).reverse();
482495

496+
for(let item in sorted) {
497+
const column = createPlatformCardElement(sorted[item], platform_region_flag_map, metadata_key_icon_map, base_path);
498+
platforms_container.appendChild(column);
499+
}
500+
}
483501

484-
// create platform cards
485-
let initialize = function(){
486-
$.ajax({
487-
url: `${base_url}/platforms/all.json`,
488-
type: "GET",
489-
dataType:"json",
490-
success: function (result) {
491-
const platforms = processPlatformsData(result, platform_xref);
492-
const sorted = platforms.toSorted(globalThis.rankingSorter("name", "id")).reverse();
502+
/**
503+
* Load platform cross-reference and platform data, then render the platform card grid.
504+
*
505+
* @returns {Promise<void>} Resolves when platform cards have been rendered.
506+
*/
507+
function initializePlatformCards() {
508+
return fetchJson(`${base_url}/platforms/cross-reference.json`)
509+
.then(platform_xref => fetchJson(`${base_url}/platforms/all.json`)
510+
.then(result => {
511+
renderPlatformCards(result, platform_xref);
512+
}));
513+
}
493514

494-
for(let item in sorted) {
495-
const column = createPlatformCardElement(sorted[item], platform_region_flag_map, metadata_key_icon_map, base_path);
496-
platforms_container.appendChild(column);
497-
}
498-
}
515+
/* istanbul ignore next */
516+
if (typeof module === "undefined") {
517+
document.addEventListener("DOMContentLoaded", () => {
518+
initializePlatformCards().catch(error => {
519+
console.error("Failed to initialize platform cards", error);
499520
});
500-
}
501-
502-
get_platform_xref()
503-
initialize()
504-
})
521+
});
522+
}
505523

506524
/**
507525
* Search for games by name across all buckets.
@@ -609,6 +627,9 @@ if (typeof module !== "undefined") {
609627
addMetadataItemToFooter,
610628
processPlatformsData,
611629
createPlatformCardElement,
630+
fetchJson,
631+
renderPlatformCards,
632+
initializePlatformCards,
612633
run_search,
613634
};
614635
}

gh-pages-template/index.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
ext-css:
1212
- href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200
1313
ext-js:
14-
- https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
15-
- https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/levenshtein-distance.js
16-
- https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/ranking-sorter.js
14+
- href: "https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/levenshtein-distance.js"
15+
- href: "https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/ranking-sorter.js"
1716
js:
1817
- /GameDB/assets/js/item_loader.js
1918
---

tests/item_loader.test.js

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,6 @@ document.body.innerHTML = `
2222
<input id="search_term" value="" />
2323
`;
2424

25-
// Mock jQuery — fire ready callback synchronously, and fire ajax success immediately
26-
globalThis.$ = function() {
27-
return { ready: (fn) => fn() };
28-
};
29-
globalThis.$.ajaxSetup = () => {};
30-
globalThis.$.ajax = function(opts) {
31-
if (opts?.success) {
32-
// Return mock data appropriate to the URL
33-
if (opts?.url?.includes('cross-reference')) {
34-
opts.success({});
35-
} else if (opts?.url?.includes('all.json')) {
36-
opts.success({
37-
'1': {
38-
id: 1, name: 'PC', url: 'https://igdb.com', summary: 'Personal computer.',
39-
screenscraper_id: null, screenscraper_region: null,
40-
},
41-
});
42-
}
43-
}
44-
};
45-
globalThis.jQuery = globalThis.$;
46-
4725
const {
4826
splitString,
4927
fetchGameData,
@@ -58,6 +36,8 @@ const {
5836
addMetadataItemToFooter,
5937
processPlatformsData,
6038
createPlatformCardElement,
39+
fetchJson,
40+
initializePlatformCards,
6141
run_search,
6242
} = require('../gh-pages-template/assets/js/item_loader.js');
6343

@@ -117,6 +97,16 @@ describe('item_loader.js', () => {
11797
});
11898
});
11999

100+
describe('fetchJson', () => {
101+
test('fetches JSON with no-store cache behavior', async () => {
102+
globalThis.fetch = jest.fn().mockResolvedValue({
103+
json: () => Promise.resolve({ ok: true }),
104+
});
105+
await expect(fetchJson('/data.json')).resolves.toEqual({ ok: true });
106+
expect(globalThis.fetch).toHaveBeenCalledWith('/data.json', { cache: 'no-store' });
107+
});
108+
});
109+
120110
describe('createGameCard', () => {
121111
test('renders card with cover image', () => {
122112
const col = createGameCard(1, {
@@ -398,7 +388,38 @@ describe('item_loader.js', () => {
398388
});
399389
});
400390

391+
describe('initializePlatformCards', () => {
392+
test('loads cross-reference before rendering platform cards', async () => {
393+
globalThis.fetch = jest.fn(url => Promise.resolve({
394+
json: () => Promise.resolve(
395+
url.includes('cross-reference')
396+
? { pc: { ids: { igdb: 1, screenscraper: 123 }, variables: { screenscraper: { region: 'us' } } } }
397+
: { '1': { id: 1, name: 'PC', url: 'https://igdb.com', summary: 'Personal computer.' } },
398+
),
399+
}));
400+
401+
await initializePlatformCards();
402+
403+
expect(globalThis.fetch).toHaveBeenNthCalledWith(
404+
1,
405+
'http://localhost/GameDB/platforms/cross-reference.json',
406+
{ cache: 'no-store' },
407+
);
408+
expect(globalThis.fetch).toHaveBeenNthCalledWith(
409+
2,
410+
'http://localhost/GameDB/platforms/all.json',
411+
{ cache: 'no-store' },
412+
);
413+
expect(document.getElementById('platforms-container').textContent).toContain('PC');
414+
});
415+
});
416+
401417
describe('run_search', () => {
418+
/**
419+
* Wait for queued promise callbacks in fetch-driven rendering tests.
420+
*
421+
* @returns {Promise<void>} Resolves after the current macrotask.
422+
*/
402423
const flushPromises = () => new Promise(r => setTimeout(r, 0));
403424

404425
test('does nothing when search term is empty', () => {

0 commit comments

Comments
 (0)