This commit is contained in:
David Arranz 2024-08-26 15:58:31 +02:00
parent 6ff591d359
commit 74e6316583
11 changed files with 163 additions and 139 deletions

View File

@ -1,9 +1,8 @@
import { AuthProvider, ThemeProvider, UnsavedWarnProvider } from "@/lib/hooks"; import { AuthProvider, ThemeProvider, UnsavedWarnProvider } from "@/lib/hooks";
import { TooltipProvider } from "@/ui"; import { Toaster, TooltipProvider } from "@/ui";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { Suspense } from "react"; import { Suspense } from "react";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import { Routes } from "./Routes"; import { Routes } from "./Routes";
import { LoadingOverlay, TailwindIndicator } from "./components"; import { LoadingOverlay, TailwindIndicator } from "./components";
@ -30,7 +29,7 @@ function App() {
<Suspense fallback={<LoadingOverlay />}> <Suspense fallback={<LoadingOverlay />}>
<Routes /> <Routes />
<ToastContainer /> <Toaster />
</Suspense> </Suspense>
</UnsavedWarnProvider> </UnsavedWarnProvider>
</TooltipProvider> </TooltipProvider>

View File

@ -23,7 +23,7 @@ export const DownloadQuoteDialog = (props: DownloadQuoteDialogProps) => {
const panelId = useId(); const panelId = useId();
useEffect(() => { useEffect(() => {
if (!isInProgress && !error && percentage === 100) { if (isInProgress && !error && percentage === 100) {
if (onFinishDownload) { if (onFinishDownload) {
onFinishDownload(); onFinishDownload();
} }

View File

@ -8,6 +8,7 @@ import {
import { DataTableProvider } from "@/lib/hooks"; import { DataTableProvider } from "@/lib/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui";
import { useToast } from "@/ui/use-toast";
import { CurrencyData, Language, Quantity } from "@shared/contexts"; import { CurrencyData, Language, Quantity } from "@shared/contexts";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { t } from "i18next"; import { t } from "i18next";
@ -28,6 +29,7 @@ export const QuoteDetailsCardEditor = ({
language: Language; language: Language;
defaultValues: Readonly<{ [x: string]: any }> | undefined; defaultValues: Readonly<{ [x: string]: any }> | undefined;
}) => { }) => {
const { toast } = useToast();
const { control, register } = useFormContext(); const { control, register } = useFormContext();
const [pickerMode] = useState<"dialog" | "panel">("dialog"); const [pickerMode] = useState<"dialog" | "panel">("dialog");
@ -200,6 +202,10 @@ export const QuoteDetailsCardEditor = ({
}, },
unit_price: article.retail_price, unit_price: article.retail_price,
}); });
toast({
title: "Artículo del catálog añadido:",
description: article.description,
});
}, },
[fieldActions] [fieldActions]
); );

View File

@ -5,7 +5,7 @@ import { QuotesDataTable } from "./components";
import { Button, Tabs, TabsContent, TabsList, TabsTrigger, Toggle } from "@/ui"; import { Button, Tabs, TabsContent, TabsList, TabsTrigger, Toggle } from "@/ui";
import { useToggle } from "@wojtekmaj/react-hooks"; import { useToggle } from "@wojtekmaj/react-hooks";
import { t } from "i18next"; import { t } from "i18next";
import { InfoIcon, PlusIcon } from "lucide-react"; import { EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export const QuotesList = () => { export const QuotesList = () => {
@ -56,8 +56,17 @@ export const QuotesList = () => {
pressed={enabledPreview} pressed={enabledPreview}
onPressedChange={toggleEnabledPreview} onPressedChange={toggleEnabledPreview}
> >
<InfoIcon className='w-4 h-4 mr-2' /> {enabledPreview ? (
Quote preview <>
<EyeOffIcon className='w-4 h-4 mr-2' />
{t("common.disable_preview")}
</>
) : (
<>
<EyeIcon className='w-4 h-4 mr-2' />
{t("common.enable_preview")}
</>
)}
</Toggle> </Toggle>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import { useGetProfile, useIsLoggedIn } from "@/lib/hooks"; import { useGetProfile, useIsLoggedIn } from "@/lib/hooks";
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { LoadingOverlay } from "../LoadingOverlay"; import { LoadingOverlay } from "../LoadingOverlay";
@ -11,11 +11,17 @@ type ProctectRouteProps = {
export const ProtectedRoute = ({ children }: ProctectRouteProps) => { export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn(); const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
const { data: profile, ...profileStatus } = useGetProfile(); const { data: profile, ...profileStatus } = useGetProfile();
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const [langCode, setLangCode] = useState(i18n.language);
if (i18n.language !== langCode) {
i18n.changeLanguage(langCode);
}
useEffect(() => { useEffect(() => {
if (profileStatus.isSuccess && i18n.language !== profile?.lang_code) { if (profileStatus.isSuccess && profile && langCode !== profile.lang_code) {
i18n.changeLanguage(profile?.lang_code); setLangCode(profile.lang_code);
} }
}, [profile, profileStatus, i18n]); }, [profile, profileStatus, i18n]);
@ -29,17 +35,6 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
// along to that page after they login, which is a nicer user experience // along to that page after they login, which is a nicer user experience
// than dropping them off on the home page. // than dropping them off on the home page.
return <Navigate to={redirectTo ?? "/login"} state={{ from: location }} replace />; return <Navigate to={redirectTo ?? "/login"} state={{ from: location }} replace />;
console.log("No authenticated");
return (
<Navigate
to={redirectTo ?? "/login"}
replace
state={{
error: "No authentication, please complete the login process.",
}}
/>
);
} }
return <>{children ?? null}</>; return <>{children ?? null}</>;

View File

@ -109,4 +109,24 @@
#uecko { #uecko {
@apply flex min-h-screen min-w-[320px] flex-col; @apply flex min-h-screen min-w-[320px] flex-col;
} }
.ToastRoot[data-swipe="move"] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
.ToastRoot[data-swipe="cancel"] {
transform: translateX(0);
transition: transform 200ms ease-out;
}
.ToastRoot[data-swipe="end"] {
animation: slideRight 100ms ease-out;
}
@keyframes slideRight {
from {
transform: translateX(var(--radix-toast-swipe-end-x));
}
to {
transform: translateX(100%);
}
}
} }

View File

@ -53,7 +53,9 @@
"remove": "Remove", "remove": "Remove",
"archive": "Archive", "archive": "Archive",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"print": "Print" "print": "Print",
"disable_preview": "Disable preview",
"enable_preview": "Enable preview"
}, },
"components": { "components": {
"loading_indicator": { "loading_indicator": {
@ -116,8 +118,10 @@
"subtitle": "", "subtitle": "",
"tabs": { "tabs": {
"all": "All", "all": "All",
"emitted": "Emitted",
"draft": "Draft", "draft": "Draft",
"emitted": "Emitted",
"accepted": "Accepted",
"rejected": "Rejected",
"archived": "Archived" "archived": "Archived"
}, },
"columns": { "columns": {

View File

@ -53,7 +53,9 @@
"remove": "Eliminar", "remove": "Eliminar",
"archive": "Archivar", "archive": "Archivar",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"print": "Imprimir" "print": "Imprimir",
"disable_preview": "Ocultar vista previa",
"enable_preview": "Mostrar vista previa"
}, },
"components": { "components": {
"LoadingIndicator": { "LoadingIndicator": {
@ -116,8 +118,10 @@
"subtitle": "", "subtitle": "",
"tabs": { "tabs": {
"all": "Todas", "all": "Todas",
"emitted": "Emitidas",
"draft": "Borradores", "draft": "Borradores",
"emitted": "Emitidas",
"accepted": "Accepted",
"rejected": "Rejected",
"archived": "Archivadas" "archived": "Archivadas"
}, },
"columns": { "columns": {

View File

@ -1,11 +1,11 @@
import * as React from "react" import * as ToastPrimitives from "@radix-ui/react-toast";
import * as ToastPrimitives from "@radix-ui/react-toast" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import { X } from "lucide-react";
import { X } from "lucide-react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef< const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>, React.ElementRef<typeof ToastPrimitives.Viewport>,
@ -14,13 +14,13 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport <ToastPrimitives.Viewport
ref={ref} ref={ref}
className={cn( className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px] space-y-6",
className className
)} )}
{...props} {...props}
/> />
)) ));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
@ -36,12 +36,11 @@ const toastVariants = cva(
variant: "default", variant: "default",
}, },
} }
) );
const Toast = React.forwardRef< const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>, React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => { >(({ className, variant, ...props }, ref) => {
return ( return (
<ToastPrimitives.Root <ToastPrimitives.Root
@ -49,9 +48,9 @@ const Toast = React.forwardRef<
className={cn(toastVariants({ variant }), className)} className={cn(toastVariants({ variant }), className)}
{...props} {...props}
/> />
) );
}) });
Toast.displayName = ToastPrimitives.Root.displayName Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef< const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>, React.ElementRef<typeof ToastPrimitives.Action>,
@ -65,8 +64,8 @@ const ToastAction = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
ToastAction.displayName = ToastPrimitives.Action.displayName ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef< const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>, React.ElementRef<typeof ToastPrimitives.Close>,
@ -78,25 +77,21 @@ const ToastClose = React.forwardRef<
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className className
)} )}
toast-close="" toast-close=''
{...props} {...props}
> >
<X className="h-4 w-4" /> <X className='w-4 h-4' />
</ToastPrimitives.Close> </ToastPrimitives.Close>
)) ));
ToastClose.displayName = ToastPrimitives.Close.displayName ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef< const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>, React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Title <ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
ref={ref} ));
className={cn("text-sm font-semibold", className)} ToastTitle.displayName = ToastPrimitives.Title.displayName;
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef< const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>, React.ElementRef<typeof ToastPrimitives.Description>,
@ -107,21 +102,21 @@ const ToastDescription = React.forwardRef<
className={cn("text-sm opacity-90", className)} className={cn("text-sm opacity-90", className)}
{...props} {...props}
/> />
)) ));
ToastDescription.displayName = ToastPrimitives.Description.displayName ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction> type ToastActionElement = React.ReactElement<typeof ToastAction>;
export { export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast, Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction, ToastAction,
} ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
type ToastActionElement,
type ToastProps,
};

