Skip to content

Commit

Permalink
feat: notifications provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Jul 30, 2024
1 parent a2f57da commit 3891a51
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 0 deletions.
3 changes: 3 additions & 0 deletions client/src/Platform.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -79,6 +80,7 @@ const Platform = () => {
<ErrorBoundary FallbackComponent={ErrorScreen}>
<div className="App">
<ModalsProvider>
<NotificationsProvider>
<Switch>
{/* Commons Render Tree */}
<Route exact path={commonsPaths} component={Commons} />
Expand All @@ -87,6 +89,7 @@ const Platform = () => {
{/* Conductor and fallback Render Tree */}
<Route component={Conductor} />
</Switch>
</NotificationsProvider>
</ModalsProvider>
<ErrorModal />
</div>
Expand Down
36 changes: 36 additions & 0 deletions client/src/components/util/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";

interface CopyButtonProps {
children: (payload: {
copied: boolean;
copy: (val?: string) => Promise<void>;
}) => React.ReactNode;
val?: string;
timeout?: number;
}

const CopyButton: React.FC<CopyButtonProps> = ({
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;
28 changes: 28 additions & 0 deletions client/src/context/NotificationContext.ts
Original file line number Diff line number Diff line change
@@ -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;
};
95 changes: 95 additions & 0 deletions client/src/providers/NotificationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<NotificationContext.Provider
value={{
notifications,
addNotification,
removeNotification,
removeAllNotifications,
}}
>
{children}
<div className="!fixed bottom-16 right-0 p-4">
{notifications.map((notification) => (
<div
key={notification.id}
className={`mb-4 p-4 rounded shadow-md border border-slate-300 bg-white max-w-96`}
onClick={() => removeNotification(notification.id)}
>
<div className="flex flex-row justify-between items-center">
<div className="mr-2">
<Icon
name={getIcon(notification.type)}
size={"large"}
color={getColor(notification.type)}
/>
</div>
<div>
<p className="text-lg">{notification.message}</p>
</div>
</div>
</div>
))}
</div>
</NotificationContext.Provider>
);
};

export default NotificationsProvider;

0 comments on commit 3891a51

Please sign in to comment.