diff --git a/app/components/auth-page.tsx b/app/components/auth-page.tsx new file mode 100644 index 00000000..191d8724 --- /dev/null +++ b/app/components/auth-page.tsx @@ -0,0 +1,41 @@ +import { type ReactNode } from 'react' + +export function AuthPage({ + title, + description, + children, + footer, +}: { + title: string + description?: ReactNode + children: ReactNode + footer?: ReactNode +}) { + return ( +
+
+
+

+ GratiText +

+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+
+ {children} +
+ {footer ? ( +
+ {footer} +
+ ) : null} +
+
+ ) +} diff --git a/app/components/error-boundary.tsx b/app/components/error-boundary.tsx index 66108f32..43f60269 100644 --- a/app/components/error-boundary.tsx +++ b/app/components/error-boundary.tsx @@ -3,24 +3,90 @@ import { type ReactElement, useEffect } from 'react' import { type ErrorResponse, isRouteErrorResponse, + Link, useParams, useRouteError, } from 'react-router' import { getErrorMessage } from '#app/utils/misc.tsx' +import { Icon } from './ui/icon.tsx' type StatusHandler = (info: { error: ErrorResponse params: Record }) => ReactElement | null +function statusTitle(status: number) { + switch (status) { + case 400: + return 'That request did not look right' + case 401: + return 'Please log in to continue' + case 403: + return 'You are not allowed to do that' + case 404: + return 'We could not find that' + case 429: + return 'Slow down a little' + default: + return status >= 500 + ? 'Something went wrong on our end' + : 'Something went wrong' + } +} + +export function ErrorMessage({ + eyebrow, + title, + description, +}: { + eyebrow?: string + title: string + description?: string | null +}) { + return ( +
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} + +
+ ) +} + export function GeneralErrorBoundary({ defaultStatusHandler = ({ error }) => ( -

- {error.status} {error.data} -

+ ), statusHandlers, - unexpectedErrorHandler = (error) =>

{getErrorMessage(error)}

, + unexpectedErrorHandler = (error) => ( + + ), }: { defaultStatusHandler?: StatusHandler statusHandlers?: Record @@ -39,13 +105,15 @@ export function GeneralErrorBoundary({ } return ( -
- {isRouteErrorResponse(error) - ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({ - error, - params, - }) - : unexpectedErrorHandler(error)} +
+
+ {isRouteErrorResponse(error) + ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({ + error, + params, + }) + : unexpectedErrorHandler(error)} +
) } diff --git a/app/components/forms.tsx b/app/components/forms.tsx index 0a6d2dfd..f9995f03 100644 --- a/app/components/forms.tsx +++ b/app/components/forms.tsx @@ -5,6 +5,7 @@ import { type OTPInputProps, } from 'input-otp' import React, { useId } from 'react' +import { cn } from '#app/utils/misc.tsx' import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx' import { Icon } from './ui/icon.tsx' import { @@ -46,7 +47,7 @@ export function Field({ className, }: { labelProps: React.LabelHTMLAttributes - inputProps: React.InputHTMLAttributes + inputProps: React.ComponentProps<'input'> errors?: ListOfErrors className?: string }) { @@ -61,6 +62,7 @@ export function Field({ aria-invalid={errorId ? true : undefined} aria-describedby={errorId} {...inputProps} + className={cn('mt-2', inputProps.className)} />
{errorId ? : null} @@ -76,7 +78,7 @@ export function SelectField({ className, }: { labelProps: React.LabelHTMLAttributes - selectProps: React.SelectHTMLAttributes + selectProps: React.ComponentProps<'select'> errors?: ListOfErrors className?: string }) { @@ -86,7 +88,7 @@ export function SelectField({ return (