feat(ui): changed to a better toast for ui and a global error handler for all server error messages

This commit is contained in:
Akhil Mohan
2024-03-26 19:37:13 +05:30
parent 1f939a5e58
commit 282434de8e
105 changed files with 355 additions and 382 deletions
@@ -1,66 +0,0 @@
import { useEffect, useRef } from "react";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type NotificationType = "success" | "error" | "info";
export type TNotification = {
text: string;
type?: NotificationType;
timeoutMs?: number;
};
interface NotificationProps {
notification: Required<TNotification>;
clearNotification: (text: string) => void;
}
const Notification = ({ notification, clearNotification }: NotificationProps) => {
const timeout = useRef<number>();
const handleClearNotification = () => clearNotification(notification.text);
const setNotifTimeout = () => {
timeout.current = window.setTimeout(handleClearNotification, notification.timeoutMs);
};
const cancelNotifTimeout = () => {
clearTimeout(timeout.current);
};
useEffect(() => {
setNotifTimeout();
return cancelNotifTimeout;
}, []);
return (
<div
className="pointer-events-auto relative right-3 mb-3 flex w-full items-center justify-between rounded-md border border-bunker-500 bg-mineshaft-700 px-6 py-4"
role="alert"
>
{notification.type === "error" && (
<div className="absolute top-0 left-0 h-1 w-full rounded-t-md bg-red" />
)}
{notification.type === "success" && (
<div className="absolute top-0 left-0 h-1 w-full rounded-t-md bg-green" />
)}
{notification.type === "info" && (
<div className="absolute top-0 left-0 h-1 w-full rounded-t-md bg-yellow" />
)}
<p className="text-md font-base mt-0.5 text-bunker-200">{notification.text}</p>
<button
type="button"
className="rounded-lg"
onClick={() => clearNotification(notification.text)}
>
<FontAwesomeIcon
className="absolute right-2 top-3 h-4 w-4 pl-2 text-bunker-300 hover:text-white"
icon={faXmark}
/>
</button>
</div>
);
};
export default Notification;
@@ -1,52 +0,0 @@
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from "react";
import { TNotification } from "./Notification";
import Notifications from "./Notifications";
type NotificationContextState = {
createNotification: (newNotification: TNotification) => void;
};
const NotificationContext = createContext<NotificationContextState>({
createNotification: () => console.log("createNotification not set!")
});
export const useNotificationContext = () => useContext(NotificationContext);
interface NotificationProviderProps {
children: ReactNode;
}
// TODO: Migration to radix toast
const NotificationProvider = ({ children }: NotificationProviderProps) => {
const [notifications, setNotifications] = useState<Required<TNotification>[]>([]);
const clearNotification = (text: string) =>
setNotifications((state) => state.filter((notif) => notif.text !== text));
const createNotification = useCallback(
({ text, type = "success", timeoutMs = 4000 }: TNotification) => {
const doesNotifExist = notifications.some((notif) => notif.text === text);
if (doesNotifExist) {
return;
}
const newNotification: Required<TNotification> = { text, type, timeoutMs };
setNotifications((state) => [...state, newNotification]);
},
[notifications]
);
const value = useMemo(() => ({ createNotification }), [createNotification]);
return (
<NotificationContext.Provider value={value}>
<Notifications notifications={notifications} clearNotification={clearNotification} />
{children}
</NotificationContext.Provider>
);
};
export default NotificationProvider;
@@ -1,22 +0,0 @@
import Notification, { TNotification } from "./Notification";
interface NoticationsProps {
notifications: Required<TNotification>[];
clearNotification: (text: string) => void;
}
const Notifications = ({ notifications, clearNotification }: NoticationsProps) => {
if (!notifications.length) {
return null;
}
return (
<div className="pointer-events-none fixed right-2 bottom-2 z-[100] hidden h-full w-96 gap-y-2 md:flex md:flex-col-reverse">
{notifications.map((notif) => (
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
))}
</div>
);
};
export default Notifications;
@@ -8,7 +8,7 @@ import { parseDocument, Scalar, YAMLMap } from "yaml";
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import { useNotificationContext } from "../context/Notifications/NotificationProvider";
import { createNotification } from "../notifications";
import { parseDotEnv } from "../utilities/parseDotEnv";
import guidGenerator from "../utilities/randomId";
@@ -33,7 +33,7 @@ const DropZone = ({
numCurrentRows
}: DropZoneProps) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
@@ -133,7 +133,6 @@ const DropZone = ({
createNotification({
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
type: "error",
timeoutMs: 10000
});
setLoading(false);
return;
@@ -0,0 +1,29 @@
import { ReactNode } from "react";
import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toastify";
export type TNotification = {
title?: string;
text: ReactNode;
};
export const NotificationContent = ({ title, text }: TNotification) => {
return (
<div className="msg-container">
{title && <div className="text-md mb-1 font-medium">{title}</div>}
<div className={title ? "text-sm" : "text-md"}>{text}</div>
</div>
);
};
export const createNotification = (
myProps: TNotification & { type?: TypeOptions },
toastProps: ToastOptions = {}
): Id =>
toast(<NotificationContent {...myProps} />, {
position: "bottom-right",
...toastProps,
theme: "dark",
type: myProps?.type || "info"
});
export const NotificationContainer = () => <ToastContainer hideProgressBar />;
@@ -0,0 +1 @@
export { createNotification, NotificationContainer } from "./Notifications";
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useToggle } from "@app/hooks";
import { generateUserBackupKey } from "@app/lib/crypto";
import { useNotificationContext } from "../context/Notifications/NotificationProvider";
import { createNotification } from "../notifications";
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
import { Button } from "../v2";
@@ -32,7 +32,7 @@ export default function DonwloadBackupPDFStep({
name
}: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const [isLoading, setIsLoading] = useToggle();
const handleBackupKeyGenerate = async () => {
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import Link from "next/link";
import axios from "axios";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { createNotification } from "@app/components/notifications";
import { useSendVerificationEmail } from "@app/hooks/api";
import { Button, Input } from "../v2";
@@ -27,7 +27,7 @@ export default function EnterEmailStep({
setEmail,
incrementStep
}: DownloadBackupPDFStepProps): JSX.Element {
const { createNotification } = useNotificationContext();
const { mutateAsync, isLoading } = useSendVerificationEmail();
const [emailError, setEmailError] = useState(false);
const { t } = useTranslation();
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
@@ -110,7 +110,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
} = useForm<FormData>({
resolver: zodResolver(createTagSchema)
});
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
@@ -5,7 +5,7 @@ import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { createNotification } from "@app/components/notifications";
import { useProjectPermission } from "@app/context";
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/types";
@@ -25,7 +25,7 @@ export const UpgradeProjectAlert = ({
project,
transparent
}: UpgradeProjectAlertProps): JSX.Element | null => {
const { createNotification } = useNotificationContext();
const router = useRouter();
const { membership } = useProjectPermission();
const upgradeProject = useUpgradeProject();