fix(api): fix api integration for quest map and adjacent components
This commit is contained in:
120
src/stores/useInventoryStore.ts
Normal file
120
src/stores/useInventoryStore.ts
Normal file
@ -0,0 +1,120 @@
|
||||
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<InventoryStore>()(
|
||||
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`;
|
||||
}
|
||||
Reference in New Issue
Block a user