fix(api): fix api integration for quest map and adjacent components

This commit is contained in:
shafin-r
2026-03-01 12:57:54 +06:00
parent c7f0183956
commit 2eaf77e13c
12 changed files with 2039 additions and 618 deletions

View 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`;
}