commit fa40dcba2164f7935d8357cc627a1eba93c8f75a Author: Ponte Date: Wed Feb 11 13:33:02 2026 +0100 Initial version: video games collection app with Docker diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..35706e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +.gitignore +.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ae2e87 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Mets 7000 si le port est libre, sinon garde 7001 +APP_PORT=7001 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..716dfc1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM nginx:1.27-alpine + +WORKDIR /usr/share/nginx/html + +COPY index.html ./index.html +COPY styles.css ./styles.css +COPY app.js ./app.js + +EXPOSE 80 diff --git a/app.js b/app.js new file mode 100644 index 0000000..a337f6b --- /dev/null +++ b/app.js @@ -0,0 +1,337 @@ +const STORAGE_KEY = "video_game_collection_v1"; + +const initialState = { + brands: { + SONY: ["PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "PlayStation 5"], + MICROSOFT: ["Xbox", "Xbox 360", "Xbox One", "Xbox One X", "Xbox Series X"], + }, + gamesByConsole: {}, + selectedBrand: "SONY", + selectedConsole: "PlayStation", +}; + +const state = loadState(); +normalizeState(); + +const platformForm = document.getElementById("platformForm"); +const gameForm = document.getElementById("gameForm"); +const brandInput = document.getElementById("brandInput"); +const consoleInput = document.getElementById("consoleInput"); +const titleInput = document.getElementById("titleInput"); +const genreInput = document.getElementById("genreInput"); +const publisherInput = document.getElementById("publisherInput"); +const yearInput = document.getElementById("yearInput"); +const valueInput = document.getElementById("valueInput"); +const loanedToInput = document.getElementById("loanedToInput"); +const gameSubmitBtn = document.getElementById("gameSubmitBtn"); +const cancelEditBtn = document.getElementById("cancelEditBtn"); + +const brandTabs = document.getElementById("brandTabs"); +const consoleTabs = document.getElementById("consoleTabs"); +const gameSectionTitle = document.getElementById("gameSectionTitle"); +const gamesList = document.getElementById("gamesList"); +const gameCardTemplate = document.getElementById("gameCardTemplate"); +let editingGameId = null; + +platformForm.addEventListener("submit", (event) => { + event.preventDefault(); + + const brand = brandInput.value.trim().toUpperCase(); + const consoleName = consoleInput.value.trim(); + + if (!brand || !consoleName) { + return; + } + + state.brands[brand] = state.brands[brand] || []; + if (!state.brands[brand].includes(consoleName)) { + state.brands[brand].push(consoleName); + } + + state.selectedBrand = brand; + state.selectedConsole = consoleName; + state.gamesByConsole[consoleName] = state.gamesByConsole[consoleName] || []; + + platformForm.reset(); + persist(); + render(); +}); + +gameForm.addEventListener("submit", (event) => { + event.preventDefault(); + + const title = titleInput.value.trim(); + if (!title || !state.selectedConsole) { + return; + } + + state.gamesByConsole[state.selectedConsole] = state.gamesByConsole[state.selectedConsole] || []; + + if (editingGameId) { + const games = state.gamesByConsole[state.selectedConsole]; + const idx = games.findIndex((game) => game.id === editingGameId); + if (idx !== -1) { + games[idx] = { + ...games[idx], + title, + genre: genreInput.value.trim(), + publisher: publisherInput.value.trim(), + year: yearInput.value ? Number(yearInput.value) : null, + value: valueInput.value ? Number(valueInput.value) : null, + loanedTo: loanedToInput.value.trim(), + }; + } + } else { + const game = { + id: crypto.randomUUID(), + title, + genre: genreInput.value.trim(), + publisher: publisherInput.value.trim(), + year: yearInput.value ? Number(yearInput.value) : null, + value: valueInput.value ? Number(valueInput.value) : null, + loanedTo: loanedToInput.value.trim(), + createdAt: new Date().toISOString(), + }; + state.gamesByConsole[state.selectedConsole].unshift(game); + } + + resetEditMode(); + persist(); + render(); +}); + +brandTabs.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) { + return; + } + const target = event.target.closest("button[data-brand]"); + if (!(target instanceof HTMLButtonElement)) { + return; + } + + const brand = target.dataset.brand; + if (!brand) { + return; + } + + state.selectedBrand = brand; + const consoles = state.brands[brand] || []; + state.selectedConsole = consoles[0] || ""; + resetEditMode(); + persist(); + render(); +}); + +consoleTabs.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) { + return; + } + const target = event.target.closest("button[data-console]"); + if (!(target instanceof HTMLButtonElement)) { + return; + } + + const consoleName = target.dataset.console; + if (!consoleName) { + return; + } + + state.selectedConsole = consoleName; + resetEditMode(); + persist(); + render(); +}); + +gamesList.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) { + return; + } + const target = event.target.closest("button[data-action]"); + if (!(target instanceof HTMLButtonElement)) { + return; + } + + const action = target.dataset.action; + const id = target.dataset.id; + if (!action || !id || !state.selectedConsole) { + return; + } + + const games = state.gamesByConsole[state.selectedConsole] || []; + const idx = games.findIndex((game) => game.id === id); + if (idx === -1) { + return; + } + + if (action === "delete") { + games.splice(idx, 1); + if (editingGameId === id) { + resetEditMode(); + } + } + + if (action === "toggle-loan") { + games[idx].loanedTo = games[idx].loanedTo ? "" : "A renseigner"; + } + + if (action === "edit") { + startEditMode(games[idx]); + return; + } + + persist(); + render(); +}); + +cancelEditBtn.addEventListener("click", () => { + resetEditMode(); +}); + +function render() { + renderBrandTabs(); + renderConsoleTabs(); + renderGames(); +} + +function renderBrandTabs() { + const brands = Object.keys(state.brands); + brandTabs.innerHTML = ""; + + for (const brand of brands) { + const button = document.createElement("button"); + button.type = "button"; + button.className = `tab ${state.selectedBrand === brand ? "active" : ""}`; + button.textContent = brand; + button.dataset.brand = brand; + brandTabs.append(button); + } +} + +function renderConsoleTabs() { + const consoles = state.brands[state.selectedBrand] || []; + if (!consoles.includes(state.selectedConsole)) { + state.selectedConsole = consoles[0] || ""; + } + + consoleTabs.innerHTML = ""; + for (const consoleName of consoles) { + const button = document.createElement("button"); + button.type = "button"; + button.className = `tab ${state.selectedConsole === consoleName ? "active" : ""}`; + button.dataset.console = consoleName; + const count = (state.gamesByConsole[consoleName] || []).length; + button.innerHTML = `${consoleName}${count}`; + consoleTabs.append(button); + } +} + +function renderGames() { + const selectedConsole = state.selectedConsole; + gameSectionTitle.textContent = selectedConsole ? `Jeux - ${selectedConsole}` : "Jeux"; + gamesList.innerHTML = ""; + + if (!selectedConsole) { + gamesList.innerHTML = '

Ajoute une section pour commencer.

'; + return; + } + + const games = state.gamesByConsole[selectedConsole] || []; + if (!games.length) { + gamesList.innerHTML = '

Aucun jeu sur cette console pour le moment.

'; + return; + } + + for (const game of games) { + const card = gameCardTemplate.content.cloneNode(true); + const article = card.querySelector(".game-card"); + if (editingGameId === game.id) { + article.classList.add("editing"); + } + + card.querySelector(".game-title").textContent = game.title; + + const metaParts = [ + game.genre ? `Genre: ${game.genre}` : null, + game.publisher ? `Editeur: ${game.publisher}` : null, + game.year ? `Annee: ${game.year}` : null, + game.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null, + ].filter(Boolean); + + card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire"; + + card.querySelector(".game-loan").textContent = game.loanedTo + ? `Pret en cours: ${game.loanedTo}` + : "Disponible dans ta collection"; + + const editBtn = card.querySelector('[data-action="edit"]'); + const toggleBtn = card.querySelector('[data-action="toggle-loan"]'); + const deleteBtn = card.querySelector('[data-action="delete"]'); + + editBtn.dataset.id = game.id; + toggleBtn.dataset.id = game.id; + toggleBtn.textContent = game.loanedTo ? "Marquer comme rendu" : "Marquer comme prete"; + + deleteBtn.dataset.id = game.id; + + gamesList.append(card); + } +} + +function startEditMode(game) { + editingGameId = game.id; + titleInput.value = game.title || ""; + genreInput.value = game.genre || ""; + publisherInput.value = game.publisher || ""; + yearInput.value = game.year || ""; + valueInput.value = game.value != null ? game.value : ""; + loanedToInput.value = game.loanedTo || ""; + + gameSubmitBtn.textContent = "Mettre a jour le jeu"; + cancelEditBtn.classList.remove("hidden"); + renderGames(); +} + +function resetEditMode() { + editingGameId = null; + gameForm.reset(); + gameSubmitBtn.textContent = "Ajouter le jeu"; + cancelEditBtn.classList.add("hidden"); +} + +function loadState() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + return structuredClone(initialState); + } + + return JSON.parse(raw); + } catch { + return structuredClone(initialState); + } +} + +function normalizeState() { + state.brands = state.brands || structuredClone(initialState.brands); + state.gamesByConsole = state.gamesByConsole || {}; + + const brands = Object.keys(state.brands); + if (!brands.length) { + state.brands = structuredClone(initialState.brands); + } + + if (!state.selectedBrand || !state.brands[state.selectedBrand]) { + state.selectedBrand = Object.keys(state.brands)[0]; + } + + const consoles = state.brands[state.selectedBrand] || []; + if (!state.selectedConsole || !consoles.includes(state.selectedConsole)) { + state.selectedConsole = consoles[0] || ""; + } +} + +function persist() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); +} + +render(); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5941432 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + video-games-app: + build: . + container_name: video-games-app + ports: + - "${APP_PORT:-7001}:80" + restart: unless-stopped diff --git a/index.html b/index.html new file mode 100644 index 0000000..c582f08 --- /dev/null +++ b/index.html @@ -0,0 +1,104 @@ + + + + + + Ma Collection Jeux Video + + + + + + +
+
+
+

Catalogue perso

+

Collection Jeux Video

+

+ Organise tes jeux par plateforme/console. Ajoute des sections librement + et garde une base propre pour la gestion des prêts et de la cote. +

+
+
+ +
+
+

Plateformes et consoles

+
+ +
+ + + +
+ +
+
+
+ +
+
+

Jeux

+
+ +
+ + + + + + + + +
+ +
+
+
+ + + + + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..b8ba4ff --- /dev/null +++ b/styles.css @@ -0,0 +1,250 @@ +:root { + --bg: #f2f4f8; + --surface: #ffffff; + --text: #12202f; + --muted: #4b5968; + --accent: #0a7f5a; + --accent-2: #0c5ea8; + --danger: #bf2f47; + --border: #d6dde6; + --shadow: 0 10px 24px rgba(17, 36, 57, 0.08); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Space Grotesk", sans-serif; + color: var(--text); + background: + radial-gradient(circle at top right, #d9f0e7 0, transparent 30%), + linear-gradient(160deg, #f5f7fa, #ebeff4); + min-height: 100vh; +} + +.app-shell { + width: min(1100px, 94vw); + margin: 2rem auto; + display: grid; + gap: 1rem; +} + +.hero, +.panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 18px; + box-shadow: var(--shadow); +} + +.hero { + padding: 1.2rem 1.4rem; + background: + linear-gradient(125deg, #f8fff7, #f4f8ff), + var(--surface); +} + +.eyebrow { + margin: 0; + color: var(--accent-2); + font-weight: 700; + text-transform: uppercase; + font-size: 0.78rem; + letter-spacing: 0.06em; +} + +h1 { + margin: 0.2rem 0 0.5rem; + font-size: clamp(1.3rem, 2.5vw, 2rem); +} + +.subtitle { + margin: 0; + color: var(--muted); +} + +.panel { + padding: 1rem; +} + +.panel-header h2 { + margin: 0 0 0.8rem; + font-size: 1.1rem; +} + +.grid-form { + display: grid; + gap: 0.7rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); + align-items: end; + margin-bottom: 0.9rem; +} + +.game-form { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +label { + display: grid; + gap: 0.35rem; + font-size: 0.9rem; + color: var(--muted); +} + +input { + width: 100%; + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.65rem 0.7rem; + font-family: inherit; + font-size: 0.95rem; +} + +input:focus { + outline: 2px solid color-mix(in hsl, var(--accent-2), white 60%); + outline-offset: 1px; +} + +button { + border: 0; + border-radius: 10px; + padding: 0.7rem 0.9rem; + font-family: inherit; + font-weight: 600; + cursor: pointer; + background: var(--accent); + color: white; +} + +.tabs { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.7rem; +} + +.tab { + background: #e9eef5; + color: var(--text); + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.45rem; + height: 1.45rem; + border-radius: 999px; + padding: 0 0.35rem; + font-size: 0.78rem; + font-weight: 700; + color: #173a5d; + background: #d3e4f7; +} + +.tab.active { + background: var(--accent-2); + color: #fff; +} + +.tab.active .count-badge { + background: color-mix(in hsl, white, transparent 25%); + color: #ffffff; +} + +.tabs.secondary .tab.active { + background: var(--accent); +} + +.games-list { + display: grid; + gap: 0.6rem; +} + +.empty { + color: var(--muted); + font-style: italic; + padding: 0.5rem 0; +} + +.game-card { + border: 1px solid var(--border); + border-radius: 12px; + padding: 0.85rem; + display: flex; + justify-content: space-between; + gap: 0.8rem; + align-items: center; +} + +.game-card.editing { + border-color: color-mix(in hsl, var(--accent-2), white 35%); + box-shadow: 0 0 0 2px color-mix(in hsl, var(--accent-2), white 80%); +} + +.game-title { + margin: 0; + font-size: 1rem; +} + +.game-meta, +.game-loan { + margin: 0.25rem 0 0; + color: var(--muted); + font-size: 0.9rem; +} + +.game-actions { + display: grid; + gap: 0.45rem; +} + +.btn-inline { + background: #dde8f5; + color: #1e3045; + padding: 0.45rem 0.65rem; + font-size: 0.85rem; +} + +.btn-inline.danger { + background: #fbe7eb; + color: var(--danger); +} + +.btn-secondary { + background: #dce4ee; + color: #1b2d43; +} + +.hidden { + display: none; +} + +@media (max-width: 900px) { + .grid-form, + .game-form { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .grid-form, + .game-form { + grid-template-columns: 1fr; + } + + .game-card { + align-items: flex-start; + flex-direction: column; + } + + .game-actions { + width: 100%; + grid-template-columns: 1fr 1fr; + } +}