diff --git a/client/src/Platform.jsx b/client/src/Platform.jsx index e0a7760c..dfd108c6 100644 --- a/client/src/Platform.jsx +++ b/client/src/Platform.jsx @@ -15,6 +15,7 @@ import ErrorScreen from "./screens/ErrorScreen"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import ModalsProvider from "./providers/ModalsProvider"; +import NotificationsProvider from "./providers/NotificationsProvider"; /** * Exposes the applications and global configuration. @@ -79,6 +80,7 @@ const Platform = () => {
+ {/* Commons Render Tree */} @@ -87,6 +89,7 @@ const Platform = () => { {/* Conductor and fallback Render Tree */} +
diff --git a/client/src/components/util/CopyButton.tsx b/client/src/components/util/CopyButton.tsx new file mode 100644 index 00000000..f394f277 --- /dev/null +++ b/client/src/components/util/CopyButton.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +interface CopyButtonProps { + children: (payload: { + copied: boolean; + copy: (val?: string) => Promise; + }) => React.ReactNode; + val?: string; + timeout?: number; +} + +const CopyButton: React.FC = ({ + children, + val: parentVal, + timeout = 1000, +}) => { + const [copied, setCopied] = useState(false); + + const copy = async (_val?: string) => { + try { + if (!_val && !parentVal) return; + const valToCopy = _val ? _val : (parentVal as string); + + await navigator.clipboard.writeText(valToCopy); + setCopied(true); + setTimeout(() => setCopied(false), timeout); + } catch (e) { + console.error(e); + setCopied(false); + } + }; + + return <>{children({ copied, copy })}; +}; + +export default CopyButton; diff --git a/client/src/context/NotificationContext.ts b/client/src/context/NotificationContext.ts new file mode 100644 index 00000000..eec09540 --- /dev/null +++ b/client/src/context/NotificationContext.ts @@ -0,0 +1,28 @@ +import { createContext, useContext } from "react"; + +export interface Notification { + message: string; + type: "error" | "success" | "info"; + duration?: number; // in milliseconds +} + +export interface NotificationContextType { + notifications: Notification[]; + addNotification: (notification: Notification) => void; + removeNotification: (id: string) => void; + removeAllNotifications: () => void; +} + +export const NotificationContext = createContext< + NotificationContextType | undefined +>(undefined); + +export const useNotifications = () => { + const context = useContext(NotificationContext); + if (!context) { + throw new Error( + "useNotifications must be used within a NotificationProvider" + ); + } + return context; +}; diff --git a/client/src/providers/NotificationsProvider.tsx b/client/src/providers/NotificationsProvider.tsx new file mode 100644 index 00000000..37f5ad28 --- /dev/null +++ b/client/src/providers/NotificationsProvider.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { + NotificationContext, + Notification, +} from "../context/NotificationContext"; +import { Icon } from "semantic-ui-react"; + +const NotificationsProvider = ({ children }: { children: React.ReactNode }) => { + const [notifications, setNotifications] = useState< + (Notification & { id: string })[] + >([]); + + const addNotification = (newNotif: Notification) => { + const { message, type, duration = 5000 } = newNotif; + const id = crypto.randomUUID(); + setNotifications((prev) => [...prev, { id, message, type, duration }]); + + if (duration > 0) { + setTimeout(() => { + removeNotification(id); + }, duration); + } + }; + + const removeNotification = (id: string) => { + setNotifications(notifications.filter((n) => n.id !== id)); + }; + + const removeAllNotifications = () => { + setNotifications([]); + }; + + const getIcon = (type: string) => { + switch (type) { + case "error": + return "exclamation circle"; + case "success": + return "check circle"; + case "info": + return "info circle"; + default: + return "info circle"; + } + }; + + const getColor = (type: string) => { + switch (type) { + case "error": + return "red"; + case "success": + return "green"; + case "info": + return "blue"; + default: + return "blue"; + } + }; + + return ( + + {children} +
+ {notifications.map((notification) => ( +
removeNotification(notification.id)} + > +
+
+ +
+
+

{notification.message}

+
+
+
+ ))} +
+
+ ); +}; + +export default NotificationsProvider;