Step 4 migration: enable API/DB write operations and frontend CRUD
This commit is contained in:
@@ -130,6 +130,12 @@ git pull
|
|||||||
- trigger `updated_at` sur `games`
|
- trigger `updated_at` sur `games`
|
||||||
- endpoints de lecture pour validation: `summary` et `tree`
|
- endpoints de lecture pour validation: `summary` et `tree`
|
||||||
- Etape 3: frontend lit l'API (`/api/catalog/full`) avec fallback `localStorage` si API vide ou indisponible
|
- Etape 3: frontend lit l'API (`/api/catalog/full`) avec fallback `localStorage` si API vide ou indisponible
|
||||||
|
- Etape 4: ecriture active en base via API
|
||||||
|
- POST `/api/catalog/consoles`
|
||||||
|
- POST `/api/catalog/games`
|
||||||
|
- PUT `/api/catalog/games/:id`
|
||||||
|
- DELETE `/api/catalog/games/:id`
|
||||||
|
- POST `/api/catalog/games/:id/toggle-loan`
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
245
api/server.js
245
api/server.js
@@ -16,6 +16,68 @@ function sendJson(response, statusCode, payload) {
|
|||||||
response.end(JSON.stringify(payload));
|
response.end(JSON.stringify(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readJsonBody(request) {
|
||||||
|
const chunks = [];
|
||||||
|
let size = 0;
|
||||||
|
|
||||||
|
for await (const chunk of request) {
|
||||||
|
size += chunk.length;
|
||||||
|
if (size > 1024 * 1024) {
|
||||||
|
throw new Error("Payload too large");
|
||||||
|
}
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chunks.length) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureConsole(client, brandNameRaw, consoleNameRaw) {
|
||||||
|
const brandName = normalizeText(brandNameRaw).toUpperCase();
|
||||||
|
const consoleName = normalizeText(consoleNameRaw);
|
||||||
|
|
||||||
|
if (!brandName || !consoleName) {
|
||||||
|
throw new Error("brand and consoleName are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const brandResult = await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO brands(name)
|
||||||
|
VALUES ($1)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||||
|
RETURNING id;
|
||||||
|
`,
|
||||||
|
[brandName],
|
||||||
|
);
|
||||||
|
|
||||||
|
const brandId = brandResult.rows[0].id;
|
||||||
|
const consoleResult = await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO consoles(brand_id, name)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (brand_id, name) DO UPDATE SET name = EXCLUDED.name
|
||||||
|
RETURNING id;
|
||||||
|
`,
|
||||||
|
[brandId, consoleName],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
brand: brandName,
|
||||||
|
consoleName,
|
||||||
|
consoleId: consoleResult.rows[0].id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
await pool.query("CREATE EXTENSION IF NOT EXISTS pgcrypto;");
|
await pool.query("CREATE EXTENSION IF NOT EXISTS pgcrypto;");
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
@@ -176,6 +238,125 @@ async function getCatalogFull() {
|
|||||||
return { brands, gamesByConsole };
|
return { brands, gamesByConsole };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createConsole(brand, consoleName) {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const result = await ensureConsole(client, brand, consoleName);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGame(payload) {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const consoleData = await ensureConsole(client, payload.brand, payload.consoleName);
|
||||||
|
|
||||||
|
const title = normalizeText(payload.title);
|
||||||
|
if (!title) {
|
||||||
|
throw new Error("title is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const genre = normalizeText(payload.genre) || null;
|
||||||
|
const publisher = normalizeText(payload.publisher) || null;
|
||||||
|
const loanedTo = normalizeText(payload.loanedTo) || null;
|
||||||
|
const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null;
|
||||||
|
const value = payload.value != null && payload.value !== "" ? Number(payload.value) : null;
|
||||||
|
|
||||||
|
const insertResult = await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO games(
|
||||||
|
console_id, title, genre, publisher, release_year, estimated_value, loaned_to
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id::text AS id;
|
||||||
|
`,
|
||||||
|
[consoleData.consoleId, title, genre, publisher, year, value, loanedTo],
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
return { id: insertResult.rows[0].id };
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGame(id, payload) {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const consoleData = await ensureConsole(client, payload.brand, payload.consoleName);
|
||||||
|
|
||||||
|
const title = normalizeText(payload.title);
|
||||||
|
if (!title) {
|
||||||
|
throw new Error("title is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const genre = normalizeText(payload.genre) || null;
|
||||||
|
const publisher = normalizeText(payload.publisher) || null;
|
||||||
|
const loanedTo = normalizeText(payload.loanedTo) || null;
|
||||||
|
const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null;
|
||||||
|
const value = payload.value != null && payload.value !== "" ? Number(payload.value) : null;
|
||||||
|
|
||||||
|
const updateResult = await client.query(
|
||||||
|
`
|
||||||
|
UPDATE games
|
||||||
|
SET
|
||||||
|
console_id = $2,
|
||||||
|
title = $3,
|
||||||
|
genre = $4,
|
||||||
|
publisher = $5,
|
||||||
|
release_year = $6,
|
||||||
|
estimated_value = $7,
|
||||||
|
loaned_to = $8
|
||||||
|
WHERE id = $1::uuid
|
||||||
|
RETURNING id::text AS id;
|
||||||
|
`,
|
||||||
|
[id, consoleData.consoleId, title, genre, publisher, year, value, loanedTo],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult.rowCount) {
|
||||||
|
throw new Error("game not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
return { id: updateResult.rows[0].id };
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGame(id) {
|
||||||
|
const result = await pool.query("DELETE FROM games WHERE id = $1::uuid;", [id]);
|
||||||
|
return result.rowCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleGameLoan(id) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
UPDATE games
|
||||||
|
SET loaned_to = CASE WHEN COALESCE(loaned_to, '') = '' THEN 'A renseigner' ELSE NULL END
|
||||||
|
WHERE id = $1::uuid
|
||||||
|
RETURNING id::text AS id;
|
||||||
|
`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
return result.rowCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRequest(request, response) {
|
async function handleRequest(request, response) {
|
||||||
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
|
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
|
||||||
|
|
||||||
@@ -229,6 +410,70 @@ async function handleRequest(request, response) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.method === "POST" && url.pathname === "/api/catalog/consoles") {
|
||||||
|
try {
|
||||||
|
const body = await readJsonBody(request);
|
||||||
|
const created = await createConsole(body.brand, body.consoleName);
|
||||||
|
sendJson(response, 201, created);
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(response, 400, { status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "POST" && url.pathname === "/api/catalog/games") {
|
||||||
|
try {
|
||||||
|
const body = await readJsonBody(request);
|
||||||
|
const created = await createGame(body);
|
||||||
|
sendJson(response, 201, created);
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(response, 400, { status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameIdMatch = url.pathname.match(/^\/api\/catalog\/games\/([0-9a-fA-F-]+)$/);
|
||||||
|
if (request.method === "PUT" && gameIdMatch) {
|
||||||
|
try {
|
||||||
|
const body = await readJsonBody(request);
|
||||||
|
const updated = await updateGame(gameIdMatch[1], body);
|
||||||
|
sendJson(response, 200, updated);
|
||||||
|
} catch (error) {
|
||||||
|
const statusCode = error.message === "game not found" ? 404 : 400;
|
||||||
|
sendJson(response, statusCode, { status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "DELETE" && gameIdMatch) {
|
||||||
|
try {
|
||||||
|
const deleted = await deleteGame(gameIdMatch[1]);
|
||||||
|
if (!deleted) {
|
||||||
|
sendJson(response, 404, { status: "error", message: "game not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendJson(response, 200, { status: "ok" });
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(response, 400, { status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameToggleMatch = url.pathname.match(/^\/api\/catalog\/games\/([0-9a-fA-F-]+)\/toggle-loan$/);
|
||||||
|
if (request.method === "POST" && gameToggleMatch) {
|
||||||
|
try {
|
||||||
|
const toggled = await toggleGameLoan(gameToggleMatch[1]);
|
||||||
|
if (!toggled) {
|
||||||
|
sendJson(response, 404, { status: "error", message: "game not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendJson(response, 200, { status: "ok" });
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(response, 400, { status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sendJson(response, 404, {
|
sendJson(response, 404, {
|
||||||
status: "not_found",
|
status: "not_found",
|
||||||
message: "Route not found",
|
message: "Route not found",
|
||||||
|
|||||||
171
app.js
171
app.js
@@ -12,6 +12,7 @@ const initialState = {
|
|||||||
|
|
||||||
const state = loadState();
|
const state = loadState();
|
||||||
let dataMode = "local";
|
let dataMode = "local";
|
||||||
|
let apiReachable = false;
|
||||||
|
|
||||||
const platformForm = document.getElementById("platformForm");
|
const platformForm = document.getElementById("platformForm");
|
||||||
const gameForm = document.getElementById("gameForm");
|
const gameForm = document.getElementById("gameForm");
|
||||||
@@ -34,7 +35,7 @@ const gamesList = document.getElementById("gamesList");
|
|||||||
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
||||||
let editingGameId = null;
|
let editingGameId = null;
|
||||||
|
|
||||||
platformForm.addEventListener("submit", (event) => {
|
platformForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const brand = brandInput.value.trim().toUpperCase();
|
const brand = brandInput.value.trim().toUpperCase();
|
||||||
@@ -44,6 +45,22 @@ platformForm.addEventListener("submit", (event) => {
|
|||||||
return;
|
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] || [];
|
state.brands[brand] = state.brands[brand] || [];
|
||||||
if (!state.brands[brand].includes(consoleName)) {
|
if (!state.brands[brand].includes(consoleName)) {
|
||||||
state.brands[brand].push(consoleName);
|
state.brands[brand].push(consoleName);
|
||||||
@@ -58,7 +75,7 @@ platformForm.addEventListener("submit", (event) => {
|
|||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
gameForm.addEventListener("submit", (event) => {
|
gameForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const title = titleInput.value.trim();
|
const title = titleInput.value.trim();
|
||||||
@@ -66,6 +83,40 @@ gameForm.addEventListener("submit", (event) => {
|
|||||||
return;
|
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] || [];
|
state.gamesByConsole[state.selectedConsole] = state.gamesByConsole[state.selectedConsole] || [];
|
||||||
|
|
||||||
if (editingGameId) {
|
if (editingGameId) {
|
||||||
@@ -143,7 +194,7 @@ consoleTabs.addEventListener("click", (event) => {
|
|||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
gamesList.addEventListener("click", (event) => {
|
gamesList.addEventListener("click", async (event) => {
|
||||||
if (!(event.target instanceof Element)) {
|
if (!(event.target instanceof Element)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,6 +215,32 @@ gamesList.addEventListener("click", (event) => {
|
|||||||
return;
|
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") {
|
if (action === "delete") {
|
||||||
games.splice(idx, 1);
|
games.splice(idx, 1);
|
||||||
if (editingGameId === id) {
|
if (editingGameId === id) {
|
||||||
@@ -175,11 +252,6 @@ gamesList.addEventListener("click", (event) => {
|
|||||||
games[idx].loanedTo = games[idx].loanedTo ? "" : "A renseigner";
|
games[idx].loanedTo = games[idx].loanedTo ? "" : "A renseigner";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "edit") {
|
|
||||||
startEditMode(games[idx]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
persist();
|
persist();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
@@ -201,12 +273,17 @@ function renderDataMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataMode === "api") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataMode === "local-fallback") {
|
if (dataMode === "local-fallback") {
|
||||||
dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible ou vide.";
|
dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,16 +409,16 @@ function loadState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeState() {
|
function normalizeState() {
|
||||||
state.brands = state.brands || structuredClone(initialState.brands);
|
state.brands = state.brands || {};
|
||||||
state.gamesByConsole = state.gamesByConsole || {};
|
state.gamesByConsole = state.gamesByConsole || {};
|
||||||
|
|
||||||
const brands = Object.keys(state.brands);
|
const brands = Object.keys(state.brands);
|
||||||
if (!brands.length) {
|
if (!brands.length && !apiReachable) {
|
||||||
state.brands = structuredClone(initialState.brands);
|
state.brands = structuredClone(initialState.brands);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.selectedBrand || !state.brands[state.selectedBrand]) {
|
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] || [];
|
const consoles = state.brands[state.selectedBrand] || [];
|
||||||
@@ -354,6 +431,46 @@ function persist() {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
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) {
|
function payloadHasCatalogData(payload) {
|
||||||
if (!payload || typeof payload !== "object") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return false;
|
return false;
|
||||||
@@ -380,26 +497,20 @@ function payloadHasCatalogData(payload) {
|
|||||||
return consolesCount > 0 || gamesCount > 0;
|
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() {
|
async function hydrateFromApi() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/catalog/full");
|
await refreshFromApi(state.selectedBrand, state.selectedConsole);
|
||||||
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();
|
|
||||||
} catch {
|
} catch {
|
||||||
|
apiReachable = false;
|
||||||
dataMode = "local-fallback";
|
dataMode = "local-fallback";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user