From 7d7167f728274ae5a5f5947a4fccb4ab7ad0085b Mon Sep 17 00:00:00 2001 From: StefanWout Date: Tue, 3 Dec 2024 17:56:19 +0000 Subject: [PATCH 1/2] changed state variable names and tried to implement toast functionality --- src/app/hooks/use-toast.ts | 194 ------------------ .../components/RemoteAssessmentModal.tsx | 17 +- src/app/shadcn_components/ui/toaster.tsx | 4 +- 3 files changed, 16 insertions(+), 199 deletions(-) delete mode 100644 src/app/hooks/use-toast.ts diff --git a/src/app/hooks/use-toast.ts b/src/app/hooks/use-toast.ts deleted file mode 100644 index b18ceb84..00000000 --- a/src/app/hooks/use-toast.ts +++ /dev/null @@ -1,194 +0,0 @@ -"use client" - -// Inspired by react-hot-toast library -import * as React from "react" - -import type { - ToastActionElement, - ToastProps, -} from "src/app/shadcn_components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { useToast, toast } diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 85b191b8..c864e79e 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -19,7 +19,10 @@ import { FormMessage, } from "@/app/shadcn_components/ui/form"; import { Toast } from "@/app/shadcn_components/ui/toast"; +import { Toaster } from "@/app/shadcn_components/ui/toaster" +import { useToast } from "@/app/shadcn_components/hooks/use-toast"; import { FileKey } from "lucide-react"; +import { use } from "chai"; type Option = { label: string; @@ -417,12 +420,18 @@ export default function RemoteAssessmentModal({ }, }); + const [toastState, setToastState] = useState([]); + const { toast } = useToast(toastState, setToastState); const onSubmit = async (data: FormValues) => { try { await handleSubmit(data); form.reset(); setIsOpen(false); + toast({ + title: "Success", + description: "Your remote assessment request has been sent.", + }); } catch (error) { console.error("Error submitting form:", error); } @@ -631,11 +640,13 @@ export default function RemoteAssessmentModal({ - diff --git a/src/app/shadcn_components/ui/toaster.tsx b/src/app/shadcn_components/ui/toaster.tsx index 04165e35..20722b1f 100644 --- a/src/app/shadcn_components/ui/toaster.tsx +++ b/src/app/shadcn_components/ui/toaster.tsx @@ -1,6 +1,6 @@ "use client" -import { useToast } from "src/app/shadcn_components/hooks/use-toast" +import { useToast } from "../hooks/use-toast"; import { Toast, ToastClose, @@ -8,7 +8,7 @@ import { ToastProvider, ToastTitle, ToastViewport, -} from "src/app/shadcn_components/ui/toast" +} from "../ui/toast" export function Toaster() { const { toasts } = useToast() From d1cbf076dde9fcfb80e9d41c2668ada475f68263 Mon Sep 17 00:00:00 2001 From: StefanWout Date: Wed, 4 Dec 2024 11:17:47 +0000 Subject: [PATCH 2/2] we fully toasty now, user can enjoy the feedback! --- src/app/hooks/use-toast.ts | 194 ++++++++++++++++++ src/app/layout.tsx | 2 + .../components/RemoteAssessmentModal.tsx | 18 +- src/app/shadcn_components/ui/toast.tsx | 2 +- src/app/shadcn_components/ui/toaster.tsx | 4 +- src/lib/utils.ts | 2 +- 6 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 src/app/hooks/use-toast.ts diff --git a/src/app/hooks/use-toast.ts b/src/app/hooks/use-toast.ts new file mode 100644 index 00000000..b18ceb84 --- /dev/null +++ b/src/app/hooks/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "src/app/shadcn_components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c2f4e51f..7b30618b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import { AuthOptions } from "@/app/api/auth/[...nextauth]/route"; import { getServerSession } from "next-auth/next"; import { cache } from "react"; import { Inter } from "next/font/google"; +import { Toaster } from "@/app/shadcn_components/ui/toaster"; // If loading a variable font, you don't need to specify the font weight const inter = Inter({ @@ -51,6 +52,7 @@ export default async function RootLayout({