diff --git a/app/components/errors/seen-error-layout.tsx b/app/components/errors/seen-error-layout.tsx new file mode 100644 index 00000000..68ba0ff8 --- /dev/null +++ b/app/components/errors/seen-error-layout.tsx @@ -0,0 +1,33 @@ +import { Box, Button, Heading, Text } from '@chakra-ui/react'; +import type { ThrownResponse } from '@remix-run/react'; +import { useNavigate } from '@remix-run/react'; +import { getErrorMessageFromStatusCode } from '~/utils'; + +interface SeenErrorLayoutProps { + result: ThrownResponse; + mapStatusToErrorText?: (statusCode: number) => string; +} + +export default function SeenErrorLayout({ + result, + mapStatusToErrorText = getErrorMessageFromStatusCode, +}: SeenErrorLayoutProps) { + const navigate = useNavigate(); + + return ( + + + {result.status} + + + {mapStatusToErrorText(result.status)} + + + {result.data} + + + + ); +} diff --git a/app/components/errors/unseen-error-layout.tsx b/app/components/errors/unseen-error-layout.tsx new file mode 100644 index 00000000..5ca7703b --- /dev/null +++ b/app/components/errors/unseen-error-layout.tsx @@ -0,0 +1,24 @@ +import { Box, Button, Heading, Text } from '@chakra-ui/react'; +import { useNavigate } from '@remix-run/react'; + +interface UnseenErrorLayoutProps { + errorText: string; +} + +export default function UnseenErrorLayout({ errorText }: UnseenErrorLayoutProps) { + const navigate = useNavigate(); + + return ( + + + Ooops, unexpected error + + + {errorText} + + + + ); +} diff --git a/app/routes/__index.tsx b/app/routes/__index.tsx index 47d3e238..bd6fe4b8 100644 --- a/app/routes/__index.tsx +++ b/app/routes/__index.tsx @@ -1,5 +1,7 @@ import { Box, Container } from '@chakra-ui/react'; -import { Outlet } from '@remix-run/react'; +import { Outlet, useCatch } from '@remix-run/react'; +import SeenErrorLayout from '~/components/errors/seen-error-layout'; +import UnseenErrorLayout from '~/components/errors/unseen-error-layout'; import Header from '~/components/header'; export default function Index() { @@ -14,3 +16,13 @@ export default function Index() { ); } + +export function ErrorBoundary({ error }: { error: Error }) { + return ; +} + +export function CatchBoundary() { + const caught = useCatch(); + + return ; +} diff --git a/app/routes/__index/certificate/index.tsx b/app/routes/__index/certificate/index.tsx index d42e6a03..ed4da1c3 100644 --- a/app/routes/__index/certificate/index.tsx +++ b/app/routes/__index/certificate/index.tsx @@ -2,7 +2,7 @@ import { Flex, Heading } from '@chakra-ui/react'; import type { LoaderArgs, ActionArgs } from '@remix-run/node'; import { json } from '@remix-run/node'; import { typedjson, useTypedLoaderData } from 'remix-typedjson'; -import { useRevalidator } from '@remix-run/react'; +import { useCatch, useRevalidator } from '@remix-run/react'; import { useInterval } from 'react-use'; import { useMemo } from 'react'; import dayjs from 'dayjs'; @@ -12,9 +12,11 @@ import pendingSvg from '~/assets/undraw_processing_re_tbdu.svg'; import Loading from '~/components/display-page'; import CertificateAvailable from '~/components/certificate/certificate-available'; import CertificateRequestView from '~/components/certificate/certificate-request'; -import { useEffectiveUser } from '~/utils'; +import { getErrorMessageFromStatusCode, useEffectiveUser } from '~/utils'; import { getCertificateByUsername } from '~/models/certificate.server'; import { addCertRequest } from '~/queues/certificate/certificate-flow.server'; +import UnseenErrorLayout from '~/components/errors/unseen-error-layout'; +import SeenErrorLayout from '~/components/errors/seen-error-layout'; export const loader = async ({ request }: LoaderArgs) => { const username = await requireUsername(request); @@ -75,6 +77,29 @@ function formatDate(val: Date): string { return date; } +function mapStatusToErrorText(statusCode: number): string { + switch (statusCode) { + case 404: + return 'Sorry we could not find your certificate'; + case 409: + return 'Sorry, your certificate is not issued yet. Please try again later.'; + default: + return getErrorMessageFromStatusCode(statusCode); + } +} + +export function CatchBoundary() { + const caught = useCatch(); + + return ; +} + +export function ErrorBoundary() { + return ( + + ); +} + export default function CertificateIndexRoute() { const user = useEffectiveUser(); const revalidator = useRevalidator(); diff --git a/app/routes/__index/dns-records/$dnsRecordId.tsx b/app/routes/__index/dns-records/$dnsRecordId.tsx index 85f6358e..32729e44 100644 --- a/app/routes/__index/dns-records/$dnsRecordId.tsx +++ b/app/routes/__index/dns-records/$dnsRecordId.tsx @@ -6,15 +6,18 @@ import DnsRecordForm from '~/components/dns-record/form'; import { requireUser } from '~/session.server'; import { getDnsRecordById, updateDnsRecordById } from '~/models/dns-record.server'; import { isNameValid, UpdateDnsRecordSchema } from '~/lib/dns.server'; -import { useActionData } from '@remix-run/react'; -import { buildDomain } from '~/utils'; +import { useActionData, useCatch, useParams } from '@remix-run/react'; +import { buildDomain, getErrorMessageFromStatusCode } from '~/utils'; +import SeenErrorLayout from '~/components/errors/seen-error-layout'; +import UnseenErrorLayout from '~/components/errors/unseen-error-layout'; export const loader = async ({ request, params }: LoaderArgs) => { await requireUser(request); const { dnsRecordId } = params; - if (!dnsRecordId) { - throw new Response('dnsRecordId should be string', { + + if (!dnsRecordId || !parseInt(dnsRecordId)) { + throw new Response('DNS Record ID is not valid', { status: 400, }); } @@ -59,6 +62,31 @@ export const action = async ({ request }: ActionArgs) => { return redirect(`/dns-records`); }; +function mapStatusToErrorText(statusCode: number): string { + switch (statusCode) { + case 400: + return 'Provided Record ID is not valid'; + default: + return getErrorMessageFromStatusCode(statusCode); + } +} + +export function CatchBoundary() { + const caught = useCatch(); + + return ; +} + +export function ErrorBoundary() { + const { dnsRecordId } = useParams(); + + return ( + + ); +} + export default function DnsRecordRoute() { const dnsRecord = useTypedLoaderData(); const actionData = useActionData(); diff --git a/app/routes/__index/dns-records/index.tsx b/app/routes/__index/dns-records/index.tsx index 9c1c25a1..1ef4e1b5 100644 --- a/app/routes/__index/dns-records/index.tsx +++ b/app/routes/__index/dns-records/index.tsx @@ -1,6 +1,6 @@ import { AddIcon } from '@chakra-ui/icons'; import { Button, Center, Flex, Heading, Stat, StatLabel, StatNumber, Text } from '@chakra-ui/react'; -import { Link } from '@remix-run/react'; +import { Link, useCatch } from '@remix-run/react'; import { typedjson, useTypedLoaderData } from 'remix-typedjson'; import { json } from '@remix-run/node'; import { z } from 'zod'; @@ -17,6 +17,9 @@ import { requireUsername } from '~/session.server'; import logger from '~/lib/logger.server'; import type { LoaderArgs, ActionArgs } from '@remix-run/node'; +import SeenErrorLayout from '~/components/errors/seen-error-layout'; +import UnseenErrorLayout from '~/components/errors/unseen-error-layout'; +import { getErrorMessageFromStatusCode } from '~/utils'; export type DnsRecordActionIntent = 'renew-dns-record' | 'delete-dns-record'; @@ -77,6 +80,29 @@ export const action = async ({ request }: ActionArgs) => { } }; +function mapStatusToErrorText(statusCode: number): string { + switch (statusCode) { + case 404: + return 'Sorry we could not find your DNS Record'; + case 400: + return 'We got an error processing requested action on your DNS Record'; + default: + return getErrorMessageFromStatusCode(statusCode); + } +} + +export function CatchBoundary() { + const caught = useCatch(); + + return ; +} + +export function ErrorBoundary() { + return ( + + ); +} + export default function DnsRecordsIndexRoute() { const data = useTypedLoaderData(); diff --git a/app/utils.ts b/app/utils.ts index 0cdab6da..f36fe403 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -149,3 +149,24 @@ export async function getChildrenValuesOfQueueName({ return filteredChildrenValues; } + +export function getErrorMessageFromStatusCode(statusCode: number): string { + switch (statusCode) { + case 400: + return 'Bad Request: The server cannot process the request because it is malformed or invalid.'; + case 401: + return 'Unauthorized: The request requires authentication, and the user is not authenticated.'; + case 403: + return 'Forbidden: The server understands the request but refuses to authorize it.'; + case 404: + return 'Not Found: The server cannot find the requested resource.'; + case 500: + return 'Internal Server Error: The server encountered an unexpected condition that prevented it from fulfilling the request.'; + case 502: + return 'Bad Gateway: The server received an invalid response from the upstream server.'; + case 503: + return 'Service Unavailable: The server is currently unable to handle the request due to a temporary overload or maintenance.'; + default: + return 'An error has occurred.'; + } +}