Step 5 migration: add localStorage to DB import flow

This commit is contained in:
Ponte
2026-02-11 15:11:34 +01:00
parent de1da956fc
commit e58ee18936
5 changed files with 208 additions and 15 deletions

79
app.js
View File

@@ -11,6 +11,7 @@ const initialState = {
};
const state = loadState();
let pendingLocalImport = payloadHasCatalogData(state) ? structuredClone(state) : null;
let dataMode = "local";
let apiReachable = false;
@@ -31,6 +32,7 @@ const brandTabs = document.getElementById("brandTabs");
const consoleTabs = document.getElementById("consoleTabs");
const gameSectionTitle = document.getElementById("gameSectionTitle");
const dataModeInfo = document.getElementById("dataModeInfo");
const migrateBtn = document.getElementById("migrateBtn");
const gamesList = document.getElementById("gamesList");
const gameCardTemplate = document.getElementById("gameCardTemplate");
let editingGameId = null;
@@ -45,7 +47,7 @@ platformForm.addEventListener("submit", async (event) => {
return;
}
if (apiReachable) {
if (apiReachable && dataMode !== "local-pending-import") {
try {
await apiRequest("/api/catalog/consoles", {
method: "POST",
@@ -72,6 +74,7 @@ platformForm.addEventListener("submit", async (event) => {
platformForm.reset();
persist();
markLocalDataForImport();
render();
});
@@ -83,7 +86,7 @@ gameForm.addEventListener("submit", async (event) => {
return;
}
if (apiReachable) {
if (apiReachable && dataMode !== "local-pending-import") {
const payload = {
brand: state.selectedBrand,
consoleName: state.selectedConsole,
@@ -149,6 +152,7 @@ gameForm.addEventListener("submit", async (event) => {
resetEditMode();
persist();
markLocalDataForImport();
render();
});
@@ -220,7 +224,7 @@ gamesList.addEventListener("click", async (event) => {
return;
}
if (apiReachable) {
if (apiReachable && dataMode !== "local-pending-import") {
try {
if (action === "delete") {
await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" });
@@ -253,6 +257,7 @@ gamesList.addEventListener("click", async (event) => {
}
persist();
markLocalDataForImport();
render();
});
@@ -260,6 +265,33 @@ cancelEditBtn.addEventListener("click", () => {
resetEditMode();
});
migrateBtn.addEventListener("click", async () => {
if (!apiReachable || !pendingLocalImport || !payloadHasCatalogData(pendingLocalImport)) {
return;
}
const confirmed = window.confirm(
"Importer les donnees locales dans la base de donnees ? (deduplication par console + titre + annee)",
);
if (!confirmed) {
return;
}
try {
const result = await apiRequest("/api/catalog/import", {
method: "POST",
body: pendingLocalImport,
});
pendingLocalImport = null;
await refreshFromApi(state.selectedBrand, state.selectedConsole);
alert(`Migration terminee: ${result.insertedGames || 0} jeu(x) importe(s).`);
} catch (error) {
console.error(error);
alert("Echec de la migration locale vers DB.");
}
});
function render() {
renderDataMode();
renderBrandTabs();
@@ -274,20 +306,22 @@ function renderDataMode() {
if (dataMode === "api") {
dataModeInfo.textContent = "Source: API (lecture/ecriture active sur la base de donnees).";
return;
}
if (dataMode === "api-empty") {
} else if (dataMode === "api-empty") {
dataModeInfo.textContent = "Source: API (base vide). Ajoute une section pour demarrer.";
return;
}
if (dataMode === "local-fallback") {
} else if (dataMode === "local-pending-import") {
dataModeInfo.textContent = "Source: localStorage detectee. Migration vers DB disponible.";
} else if (dataMode === "local-fallback") {
dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible.";
return;
} else {
dataModeInfo.textContent = "Source: localStorage";
}
dataModeInfo.textContent = "Source: localStorage";
const showMigrateBtn = apiReachable && pendingLocalImport && payloadHasCatalogData(pendingLocalImport);
if (showMigrateBtn) {
migrateBtn.classList.remove("hidden");
} else {
migrateBtn.classList.add("hidden");
}
}
function renderBrandTabs() {
@@ -431,6 +465,10 @@ function persist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function markLocalDataForImport() {
pendingLocalImport = payloadHasCatalogData(state) ? structuredClone(state) : null;
}
async function apiRequest(path, options = {}) {
const requestOptions = {
method: options.method || "GET",
@@ -500,8 +538,19 @@ function payloadHasCatalogData(payload) {
async function refreshFromApi(preferredBrand, preferredConsole) {
const payload = await apiRequest("/api/catalog/full");
apiReachable = true;
dataMode = payloadHasCatalogData(payload) ? "api" : "api-empty";
applyCatalogPayload(payload, preferredBrand, preferredConsole);
const payloadHasData = payloadHasCatalogData(payload);
if (payloadHasData) {
dataMode = "api";
applyCatalogPayload(payload, preferredBrand, preferredConsole);
} else if (pendingLocalImport && payloadHasCatalogData(pendingLocalImport)) {
dataMode = "local-pending-import";
applyCatalogPayload(pendingLocalImport, preferredBrand, preferredConsole);
} else {
dataMode = "api-empty";
applyCatalogPayload(payload, preferredBrand, preferredConsole);
}
persist();
render();
}