Skip to content

Commit

Permalink
Pretty 404/500 pages (#48)
Browse files Browse the repository at this point in the history
Add pretty messages with call to action for Not Found and Error states:


![image](https://github.com/user-attachments/assets/9df8e556-88ed-4400-80f6-b62f46d3807e)
  • Loading branch information
mplewis authored Dec 3, 2024
1 parent 78258a8 commit d5bfd7c
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 22 deletions.
2 changes: 1 addition & 1 deletion NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
- [ ] Make required env vars required with `mustEnv`, etc. (e.g. `SITE_HOST`)
- [ ] Make Sentry DSN optional
- [ ] Consensually gather user emails for mailing list
- [ ] Add pretty error messages for 404s (e.g. clicked an expired/tidied link)
- [ ] Redirect old slugs on slug change
- [ ] Add sticky bit to "sent you a confirmation email for your RSVP"
- [ ] Site-wide announcement feature
Expand All @@ -48,6 +47,7 @@
- [ ] Replace shared API lib dir with Yarn "shared package" workspace
- [ ] Add advanced crash logging
- [ ] Linter for secrets in build
- [x] Add pretty error messages for 404s (e.g. clicked an expired/tidied link)
- [x] Resend confirmation if an RSVP enters an existing email
- [x] Captcha
- [x] Hold RSVP locally with cookie
Expand Down
2 changes: 1 addition & 1 deletion api/src/services/responses/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const responseByEditToken: QueryResolvers['responseByEditToken'] =
include: { event: true, reminders: true },
})

if (!resp) throw new RedwoodError("Sorry, we couldn't find that RSVP.")
if (!resp) return null

