Skip to content

Commit

Permalink
feat(frontend): use react-hot-toast (#429)
Browse files Browse the repository at this point in the history
  • Loading branch information
zax-xyz authored May 14, 2023
1 parent dd6fbc4 commit e0f1f53
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 168 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "14.2.3",
"react-hot-toast": "2.4.1",
"react-router-dom": "6.4.3",
"typescript": "5.0.4",
"web-vitals": "3.1.0"
Expand Down
62 changes: 20 additions & 42 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Box, CssBaseline, ThemeProvider, createTheme } from "@mui/material";
import { SnackbarProvider } from "notistack";
import { Suspense, useCallback, useRef, useState } from "react";
import { Suspense, useState } from "react";
import { Toaster } from "react-hot-toast";
import { BrowserRouter, Routes } from "react-router-dom";
import "twin.macro";

import { MessagePopupContext } from "contexts/MessagePopupContext";

import { LoadingIndicator, MessagePopup, NavBar } from "./components";
import { LoadingIndicator, NavBar } from "./components";
import { SetNavBarTitleContext } from "./contexts/SetNavbarTitleContext";
import routes from "./routes";

import type { Message } from "contexts/MessagePopupContext";

const theme = createTheme({
palette: {
primary: {
Expand All @@ -37,47 +34,28 @@ const theme = createTheme({

const App = () => {
const [AppBarTitle, setNavBarTitle] = useState("");
const [messagePopup, setMessagePopup] = useState<Message[]>([]);
const nextMessageId = useRef(1);

const pushMessage = useCallback(
(message: Omit<Message, "id">) => {
setMessagePopup([
...messagePopup,
// eslint-disable-next-line no-plusplus
{ ...message, id: nextMessageId.current++ },
]);

setTimeout(() => {
setMessagePopup(messagePopup.slice(1));
}, 5000);
},
[setMessagePopup, messagePopup]
);

return (
<ThemeProvider theme={theme}>
<CssBaseline />
<SnackbarProvider maxSnack={3}>
<MessagePopupContext.Provider value={pushMessage}>
<SetNavBarTitleContext.Provider value={setNavBarTitle}>
<BrowserRouter>
<NavBar campaign={AppBarTitle} />
<Box pt={8} minHeight="100vh" display="flex" tw="bg-gray-50">
<Suspense fallback={<LoadingIndicator />}>
<Routes>{routes}</Routes>
</Suspense>
</Box>
<div tw="fixed right-4 bottom-4 space-y-3">
{messagePopup.map((message) => (
<MessagePopup key={message.id} type={message.type}>
{message.message}
</MessagePopup>
))}
</div>
</BrowserRouter>
</SetNavBarTitleContext.Provider>
</MessagePopupContext.Provider>
<SetNavBarTitleContext.Provider value={setNavBarTitle}>
<BrowserRouter>
<NavBar campaign={AppBarTitle} />
<Box pt={8} minHeight="100vh" display="flex" tw="bg-gray-50">
<Suspense fallback={<LoadingIndicator />}>
<Routes>{routes}</Routes>
</Suspense>
</Box>
<Toaster
position="bottom-right"
reverseOrder={false}
toastOptions={{
duration: 5000,
}}
/>
</BrowserRouter>
</SetNavBarTitleContext.Provider>
</SnackbarProvider>
</ThemeProvider>
);
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions -- needed for target other components in stitches styles */
import { Fragment } from "react";
import toast from "react-hot-toast";
import tw, { styled } from "twin.macro";

import Transition from "./Transition";

import type { VariantProps } from "@stitches/react";
import type { Toast as ToastObject } from "react-hot-toast";

const ButtonContainer = styled("div", tw`border-l border-gray-200`);

const ToastContainer = styled("div", {
...tw`flex w-full max-w-md bg-white rounded border shadow`,

variants: {
type: {
notification: {
[`&, & ${ButtonContainer}`]: tw`border-gray-200`,
},
success: {
[`&, & ${ButtonContainer}`]: tw`border-green-200`,
"& h1": tw`text-green-600`,
},
error: {
[`&, & ${ButtonContainer}`]: tw`border-red-200`,
"& h1": tw`text-red-600`,
},
},
},
});

export type ToastType = Extract<
VariantProps<typeof ToastContainer>["type"],
string
>;
type Props = {
t: ToastObject;
title: string;
description: string;
type?: ToastType;
};
const Toast = ({ t, title, description, type = "notification" }: Props) => (
<Transition
as={Fragment}
show={t.visible}
appear
enter={tw`duration-200 ease-out`}
enterFrom={tw`opacity-0 scale-95`}
leave={tw`duration-150 ease-in`}
leaveTo={tw`opacity-0 scale-95`}
>
<ToastContainer type={type}>
<div tw="flex-1 flex flex-col px-4 py-3">
<h1 tw="font-semibold">{title}</h1>
<p tw="text-sm">{description}</p>
</div>
<ButtonContainer>
<button
type="button"
tw="
w-12 h-full py-3 px-8
flex items-center justify-center
text-blue-600 text-sm font-medium rounded-r
hover:(text-blue-700 bg-slate-50)
focus-visible:(outline-none ring-2 ring-blue-600)
"
onClick={() => toast.dismiss(t.id)}
>
Close
</button>
</ButtonContainer>
</ToastContainer>
</Transition>
);

export default Toast;
12 changes: 3 additions & 9 deletions frontend/src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";

import { MessagePopupContext } from "contexts/MessagePopupContext";
import { getStore } from "utils";
import { getStore, pushToast } from "utils";

import type { Json } from "types/api";

Expand Down Expand Up @@ -61,8 +60,6 @@ const useFetch = <T = void>(url: string, options?: Options<T>) => {
const [abortBehaviour] = useState(options?.abortBehaviour ?? "all");
const controllers = useRef<Controllers>({});

const pushMessage = useContext(MessagePopupContext);

const refetch = useCallback(() => {
setRetry({});
}, []);
Expand Down Expand Up @@ -141,10 +138,7 @@ const useFetch = <T = void>(url: string, options?: Options<T>) => {
if (options?.errorSummary) {
message = `${options.errorSummary}: ${message}`;
}
pushMessage({
type: "error",
message,
});
pushToast("Error in fetch", message, "error");
}
} finally {
setLoading(false);
Expand Down
52 changes: 24 additions & 28 deletions frontend/src/pages/admin/AdminContent/AdminCampaignContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DeleteForeverRounded } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import { Divider, IconButton, ListItemIcon, ListItemText } from "@mui/material";
import { useContext, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import "twin.macro";

Expand All @@ -11,8 +11,7 @@ import { FetchError } from "api/api";
import { Modal } from "components";
import Button from "components/Button";
import Dropzone from "components/Dropzone";
import { MessagePopupContext } from "contexts/MessagePopupContext";
import { dateToDateString } from "utils";
import { dateToDateString, pushToast } from "utils";

import {
AdminContentList,
Expand Down Expand Up @@ -47,7 +46,6 @@ const AdminCampaignContent = ({ campaigns, setCampaigns, orgId }: Props) => {
startDate: "",
endDate: "",
});
const pushMessage = useContext(MessagePopupContext);

useEffect(() => {
if (coverImage === undefined) {
Expand Down Expand Up @@ -81,10 +79,7 @@ const AdminCampaignContent = ({ campaigns, setCampaigns, orgId }: Props) => {
message += "unknown error";
}

pushMessage({
type: "error",
message,
});
pushToast("Delete Campaign", message, "error");

throw e;
}
Expand All @@ -94,10 +89,7 @@ const AdminCampaignContent = ({ campaigns, setCampaigns, orgId }: Props) => {

const uploadCoverImage = async () => {
if (coverImage === undefined) {
pushMessage({
message: "No organisation logo given.",
type: "error",
});
pushToast("Update Campaign Cover Image", "No image given", "error");
return;
}

Expand All @@ -112,25 +104,28 @@ const AdminCampaignContent = ({ campaigns, setCampaigns, orgId }: Props) => {
try {
const data = (await err.resp.json()) as string;

pushMessage({
message: `Internal Error: ${data}`,
type: "error",
});
pushToast(
"Update Campaign Cover Image",
`Internal Error: ${data}`,
"error"
);
} catch {
pushMessage({
message: `Internal Error: Response Invalid`,
type: "error",
});
pushToast(
"Update Campaign Cover Image",
"Internal Error: Response Invalid",
"error"
);
}

return;
}

console.error("Something went wrong");
pushMessage({
message: "Something went wrong on backend!",
type: "error",
});
pushToast(
"Update Campaign Cover Image",
"Something went wrong on the backend!",
"error"
);

return;
}
Expand All @@ -141,10 +136,11 @@ const AdminCampaignContent = ({ campaigns, setCampaigns, orgId }: Props) => {
].image = newCoverImage;
setCampaigns(newCampaigns);

pushMessage({
message: "Updated campaign cover image",
type: "success",
});
pushToast(
"Update Campaign Cover Image",
"Uploaded image succesfully",
"success"
);
};

return (
Expand Down
Loading

0 comments on commit e0f1f53

Please sign in to comment.