Skip to content

Commit

Permalink
feat: better error handling ux + toas fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
thomhickey committed Nov 17, 2024
1 parent 5fc0eba commit 9e70782
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 45 deletions.
6 changes: 5 additions & 1 deletion src/backend/routers/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { hasAuthenticated, hasAdmin, router } from "../trpc";
import { z } from "zod";
import { UserType, ROLE_OPTIONS } from "@/types/auth";
import { TRPCError } from "@trpc/server";

export const sortOrderSchema = z.enum(["asc", "desc"]).default("asc");
export const sortBySchema = z
Expand Down Expand Up @@ -130,7 +131,10 @@ export const user = router({
.executeTakeFirst();

if (existingUser) {
throw new Error("User with this email already exists");
throw new TRPCError({
code: "BAD_REQUEST",
message: "User with this email already exists",
});
}

const user = await req.ctx.db
Expand Down
52 changes: 23 additions & 29 deletions src/components/CustomToast.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
import React, { useState } from "react";
import React from "react";
import styles from "./styles/Toast.module.css";
import Image from "next/image";

interface CustomToastProps {
errorMessage: string;
onClose: () => void;
}

const CustomToast = ({ errorMessage }: CustomToastProps) => {
const [showToast, setShowToast] = useState(true);

const CustomToast = ({ errorMessage, onClose }: CustomToastProps) => {
const handleCloseToast = () => {
setShowToast(false);
onClose();
};

return (
<>
{showToast && (
<div className={styles.customToastWrapper}>
<div className={styles.customToast}>
<Image
src="/img/error.filled.svg"
alt="Error Img"
width={24}
height={24}
></Image>
<div>{errorMessage ?? null}</div>

<button className={styles.closeButton} onClick={handleCloseToast}>
<Image
src="/img/cross-outline.svg"
alt="Close Toast"
width={24}
height={24}
></Image>
</button>
</div>
</div>
)}
</>
<div className={styles.customToastWrapper}>
<div className={styles.customToast}>
<Image
src="/img/error.filled.svg"
alt="Error Img"
width={24}
height={24}
/>
<div>{errorMessage}</div>
<button className={styles.closeButton} onClick={handleCloseToast}>
<Image
src="/img/cross-outline.svg"
alt="Close Toast"
width={24}
height={24}
/>
</button>
</div>
</div>
);
};

Expand Down
1 change: 1 addition & 0 deletions src/components/styles/Toast.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
bottom: 0;
right: 0;
width: 400px;
z-index: 9999;
}

.customToast {
Expand Down
50 changes: 35 additions & 15 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
import { trpc } from "@/client/lib/trpc";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { httpBatchLink, loggerLink, TRPCClientError } from "@trpc/client";
import { useState } from "react";
import "../styles/globals.css";
import { QueryCache } from "@tanstack/react-query";
import { QueryCache, MutationCache } from "@tanstack/react-query";
import toast from "react-hot-toast";
import Head from "next/head";
import superjson from "superjson";
Expand All @@ -17,6 +17,7 @@ import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import { compassTheme as theme } from "@/theme";
import { FontProvider } from "@/components/font-provider";
import { AppRouter } from "@/backend/routers/_app";

interface CustomPageProps {
session: Session;
Expand All @@ -42,20 +43,21 @@ export default function App({
() =>
new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (error instanceof Error) {
const errorMessages: { [key: string]: string } = {
BAD_REQUEST: "400: Bad request, please try again",
UNAUTHORIZED: "401: Unauthorized Error",
NOT_FOUND: "404: Page not found",
};

const defaultMessage = "An error occured. Please try again";
const msg = errorMessages[error.message] || defaultMessage;
setErrorMessage(msg);
}
onError: (error: unknown) => {
handleTRPCError(error);
},
}),
mutationCache: new MutationCache({
onError: (error: unknown) => {
handleTRPCError(error);
},
}),
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
})
);

Expand All @@ -77,6 +79,21 @@ export default function App({
})
);

const handleTRPCError = (error: unknown) => {
const trpcError = error as TRPCClientError<AppRouter>;
const errorCode = trpcError.data?.code as keyof typeof errorMessages;
const errorMessage = trpcError.message;

const errorMessages = {
BAD_REQUEST: errorMessage || "400: Bad request, please try again",
UNAUTHORIZED: "401: Unauthorized Error",
NOT_FOUND: "404: Page not found",
FORBIDDEN: "403: Access denied",
} as const;

setErrorMessage(errorMessages[errorCode] ?? "An unexpected error occurred");
};

return (
<>
<Head>
Expand All @@ -92,7 +109,10 @@ export default function App({
<QueryClientProvider client={queryClient}>
<SessionProvider session={pageProps.session}>
{errorMessage && (
<CustomToast errorMessage={errorMessage} />
<CustomToast
errorMessage={errorMessage}
onClose={() => setErrorMessage("")}
/>
)}
<Layout>
<Component {...pageProps} showErrorToast={toast.error} />
Expand Down

0 comments on commit 9e70782

Please sign in to comment.