diff options
| author | diogo464 <[email protected]> | 2025-08-11 16:04:32 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-11 16:04:32 +0100 |
| commit | f4d8a26972728891de8bde4eeb94c80f027ce2d2 (patch) | |
| tree | 3c8b9c25c2a1e3fab7a86f51922c39eb2ed93697 /frontend/hooks | |
| parent | 32b008a9c0c8e0130ab10bc96ffea9232f9cf95a (diff) | |
basic v0 ui working
Diffstat (limited to 'frontend/hooks')
| -rw-r--r-- | frontend/hooks/use-toast.ts | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/frontend/hooks/use-toast.ts b/frontend/hooks/use-toast.ts new file mode 100644 index 0000000..87e7062 --- /dev/null +++ b/frontend/hooks/use-toast.ts | |||
| @@ -0,0 +1,193 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | |||
| 5 | import type { | ||
| 6 | ToastActionElement, | ||
| 7 | ToastProps, | ||
| 8 | } from "@/components/ui/toast" | ||
| 9 | |||
| 10 | const TOAST_LIMIT = 1 | ||
| 11 | const TOAST_REMOVE_DELAY = 1000000 | ||
| 12 | |||
| 13 | type ToasterToast = ToastProps & { | ||
| 14 | id: string | ||
| 15 | title?: React.ReactNode | ||
| 16 | description?: React.ReactNode | ||
| 17 | action?: ToastActionElement | ||
| 18 | } | ||
| 19 | |||
| 20 | const actionTypes = { | ||
| 21 | ADD_TOAST: "ADD_TOAST", | ||
| 22 | UPDATE_TOAST: "UPDATE_TOAST", | ||
| 23 | DISMISS_TOAST: "DISMISS_TOAST", | ||
| 24 | REMOVE_TOAST: "REMOVE_TOAST", | ||
| 25 | } as const | ||
| 26 | |||
| 27 | let count = 0 | ||
| 28 | |||
| 29 | function genId() { | ||
| 30 | count = (count + 1) % Number.MAX_SAFE_INTEGER | ||
| 31 | return count.toString() | ||
| 32 | } | ||
| 33 | |||
| 34 | type ActionType = typeof actionTypes | ||
| 35 | |||
| 36 | type Action = | ||
| 37 | | { | ||
| 38 | type: ActionType["ADD_TOAST"] | ||
| 39 | toast: ToasterToast | ||
| 40 | } | ||
| 41 | | { | ||
| 42 | type: ActionType["UPDATE_TOAST"] | ||
| 43 | toast: Partial<ToasterToast> | ||
| 44 | } | ||
| 45 | | { | ||
| 46 | type: ActionType["DISMISS_TOAST"] | ||
| 47 | toastId?: ToasterToast["id"] | ||
| 48 | } | ||
| 49 | | { | ||
| 50 | type: ActionType["REMOVE_TOAST"] | ||
| 51 | toastId?: ToasterToast["id"] | ||
| 52 | } | ||
| 53 | |||
| 54 | interface State { | ||
| 55 | toasts: ToasterToast[] | ||
| 56 | } | ||
| 57 | |||
| 58 | const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>() | ||
| 59 | |||
| 60 | const addToRemoveQueue = (toastId: string) => { | ||
| 61 | if (toastTimeouts.has(toastId)) { | ||
| 62 | return | ||
| 63 | } | ||
| 64 | |||
| 65 | const timeout = setTimeout(() => { | ||
| 66 | toastTimeouts.delete(toastId) | ||
| 67 | dispatch({ | ||
| 68 | type: "REMOVE_TOAST", | ||
| 69 | toastId: toastId, | ||
| 70 | }) | ||
| 71 | }, TOAST_REMOVE_DELAY) | ||
| 72 | |||
| 73 | toastTimeouts.set(toastId, timeout) | ||
| 74 | } | ||
| 75 | |||
| 76 | export const reducer = (state: State, action: Action): State => { | ||
| 77 | switch (action.type) { | ||
| 78 | case "ADD_TOAST": | ||
| 79 | return { | ||
| 80 | ...state, | ||
| 81 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), | ||
| 82 | } | ||
| 83 | |||
| 84 | case "UPDATE_TOAST": | ||
| 85 | return { | ||
| 86 | ...state, | ||
| 87 | toasts: state.toasts.map((t) => | ||
| 88 | t.id === action.toast.id ? { ...t, ...action.toast } : t | ||
| 89 | ), | ||
| 90 | } | ||
| 91 | |||
| 92 | case "DISMISS_TOAST": { | ||
| 93 | const { toastId } = action | ||
| 94 | |||
| 95 | // ! Side effects ! - This could be extracted into a dismissToast() action, | ||
| 96 | // but I'll keep it here for simplicity | ||
| 97 | if (toastId) { | ||
| 98 | addToRemoveQueue(toastId) | ||
| 99 | } else { | ||
| 100 | state.toasts.forEach((toast) => { | ||
| 101 | addToRemoveQueue(toast.id) | ||
| 102 | }) | ||
| 103 | } | ||
| 104 | |||
| 105 | return { | ||
| 106 | ...state, | ||
| 107 | toasts: state.toasts.map((t) => | ||
| 108 | t.id === toastId || toastId === undefined | ||
| 109 | ? { | ||
| 110 | ...t, | ||
| 111 | open: false, | ||
| 112 | } | ||
| 113 | : t | ||
| 114 | ), | ||
| 115 | } | ||
| 116 | } | ||
| 117 | case "REMOVE_TOAST": | ||
| 118 | if (action.toastId === undefined) { | ||
| 119 | return { | ||
| 120 | ...state, | ||
| 121 | toasts: [], | ||
| 122 | } | ||
| 123 | } | ||
| 124 | return { | ||
| 125 | ...state, | ||
| 126 | toasts: state.toasts.filter((t) => t.id !== action.toastId), | ||
| 127 | } | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | const listeners: Array<(state: State) => void> = [] | ||
| 132 | |||
| 133 | let memoryState: State = { toasts: [] } | ||
| 134 | |||
| 135 | function dispatch(action: Action) { | ||
| 136 | memoryState = reducer(memoryState, action) | ||
| 137 | listeners.forEach((listener) => { | ||
| 138 | listener(memoryState) | ||
| 139 | }) | ||
| 140 | } | ||
| 141 | |||
| 142 | type Toast = Omit<ToasterToast, "id"> | ||
| 143 | |||
| 144 | function toast({ ...props }: Toast) { | ||
| 145 | const id = genId() | ||
| 146 | |||
| 147 | const update = (props: ToasterToast) => | ||
| 148 | dispatch({ | ||
| 149 | type: "UPDATE_TOAST", | ||
| 150 | toast: { ...props, id }, | ||
| 151 | }) | ||
| 152 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) | ||
| 153 | |||
| 154 | dispatch({ | ||
| 155 | type: "ADD_TOAST", | ||
| 156 | toast: { | ||
| 157 | ...props, | ||
| 158 | id, | ||
| 159 | open: true, | ||
| 160 | onOpenChange: (open) => { | ||
| 161 | if (!open) dismiss() | ||
| 162 | }, | ||
| 163 | }, | ||
| 164 | }) | ||
| 165 | |||
| 166 | return { | ||
| 167 | id: id, | ||
| 168 | dismiss, | ||
| 169 | update, | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | function useToast() { | ||
| 174 | const [state, setState] = React.useState<State>(memoryState) | ||
| 175 | |||
| 176 | React.useEffect(() => { | ||
| 177 | listeners.push(setState) | ||
| 178 | return () => { | ||
| 179 | const index = listeners.indexOf(setState) | ||
| 180 | if (index > -1) { | ||
| 181 | listeners.splice(index, 1) | ||
| 182 | } | ||
| 183 | } | ||
| 184 | }, [state]) | ||
| 185 | |||
| 186 | return { | ||
| 187 | ...state, | ||
| 188 | toast, | ||
| 189 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | export { useToast, toast } \ No newline at end of file | ||
