Step 4 migration: enable API/DB write operations and frontend CRUD
This commit is contained in:
171
app.js
171
app.js
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user