import { createSeedData } from "./data"; import type { HubApp, HubData } from "./types"; const DATA_KEY = "glm-hub:state:v1"; export interface LoadDataResult { data: HubData; warning: string; } function isHubApp(value: unknown): value is HubApp { if (!value || typeof value !== "object") return false; const app = value as Partial; return ( typeof app.id === "string" && typeof app.name === "string" && typeof app.description === "string" && typeof app.category === "string" && Array.isArray(app.keywords) && typeof app.url === "string" && typeof app.mark === "string" && typeof app.accent === "string" && typeof app.iconDataUrl === "string" && (app.visibility === "published" || app.visibility === "hidden") ); } function isHubData(value: unknown): value is HubData { if (!value || typeof value !== "object") return false; const data = value as Partial; return ( data.schemaVersion === 1 && Array.isArray(data.apps) && data.apps.every(isHubApp) && !!data.favoritesByUser && typeof data.favoritesByUser === "object" && !!data.recentByUser && typeof data.recentByUser === "object" ); } function keepExistingAppIds(groups: Record, appIds: Set): Record { return Object.fromEntries( Object.entries(groups).map(([userId, ids]) => [userId, ids.filter((id) => appIds.has(id))]), ); } function migrateCategory(category: string): HubApp["category"] { switch (category) { case "Administración": case "Recursos Humanos": case "CDC": return category; case "Recursos": return "Recursos Humanos"; case "Clientes": case "Creatividad": return "CDC"; case "Operaciones": case "Analítica": default: return "Administración"; } } function migrateAppCategories(data: HubData): HubData { let changed = false; const apps = data.apps.map((app) => { const category = migrateCategory(app.category); if (category === app.category) return app; changed = true; return { ...app, category }; }); return changed ? { ...data, apps, updatedAt: new Date().toISOString() } : data; } function removeAppsWithoutLogo(data: HubData): HubData { const apps = data.apps.filter((app) => Boolean(app.iconDataUrl.trim())); if (apps.length === data.apps.length) return data; const appIds = new Set(apps.map((app) => app.id)); return { ...data, apps, favoritesByUser: keepExistingAppIds(data.favoritesByUser, appIds), recentByUser: keepExistingAppIds(data.recentByUser, appIds), updatedAt: new Date().toISOString(), }; } export function loadHubData(): LoadDataResult { const fallback = createSeedData(); try { const raw = localStorage.getItem(DATA_KEY); if (!raw) { localStorage.setItem(DATA_KEY, JSON.stringify(fallback)); return { data: fallback, warning: "" }; } const parsed: unknown = JSON.parse(raw); if (!isHubData(parsed)) { localStorage.setItem(`${DATA_KEY}:backup`, raw); localStorage.setItem(DATA_KEY, JSON.stringify(fallback)); return { data: fallback, warning: "Los datos locales no eran válidos. Restauramos el catálogo inicial.", }; } const migrated = migrateAppCategories(parsed); const cleaned = removeAppsWithoutLogo(migrated); if (cleaned !== parsed) { localStorage.setItem(DATA_KEY, JSON.stringify(cleaned)); } return { data: cleaned, warning: "" }; } catch { return { data: fallback, warning: "No pudimos leer los datos guardados. Estás viendo el catálogo inicial.", }; } } export function saveHubData(data: HubData): { ok: boolean; error: string } { try { localStorage.setItem(DATA_KEY, JSON.stringify(data)); return { ok: true, error: "" }; } catch (error) { const isQuota = error instanceof DOMException && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED"); return { ok: false, error: isQuota ? "El almacenamiento está lleno. Prueba con un logo más liviano." : "No pudimos guardar los cambios en este navegador.", }; } } export function resetHubData(): HubData { const next = createSeedData(); localStorage.setItem(DATA_KEY, JSON.stringify(next)); return next; } export function normalizeSearch(value: string): string { return value .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLocaleLowerCase("es-DO") .trim(); } export function getSafeUrl(value: string): string | null { if (!value.trim()) return null; try { const url = new URL(value.trim()); const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1"); if (url.protocol !== "https:" && !isLocalHttp) return null; if (url.username || url.password) return null; return url.toString(); } catch { return null; } }