From 5faa1ac3275a5798c549d8cca57b14277b24e30b Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:52:16 -0500 Subject: [PATCH 01/16] feat: implement item pages --- README.md | 2 +- gh-pages-template/_config.yml | 2 + gh-pages-template/_includes/image_modal.html | 74 ++++ .../assets/js/character_detail.js | 46 +++ .../assets/js/collection_detail.js | 28 ++ .../assets/js/franchise_detail.js | 28 ++ gh-pages-template/assets/js/game_detail.js | 366 ++++++++++++++++++ gh-pages-template/assets/js/item_detail.js | 192 +++++++++ gh-pages-template/assets/js/item_loader.js | 131 ++++++- .../assets/js/platform_detail.js | 221 +++++++++++ gh-pages-template/browse/characters.html | 56 +++ gh-pages-template/browse/collections.html | 46 +++ gh-pages-template/browse/franchises.html | 46 +++ gh-pages-template/browse/games.html | 108 ++++++ gh-pages-template/browse/platforms.html | 76 ++++ gh-pages-template/index.html | 17 +- src/platforms.py | 2 +- 17 files changed, 1425 insertions(+), 16 deletions(-) create mode 100644 gh-pages-template/_config.yml create mode 100644 gh-pages-template/_includes/image_modal.html create mode 100644 gh-pages-template/assets/js/character_detail.js create mode 100644 gh-pages-template/assets/js/collection_detail.js create mode 100644 gh-pages-template/assets/js/franchise_detail.js create mode 100644 gh-pages-template/assets/js/game_detail.js create mode 100644 gh-pages-template/assets/js/item_detail.js create mode 100644 gh-pages-template/assets/js/platform_detail.js create mode 100644 gh-pages-template/browse/characters.html create mode 100644 gh-pages-template/browse/collections.html create mode 100644 gh-pages-template/browse/franchises.html create mode 100644 gh-pages-template/browse/games.html create mode 100644 gh-pages-template/browse/platforms.html diff --git a/README.md b/README.md index ae4328d688d3..df633e669e76 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,6 @@ Information from YouTube API is also added to the database for videos. - [x] Build with Jekyll - [ ] Revamp index page - - [ ] Provide an item page for each API item + - [x] Provide an item page for each API item - [ ] Add unit tests - [ ] Add code coverage diff --git a/gh-pages-template/_config.yml b/gh-pages-template/_config.yml new file mode 100644 index 000000000000..29db1100929a --- /dev/null +++ b/gh-pages-template/_config.yml @@ -0,0 +1,2 @@ +--- +site-js: [] # disable crowdin for this site diff --git a/gh-pages-template/_includes/image_modal.html b/gh-pages-template/_includes/image_modal.html new file mode 100644 index 000000000000..98c416189532 --- /dev/null +++ b/gh-pages-template/_includes/image_modal.html @@ -0,0 +1,74 @@ + + + + diff --git a/gh-pages-template/assets/js/character_detail.js b/gh-pages-template/assets/js/character_detail.js new file mode 100644 index 000000000000..2ace1a15bd2e --- /dev/null +++ b/gh-pages-template/assets/js/character_detail.js @@ -0,0 +1,46 @@ +/** + * character_detail.js + * Renders a single character detail page. + * Depends on item_detail.js being loaded first. + */ + +function renderCharacter(data) { + document.title = (data.name || "Character") + " – GameDB"; + document.getElementById("character-name").textContent = data.name || "Unknown Character"; + + // Mug shot + const mugEl = document.getElementById("character-mug"); + const mugPlaceholder = document.getElementById("character-mug-placeholder"); + const mugUrl = data.mug_shot && data.mug_shot.url + ? igdbImageUrl(data.mug_shot.url, "t_screenshot_big") + : null; + if (mugUrl) { + mugEl.src = mugUrl; + mugEl.alt = data.name || ""; + mugEl.style.display = ""; + } else { + mugPlaceholder.style.display = null; + mugPlaceholder.style.removeProperty("display"); + } + + // Badges / meta + const badgesEl = document.getElementById("character-badges"); + + if (data.character_gender && data.character_gender.name) { + badgesEl.appendChild(makeBadge(data.character_gender.name, "bg-info text-dark")); + } + if (data.character_species && data.character_species.name) { + badgesEl.appendChild(makeBadge(data.character_species.name, "bg-secondary")); + } + + // Games + if (data.games && data.games.length > 0) { + const section = document.getElementById("character-games-section"); + section.classList.remove("d-none"); + renderGameList(document.getElementById("character-games"), data.games); + } +} + +document.addEventListener("DOMContentLoaded", () => { + loadItemDetail("characters", renderCharacter); +}); diff --git a/gh-pages-template/assets/js/collection_detail.js b/gh-pages-template/assets/js/collection_detail.js new file mode 100644 index 000000000000..cfdf91eb460c --- /dev/null +++ b/gh-pages-template/assets/js/collection_detail.js @@ -0,0 +1,28 @@ +/** + * collection_detail.js + * Renders a single collection (series) detail page. + * Depends on item_detail.js being loaded first. + */ + +function renderCollection(data) { + document.title = (data.name || "Series") + " – GameDB"; + document.getElementById("collection-name").textContent = data.name || "Unknown Series"; + + // IGDB link + if (data.url) { + const igdbLink = document.getElementById("collection-igdb-link"); + igdbLink.href = data.url; + igdbLink.classList.remove("d-none"); + } + + // Games + if (data.games && data.games.length > 0) { + const section = document.getElementById("collection-games-section"); + section.classList.remove("d-none"); + renderGameList(document.getElementById("collection-games"), data.games); + } +} + +document.addEventListener("DOMContentLoaded", () => { + loadItemDetail("collections", renderCollection); +}); diff --git a/gh-pages-template/assets/js/franchise_detail.js b/gh-pages-template/assets/js/franchise_detail.js new file mode 100644 index 000000000000..b0229d4089b7 --- /dev/null +++ b/gh-pages-template/assets/js/franchise_detail.js @@ -0,0 +1,28 @@ +/** + * franchise_detail.js + * Renders a single franchise detail page. + * Depends on item_detail.js being loaded first. + */ + +function renderFranchise(data) { + document.title = (data.name || "Franchise") + " – GameDB"; + document.getElementById("franchise-name").textContent = data.name || "Unknown Franchise"; + + // IGDB link + if (data.url) { + const igdbLink = document.getElementById("franchise-igdb-link"); + igdbLink.href = data.url; + igdbLink.classList.remove("d-none"); + } + + // Games + if (data.games && data.games.length > 0) { + const section = document.getElementById("franchise-games-section"); + section.classList.remove("d-none"); + renderGameList(document.getElementById("franchise-games"), data.games); + } +} + +document.addEventListener("DOMContentLoaded", () => { + loadItemDetail("franchises", renderFranchise); +}); diff --git a/gh-pages-template/assets/js/game_detail.js b/gh-pages-template/assets/js/game_detail.js new file mode 100644 index 000000000000..cdef7bcc4ba1 --- /dev/null +++ b/gh-pages-template/assets/js/game_detail.js @@ -0,0 +1,366 @@ +/** + * game_detail.js + * Renders a single game detail page. + * Depends on item_detail.js being loaded first. + */ + +function renderGame(data) { + // Title + document.title = (data.name || "Game") + " – GameDB"; + document.getElementById("game-name").textContent = data.name || "Unknown Game"; + + // Banner/Artworks carousel + if (data.artworks && data.artworks.length > 0) { + const bannerSection = document.getElementById("game-banner-section"); + const bannerInner = document.getElementById("game-banner-inner"); + bannerSection.classList.remove("d-none"); + + data.artworks.forEach((artwork, index) => { + const item = document.createElement("div"); + item.className = `carousel-item${index === 0 ? " active" : ""}`; + const img = document.createElement("img"); + img.src = igdbImageUrl(artwork.url, "t_screenshot_huge"); + img.className = "d-block w-100"; + img.alt = `${data.name} artwork ${index + 1}`; + img.style.maxHeight = "400px"; + img.style.objectFit = "cover"; + item.appendChild(img); + bannerInner.appendChild(item); + }); + } + + // Cover + const coverEl = document.getElementById("game-cover"); + const coverPlaceholder = document.getElementById("game-cover-placeholder"); + if (data.cover && data.cover.url) { + coverEl.src = igdbImageUrl(data.cover.url, "t_cover_big"); + coverEl.alt = data.name || ""; + coverEl.style.display = ""; + } else { + coverPlaceholder.style.display = null; + coverPlaceholder.style.removeProperty("display"); + } + + // Badges: genres, themes, game modes, player perspectives + const badgesEl = document.getElementById("game-badges"); + + const addBadgeGroup = (items, cls) => { + if (items && items.length > 0) { + items.forEach(item => badgesEl.appendChild(makeBadge(item.name, cls))); + } + }; + + addBadgeGroup(data.genres, "bg-primary"); + addBadgeGroup(data.themes, "bg-info text-dark"); + addBadgeGroup(data.game_modes, "bg-success"); + addBadgeGroup(data.player_perspectives, "bg-warning text-dark"); + + // Metadata dl + const metaDl = document.getElementById("game-meta"); + + // Ratings + if (data.rating !== undefined && data.rating !== null) { + addDlRow(metaDl, "User Rating", `${Math.round(data.rating)} / 100`); + } + if (data.aggregated_rating !== undefined && data.aggregated_rating !== null) { + addDlRow(metaDl, "Critic Rating", `${Math.round(data.aggregated_rating)} / 100`); + } + + // Age ratings + if (data.age_ratings && data.age_ratings.length > 0) { + const ratingsEl = document.createDocumentFragment(); + data.age_ratings.forEach(r => { + if (r.rating_category && r.organization) { + const badge = makeBadge(`${r.organization.name}: ${r.rating_category.rating}`, "bg-dark"); + ratingsEl.appendChild(badge); + } + }); + if (ratingsEl.childElementCount > 0) { + addDlRow(metaDl, "Age Ratings", ratingsEl); + } + } + + // Platforms + if (data.platforms && data.platforms.length > 0) { + const platformsEl = document.createDocumentFragment(); + // platforms here are just IDs (integers), not objects + // We resolve them from the all.json file + const platformUrl = `${base_url}/platforms/all.json`; + fetch(platformUrl) + .then(r => r.json()) + .then(allPlatforms => { + data.platforms.forEach(platformId => { + const p = allPlatforms[String(platformId)]; + const name = p ? p.name : `Platform #${platformId}`; + const a = document.createElement("a"); + a.href = `${base_path}/browse/platforms/?id=${platformId}`; + a.className = "text-decoration-none me-1"; + a.appendChild(makeBadge(name, "bg-secondary")); + platformsEl.appendChild(a); + }); + addDlRow(metaDl, "Platforms", platformsEl); + }) + .catch(() => { + const el = document.createDocumentFragment(); + data.platforms.forEach(pid => { + const a = document.createElement("a"); + a.href = `${base_path}/browse/platforms/?id=${pid}`; + a.className = "text-decoration-none me-1"; + a.appendChild(makeBadge(`#${pid}`, "bg-secondary")); + el.appendChild(a); + }); + addDlRow(metaDl, "Platforms", el); + }); + } + + // Release dates + if (data.release_dates && data.release_dates.length > 0) { + const releasesEl = document.createDocumentFragment(); + // Group by platform and region + data.release_dates.forEach(rd => { + if (rd.date || rd.y) { + const div = document.createElement("div"); + const regionName = (rd.release_region && rd.release_region.region) || null; + const flag = regionName ? getRegionFlag(regionName) : ""; + div.textContent = `${flag} ${rd.human || rd.y || ""}`.trim(); + releasesEl.appendChild(div); + } + }); + if (releasesEl.childElementCount > 0) { + addDlRow(metaDl, "Release Dates", releasesEl); + } + } + + // Developers / Publishers + if (data.involved_companies && data.involved_companies.length > 0) { + const devs = data.involved_companies.filter(c => c.developer).map(c => c.company && c.company.name).filter(Boolean); + const pubs = data.involved_companies.filter(c => !c.developer).map(c => c.company && c.company.name).filter(Boolean); + if (devs.length > 0) { + addDlRow(metaDl, "Developer(s)", devs.join(", ")); + } + if (pubs.length > 0) { + addDlRow(metaDl, "Publisher(s)", pubs.join(", ")); + } + } + + // Collections / series + if (data.collections && data.collections.length > 0) { + const el = document.createDocumentFragment(); + data.collections.forEach(c => { + const a = document.createElement("a"); + a.href = `${base_path}/browse/collections/?id=${c.id || c}`; + a.className = "text-decoration-none me-1"; + a.appendChild(makeBadge(c.name || `#${c}`, "bg-primary")); + el.appendChild(a); + }); + addDlRow(metaDl, "Series", el); + } + + // Franchises + if (data.franchises && data.franchises.length > 0) { + const el = document.createDocumentFragment(); + data.franchises.forEach(f => { + const a = document.createElement("a"); + a.href = `${base_path}/browse/franchises/?id=${f.id || f}`; + a.className = "text-decoration-none me-1"; + a.appendChild(makeBadge(f.name || `#${f}`, "bg-info text-dark")); + el.appendChild(a); + }); + addDlRow(metaDl, "Franchise(s)", el); + } else if (data.franchise && data.franchise.name) { + const el = document.createDocumentFragment(); + const a = document.createElement("a"); + a.href = `${base_path}/browse/franchises/?id=${data.franchise.id || ""}`; + a.className = "text-decoration-none me-1"; + a.appendChild(makeBadge(data.franchise.name, "bg-info text-dark")); + el.appendChild(a); + addDlRow(metaDl, "Franchise", el); + } + + // Multiplayer + if (data.multiplayer_modes && data.multiplayer_modes.length > 0) { + const mm = data.multiplayer_modes[0]; + const parts = []; + if (mm.offlinecoopmax) parts.push(`Co-op (offline): ${mm.offlinecoopmax} players`); + if (mm.onlinecoopmax) parts.push(`Co-op (online): ${mm.onlinecoopmax} players`); + if (mm.offlinemax) parts.push(`Versus (offline): ${mm.offlinemax} players`); + if (mm.onlinemax) parts.push(`Versus (online): ${mm.onlinemax} players`); + if (parts.length > 0) { + addDlRow(metaDl, "Multiplayer", parts.join(" · ")); + } + } + + // Summary + if (data.summary) { + const summarySection = document.getElementById("game-summary-section"); + summarySection.classList.remove("d-none"); + document.getElementById("game-summary").textContent = data.summary; + } + + // Storyline + if (data.storyline) { + const storylineSection = document.getElementById("game-storyline-section"); + storylineSection.classList.remove("d-none"); + document.getElementById("game-storyline").textContent = data.storyline; + } + + // Screenshots + if (data.screenshots && data.screenshots.length > 0) { + const section = document.getElementById("game-screenshots-section"); + section.classList.remove("d-none"); + const container = document.getElementById("game-screenshots"); + + // Prepare full-size image URLs for modal + const fullSizeUrls = data.screenshots.map(ss => igdbImageUrl(ss.url, "t_1080p")); + + data.screenshots.forEach((ss, index) => { + const col = document.createElement("div"); + col.className = "col"; + const btn = document.createElement("button"); + btn.className = "btn p-0 border-0 w-100"; + btn.onclick = () => window.openImageModal(fullSizeUrls, index); + const img = document.createElement("img"); + img.className = "img-fluid rounded shadow-sm w-100"; + img.src = igdbImageUrl(ss.url, "t_screenshot_med"); + img.alt = ""; + img.loading = "lazy"; + img.style.cursor = "pointer"; + btn.appendChild(img); + col.appendChild(btn); + container.appendChild(col); + }); + } + + // Videos + if (data.videos && data.videos.length > 0) { + const section = document.getElementById("game-videos-section"); + section.classList.remove("d-none"); + const container = document.getElementById("game-videos"); + data.videos.forEach(video => { + if (!video.video_id) return; + const col = document.createElement("div"); + col.className = "col"; + const card = document.createElement("div"); + card.className = "card h-100 border-0 shadow-sm rounded-0"; + col.appendChild(card); + container.appendChild(col); + + // Embed YouTube video + const embedContainer = document.createElement("div"); + embedContainer.className = "ratio ratio-16x9 rounded-0"; + const iframe = document.createElement("iframe"); + iframe.src = `https://www.youtube.com/embed/${video.video_id}`; + iframe.title = video.name || video.title || "Video"; + iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; + iframe.allowFullscreen = true; + iframe.loading = "lazy"; + embedContainer.appendChild(iframe); + card.appendChild(embedContainer); + + if (video.name || video.title) { + const cardBody = document.createElement("div"); + cardBody.className = "card-body p-2"; + const title = document.createElement("p"); + title.className = "card-text small mb-0 fw-semibold"; + title.textContent = video.name || video.title; + cardBody.appendChild(title); + card.appendChild(cardBody); + } + }); + } + + // External links + if (data.external_games && data.external_games.length > 0) { + const section = document.getElementById("game-external-section"); + section.classList.remove("d-none"); + const container = document.getElementById("game-external"); + data.external_games.forEach(ext => { + if (!ext.url && !ext.uid) return; + const sourceName = (ext.external_game_source && ext.external_game_source.name) || "External"; + const a = document.createElement("a"); + a.href = ext.url || "#"; + a.target = "_blank"; + a.rel = "noopener"; + a.className = "btn btn-outline-secondary btn-sm"; + a.textContent = sourceName; + container.appendChild(a); + }); + } + + // Characters + if (data.characters && data.characters.length > 0) { + const section = document.getElementById("game-characters-section"); + section.classList.remove("d-none"); + const container = document.getElementById("game-characters"); + + data.characters.forEach(char => { + const col = document.createElement("div"); + col.className = "col"; + const card = document.createElement("a"); + card.href = `${base_path}/browse/characters/?id=${char.id || char}`; + card.className = "card h-100 text-decoration-none border-0 shadow-sm"; + col.appendChild(card); + container.appendChild(col); + + if (char.mug_shot && char.mug_shot.url) { + const img = document.createElement("img"); + img.className = "card-img-top rounded-top"; + img.src = igdbImageUrl(char.mug_shot.url, "t_thumb"); + img.alt = char.name || ""; + img.loading = "lazy"; + card.appendChild(img); + } else { + const placeholder = document.createElement("div"); + placeholder.className = "card-img-top bg-secondary d-flex align-items-center justify-content-center"; + placeholder.style.height = "120px"; + const icon = document.createElement("span"); + icon.className = "material-symbols-outlined text-white"; + icon.textContent = "person"; + placeholder.appendChild(icon); + card.appendChild(placeholder); + } + + if (char.name) { + const cardBody = document.createElement("div"); + cardBody.className = "card-body p-2"; + const name = document.createElement("p"); + name.className = "card-text small mb-0 text-center"; + name.textContent = char.name; + cardBody.appendChild(name); + card.appendChild(cardBody); + } + }); + } + + // IGDB link + if (data.url) { + const igdbLink = document.getElementById("game-igdb-link"); + igdbLink.href = data.url; + igdbLink.classList.remove("d-none"); + } +} + +/** + * Map region string (from release_region.region) to an emoji flag. + * @param {string} regionName + * @returns {string} + */ +function getRegionFlag(regionName) { + const map = { + "europe": "🇪🇺", + "north_america": "🇺🇸", + "australia": "🇦🇺", + "new_zealand": "🇳🇿", + "japan": "🇯🇵", + "china": "🇨🇳", + "asia": "🌏", + "worldwide": "🌍", + "korea": "🇰🇷", + "brazil": "🇧🇷", + }; + return map[regionName] || "🌐"; +} + +document.addEventListener("DOMContentLoaded", () => { + loadItemDetail("games", renderGame); +}); diff --git a/gh-pages-template/assets/js/item_detail.js b/gh-pages-template/assets/js/item_detail.js new file mode 100644 index 000000000000..f24d7c7dd496 --- /dev/null +++ b/gh-pages-template/assets/js/item_detail.js @@ -0,0 +1,192 @@ +/** + * item_detail.js + * + * Shared utility for GameDB item detail pages. + * Each page calls `loadItemDetail(endpoint, renderFn)` on DOMContentLoaded. + * + * The page URL is expected to contain `?id=`. + * + * Requires window.GAMEDB_CONFIG to be set by an inline + +
+
+ Loading... +
+

