const sourcesConfigUrl = "./data/sources.json"; const state = { allGames: [], mergedGames: [], search: "", sourceFilter: "all", sortBy: "title", sources: [], }; const ui = { grid: document.getElementById("gamesGrid"), summary: document.getElementById("summary"), searchInput: document.getElementById("searchInput"), sourceFilter: document.getElementById("sourceFilter"), sortSelect: document.getElementById("sortSelect"), refreshButton: document.getElementById("refreshButton"), template: document.getElementById("gameCardTemplate"), }; const normalizeTitle = (title) => title.toLowerCase().replace(/[:\-]/g, " ").replace(/\s+/g, " ").trim(); const toDateValue = (value) => (value ? new Date(value).getTime() : 0); const mergeGames = (games) => { const map = new Map(); games.forEach((game) => { const key = game.canonicalId || normalizeTitle(game.title); const entry = map.get(key) || { title: game.title, canonicalId: key, platforms: new Set(), sources: [], tags: new Set(), lastPlayed: null, playtimeHours: 0, }; entry.platforms.add(game.platform); game.tags?.forEach((tag) => entry.tags.add(tag)); entry.sources.push({ name: game.source, id: game.id, url: game.url, platform: game.platform, }); if ( game.lastPlayed && (!entry.lastPlayed || game.lastPlayed > entry.lastPlayed) ) { entry.lastPlayed = game.lastPlayed; } if (Number.isFinite(game.playtimeHours)) { entry.playtimeHours += game.playtimeHours; } map.set(key, entry); }); return Array.from(map.values()).map((entry) => ({ ...entry, platforms: Array.from(entry.platforms), tags: Array.from(entry.tags), })); }; const sortGames = (games, sortBy) => { const sorted = [...games]; sorted.sort((a, b) => { if (sortBy === "lastPlayed") { return toDateValue(b.lastPlayed) - toDateValue(a.lastPlayed); } if (sortBy === "platforms") { return b.platforms.length - a.platforms.length; } return a.title.localeCompare(b.title, "de"); }); return sorted; }; const filterGames = () => { const query = state.search.trim().toLowerCase(); let filtered = [...state.mergedGames]; if (state.sourceFilter !== "all") { filtered = filtered.filter((game) => game.sources.some((source) => source.name === state.sourceFilter), ); } if (query) { filtered = filtered.filter((game) => { const haystack = [ game.title, ...game.platforms, ...game.tags, ...game.sources.map((source) => source.name), ] .join(" ") .toLowerCase(); return haystack.includes(query); }); } return sortGames(filtered, state.sortBy); }; const renderSummary = (games) => { const totalGames = state.mergedGames.length; const totalSources = state.sources.length; const duplicates = state.allGames.length - state.mergedGames.length; const totalPlaytime = state.allGames.reduce( (sum, game) => sum + (game.playtimeHours || 0), 0, ); ui.summary.innerHTML = [ { label: "Konsolidierte Spiele", value: totalGames, }, { label: "Quellen", value: totalSources, }, { label: "Zusammengeführte Duplikate", value: Math.max(duplicates, 0), }, { label: "Gesamte Spielzeit (h)", value: totalPlaytime.toFixed(1), }, ] .map( (item) => `

${item.label}

${item.value}

`, ) .join(""); }; const renderGames = (games) => { ui.grid.innerHTML = ""; games.forEach((game) => { const card = ui.template.content.cloneNode(true); card.querySelector(".title").textContent = game.title; card.querySelector(".badge").textContent = `${game.platforms.length} Plattformen`; card.querySelector(".meta").textContent = game.lastPlayed ? `Zuletzt gespielt: ${new Date(game.lastPlayed).toLocaleDateString("de")}` : "Noch nicht gespielt"; const tagList = card.querySelector(".tag-list"); game.tags.slice(0, 4).forEach((tag) => { const span = document.createElement("span"); span.className = "tag"; span.textContent = tag; tagList.appendChild(span); }); if (!game.tags.length) { const span = document.createElement("span"); span.className = "tag"; span.textContent = "Ohne Tags"; tagList.appendChild(span); } const sources = card.querySelector(".sources"); game.sources.forEach((source) => { const item = document.createElement("div"); item.className = "source-item"; const name = document.createElement("span"); name.textContent = source.name; const details = document.createElement("p"); details.textContent = `${source.platform} · ${source.id}`; item.append(name, details); sources.appendChild(item); }); ui.grid.appendChild(card); }); }; const populateSourceFilter = () => { ui.sourceFilter.innerHTML = ''; state.sources.forEach((source) => { const option = document.createElement("option"); option.value = source.name; option.textContent = source.label; ui.sourceFilter.appendChild(option); }); }; const updateUI = () => { const filtered = filterGames(); renderSummary(filtered); renderGames(filtered); }; const loadSources = async () => { const response = await fetch(sourcesConfigUrl); if (!response.ok) { throw new Error("Konnte sources.json nicht laden."); } const config = await response.json(); state.sources = config.sources; const data = await Promise.all( config.sources.map(async (source) => { const sourceResponse = await fetch(source.file); if (!sourceResponse.ok) { throw new Error(`Konnte ${source.file} nicht laden.`); } const list = await sourceResponse.json(); return list.map((game) => ({ ...game, source: source.name, platform: game.platform || source.platform, })); }), ); state.allGames = data.flat(); state.mergedGames = mergeGames(state.allGames); }; const attachEvents = () => { ui.searchInput.addEventListener("input", (event) => { state.search = event.target.value; updateUI(); }); ui.sourceFilter.addEventListener("change", (event) => { state.sourceFilter = event.target.value; updateUI(); }); ui.sortSelect.addEventListener("change", (event) => { state.sortBy = event.target.value; updateUI(); }); ui.refreshButton.addEventListener("click", async () => { ui.refreshButton.disabled = true; ui.refreshButton.textContent = "Lade ..."; try { await loadSources(); populateSourceFilter(); updateUI(); } finally { ui.refreshButton.disabled = false; ui.refreshButton.textContent = "Daten neu laden"; } }); }; const init = async () => { try { await loadSources(); populateSourceFilter(); attachEvents(); updateUI(); } catch (error) { ui.grid.innerHTML = `
${error.message}
`; } }; init();