import { create } from 'zustand' import { toast } from '../components/ToastContainer' const MAX_STACK = 20 export interface UndoEntry { id: string /** Short description of what happened, e.g. "Discarded 12 photos". */ label: string /** Function that reverses the action. May be async; errors should be * surfaced via toast.error from inside the function. */ undo: () => void | Promise } interface UndoStore { stack: UndoEntry[] /** Push a new entry. Caps the stack at MAX_STACK by dropping the oldest. */ push: (entry: Omit) => void /** Pop the most recent entry. Returns null when the stack is empty. */ pop: () => UndoEntry | null clear: () => void } export const useUndoStore = create((set, get) => ({ stack: [], push: (entry) => { const id = Date.now().toString() + Math.random().toString(36).slice(2, 6) set((s) => { const next = [...s.stack, { ...entry, id }] if (next.length > MAX_STACK) next.shift() return { stack: next } }) }, pop: () => { const stack = get().stack if (stack.length === 0) return null const last = stack[stack.length - 1] set({ stack: stack.slice(0, -1) }) return last }, clear: () => set({ stack: [] }), })) /** * Convenience: register an undoable action AND show the user a success * toast with an inline Undo button. The toast and Cmd+Z hotkey both pop * from the same stack so either path works. */ export function registerUndoable(label: string, undo: () => void | Promise) { useUndoStore.getState().push({ label, undo }) toast.success(label, 'Press ⌘Z to undo', { label: 'Undo', onClick: async () => { // Pop the entry we just pushed (or whatever is now on top, if the // user fired multiple actions in quick succession — Undo always // reverses the most recent thing). const entry = useUndoStore.getState().pop() if (!entry) return try { await entry.undo() } catch (e) { // Re-push so the user can try again, and surface the failure. useUndoStore.getState().push({ label: entry.label, undo: entry.undo }) toast.error('Undo failed', e instanceof Error ? e.message : String(e)) } }, }) }