import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import type { InventoryItem, ActiveEffect, UserInventory, } from "../types/quest"; // ─── Store interface ────────────────────────────────────────────────────────── interface InventoryStore { // Raw inventory from API items: InventoryItem[]; activeEffects: ActiveEffect[]; // Loading / error loading: boolean; activatingId: string | null; // item id currently being activated error: string | null; lastActivatedId: string | null; // shows success state briefly // Actions syncFromAPI: (inv: UserInventory) => void; activateItemOptimistic: (itemId: string) => void; activateItemSuccess: (inv: UserInventory, itemId: string) => void; activateItemError: (itemId: string, error: string) => void; clearLastActivated: () => void; setLoading: (v: boolean) => void; } export const useInventoryStore = create()( persist( (set) => ({ items: [], activeEffects: [], loading: false, activatingId: null, error: null, lastActivatedId: null, syncFromAPI: (inv) => set({ items: inv.items, activeEffects: inv.active_effects }), // Optimistic — mark as "activating" immediately for instant UI feedback activateItemOptimistic: (itemId) => set({ activatingId: itemId, error: null }), // On API success — replace inventory with fresh server state activateItemSuccess: (inv, itemId) => set({ items: inv.items, activeEffects: inv.active_effects, activatingId: null, lastActivatedId: itemId, }), activateItemError: (itemId, error) => set({ activatingId: null, error }), clearLastActivated: () => set({ lastActivatedId: null }), setLoading: (v) => set({ loading: v }), }), { name: "inventory-store", storage: createJSONStorage(() => localStorage), // Persist items + active effects so the app can show active item banners // without waiting for a network request on every mount partialize: (state) => ({ items: state.items, activeEffects: state.activeEffects, }), }, ), ); // ─── Selector helpers (call these in components, not inside the store) ───────── /** Returns true if any active effect has the given effect_type */ export function hasActiveEffect( activeEffects: ActiveEffect[], effectType: string, ): boolean { const now = Date.now(); return activeEffects.some( (e) => e.item.effect_type === effectType && new Date(e.expires_at).getTime() > now, ); } /** Returns the active effect for a given effect_type, or null */ export function getActiveEffect( activeEffects: ActiveEffect[], effectType: string, ): ActiveEffect | null { const now = Date.now(); return ( activeEffects.find( (e) => e.item.effect_type === effectType && new Date(e.expires_at).getTime() > now, ) ?? null ); } /** Returns all non-expired active effects */ export function getLiveEffects(activeEffects: ActiveEffect[]): ActiveEffect[] { const now = Date.now(); return activeEffects.filter((e) => new Date(e.expires_at).getTime() > now); } /** Formats time remaining as "2h 14m" or "43m" */ export function formatTimeLeft(expiresAt: string): string { const msLeft = new Date(expiresAt).getTime() - Date.now(); if (msLeft <= 0) return "Expired"; const totalMin = Math.floor(msLeft / 60_000); const h = Math.floor(totalMin / 60); const m = totalMin % 60; return h > 0 ? `${h}h ${m}m` : `${m}m`; }