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(); let pendingLocalImport = payloadHasCatalogData(state) ? structuredClone(state) : null; let dataMode = "local"; let apiReachable = false; 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 versionInput = document.getElementById("versionInput"); const genreInput = document.getElementById("genreInput"); const publisherInput = document.getElementById("publisherInput"); const yearInput = document.getElementById("yearInput"); const valueInput = document.getElementById("valueInput"); const purchasePriceInput = document.getElementById("purchasePriceInput"); const conditionInput = document.getElementById("conditionInput"); const isDuplicateInput = document.getElementById("isDuplicateInput"); 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 dataModeInfo = document.getElementById("dataModeInfo"); const storageState = document.getElementById("storageState"); const toolsDrawer = document.getElementById("toolsDrawer"); const toolsToggleBtn = document.getElementById("toolsToggleBtn"); const toolsCloseBtn = document.getElementById("toolsCloseBtn"); const migrateBtn = document.getElementById("migrateBtn"); const backupControls = document.getElementById("backupControls"); const backupBtn = document.getElementById("backupBtn"); const restoreMergeBtn = document.getElementById("restoreMergeBtn"); const restoreReplaceBtn = document.getElementById("restoreReplaceBtn"); const restoreFileInput = document.getElementById("restoreFileInput"); const googleDriveState = document.getElementById("googleDriveState"); const googleConnectBtn = document.getElementById("googleConnectBtn"); const googleBackupBtn = document.getElementById("googleBackupBtn"); const googleRestoreBtn = document.getElementById("googleRestoreBtn"); const quickSearchInput = document.getElementById("quickSearchInput"); const quickSearchResults = document.getElementById("quickSearchResults"); const gamesList = document.getElementById("gamesList"); const gameCardTemplate = document.getElementById("gameCardTemplate"); let editingGameId = null; let pendingRestoreMode = "merge"; let quickSearchTerm = ""; let googleStatus = { configured: false, connected: false, email: "" }; toolsToggleBtn.addEventListener("click", () => { toolsDrawer.classList.toggle("open"); }); toolsCloseBtn.addEventListener("click", () => { toolsDrawer.classList.remove("open"); }); document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } if (!toolsDrawer.classList.contains("open")) { return; } if (toolsDrawer.contains(event.target) || toolsToggleBtn.contains(event.target)) { return; } toolsDrawer.classList.remove("open"); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { toolsDrawer.classList.remove("open"); } }); googleConnectBtn.addEventListener("click", async () => { try { const payload = await apiRequest("/api/google/connect-url"); if (!payload.url) { throw new Error("Missing Google connect URL"); } window.location.href = payload.url; } catch (error) { console.error(error); alert("Connexion Google indisponible. Verifie la configuration OAuth."); } }); googleBackupBtn.addEventListener("click", async () => { try { const payload = await apiRequest("/api/google/backup/upload", { method: "POST" }); await refreshGoogleStatus(); alert(`Sauvegarde Drive OK: ${payload.fileName || "fichier cree"}.`); } catch (error) { console.error(error); alert("Echec sauvegarde Google Drive."); } }); googleRestoreBtn.addEventListener("click", async () => { const confirmed = window.confirm("Restaurer depuis le dernier backup Google Drive ?"); if (!confirmed) { return; } try { const payload = await apiRequest("/api/google/backup/restore", { method: "POST", body: { mode: "merge" }, }); await refreshFromApi(state.selectedBrand, state.selectedConsole); alert(`Restauration Drive OK (${payload.mode})`); } catch (error) { console.error(error); alert("Echec restauration Google Drive."); } }); quickSearchInput.addEventListener("input", (event) => { quickSearchTerm = event.target.value.trim(); renderSearchResults(); }); platformForm.addEventListener("submit", async (event) => { event.preventDefault(); const brand = brandInput.value.trim().toUpperCase(); const consoleName = consoleInput.value.trim(); if (!brand || !consoleName) { return; } if (apiReachable && dataMode !== "local-pending-import") { 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); } state.selectedBrand = brand; state.selectedConsole = consoleName; state.gamesByConsole[consoleName] = state.gamesByConsole[consoleName] || []; platformForm.reset(); persist(); markLocalDataForImport(); render(); }); gameForm.addEventListener("submit", async (event) => { event.preventDefault(); const title = titleInput.value.trim(); if (!title || !state.selectedConsole) { return; } if (apiReachable && dataMode !== "local-pending-import") { const payload = { brand: state.selectedBrand, consoleName: state.selectedConsole, title, version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, condition: conditionInput.value ? Number(conditionInput.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) { const games = state.gamesByConsole[state.selectedConsole]; const idx = games.findIndex((game) => game.id === editingGameId); if (idx !== -1) { games[idx] = { ...games[idx], title, version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), }; } } else { const game = { id: crypto.randomUUID(), title, version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), createdAt: new Date().toISOString(), }; state.gamesByConsole[state.selectedConsole].unshift(game); } resetEditMode(); persist(); markLocalDataForImport(); 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", async (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 === "edit") { startEditMode(games[idx]); return; } if (apiReachable && dataMode !== "local-pending-import") { 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) { resetEditMode(); } } if (action === "toggle-loan") { games[idx].loanedTo = games[idx].loanedTo ? "" : "A renseigner"; } persist(); markLocalDataForImport(); render(); }); 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."); } }); backupBtn.addEventListener("click", async () => { if (!apiReachable) { alert("API indisponible. Sauvegarde JSON impossible."); return; } try { const dump = await apiRequest("/api/backup/export"); const blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); a.href = url; a.download = `video-games-backup-${stamp}.json`; document.body.append(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (error) { console.error(error); alert("Echec de la sauvegarde JSON."); } }); restoreMergeBtn.addEventListener("click", () => { pendingRestoreMode = "merge"; restoreFileInput.click(); }); restoreReplaceBtn.addEventListener("click", () => { pendingRestoreMode = "replace"; restoreFileInput.click(); }); restoreFileInput.addEventListener("change", async (event) => { const input = event.target; const file = input.files && input.files[0] ? input.files[0] : null; input.value = ""; if (!file) { return; } if (!apiReachable) { alert("API indisponible. Restauration impossible."); return; } try { const fileText = await file.text(); const dump = JSON.parse(fileText); if (pendingRestoreMode === "replace") { const confirmed = window.confirm( "Mode remplacement: la base actuelle sera remplacee. Une sauvegarde pre-restore sera creee. Continuer ?", ); if (!confirmed) { return; } } const result = await apiRequest("/api/backup/restore", { method: "POST", body: { mode: pendingRestoreMode, dump, }, }); pendingLocalImport = null; await refreshFromApi(state.selectedBrand, state.selectedConsole); alert( `Restauration terminee (${result.mode}): ${result.insertedGames || 0} jeu(x), ${result.insertedConsoles || 0} console(s).`, ); } catch (error) { console.error(error); alert("Echec de la restauration JSON."); } }); function render() { renderDataMode(); renderGoogleStatus(); renderBrandTabs(); renderConsoleTabs(); renderGames(); renderSearchResults(); } function renderDataMode() { if (!dataModeInfo) { return; } if (dataMode === "api") { dataModeInfo.textContent = "Source: API (lecture/ecriture active sur la base de donnees)."; if (storageState) { storageState.textContent = "Etat: Base de donnees active"; } } else if (dataMode === "api-empty") { dataModeInfo.textContent = "Source: API (base vide). Ajoute une section pour demarrer."; if (storageState) { storageState.textContent = "Etat: Base de donnees active (vide)"; } } else if (dataMode === "local-pending-import") { dataModeInfo.textContent = "Source: localStorage detectee. Migration vers DB disponible."; if (storageState) { storageState.textContent = "Etat: LocalStorage (migration en attente)"; } } else if (dataMode === "local-fallback") { dataModeInfo.textContent = "Source: localStorage (fallback). API indisponible."; if (storageState) { storageState.textContent = "Etat: LocalStorage (API indisponible)"; } } else { dataModeInfo.textContent = "Source: localStorage"; if (storageState) { storageState.textContent = "Etat: LocalStorage"; } } const showMigrateBtn = dataMode === "local-pending-import" && apiReachable && pendingLocalImport && payloadHasCatalogData(pendingLocalImport); if (showMigrateBtn) { migrateBtn.classList.remove("hidden"); } else { migrateBtn.classList.add("hidden"); } if (apiReachable) { backupControls.classList.remove("hidden"); } else { backupControls.classList.add("hidden"); } } function renderGoogleStatus() { if (!googleDriveState) { return; } if (!googleStatus.configured) { googleDriveState.textContent = "Etat Google Drive: non configure (OAuth manquant)."; googleConnectBtn.disabled = true; googleBackupBtn.disabled = true; googleRestoreBtn.disabled = true; return; } if (!googleStatus.connected) { googleDriveState.textContent = "Etat Google Drive: non connecte."; googleConnectBtn.disabled = false; googleBackupBtn.disabled = true; googleRestoreBtn.disabled = true; return; } const email = googleStatus.email ? ` (${googleStatus.email})` : ""; googleDriveState.textContent = `Etat Google Drive: connecte${email}.`; googleConnectBtn.disabled = false; googleBackupBtn.disabled = false; googleRestoreBtn.disabled = false; } function findBrandByConsole(consoleName) { for (const [brand, consoles] of Object.entries(state.brands)) { if (Array.isArray(consoles) && consoles.includes(consoleName)) { return brand; } } return "INCONNUE"; } function collectAllGames() { const all = []; for (const [consoleName, games] of Object.entries(state.gamesByConsole)) { const brand = findBrandByConsole(consoleName); for (const game of games || []) { all.push({ ...game, consoleName, brand }); } } return all; } function renderSearchResults() { if (!quickSearchResults) { return; } const query = quickSearchTerm.trim().toLowerCase(); if (!query) { quickSearchResults.innerHTML = '

Saisis un titre pour verifier si tu possedes deja le jeu.

'; return; } const normalizedQuery = query.replace(/\s+/g, " "); const allGames = collectAllGames(); const matches = allGames.filter((game) => { const title = String(game.title || "").toLowerCase(); return title.includes(normalizedQuery); }); const exactMatches = allGames.filter((game) => { const title = String(game.title || "") .toLowerCase() .replace(/\s+/g, " ") .trim(); return title === normalizedQuery; }); if (!matches.length) { quickSearchResults.innerHTML = '

Aucun jeu trouve dans ta collection.

'; return; } const maxShown = 20; const shown = matches.slice(0, maxShown); const header = exactMatches.length > 0 ? `

Deja possede: OUI (${exactMatches.length} correspondance${exactMatches.length > 1 ? "s" : ""} exacte${exactMatches.length > 1 ? "s" : ""}).

` : `

Jeu similaire trouve: ${matches.length} resultat${matches.length > 1 ? "s" : ""}.

`; const items = shown .map((game) => { const meta = [ `${game.brand} / ${game.consoleName}`, game.version ? `Version: ${game.version}` : null, game.isDuplicate ? "Double: OUI" : null, ] .filter(Boolean) .join(" | "); return `
${escapeHtml(game.title || "")}

${escapeHtml(meta)}

`; }) .join(""); const more = matches.length > maxShown ? `

+${matches.length - maxShown} autre(s) resultat(s).

` : ""; quickSearchResults.innerHTML = `${header}${items}${more}`; } function escapeHtml(text) { return String(text) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } 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.version ? `Version: ${game.version}` : null, game.genre ? `Genre: ${game.genre}` : null, game.publisher ? `Editeur: ${game.publisher}` : null, game.isDuplicate ? "Double: OUI" : null, game.year ? `Annee: ${game.year}` : null, game.purchasePrice != null ? `Prix achat: ${game.purchasePrice.toFixed(2)} EUR` : null, game.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null, game.condition != null ? `Etat: ${game.condition}` : 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 || ""; versionInput.value = game.version || ""; genreInput.value = game.genre || ""; publisherInput.value = game.publisher || ""; isDuplicateInput.checked = Boolean(game.isDuplicate); yearInput.value = game.year || ""; purchasePriceInput.value = game.purchasePrice != null ? game.purchasePrice : ""; valueInput.value = game.value != null ? game.value : ""; conditionInput.value = game.condition != null ? game.condition : ""; 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 || {}; state.gamesByConsole = state.gamesByConsole || {}; const brands = Object.keys(state.brands); if (!brands.length && !apiReachable) { 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)); } function markLocalDataForImport() { pendingLocalImport = payloadHasCatalogData(state) ? structuredClone(state) : null; } async function apiRequest(path, options = {}) { const controller = new AbortController(); const timeoutMs = options.timeoutMs || 6000; const timeoutId = setTimeout(() => controller.abort(), timeoutMs); const requestOptions = { method: options.method || "GET", headers: {}, signal: controller.signal, }; if (options.body !== undefined) { requestOptions.headers["Content-Type"] = "application/json"; requestOptions.body = JSON.stringify(options.body); } try { 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; } finally { clearTimeout(timeoutId); } } 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; } const brands = payload.brands && typeof payload.brands === "object" ? payload.brands : {}; const gamesByConsole = payload.gamesByConsole && typeof payload.gamesByConsole === "object" ? payload.gamesByConsole : {}; const consolesCount = Object.values(brands).reduce((count, consoles) => { if (!Array.isArray(consoles)) { return count; } return count + consoles.length; }, 0); const gamesCount = Object.values(gamesByConsole).reduce((count, games) => { if (!Array.isArray(games)) { return count; } return count + games.length; }, 0); return consolesCount > 0 || gamesCount > 0; } async function refreshFromApi(preferredBrand, preferredConsole) { const payload = await apiRequest("/api/catalog/full"); apiReachable = true; 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(); } async function refreshGoogleStatus() { try { googleStatus = await apiRequest("/api/google/status"); } catch { googleStatus = { configured: false, connected: false, email: "" }; } renderGoogleStatus(); } async function hydrateFromApi() { try { await refreshFromApi(state.selectedBrand, state.selectedConsole); } catch { apiReachable = false; dataMode = "local-fallback"; } } async function bootstrap() { await refreshGoogleStatus(); await hydrateFromApi(); normalizeState(); render(); handleGoogleCallbackResult(); } bootstrap(); function handleGoogleCallbackResult() { const url = new URL(window.location.href); const googleParam = url.searchParams.get("google"); if (!googleParam) { return; } if (googleParam === "connected") { alert("Google Drive connecte avec succes."); refreshGoogleStatus(); } else if (googleParam === "error") { alert("Connexion Google Drive echouee."); } url.searchParams.delete("google"); window.history.replaceState({}, "", url.toString()); }