Loading character data…

+
+ + + +
+ +
+ +
+ +
+ person +
+
+ + +
+

+
+
+
+ + +
+

Appears In

+
+
+ +
+ +{% include image_modal.html %} diff --git a/gh-pages-template/browse/collections.html b/gh-pages-template/browse/collections.html new file mode 100644 index 000000000000..5b2931afca55 --- /dev/null +++ b/gh-pages-template/browse/collections.html @@ -0,0 +1,46 @@ +--- +title: Series +layout: page +full-width: false +css: + - /assets/css/hide-heading.css +js: + - /GameDB/assets/js/item_detail.js + - /GameDB/assets/js/collection_detail.js +ext-css: + - href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200 +--- + + + +
+
+ Loading... +
+

Loading series data…

+
+ + + +
+ +

+ + + +
+

Games in this Series

+
+
+ +
+ +{% include image_modal.html %} diff --git a/gh-pages-template/browse/franchises.html b/gh-pages-template/browse/franchises.html new file mode 100644 index 000000000000..6763b9592be6 --- /dev/null +++ b/gh-pages-template/browse/franchises.html @@ -0,0 +1,46 @@ +--- +title: Franchise +layout: page +full-width: false +css: + - /assets/css/hide-heading.css +js: + - /GameDB/assets/js/item_detail.js + - /GameDB/assets/js/franchise_detail.js +ext-css: + - href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200 +--- + + + +
+
+ Loading... +
+

