summaryrefslogtreecommitdiff
path: root/frontend/hooks
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-11 16:04:32 +0100
committerdiogo464 <[email protected]>2025-08-11 16:04:32 +0100
commitf4d8a26972728891de8bde4eeb94c80f027ce2d2 (patch)
tree3c8b9c25c2a1e3fab7a86f51922c39eb2ed93697 /frontend/hooks
parent32b008a9c0c8e0130ab10bc96ffea9232f9cf95a (diff)
basic v0 ui working
Diffstat (limited to 'frontend/hooks')
-rw-r--r--frontend/hooks/use-toast.ts193
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
3import * as React from "react"
4
5import type {
6 ToastActionElement,
7 ToastProps,
8} from "@/components/ui/toast"
9
10const TOAST_LIMIT = 1
11const TOAST_REMOVE_DELAY = 1000000
12
13type ToasterToast = ToastProps & {
14 id: string
15 title?: React.ReactNode
16 description?: React.ReactNode
17 action?: ToastActionElement
18}
19
20const 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
27let count = 0
28
29function genId() {
30 count = (count + 1) % Number.MAX_SAFE_INTEGER
31 return count.toString()
32}
33
34type ActionType = typeof actionTypes
35
36type 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
54interface State {
55 toasts: ToasterToast[]
56}
57
58const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
59
60const 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
76export 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
131const listeners: Array<(state: State) => void> = []
132
133let memoryState: State = { toasts: [] }
134
135function dispatch(action: Action) {
136 memoryState = reducer(memoryState, action)
137 listeners.forEach((listener) => {
138 listener(memoryState)
139 })
140}
141
142type Toast = Omit<ToasterToast, "id">
143
144function 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
173function 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
193export { useToast, toast } \ No newline at end of file