diff --git a/package-lock.json b/package-lock.json index 106518926f4..8c959e086d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,6 @@ "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.2.0", "@hello-pangea/dnd": "^17.0.0", - "@pnotify/core": "^5.2.0", - "@pnotify/mobile": "^5.2.0", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", @@ -49,6 +47,7 @@ "i18next": "^23.16.4", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^3.0.1", + "next-themes": "^0.4.4", "postcss-loader": "^8.1.1", "qrcode.react": "^4.1.0", "raviger": "^4.1.2", @@ -60,6 +59,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-pdf": "^9.1.1", "react-webcam": "^7.2.0", + "sonner": "^1.7.1", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "use-keyboard-shortcut": "^1.1.6", @@ -3276,21 +3276,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@pnotify/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@pnotify/core/-/core-5.2.0.tgz", - "integrity": "sha512-d9ZM7Q6ZxuwTJ14QbOa7pWmoUqObPis7S1K+TzISffrL8w2f7JigEPI281agLShVAMIsE5SsC6O9YhkutwekbA==", - "license": "Apache-2.0" - }, - "node_modules/@pnotify/mobile": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@pnotify/mobile/-/mobile-5.2.0.tgz", - "integrity": "sha512-uu0V7h41Y8UTQA6ZB26PcHU7wHu5nwP/FEmMyimHEpEqhrvkzIRD2CxB7zu9SYp3Jk2NFcJC3NiueO3Kh25CVA==", - "license": "Apache-2.0", - "dependencies": { - "@pnotify/core": "^5.2.0" - } - }, "node_modules/@poppinss/cliui": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/@poppinss/cliui/-/cliui-6.4.1.tgz", @@ -14464,6 +14449,16 @@ "optional": true, "peer": true }, + "node_modules/next-themes": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz", + "integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -17166,6 +17161,16 @@ "node": ">=12" } }, + "node_modules/sonner": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.1.tgz", + "integrity": "sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 7bc7e5c117b..dadc529207a 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,6 @@ "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.2.0", "@hello-pangea/dnd": "^17.0.0", - "@pnotify/core": "^5.2.0", - "@pnotify/mobile": "^5.2.0", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", @@ -88,6 +86,7 @@ "i18next": "^23.16.4", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^3.0.1", + "next-themes": "^0.4.4", "postcss-loader": "^8.1.1", "qrcode.react": "^4.1.0", "raviger": "^4.1.2", @@ -99,6 +98,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-pdf": "^9.1.1", "react-webcam": "^7.2.0", + "sonner": "^1.7.1", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "use-keyboard-shortcut": "^1.1.6", diff --git a/src/App.tsx b/src/App.tsx index c91849de559..a0fa99c8bef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Suspense } from "react"; -import { Toaster } from "@/components/ui/toaster"; +import { Toaster } from "@/components/ui/sonner"; import Loading from "@/components/Common/Loading"; diff --git a/src/CAREUI/icons/Index.tsx b/src/CAREUI/icons/Index.tsx index c7f553512aa..7ebd4574b71 100644 --- a/src/CAREUI/icons/Index.tsx +++ b/src/CAREUI/icons/Index.tsx @@ -1,16 +1,14 @@ /* eslint-disable i18next/no-literal-string */ import { t } from "i18next"; import React, { useState } from "react"; +import { toast } from "sonner"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; import iconPaths from "@/CAREUI/icons/UniconPaths.json"; import PageTitle from "@/components/Common/PageTitle"; -import { useToast } from "@/hooks/useToast"; - const IconIndex: React.FC = () => { - const { toast } = useToast(); const [searchTerm, setSearchTerm] = useState(""); const filteredIcons = Object.keys(iconPaths).filter((iconName) => @@ -20,10 +18,7 @@ const IconIndex: React.FC = () => { const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); - toast({ - description: "Icon copied to clipboard successfully", - variant: "success", - }); + toast.success("Icon copied to clipboard successfully"); }; return ( diff --git a/src/Utils/Notifications.js b/src/Utils/Notifications.js index 08bc90443b5..26fd54f7ea0 100644 --- a/src/Utils/Notifications.js +++ b/src/Utils/Notifications.js @@ -1,38 +1,4 @@ -import { Stack, alert, defaultModules } from "@pnotify/core"; -import * as PNotifyMobile from "@pnotify/mobile"; - -defaultModules.set(PNotifyMobile, {}); - -const notifyStack = new Stack({ - dir1: "down", - dir2: "left", - firstpos1: 25, - firstpos2: 25, - modal: false, - maxOpen: 3, - maxStrategy: "close", - maxClosureCausesWait: false, - push: "top", -}); - -const notify = (text, type) => { - const notification = alert({ - type: type, - text: text, - styling: "brighttheme", - mode: "light", - sticker: false, - buttons: { - closer: false, - sticker: false, - }, - stack: notifyStack, - delay: 3000, - }); - notification.refs.elem.addEventListener("click", () => { - notification.close(); - }); -}; +import { toast } from "sonner"; /** * Formats input string to a more human readable format @@ -74,27 +40,29 @@ const notifyError = (error) => { errorMsg += "\n"; } } - notify(errorMsg, "error"); + toast.error(errorMsg, { duration: 5000 }); }; -/** Close all Notifications **/ +/** Close all Notifications (Sonner doesn't need this but can be kept for custom implementations) **/ export const closeAllNotifications = () => { - notifyStack.close(); + // Sonner doesn't require a close method, but you can manage this with custom logic if needed + // Example: toast.dismiss() could be used to close all toasts if necessary. + toast.dismiss(); }; /** Success message handler */ export const Success = ({ msg }) => { - notify(msg, "success"); + toast.success(msg, { duration: 3000 }); }; /** Error message handler */ export const Error = ({ msg }) => { - notify(msg, "error"); + toast.error(msg, { duration: 5000 }); }; /** Warning message handler */ export const Warn = ({ msg }) => { - notify(msg, "notice"); + toast.info(msg, { duration: 3000 }); }; /** 400 Bad Request handler */ diff --git a/src/components/Patient/DailyRounds.tsx b/src/components/Patient/DailyRounds.tsx index f0c78522e40..496253bde1e 100644 --- a/src/components/Patient/DailyRounds.tsx +++ b/src/components/Patient/DailyRounds.tsx @@ -1,4 +1,3 @@ -import { error } from "@pnotify/core"; import dayjs from "dayjs"; import { navigate } from "raviger"; import { useCallback, useEffect, useState } from "react"; @@ -339,7 +338,9 @@ export const DailyRounds = (props: any) => { ); if (investigationError) { - Notification.Error({ msg: error }); + Notification.Error({ + msg: investigationError.message || investigationError, + }); return; } } diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 00000000000..a7b1b346b84 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,30 @@ +import { useTheme } from "next-themes"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx deleted file mode 100644 index dbf76c76a37..00000000000 --- a/src/components/ui/toast.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Cross2Icon } from "@radix-ui/react-icons"; -import * as ToastPrimitives from "@radix-ui/react-toast"; -import { type VariantProps, cva } from "class-variance-authority"; -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-gray-200 p-4 pr-6 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 dark:border-gray-800", - { - variants: { - variant: { - default: - "border bg-white text-gray-950 dark:bg-gray-950 dark:text-gray-50", - destructive: - "destructive group border-red-500 bg-red-500 text-gray-50 dark:border-red-900 dark:bg-red-900 dark:text-gray-50", - success: - "border-green-500 bg-green-500 text-gray-50 dark:border-green-900 dark:bg-green-900 dark:text-gray-50", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ); -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef; - -type ToastActionElement = React.ReactElement; - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -}; diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx deleted file mode 100644 index e3582e7ff0b..00000000000 --- a/src/components/ui/toaster.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast"; - -import { useToast } from "@/hooks/useToast"; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ); - })} - -
- ); -} diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts deleted file mode 100644 index 01f0976bade..00000000000 --- a/src/hooks/useToast.ts +++ /dev/null @@ -1,188 +0,0 @@ -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/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/style/index.css b/src/style/index.css index e80d1b2733f..0b26ba5c43b 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -6,9 +6,6 @@ @import "@fontsource/figtree/800.css"; @import "@fontsource/figtree/900.css"; -@import "@pnotify/core/dist/PNotify.css"; -@import "@pnotify/mobile/dist/PNotifyMobile.css"; -@import "@pnotify/core/dist/BrightTheme.css"; @import "./CAREUI.css"; @tailwind base;