import { createStore } from 'zustand/vanilla'; import { useSyncExternalStore } from 'react'; export interface PersonaOption { id: string; label: string; description: string; colors: { torso: string; legs: string }; modelPath?: string; } export interface PersonaState { personas: PersonaOption[]; isHydrated: boolean; } export interface PersonaActions { addPersona: (persona: Omit & { id?: string }) => void; removePersona: (id: string) => void; updatePersona: (id: string, updates: Partial>) => void; resetPersonas: () => void; hydrate: () => void; } export type PersonaStore = PersonaState & PersonaActions; export const DEFAULT_PERSONAS: PersonaOption[] = [ { id: 'none', label: 'Default', description: 'Original robot appearance', colors: { torso: '#3b82f6', legs: '#3b82f6' }, }, { id: 'emarati-kandura', label: 'Emarati Kandura', description: 'Traditional white robe attire', colors: { torso: '#f8fafc', legs: '#f8fafc' }, }, { id: 'industrial-vest', label: 'Industrial Vest', description: 'High-visibility safety vest', colors: { torso: '#f59e0b', legs: '#3b82f6' }, }, { id: 'business-suit', label: 'Business Suit', description: 'Professional navy suit', colors: { torso: '#1e293b', legs: '#1e293b' }, }, ]; const STORAGE_KEY = 'lootah-personas'; function loadFromStorage(): PersonaOption[] | null { if (typeof window === 'undefined') return null; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return null; return parsed; } catch { return null; } } function saveToStorage(personas: PersonaOption[]) { if (typeof window === 'undefined') return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(personas)); } catch { // Storage unavailable } } function generateId(): string { return `persona-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; } export const personaStore = createStore((set, get) => ({ personas: DEFAULT_PERSONAS, isHydrated: false, addPersona: (persona) => { const newPersona: PersonaOption = { id: persona.id ?? generateId(), label: persona.label, description: persona.description, colors: persona.colors, ...(persona.modelPath ? { modelPath: persona.modelPath } : {}), }; set((state) => { const updated = [...state.personas, newPersona]; saveToStorage(updated); return { personas: updated }; }); }, removePersona: (id) => { // Prevent removing the default 'none' persona if (id === 'none') return; set((state) => { const updated = state.personas.filter((p) => p.id !== id); saveToStorage(updated); return { personas: updated }; }); }, updatePersona: (id, updates) => { set((state) => { const updated = state.personas.map((p) => p.id === id ? { ...p, ...updates } : p ); saveToStorage(updated); return { personas: updated }; }); }, resetPersonas: () => { saveToStorage(DEFAULT_PERSONAS); set({ personas: [...DEFAULT_PERSONAS] }); }, hydrate: () => { const stored = loadFromStorage(); if (stored && stored.length > 0) { // Only re-inject truly built-in personas (those still in DEFAULT_PERSONAS) if missing. // Dynamic/uploaded personas that were deleted via the dashboard must NOT be re-added. const storedIds = new Set(stored.map((s) => s.id)); const missing = DEFAULT_PERSONAS.filter((d) => !storedIds.has(d.id)); set({ personas: [...stored, ...missing], isHydrated: true }); } else { set({ personas: [...DEFAULT_PERSONAS], isHydrated: true }); } // Fetch pricing items from server DB and auto-register personas for all attire items if (typeof window !== 'undefined') { fetch('/api/admin/pricing/') .then((r) => r.json()) .then((data) => { const serverItems: { id: string; label: string; modelPath: string | null }[] = data.items ?? []; const current = get().personas; const currentIds = new Set(current.map((p) => p.id)); const newPersonas: PersonaOption[] = []; // Items that should not appear as selectable personas const excludeIds = new Set(['base', 'custom-color']); serverItems.forEach(({ id, label, modelPath }) => { if (excludeIds.has(id)) return; if (currentIds.has(id)) { // Update modelPath if it changed const existing = current.find((p) => p.id === id); if (existing && modelPath && existing.modelPath !== modelPath) { set((state) => ({ personas: state.personas.map((p) => p.id === id ? { ...p, modelPath } : p ), })); } } else { // Auto-create a persona entry for every pricing item newPersonas.push({ id, label, description: label, colors: { torso: '#3b82f6', legs: '#3b82f6' }, ...(modelPath ? { modelPath } : {}), }); } }); if (newPersonas.length > 0) { set((state) => { const updated = [...state.personas, ...newPersonas]; saveToStorage(updated); return { personas: updated }; }); } else { // Save current state to localStorage so it persists saveToStorage(get().personas); } }) .catch(() => {}); // silent — use local data as fallback } }, })); export const usePersonaStore = (selector: (state: PersonaStore) => T): T => { return useSyncExternalStore( personaStore.subscribe, () => selector(personaStore.getState()), () => selector(personaStore.getState()) ); };