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=Loading character data…
+Loading series data…
+Loading franchise data…
+Loading game data…
+Loading platform data…
+