Loading franchise data…

+
+ + + +
+ +

+ + + +
+

Games in this Franchise

+
+
+ +
+ +{% include image_modal.html %} diff --git a/gh-pages-template/browse/games.html b/gh-pages-template/browse/games.html new file mode 100644 index 000000000000..173135a4108d --- /dev/null +++ b/gh-pages-template/browse/games.html @@ -0,0 +1,108 @@ +--- +title: Game +layout: page +full-width: false +css: + - /assets/css/hide-heading.css +js: + - /GameDB/assets/js/item_detail.js + - /GameDB/assets/js/game_detail.js +ext-css: + - href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200 +--- + + + +
+
+ Loading... +
+

Loading game data…

+
+ + + +
+ + +
+ +
+ +
+ +
+ +
+ sports_esports +
+
+ + +
+

+
+ +
+ +
+
Summary
+

+
+ +
+
Storyline
+

+
+
+
+ + +
+

Characters

+
+
+ + +
+

Screenshots

+
+
+ + +
+

Videos

+
+
+ + +
+

External Links

+
+
+ + + + +
+ +{% include image_modal.html %} diff --git a/gh-pages-template/browse/platforms.html b/gh-pages-template/browse/platforms.html new file mode 100644 index 000000000000..7b1091521dc1 --- /dev/null +++ b/gh-pages-template/browse/platforms.html @@ -0,0 +1,76 @@ +--- +title: Platform +layout: page +full-width: false +css: + - /assets/css/hide-heading.css +js: + - /GameDB/assets/js/item_detail.js + - /GameDB/assets/js/platform_detail.js +ext-css: + - href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200 +--- + + + +
+
+ Loading... +
+