if (!resp.confirmed) {
const updated = await db.response.update({
Expand Down
21 changes: 21 additions & 0 deletions web/src/components/DeadEnd/DeadEnd.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react'

import DeadEnd from './DeadEnd'

const meta: Meta<typeof DeadEnd> = {
component: DeadEnd,
tags: ['autodocs'],
}

export default meta

type Story = StoryObj<typeof DeadEnd>

/** DeadEnd tells users when their request can't be completed and they have to turn back. */
export const Primary: Story = {
args: {
title: 'Page not found',
desc: "Sorry, we couldn't find the page you were looking for.",
c2a: { text: 'Go home', to: '#' },
},
}
45 changes: 45 additions & 0 deletions web/src/components/DeadEnd/DeadEnd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Link } from '@redwoodjs/router'

import PageHead from '../PageHead/PageHead'
import Typ from '../Typ/Typ'

export type Props = {
title: string
desc: string | string[]
c2a: { text: string } & ({ href: string } | { to: string })
}

const DeadEnd = (props: Props) => {
const { title, c2a } = props
const desc = Array.isArray(props.desc) ? props.desc : [props.desc]

const action = (() => {
if ('href' in c2a) {
return (
<a className="button is-primary mt-3" href={c2a.href}>
{c2a.text}
</a>
)
} else {
return (
<Link className="button is-primary mt-3" to={c2a.to}>
{c2a.text}
</Link>
)
}
})()

return (
<div className="has-text-centered mx-auto" style={{ maxWidth: '600px' }}>
<PageHead title={title} desc={desc.join(' ')} />
{desc.map((p, i) => (
<Typ x="p" key={i}>
{p}
</Typ>
))}
{action}
</div>
)
}

export default DeadEnd
31 changes: 27 additions & 4 deletions web/src/components/EditEventCell/EditEventCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

import DeadEnd from '../DeadEnd/DeadEnd'
import EditEventForm from '../EditEventForm/EditEventForm'
import LoadingBuddy from '../LoadingBuddy/LoadingBuddy'
export const QUERY = gql`
Expand Down Expand Up @@ -38,13 +39,35 @@ export const Loading = () => (
</div>
)

export const Empty = () => <div>Event not found</div>
export const Empty = () => (
<DeadEnd
title="Event not found"
desc={[
"Sorry, we couldn't find the event you were looking for.",
'Please double-check that you have the correct link.',
'Our system automatically cleans up unconfirmed and completed events. ' +
"If you're using an old link, your event may have expired.",
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)

export const Failure = ({
error,
}: CellFailureProps<FindEditEventQueryVariables>) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)
}: CellFailureProps<FindEditEventQueryVariables>) => {
console.error({ error })
return (
<DeadEnd
title="Something went wrong"
desc={[
"Sorry, we weren't able to load your event.",
"We've notified the engineering team who will work to resolve this issue. " +
'Please try again later.',
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)
}

export const Success = ({
event,
Expand Down
31 changes: 27 additions & 4 deletions web/src/components/EditResponseCell/EditResponseCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { responseTokenAtom } from 'src/data/atoms'
import { promptConfirm } from 'src/logic/prompt'

import CalButtons from '../CalButtons/CalButtons'
import DeadEnd from '../DeadEnd/DeadEnd'
import DeleteButton from '../DeleteButton/DeleteButton'
import LoadingBuddy from '../LoadingBuddy/LoadingBuddy'
import PageHead from '../PageHead/PageHead'
Expand Down Expand Up @@ -87,13 +88,35 @@ export const Loading = () => (
</div>
)

export const Empty = () => <div>Empty</div>
export const Empty = () => (
<DeadEnd
title="RSVP not found"
desc={[
"Sorry, we couldn't find your RSVP.",
'Please double-check that you have the correct link.',
'Our system automatically cleans up completed events. ' +
"If you're using an old link, the event may have expired.",
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)

export const Failure = ({
error,
}: CellFailureProps<GetResponseQueryVariables>) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)
}: CellFailureProps<GetResponseQueryVariables>) => {
console.error({ error })
return (
<DeadEnd
title="Something went wrong"
desc={[
"Sorry, we weren't able to load your RSVP.",
"We've notified the engineering team who will work to resolve this issue. " +
'Please try again later.',
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)
}

export const Success = ({
response,
Expand Down
21 changes: 17 additions & 4 deletions web/src/components/IgnoredEmailCell/IgnoredEmailCell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {
IgnoredEmailQuery,
IgnoredEmailQueryVariables,
ResubscribeMutation,
ResubscribeMutationVariables,
UnsubscribeMutation,
Expand All @@ -10,6 +10,7 @@ import { type CellFailureProps, useMutation } from '@redwoodjs/web'

import { queryValue } from 'src/logic/path'

import DeadEnd from '../DeadEnd/DeadEnd'
import Typ from '../Typ/Typ'

export const QUERY = gql`
Expand Down Expand Up @@ -140,9 +141,21 @@ const ResubscribeForm = ({

export const Loading = () => <div>Loading...</div>

export const Failure = ({ error }: CellFailureProps<IgnoredEmailQuery>) => (
<ShowError error={error} />
)
export const Failure = ({
error,
}: CellFailureProps<IgnoredEmailQueryVariables>) => {
console.error({ error })
return (
<DeadEnd
title="Something went wrong"
desc={[
"Sorry, we weren't able to process your request due to a technical issue.",
'Please contact us at [email protected] for assistance.',
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)
}

export const Empty = () => (
<UnsubscribeForm email={queryValue('email')} token={queryValue('token')} />
Expand Down
31 changes: 27 additions & 4 deletions web/src/components/PreviewEventCell/PreviewEventCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

import DeadEnd from '../DeadEnd/DeadEnd'
import LoadingBuddy from '../LoadingBuddy/LoadingBuddy'
import PageHead from '../PageHead/PageHead'
import ShowEvent from '../ShowEvent/ShowEvent'
Expand All @@ -31,13 +32,35 @@ export const Loading = () => (
</div>
)

export const Empty = () => <div>Event not found</div>
export const Empty = () => (
<DeadEnd
title="Event not found"
desc={[
"Sorry, we couldn't find the event you were looking for.",
'Please double-check that you have the correct link.',
'Our system automatically cleans up unconfirmed and completed events. ' +
"If you're using an old link, your event may have expired.",
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)

export const Failure = ({
error,
}: CellFailureProps<FindPreviewEventQueryVariables>) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)
}: CellFailureProps<FindPreviewEventQueryVariables>) => {
console.error({ error })
return (
<DeadEnd
title="Something went wrong"
desc={[
"Sorry, we weren't able to load your RSVP.",
"We've notified the engineering team who will work to resolve this issue. " +
'Please try again later.',
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)
}

export const Success = ({
event,
Expand Down
31 changes: 27 additions & 4 deletions web/src/components/ViewEventCell/ViewEventCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {

import { CellSuccessProps, CellFailureProps, Metadata } from '@redwoodjs/web'

import DeadEnd from '../DeadEnd/DeadEnd'
import LoadingBuddy from '../LoadingBuddy/LoadingBuddy'
import ShowEvent from '../ShowEvent/ShowEvent'

Expand Down Expand Up @@ -39,13 +40,35 @@ export const Loading = () => (
</div>
)

export const Empty = () => <div>Event not found</div>
export const Empty = () => (
<DeadEnd
title="Event not found"
desc={[
"Sorry, we couldn't find the event you were looking for.",
'Please double-check that you have the correct link.',
'Our system automatically cleans up unconfirmed and completed events. ' +
"If you're using an old link, your event may have expired.",
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)

export const Failure = ({
error,
}: CellFailureProps<FindViewEventQueryVariables>) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)
}: CellFailureProps<FindViewEventQueryVariables>) => {
console.error({ error })
return (
<DeadEnd
title="Something went wrong"
desc={[
"Sorry, we weren't able to load your RSVP.",
"We've notified the engineering team who will work to resolve this issue. " +
'Please try again later.',
]}
c2a={{ text: 'Go home', to: '/' }}
/>
)
}

export const Success = ({
event,
Expand Down

0 comments on commit d5bfd7c

Please sign in to comment.