View File

@ -12,15 +12,13 @@ export function Toaster() {
const { toasts } = useToast(); const { toasts } = useToast();
return ( return (
<ToastProvider> <ToastProvider swipeDirection='right'>
{toasts.map(function ({ id, title, description, action, ...props }) { {toasts.map(function ({ id, title, description, action, ...props }) {
return ( return (
<Toast key={id} {...props}> <Toast key={id} {...props}>
<div className="grid gap-1"> <div className='grid gap-1'>
{title && <ToastTitle>{title}</ToastTitle>} {title && <ToastTitle>º{title}</ToastTitle>}
{description && ( {description && <ToastDescription>{description}</ToastDescription>}
<ToastDescription>{description}</ToastDescription>
)}
</div> </div>
{action} {action}
<ToastClose /> <ToastClose />

View File

@ -1,76 +1,73 @@
// Inspired by react-hot-toast library // Inspired by react-hot-toast library
import * as React from "react" import * as React from "react";
import type { import type { ToastActionElement, ToastProps } from "./toast";
ToastActionElement,
ToastProps,
} from "./toast"
const TOAST_LIMIT = 1 const TOAST_LIMIT = 5;
const TOAST_REMOVE_DELAY = 1000000 const TOAST_REMOVE_DELAY = 10000;
type ToasterToast = ToastProps & { type ToasterToast = ToastProps & {
id: string id: string;
title?: React.ReactNode title?: React.ReactNode;
description?: React.ReactNode description?: React.ReactNode;
action?: ToastActionElement action?: ToastActionElement;
} };
const actionTypes = { const actionTypes = {
ADD_TOAST: "ADD_TOAST", ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST", UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST", DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST", REMOVE_TOAST: "REMOVE_TOAST",
} as const } as const;
let count = 0 let count = 0;
function genId() { function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString() return count.toString();
} }
type ActionType = typeof actionTypes type ActionType = typeof actionTypes;
type Action = type Action =
| { | {
type: ActionType["ADD_TOAST"] type: ActionType["ADD_TOAST"];
toast: ToasterToast toast: ToasterToast;
} }
| { | {
type: ActionType["UPDATE_TOAST"] type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast> toast: Partial<ToasterToast>;
} }
| { | {
type: ActionType["DISMISS_TOAST"] type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"];
} }
| { | {
type: ActionType["REMOVE_TOAST"] type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"];
} };
interface State { interface State {
toasts: ToasterToast[] toasts: ToasterToast[];
} }
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>() const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => { const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) { if (toastTimeouts.has(toastId)) {
return return;
} }
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
toastTimeouts.delete(toastId) toastTimeouts.delete(toastId);
dispatch({ dispatch({
type: "REMOVE_TOAST", type: "REMOVE_TOAST",
toastId: toastId, toastId: toastId,
}) });
}, TOAST_REMOVE_DELAY) }, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout) toastTimeouts.set(toastId, timeout);
} };
export const reducer = (state: State, action: Action): State => { export const reducer = (state: State, action: Action): State => {
switch (action.type) { switch (action.type) {
@ -78,27 +75,25 @@ export const reducer = (state: State, action: Action): State => {
return { return {
...state, ...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
} };
case "UPDATE_TOAST": case "UPDATE_TOAST":
return { return {
...state, ...state,
toasts: state.toasts.map((t) => toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
t.id === action.toast.id ? { ...t, ...action.toast } : t };
),
}
case "DISMISS_TOAST": { case "DISMISS_TOAST": {
const { toastId } = action const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action, // ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity // but I'll keep it here for simplicity
if (toastId) { if (toastId) {
addToRemoveQueue(toastId) addToRemoveQueue(toastId);
} else { } else {
state.toasts.forEach((toast) => { state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id) addToRemoveQueue(toast.id);
}) });
} }
return { return {
@ -111,44 +106,44 @@ export const reducer = (state: State, action: Action): State => {
} }
: t : t
), ),
} };
} }
case "REMOVE_TOAST": case "REMOVE_TOAST":
if (action.toastId === undefined) { if (action.toastId === undefined) {
return { return {
...state, ...state,
toasts: [], toasts: [],
} };
} }
return { return {
...state, ...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId), toasts: state.toasts.filter((t) => t.id !== action.toastId),
} };
} }
} };
const listeners: Array<(state: State) => void> = [] const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] } let memoryState: State = { toasts: [] };
function dispatch(action: Action) { function dispatch(action: Action) {
memoryState = reducer(memoryState, action) memoryState = reducer(memoryState, action);
listeners.forEach((listener) => { listeners.forEach((listener) => {
listener(memoryState) listener(memoryState);
}) });
} }
type Toast = Omit<ToasterToast, "id"> type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) { function toast({ ...props }: Toast) {
const id = genId() const id = genId();
const update = (props: ToasterToast) => const update = (props: ToasterToast) =>
dispatch({ dispatch({
type: "UPDATE_TOAST", type: "UPDATE_TOAST",
toast: { ...props, id }, toast: { ...props, id },
}) });
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({ dispatch({
type: "ADD_TOAST", type: "ADD_TOAST",
@ -157,37 +152,36 @@ function toast({ ...props }: Toast) {
id, id,
open: true, open: true,
onOpenChange: (open) => { onOpenChange: (open) => {
if (!open) dismiss() if (!open) dismiss();
}, },
}, },
}) });
return { return {
id: id, id: id,
dismiss, dismiss,
update, update,
} };
} }
function useToast() { function useToast() {
const [state, setState] = React.useState<State>(memoryState) const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => { React.useEffect(() => {
listeners.push(setState) listeners.push(setState);
return () => { return () => {
const index = listeners.indexOf(setState) const index = listeners.indexOf(setState);
if (index > -1) { if (index > -1) {
listeners.splice(index, 1) listeners.splice(index, 1);
} }
} };
}, [state]) }, [state]);
return { return {
...state, ...state,
toast, toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
} };
} }
export { toast, useToast } export { toast, useToast };