Step 4 migration: enable API/DB write operations and frontend CRUD

This commit is contained in:
Ponte
2026-02-11 15:06:15 +01:00
parent adc7ec193e
commit de1da956fc
3 changed files with 392 additions and 30 deletions

171
app.js
View File

@@ -12,6 +12,7 @@ const initialState = {
const state = loadState();
let dataMode = "local";
let apiReachable = false;
const platformForm = document.getElementById("platformForm");
const gameForm = document.getElementById("gameForm");
@@ -34,7 +35,7 @@ const gamesList = document.getElementById("gamesList");
const gameCardTemplate = document.getElementById("gameCardTemplate");
let editingGameId = null;
platformForm.addEventListener("submit", (event) => {
platformForm.addEventListener("submit", async (event) => {
event.preventDefault();
const brand = brandInput.value.trim().toUpperCase();
@@ -44,6 +45,22 @@ platformForm.addEventListener("submit", (event) => {
return;
}
if (apiReachable) {
try {
await apiRequest("/api/catalog/consoles", {
method: "POST",
body: { brand, consoleName },
});
platformForm.reset();
await refreshFromApi(brand, consoleName);
return;
} catch (error) {
console.error(error);
alert("Impossible d'ajouter cette section via l'API.");
}
}
state.brands[brand] = state.brands[brand] || [];
if (!state.brands[brand].includes(consoleName)) {
state.brands[brand].push(consoleName);
@@ -58,7 +75,7 @@ platformForm.addEventListener("submit", (event) => {
render();
});
gameForm.addEventListener("submit", (event) => {
gameForm.addEventListener("submit", async (event) => {
event.preventDefault();
const title = titleInput.value.trim();
@@ -66,6 +83,40 @@ gameForm.addEventListener("submit", (event) => {
return;
}
if (apiReachable) {
const payload = {
brand: state.selectedBrand,
consoleName: state.selectedConsole,
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(),
};
try {
if (editingGameId) {
await apiRequest(`/api/catalog/games/${editingGameId}`, {
method: "PUT",
body: payload,
});
} else {
await apiRequest("/api/catalog/games", {
method: "POST",
body: payload,
});
}
resetEditMode();
await refreshFromApi(state.selectedBrand, state.selectedConsole);
return;
} catch (error) {
console.error(error);
alert("Impossible d'enregistrer ce jeu via l'API.");
}
}
state.gamesByConsole[state.selectedConsole] = state.gamesByConsole[state.selectedConsole] || [];
if (editingGameId) {
@@ -143,7 +194,7 @@ consoleTabs.addEventListener("click", (event) => {
render();
});
gamesList.addEventListener("click", (event) => {
gamesList.addEventListener("click", async (event) => {
if (!(event.target instanceof Element)) {
return;
}
@@ -164,6 +215,32 @@ gamesList.addEventListener("click", (event) => {
return;
}
if (action === "edit") {
startEditMode(games[idx]);
return;
}
if (apiReachable) {
try {
if (action === "delete") {
await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" });
if (editingGameId === id) {
resetEditMode();
}
}
if (action === "toggle-loan") {
await apiRequest(`/api/catalog/games/${id}/toggle-loan`, { method: "POST" });
}
await refreshFromApi(state.selectedBrand, state.selectedConsole);
return;
} catch (error) {
console.error(error);
alert("Action impossible via l'API.");
}
}
if (action === "delete") {
games.splice(idx, 1);
if (editingGameId === id) {
@@ -175,11 +252,6 @@ gamesList.addEventListener("click", (event) => {
games[idx].loanedTo = games[idx].loanedTo ? "" : "A renseigner";
}
if (action === "edit") {
startEditMode(games[idx]);
return;
}
persist();
render();
});
@@ -201,12 +273,17 @@ function renderDataMode() {
}
if (dataMode === "api") {
dataModeInfo.textContent = "Source: API (lecture). Ecriture DB prevue a l'etape 4.";
dataModeInfo.textContent = "Source: API (lecture/ecriture active sur la base de donnees).";
return;
}
if (dataMode === "api-empty") {
dataModeInfo.textContent = "Source: API (base vide). Ajoute une section pour demarrer.";
return;
}
if (dataMode === "local-fallback") {
dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible ou vide.";
dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible.";
return;
}
@@ -332,16 +409,16 @@ function loadState() {
}
function normalizeState() {
state.brands = state.brands || structuredClone(initialState.brands);
state.brands = state.brands || {};
state.gamesByConsole = state.gamesByConsole || {};
const brands = Object.keys(state.brands);
if (!brands.length) {
if (!brands.length && !apiReachable) {
state.brands = structuredClone(initialState.brands);
}
if (!state.selectedBrand || !state.brands[state.selectedBrand]) {
state.selectedBrand = Object.keys(state.brands)[0];
state.selectedBrand = Object.keys(state.brands)[0] || "";
}
const consoles = state.brands[state.selectedBrand] || [];
@@ -354,6 +431,46 @@ function persist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
async function apiRequest(path, options = {}) {
const requestOptions = {
method: options.method || "GET",
headers: {},
};
if (options.body !== undefined) {
requestOptions.headers["Content-Type"] = "application/json";
requestOptions.body = JSON.stringify(options.body);
}
const response = await fetch(path, requestOptions);
const rawText = await response.text();
const payload = rawText ? JSON.parse(rawText) : {};
if (!response.ok) {
const message = payload && payload.message ? payload.message : `HTTP ${response.status}`;
throw new Error(message);
}
return payload;
}
function applyCatalogPayload(payload, preferredBrand, preferredConsole) {
state.brands = payload.brands || {};
state.gamesByConsole = payload.gamesByConsole || {};
normalizeState();
if (preferredBrand && state.brands[preferredBrand]) {
state.selectedBrand = preferredBrand;
const consoles = state.brands[preferredBrand] || [];
if (preferredConsole && consoles.includes(preferredConsole)) {
state.selectedConsole = preferredConsole;
} else {
state.selectedConsole = consoles[0] || "";
}
}
}
function payloadHasCatalogData(payload) {
if (!payload || typeof payload !== "object") {
return false;
@@ -380,26 +497,20 @@ function payloadHasCatalogData(payload) {
return consolesCount > 0 || gamesCount > 0;
}
async function refreshFromApi(preferredBrand, preferredConsole) {
const payload = await apiRequest("/api/catalog/full");
apiReachable = true;
dataMode = payloadHasCatalogData(payload) ? "api" : "api-empty";
applyCatalogPayload(payload, preferredBrand, preferredConsole);
persist();
render();
}
async function hydrateFromApi() {
try {
const response = await fetch("/api/catalog/full");
if (!response.ok) {
throw new Error(`API error ${response.status}`);
}
const payload = await response.json();
if (!payloadHasCatalogData(payload)) {
dataMode = "local-fallback";
return;
}
state.brands = payload.brands || {};
state.gamesByConsole = payload.gamesByConsole || {};
state.selectedBrand = Object.keys(state.brands)[0] || "";
state.selectedConsole = (state.brands[state.selectedBrand] || [])[0] || "";
dataMode = "api";
persist();
await refreshFromApi(state.selectedBrand, state.selectedConsole);
} catch {
apiReachable = false;
dataMode = "local-fallback";
}
}