TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import { create } from "zustand";4import type { PublicUser, UserSettings, WalletView } from "@spinza/shared";5import { api } from "./api";67export interface SessionState {8 status: "loading" | "guest" | "authenticated";9 user: PublicUser | null;10 wallet: WalletView | null;11 settings: UserSettings | null;12 /** Session start (for break reminders). */13 sessionStartedAt: number;14 refresh: () => Promise<void>;15 setBalance: (balance: number) => void;16 setUserXp: (xp: number, level: number) => void;17 setSettings: (s: Partial<UserSettings>) => void;18 signOut: () => Promise<void>;19 hydrate: (data: { user: PublicUser; wallet: WalletView; settings: UserSettings } | null) => void;20}2122export const useSession = create<SessionState>((set, get) => ({23 status: "loading",24 user: null,25 wallet: null,26 settings: null,27 sessionStartedAt: Date.now(),28 hydrate: (data) => {29 if (data) set({ status: "authenticated", user: data.user, wallet: data.wallet, settings: data.settings });30 else set({ status: "guest", user: null, wallet: null, settings: null });31 },32 refresh: async () => {33 try {34 const data = await api<{ user: PublicUser; wallet: WalletView; settings: UserSettings }>("/api/user");35 set({ status: "authenticated", user: data.user, wallet: data.wallet, settings: data.settings });36 } catch {37 set({ status: "guest", user: null, wallet: null, settings: null });38 }39 },40 setBalance: (balance) => {41 const w = get().wallet;42 if (w) set({ wallet: { ...w, balance } });43 },44 setUserXp: (xp, level) => {45 const u = get().user;46 if (u) set({ user: { ...u, xp, level } });47 },48 setSettings: (s) => {49 const cur = get().settings;50 if (cur) set({ settings: { ...cur, ...s } });51 api("/api/user/settings", { method: "PATCH", json: s }).catch(() => {});52 },53 signOut: async () => {54 await api("/api/auth/logout", { method: "POST" }).catch(() => {});55 set({ status: "guest", user: null, wallet: null, settings: null });56 },57}));5859/* ------------------------------------------------------------- toasts */6061export interface Toast {62 id: number;63 title: string;64 description?: string;65 tone?: "default" | "success" | "danger" | "credit";66 ttl?: number;67}6869interface ToastState {70 toasts: Toast[];71 push: (t: Omit<Toast, "id">) => void;72 dismiss: (id: number) => void;73}7475let toastSeq = 1;76export const useToasts = create<ToastState>((set) => ({77 toasts: [],78 push: (t) => {79 const id = toastSeq++;80 set((s) => ({ toasts: [...s.toasts, { id, ttl: 4200, ...t }] }));81 setTimeout(() => set((s) => ({ toasts: s.toasts.filter((x) => x.id !== id) })), t.ttl ?? 4200);82 },83 dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((x) => x.id !== id) })),84}));8586export const toast = (t: Omit<Toast, "id">) => useToasts.getState().push(t);87