feat: add popup error messages
This commit is contained in:
76
src/components/Popup.tsx
Normal file
76
src/components/Popup.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { HiOutlineExclamationCircle } from "react-icons/hi2";
|
||||||
|
import { HiXMark } from "react-icons/hi2";
|
||||||
|
import { Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
import { usePopup } from "~/providers/popup";
|
||||||
|
|
||||||
|
const Popup: React.FC = () => {
|
||||||
|
const { isOpen, popupHeader, popupMessage, hidePopup } = usePopup();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
hidePopup();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen, hidePopup]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-live="assertive"
|
||||||
|
className="pointer-events-none fixed inset-0 z-10 flex items-end px-4 py-6 sm:items-end sm:p-6"
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
show={isOpen}
|
||||||
|
className="flex w-full flex-col items-center space-y-4 sm:items-end"
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg border border-light-400 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 transition data-[closed]:data-[enter]:translate-y-2 data-[enter]:transform data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-100 data-[enter]:ease-out data-[leave]:ease-in dark:border-dark-300 dark:bg-dark-200 data-[closed]:data-[enter]:sm:translate-x-2 data-[closed]:data-[enter]:sm:translate-y-0">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<HiOutlineExclamationCircle
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-red-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||||
|
<p className="text-sm font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
|
{popupHeader}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
|
{popupMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
hidePopup();
|
||||||
|
}}
|
||||||
|
className="dark:hover:bg-dark-4000 mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-light-100"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<HiXMark
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-5 w-5 text-dark-900"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Popup;
|
||||||
@@ -5,6 +5,7 @@ import { Plus_Jakarta_Sans } from "next/font/google";
|
|||||||
|
|
||||||
import { ModalProvider } from "~/providers/modal";
|
import { ModalProvider } from "~/providers/modal";
|
||||||
import { BoardProvider } from "~/providers/board";
|
import { BoardProvider } from "~/providers/board";
|
||||||
|
import { PopupProvider } from "~/providers/popup";
|
||||||
import { ThemeProvider } from "~/providers/theme";
|
import { ThemeProvider } from "~/providers/theme";
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
@@ -33,9 +34,11 @@ const MyApp: AppType = ({ Component, pageProps }) => {
|
|||||||
<main className="font-sans">
|
<main className="font-sans">
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<BoardProvider>
|
<PopupProvider>
|
||||||
<Component {...pageProps} />
|
<BoardProvider>
|
||||||
</BoardProvider>
|
<Component {...pageProps} />
|
||||||
|
</BoardProvider>
|
||||||
|
</PopupProvider>
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WorkspaceProvider } from "~/providers/workspace";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import Dashboard from "~/components/dashboard";
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import Popup from "~/components/Popup";
|
||||||
import BoardView from "~/views/board";
|
import BoardView from "~/views/board";
|
||||||
|
|
||||||
export default function BoardPage() {
|
export default function BoardPage() {
|
||||||
@@ -8,6 +9,7 @@ export default function BoardPage() {
|
|||||||
<Dashboard>
|
<Dashboard>
|
||||||
<BoardView />
|
<BoardView />
|
||||||
</Dashboard>
|
</Dashboard>
|
||||||
|
<Popup />
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WorkspaceProvider } from "~/providers/workspace";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import Dashboard from "~/components/dashboard";
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import Popup from "~/components/Popup";
|
||||||
import BoardsView from "~/views/boards";
|
import BoardsView from "~/views/boards";
|
||||||
|
|
||||||
export default function BoardsPage() {
|
export default function BoardsPage() {
|
||||||
@@ -8,6 +9,7 @@ export default function BoardsPage() {
|
|||||||
<Dashboard>
|
<Dashboard>
|
||||||
<BoardsView />
|
<BoardsView />
|
||||||
</Dashboard>
|
</Dashboard>
|
||||||
|
<Popup />
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import { usePopup } from "~/providers/popup";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type GetBoardByIdOutput,
|
type GetBoardByIdOutput,
|
||||||
@@ -40,6 +41,8 @@ export const BoardProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const [boardData, setBoardData] =
|
const [boardData, setBoardData] =
|
||||||
useState<GetBoardByIdOutput>(initialBoardData);
|
useState<GetBoardByIdOutput>(initialBoardData);
|
||||||
|
|
||||||
|
const { showPopup } = usePopup();
|
||||||
|
|
||||||
const refetchBoard = async () => {
|
const refetchBoard = async () => {
|
||||||
if (boardData?.publicId) {
|
if (boardData?.publicId) {
|
||||||
try {
|
try {
|
||||||
@@ -52,10 +55,24 @@ export const BoardProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
|
|
||||||
const updateCardMutation = api.card.reorder.useMutation({
|
const updateCardMutation = api.card.reorder.useMutation({
|
||||||
onSuccess: () => refetchBoard(),
|
onSuccess: () => refetchBoard(),
|
||||||
|
onError: () => {
|
||||||
|
refetchBoard();
|
||||||
|
showPopup({
|
||||||
|
header: "Unable to update card",
|
||||||
|
message: "Please try again later, or contact customer support.",
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateListMutation = api.list.reorder.useMutation({
|
const updateListMutation = api.list.reorder.useMutation({
|
||||||
onSuccess: () => refetchBoard(),
|
onSuccess: () => refetchBoard(),
|
||||||
|
onError: () => {
|
||||||
|
refetchBoard();
|
||||||
|
showPopup({
|
||||||
|
header: "Unable to update list",
|
||||||
|
message: "Please try again later, or contact customer support.",
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateList = ({
|
const updateList = ({
|
||||||
|
|||||||
53
src/providers/popup.tsx
Normal file
53
src/providers/popup.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
type PopupContextType = {
|
||||||
|
isOpen: boolean;
|
||||||
|
showPopup: (params: { header: string; message: string }) => void;
|
||||||
|
hidePopup: () => void;
|
||||||
|
popupHeader: string;
|
||||||
|
popupMessage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContext = createContext<PopupContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const PopupProvider: React.FC<Props> = ({ children }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [popupHeader, setPopupHeader] = useState("");
|
||||||
|
const [popupMessage, setPopupMessage] = useState("");
|
||||||
|
|
||||||
|
const showPopup = ({
|
||||||
|
header,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
header: string;
|
||||||
|
message: string;
|
||||||
|
}) => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setPopupHeader(header);
|
||||||
|
setPopupMessage(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hidePopup = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopupContext.Provider
|
||||||
|
value={{ isOpen, showPopup, hidePopup, popupHeader, popupMessage }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PopupContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePopup = () => {
|
||||||
|
const context = useContext(PopupContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("usePopup must be used within a PopupProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user