Loading platform data…

+
+ + + +
+ +
+ +
+ +
+ devices +
+
+ + +
+

+
+ +
+ +
+
About
+

+
+
+
+ + +
+

Hardware Versions

+
+
+ + +
+

Games

+
+
+ + + + +
+ +{% include image_modal.html %} diff --git a/gh-pages-template/index.html b/gh-pages-template/index.html index 2e51e1aa29d7..214062d6849c 100644 --- a/gh-pages-template/index.html +++ b/gh-pages-template/index.html @@ -11,13 +11,19 @@ ext-css: - href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200 ext-js: -- https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js -- https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2024.921.191855/dist/levenshtein-distance.js -- https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2024.921.191855/dist/ranking-sorter.js + - https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js + - https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2024.921.191855/dist/levenshtein-distance.js + - https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2024.921.191855/dist/ranking-sorter.js js: -- /GameDB/assets/js/item_loader.js + - /GameDB/assets/js/item_loader.js --- + +
@@ -31,7 +37,8 @@
- +
diff --git a/src/platforms.py b/src/platforms.py index 02773644254f..987dc19424a3 100644 --- a/src/platforms.py +++ b/src/platforms.py @@ -1793,7 +1793,7 @@ { "ids": { "igdb": 167, - "screenscraper": None, + "screenscraper": 284, }, "name": "PlayStation 5", "variables": { From 422bb9064e6f2d27e7ed1bcfa8ed278c8ff61c2f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:37:08 -0500 Subject: [PATCH 02/16] Replace carousel with beautiful-jekyll-next banner Replace the Bootstrap carousel-based banner with a beautiful-jekyll-next style banner implementation. HTML: remove the carousel markup and add a hidden #game-big-imgs element to store artwork data attributes and a visible #game-banner-header with an .img-desc for captions. JS: stop building carousel items; instead set data-img-src-X and data-num-img on #game-big-imgs and introduce initGameBanner() which initializes the header background, prefetches images, and cycles them with a fade transition (timing/prefetch logic included). Keeps existing content rendering and preserves fallback when no artworks are present. --- gh-pages-template/assets/js/game_detail.js | 97 ++++++++++++++++++---- gh-pages-template/browse/games.html | 19 ++--- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/gh-pages-template/assets/js/game_detail.js b/gh-pages-template/assets/js/game_detail.js index cdef7bcc4ba1..bc5d4230a56d 100644 --- a/gh-pages-template/assets/js/game_detail.js +++ b/gh-pages-template/assets/js/game_detail.js @@ -9,24 +9,23 @@ function renderGame(data) { document.title = (data.name || "Game") + " – GameDB"; document.getElementById("game-name").textContent = data.name || "Unknown Game"; - // Banner/Artworks carousel + // Banner/Artworks (beautiful-jekyll-next style) if (data.artworks && data.artworks.length > 0) { - const bannerSection = document.getElementById("game-banner-section"); - const bannerInner = document.getElementById("game-banner-inner"); - bannerSection.classList.remove("d-none"); + const bigImgsEl = document.getElementById("game-big-imgs"); + const bannerHeader = document.getElementById("game-banner-header"); + // Set data attributes for each artwork + bigImgsEl.setAttribute("data-num-img", data.artworks.length); data.artworks.forEach((artwork, index) => { - const item = document.createElement("div"); - item.className = `carousel-item${index === 0 ? " active" : ""}`; - const img = document.createElement("img"); - img.src = igdbImageUrl(artwork.url, "t_screenshot_huge"); - img.className = "d-block w-100"; - img.alt = `${data.name} artwork ${index + 1}`; - img.style.maxHeight = "400px"; - img.style.objectFit = "cover"; - item.appendChild(img); - bannerInner.appendChild(item); + const imgNum = index + 1; + bigImgsEl.setAttribute(`data-img-src-${imgNum}`, igdbImageUrl(artwork.url, "t_screenshot_huge")); }); + + // Show the banner + bannerHeader.classList.remove("d-none"); + + // Initialize the image display (mimics beautifuljekyll.js behavior) + initGameBanner(); } // Cover @@ -361,6 +360,76 @@ function getRegionFlag(regionName) { return map[regionName] || "🌐"; } +/** + * Initialize game banner with cycling images (mimics beautiful-jekyll-next behavior) + */ +function initGameBanner() { + const bigImgsEl = document.getElementById("game-big-imgs"); + const numImgs = parseInt(bigImgsEl.getAttribute("data-num-img")); + + if (!numImgs || numImgs === 0) return; + + // Set initial image + const getImgInfo = function(imgNum) { + const src = bigImgsEl.getAttribute(`data-img-src-${imgNum}`); + const desc = bigImgsEl.getAttribute(`data-img-desc-${imgNum}`); + return { src, desc }; + }; + + const setImg = function(src, desc) { + const bannerHeader = document.getElementById("game-banner-header"); + bannerHeader.style.backgroundImage = `url(${src})`; + + const imgDesc = bannerHeader.querySelector(".img-desc"); + if (desc && desc !== "null") { + imgDesc.textContent = desc; + imgDesc.style.display = "block"; + } else { + imgDesc.style.display = "none"; + } + }; + + // Set first image + const firstImg = getImgInfo(1); + setImg(firstImg.src, firstImg.desc); + + // Cycle through images if multiple + if (numImgs > 1) { + let currentImgNum = 1; + + const getNextImg = function() { + currentImgNum = (currentImgNum % numImgs) + 1; + const imgInfo = getImgInfo(currentImgNum); + const src = imgInfo.src; + const desc = imgInfo.desc; + + // Prefetch next image + const prefetchImg = new Image(); + prefetchImg.src = src; + + setTimeout(function() { + const img = document.createElement("div"); + img.className = "big-img-transition"; + img.style.backgroundImage = `url(${src})`; + document.getElementById("game-banner-header").prepend(img); + + setTimeout(function() { + img.style.opacity = "1"; + }, 50); + + // After fade-in completes, update main image and remove transition element + setTimeout(function() { + setImg(src, desc); + img.remove(); + getNextImg(); + }, 1000); + }, 6000); + }; + + getNextImg(); + } +} + document.addEventListener("DOMContentLoaded", () => { loadItemDetail("games", renderGame); }); diff --git a/gh-pages-template/browse/games.html b/gh-pages-template/browse/games.html index 173135a4108d..9eb41a076467 100644 --- a/gh-pages-template/browse/games.html +++ b/gh-pages-template/browse/games.html @@ -28,19 +28,12 @@
- -
- + + + + +
+
From f0bf8286b1b486f6a5f8e261a61642767157844d Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:09:07 -0500 Subject: [PATCH 03/16] Improve search UI and results display; tweak modal Replace the old search form with a centered input+button and onsubmit/onkeydown handlers to call run_search, removing the platform select. Rewrite search results rendering in item_loader.js: change grid breakpoints, fetch platform data to show cover images, platform badges (with earliest release year), and a fallback rendering when platform data fails; limit to first 60 matches and show a note. Remove code that previously populated a search_type select. Minor modal styling adjustments in image_modal.html (remove explicit text-white/btn-close-white classes and plain modal counter) to rely on default styles. --- gh-pages-template/_includes/image_modal.html | 6 +- gh-pages-template/assets/js/item_loader.js | 192 ++++++++++++++----- gh-pages-template/index.html | 21 +- 3 files changed, 159 insertions(+), 60 deletions(-) diff --git a/gh-pages-template/_includes/image_modal.html b/gh-pages-template/_includes/image_modal.html index 98c416189532..aab50ee075d3 100644 --- a/gh-pages-template/_includes/image_modal.html +++ b/gh-pages-template/_includes/image_modal.html @@ -3,8 +3,8 @@ diff --git a/gh-pages-template/assets/js/item_loader.js b/gh-pages-template/assets/js/item_loader.js index 1e0f3c33b01a..46e2024e4141 100644 --- a/gh-pages-template/assets/js/item_loader.js +++ b/gh-pages-template/assets/js/item_loader.js @@ -5,8 +5,6 @@ let base_path = _cfg.base_path : "/GameDB"; let base_url = window.location.origin + base_path; -// get search options, we will append each platform to this list -let search_options = document.getElementById("search_type") // get platforms container let platforms_container = document.getElementById("platforms-container") @@ -143,11 +141,6 @@ $(document).ready(function(){ let sorted = platforms.sort(window.rankingSorter("name", "id")).reverse() for(let item in sorted) { - // create search option - let search_option = document.createElement("option") - search_option.value = sorted[item]['id'] - search_option.textContent = sorted[item]['name'] - search_options.appendChild(search_option) let column = document.createElement("div") column.className = "col-lg-4 mb-5" @@ -386,47 +379,154 @@ function run_search() { search_container.appendChild(resultsHeading) const row = document.createElement("div") - row.className = "row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-6 g-2" + row.className = "row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-3" search_container.appendChild(row) - matches.slice(0, 60).forEach(([id, game]) => { - const col = document.createElement("div") - col.className = "col" - row.appendChild(col) - - const card = document.createElement("a") - card.className = "card h-100 text-decoration-none shadow-sm border-0 rounded-0" - card.href = `${base_path}/browse/games/?id=${id}` - col.appendChild(card) - - // Placeholder cover - const placeholder = document.createElement("div") - placeholder.className = "card-img-top bg-dark d-flex align-items-center justify-content-center" - placeholder.style.height = "120px" - const icon = document.createElement("span") - icon.className = "material-symbols-outlined text-white" - icon.style.fontSize = "3rem" - icon.textContent = "sports_esports" - placeholder.appendChild(icon) - card.appendChild(placeholder) - - const cardBody = document.createElement("div") - cardBody.className = "card-body p-2" - card.appendChild(cardBody) - - const nameEl = document.createElement("p") - nameEl.className = "card-text small mb-0" - nameEl.textContent = game.name - nameEl.title = game.name - cardBody.appendChild(nameEl) - }) - - if (matches.length > 60) { - const moreNote = document.createElement("p") - moreNote.className = "text-muted mt-2 small" - moreNote.textContent = `Showing first 60 of ${matches.length} results. Try a more specific search term.` - search_container.appendChild(moreNote) - } + // Fetch platform names to display + fetch(`${base_url}/platforms/all.json`) + .then(r => r.json()) + .then(allPlatforms => { + matches.slice(0, 60).forEach(([id, game]) => { + const col = document.createElement("div") + col.className = "col" + row.appendChild(col) + + const card = document.createElement("a") + card.className = "card h-100 text-decoration-none shadow-sm border-0 rounded-0" + card.href = `${base_path}/browse/games/?id=${id}` + col.appendChild(card) + + // Cover image or placeholder + if (game.cover && game.cover.url) { + const coverImg = document.createElement("img") + coverImg.className = "card-img-top rounded-top-0" + coverImg.src = game.cover.url.replace("t_thumb", "t_cover_small") + coverImg.alt = game.name + coverImg.loading = "lazy" + coverImg.style.height = "180px" + coverImg.style.objectFit = "cover" + card.appendChild(coverImg) + } else { + const placeholder = document.createElement("div") + placeholder.className = "card-img-top bg-secondary d-flex align-items-center justify-content-center" + placeholder.style.height = "180px" + const icon = document.createElement("span") + icon.className = "material-symbols-outlined text-white" + icon.style.fontSize = "3rem" + icon.textContent = "sports_esports" + placeholder.appendChild(icon) + card.appendChild(placeholder) + } + + const cardBody = document.createElement("div") + cardBody.className = "card-body p-2" + card.appendChild(cardBody) + + // Game name + const nameEl = document.createElement("h6") + nameEl.className = "card-title small mb-2 fw-bold text-truncate" + nameEl.textContent = game.name + nameEl.title = game.name + cardBody.appendChild(nameEl) + + // Platforms with release years + if (game.platforms && game.platforms.length > 0) { + const platformsDiv = document.createElement("div") + platformsDiv.className = "mb-2" + + // Group release dates by platform + const platformYears = {} + if (game.release_dates && game.release_dates.length > 0) { + game.release_dates.forEach(rd => { + if (rd.platform && rd.y) { + if (!platformYears[rd.platform] || rd.y < platformYears[rd.platform]) { + platformYears[rd.platform] = rd.y + } + } + }) + } + + // Show first 3 platforms + game.platforms.slice(0, 3).forEach(platformId => { + const platform = allPlatforms[String(platformId)] + const platformName = platform ? platform.name : `Platform ${platformId}` + const year = platformYears[platformId] ? ` (${platformYears[platformId]})` : "" + + const badge = document.createElement("span") + badge.className = "badge bg-secondary me-1 mb-1 small" + badge.style.fontSize = "0.7rem" + badge.textContent = platformName + year + platformsDiv.appendChild(badge) + }) + + if (game.platforms.length > 3) { + const moreBadge = document.createElement("span") + moreBadge.className = "badge bg-secondary me-1 mb-1 small" + moreBadge.style.fontSize = "0.7rem" + moreBadge.textContent = `+${game.platforms.length - 3} more` + platformsDiv.appendChild(moreBadge) + } + + cardBody.appendChild(platformsDiv) + } + }) + + if (matches.length > 60) { + const moreNote = document.createElement("p") + moreNote.className = "text-muted mt-3 small" + moreNote.textContent = `Showing first 60 of ${matches.length} results. Try a more specific search term.` + search_container.appendChild(moreNote) + } + }) + .catch(() => { + // Fallback if platforms can't be loaded + matches.slice(0, 60).forEach(([id, game]) => { + const col = document.createElement("div") + col.className = "col" + row.appendChild(col) + + const card = document.createElement("a") + card.className = "card h-100 text-decoration-none shadow-sm border-0 rounded-0" + card.href = `${base_path}/browse/games/?id=${id}` + col.appendChild(card) + + if (game.cover && game.cover.url) { + const coverImg = document.createElement("img") + coverImg.className = "card-img-top rounded-top-0" + coverImg.src = game.cover.url.replace("t_thumb", "t_cover_small") + coverImg.alt = game.name + coverImg.loading = "lazy" + card.appendChild(coverImg) + } else { + const placeholder = document.createElement("div") + placeholder.className = "card-img-top bg-secondary d-flex align-items-center justify-content-center" + placeholder.style.height = "180px" + const icon = document.createElement("span") + icon.className = "material-symbols-outlined text-white" + icon.style.fontSize = "3rem" + icon.textContent = "sports_esports" + placeholder.appendChild(icon) + card.appendChild(placeholder) + } + + const cardBody = document.createElement("div") + cardBody.className = "card-body p-2" + card.appendChild(cardBody) + + const nameEl = document.createElement("p") + nameEl.className = "card-text small mb-0" + nameEl.textContent = game.name + nameEl.title = game.name + cardBody.appendChild(nameEl) + }) + + if (matches.length > 60) { + const moreNote = document.createElement("p") + moreNote.className = "text-muted mt-3 small" + moreNote.textContent = `Showing first 60 of ${matches.length} results. Try a more specific search term.` + search_container.appendChild(moreNote) + } + }) }) .catch(err => { loading.remove() diff --git a/gh-pages-template/index.html b/gh-pages-template/index.html index 214062d6849c..d87b6bdd4f0e 100644 --- a/gh-pages-template/index.html +++ b/gh-pages-template/index.html @@ -30,18 +30,17 @@