Skip to content

Commit 9bad8bc

Browse files
committed
fix(web): improve error handling and ui
closed COD-308
1 parent 4f84bc6 commit 9bad8bc

9 files changed

Lines changed: 123 additions & 58 deletions

File tree

apps/web/app/components/desktop-navigation.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function DesktopNavigation(
4040
props: React.ComponentPropsWithoutRef<'nav'>
4141
) {
4242
const navigationTree = useNavigationTree();
43+
4344
if (navigationTree.length === 0) {
4445
return null;
4546
}

apps/web/app/components/error-boundary.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Separator } from '@codeware/shared/ui/shadcn';
12
import {
23
type ErrorResponse,
34
isRouteErrorResponse,
@@ -10,6 +11,8 @@ import * as React from 'react';
1011
// import { captureRemixErrorBoundaryError } from '@sentry/remix'
1112
import { getErrorMessage } from '../utils/misc';
1213

14+
import { ErrorContainer } from './error-container';
15+
1316
type StatusHandler = (info: {
1417
error: ErrorResponse;
1518
params: Record<string, string | undefined>;
@@ -37,13 +40,15 @@ export function GeneralErrorBoundary({
3740
}
3841

3942
return (
40-
<div className="text-h2 container flex items-center justify-center p-20">
43+
<ErrorContainer severity="error">
44+
<p>Please contact the administrator if the problem persists.</p>
45+
<Separator className="my-4" />
4146
{isRouteErrorResponse(error)
4247
? (statusHandlers?.[error.status] ?? defaultStatusHandler)({
4348
error,
4449
params
4550
})
4651
: unexpectedErrorHandler(error)}
47-
</div>
52+
</ErrorContainer>
4853
);
4954
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
Alert,
3+
AlertDescription,
4+
AlertTitle
5+
} from '@codeware/shared/ui/shadcn';
6+
import { InfoIcon, LucideAlertTriangle } from 'lucide-react';
7+
8+
import { Container } from './container';
9+
10+
type Props = {
11+
children?: React.ReactNode;
12+
title?: string;
13+
severity: 'error' | 'info';
14+
stackTrace?: string;
15+
withoutContainer?: boolean;
16+
};
17+
18+
export const ErrorContainer: React.FC<Props> = ({
19+
withoutContainer = false,
20+
...props
21+
}: Props) => {
22+
return (
23+
<>
24+
{(withoutContainer && <RenderAlert {...props} />) || (
25+
<Container className="mt-16 sm:mt-32">
26+
<RenderAlert {...props} />
27+
</Container>
28+
)}
29+
</>
30+
);
31+
};
32+
33+
const RenderAlert: React.FC<Props> = ({
34+
children,
35+
title = 'Something went wrong!',
36+
severity,
37+
stackTrace
38+
}) => {
39+
return (
40+
<div className="flex min-h-[40vh] items-center justify-center">
41+
<Alert className="max-w-lg">
42+
{severity === 'error' && (
43+
<LucideAlertTriangle className="h-4 w-4 text-red-500" />
44+
)}
45+
{severity === 'info' && <InfoIcon className="h-4 w-4" />}
46+
<AlertTitle>{title}</AlertTitle>
47+
<AlertDescription>
48+
{children}
49+
{stackTrace && (
50+
<p className="text-muted-foreground border-l-2 py-2 text-sm">
51+
{stackTrace}
52+
</p>
53+
)}
54+
</AlertDescription>
55+
</Alert>
56+
</div>
57+
);
58+
};

apps/web/app/components/mobile-navigation.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export function MobileNavigation(
1313
) {
1414
const navigationTree = useNavigationTree();
1515

16+
if (navigationTree.length === 0) {
17+
return null;
18+
}
19+
1620
return (
1721
<Popover {...props}>
1822
<PopoverButton className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg ring-1 shadow-zinc-800/5 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">

apps/web/app/root.tsx

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
Outlet,
2222
Scripts,
2323
ScrollRestoration,
24-
data,
2524
useLoaderData,
2625
useNavigate
2726
} from '@remix-run/react';
@@ -31,6 +30,7 @@ import env from '../env-resolver/env';
3130
import { Container } from './components/container';
3231
import { DesktopNavigation } from './components/desktop-navigation';
3332
import { GeneralErrorBoundary } from './components/error-boundary';
33+
import { ErrorContainer } from './components/error-container';
3434
import { Footer } from './components/footer';
3535
import { MobileNavigation } from './components/mobile-navigation';
3636
import { ThemeSwitch, useTheme } from './routes/resources.theme-switch';
@@ -41,7 +41,7 @@ import { type Theme, getTheme } from './utils/theme.server';
4141

4242
export const meta: MetaFunction<typeof loader> = ({ data }) => [
4343
{
44-
title: data?.siteSettings.general.appName
44+
title: data?.siteSettings?.general?.appName ?? ''
4545
}
4646
];
4747

@@ -60,15 +60,15 @@ export const links: LinksFunction = () => [
6060
];
6161

6262
export async function loader({ context, request }: LoaderFunctionArgs) {
63+
/** Error message to display to the user when we have e.g. API issues */
64+
let loaderErrorMessage = '';
65+
6366
try {
6467
// Get the theme before fetching pages in case it fails
6568
const theme = await getTheme(request);
6669

67-
/** Error message to display to the user when we have e.g. API issues */
68-
let displayError = '';
69-
7070
let navigationTree: Array<NavigationItem> = [];
71-
let siteSettings = {} as SiteSetting;
71+
let siteSettings: SiteSetting | null = null;
7272

7373
// Fetch layout data but don't propagate the exception to the error boundary
7474
try {
@@ -77,21 +77,18 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
7777
context,
7878
request.headers
7979
);
80-
const gss = await getSiteSettings(requestOptions);
81-
if (!gss) {
82-
throw new Error('Site settings not found');
83-
}
84-
siteSettings = gss;
8580
navigationTree = await getNavigationTree(requestOptions);
81+
siteSettings = await getSiteSettings(requestOptions);
8682
} catch (e) {
8783
const error = e as Error;
8884
console.error(`Failed to load data: ${error.message}`);
89-
displayError = 'Unable to load content. Please try again later.';
85+
loaderErrorMessage =
86+
'Unable to load application content. Please try again later.';
9087
}
9188

9289
return {
93-
displayError,
9490
env,
91+
loaderErrorMessage,
9592
navigationTree,
9693
requestInfo: {
9794
hints: getHints(request),
@@ -104,10 +101,8 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
104101
};
105102
} catch (error) {
106103
console.error('Failed to load root data:\n', error);
107-
throw data(
108-
{ message: 'Failed to load application. Please try again later.' },
109-
{ status: 500 }
110-
);
104+
// Delegate to error boundary
105+
throw error;
111106
}
112107
}
113108

@@ -226,12 +221,12 @@ export default function App() {
226221
</div>
227222
</header>
228223
<main className="flex-auto">
229-
<Outlet />
230-
{loaderData.displayError && (
231-
<div className="flex items-center justify-center p-4">
232-
<p className="text-red-500">{loaderData.displayError}</p>
233-
</div>
234-
)}
224+
{/* Display loader error message if it exists instead of the outlet */}
225+
{(loaderData.loaderErrorMessage && (
226+
<ErrorContainer severity="error">
227+
{loaderData.loaderErrorMessage}
228+
</ErrorContainer>
229+
)) || <Outlet />}
235230
</main>
236231
<Footer />
237232
</div>

apps/web/app/routes/($collection).$slug.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
33
import { json, useLoaderData, useRouteError } from '@remix-run/react';
44

55
import { Container } from '../components/container';
6+
import { ErrorContainer } from '../components/error-container';
67
import { RenderPagesDoc } from '../components/render-pages-doc';
78
import { RenderPostsDoc } from '../components/render-posts-doc';
89
import { getPayloadRequestOptions } from '../utils/get-payload-request-options';
@@ -73,17 +74,8 @@ export function ErrorBoundary() {
7374
const error = useRouteError() as LoaderError;
7475

7576
return (
76-
<Container className="mt-16 sm:mt-32">
77-
<div className="flex min-h-[30vh] items-center justify-center">
78-
<div className="text-center">
79-
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
80-
{error.message}
81-
</h1>
82-
<p className="mt-2 text-zinc-600 dark:text-zinc-400">
83-
The page you're looking for doesn't exist.
84-
</p>
85-
</div>
86-
</div>
87-
</Container>
77+
<ErrorContainer severity="error" stackTrace={error.message}>
78+
The page you're looking for could not be rendered.
79+
</ErrorContainer>
8880
);
8981
}

apps/web/app/routes/_index.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { RenderBlocks } from '@codeware/shared/ui/payload-components';
22
import { type MetaFunction, useRouteError } from '@remix-run/react';
33

44
import { Container } from '../components/container';
5+
import { ErrorContainer } from '../components/error-container';
56
import { useSiteSettings } from '../utils/use-site-settings';
67

78
type LoaderError = {
@@ -12,23 +13,33 @@ type LoaderError = {
1213
// TODO: How to use it properly?
1314
export const meta: MetaFunction = () => {
1415
const { landingPage } = useSiteSettings();
15-
return [{ title: landingPage.name }];
16+
return [{ title: landingPage?.name }];
1617
};
1718

1819
export default function Index() {
1920
const { landingPage } = useSiteSettings();
2021

2122
return (
2223
<Container className="mt-16 sm:mt-32">
23-
{landingPage.header && (
24+
{landingPage?.header && (
2425
<header className="max-w-2xl">
2526
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl dark:text-zinc-100">
2627
{landingPage.header}
2728
</h1>
2829
</header>
2930
)}
3031
<article className="mt-16">
31-
<RenderBlocks blocks={landingPage.layout} />
32+
{landingPage ? (
33+
<RenderBlocks blocks={landingPage.layout} />
34+
) : (
35+
<ErrorContainer
36+
title="Landing page was not found"
37+
severity="info"
38+
withoutContainer={true}
39+
>
40+
Please create a page in the CMS and assign it to be a landing page.
41+
</ErrorContainer>
42+
)}
3243
</article>
3344
</Container>
3445
);
@@ -38,17 +49,8 @@ export function ErrorBoundary() {
3849
const error = useRouteError() as LoaderError;
3950

4051
return (
41-
<Container className="mt-16 sm:mt-32">
42-
<div className="flex min-h-[30vh] items-center justify-center">
43-
<div className="text-center">
44-
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
45-
{error.message}
46-
</h1>
47-
<p className="mt-2 text-zinc-600 dark:text-zinc-400">
48-
The page you're looking for doesn't exist.
49-
</p>
50-
</div>
51-
</div>
52-
</Container>
52+
<ErrorContainer severity="error" stackTrace={error.message}>
53+
The page you're looking for could not be rendered.
54+
</ErrorContainer>
5355
);
5456
}

apps/web/app/utils/use-navigation-tree.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { type loader as rootLoader } from '../root';
88
*/
99
export function useNavigationTree() {
1010
const data = useRouteLoaderData<typeof rootLoader>('root');
11-
invariant(data?.navigationTree, 'No navigation tree found in root loader');
11+
invariant(
12+
data && data.navigationTree,
13+
'No navigation tree found in root loader'
14+
);
1215

1316
return data.navigationTree;
1417
}

apps/web/app/utils/use-site-settings.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ import { type loader as rootLoader } from '../root';
88
*/
99
export function useSiteSettings() {
1010
const data = useRouteLoaderData<typeof rootLoader>('root');
11-
invariant(data?.siteSettings, 'No site settings found in root loader');
12-
const {
13-
general: { landingPage }
14-
} = data.siteSettings;
11+
invariant(data, 'No data found in root loader');
12+
13+
// Don't break the site when site settings haven't been configured yet.
14+
// Provide some useful information to the visitor.
15+
// TODO: Let the cms handle this with client render components?
16+
17+
const landingPage = data.siteSettings?.general?.landingPage ?? undefined;
18+
19+
// TODO: Type narrowing should be handled by the cms api
1520
invariant(
1621
typeof landingPage !== 'number',
17-
'No landing page found in site settings'
22+
'Expected landing page to be an object'
1823
);
1924

2025
return {

0 commit comments

Comments
 (0)