Files
Ghost_Kitchen/src/routes/help.tsx
T
lukasandClaude Sonnet 5 a8e655a810 Add a Help page with FAQ and contact form
Adds a /help route with an animated FAQ accordion and a contact form
(persisted via a new contact_messages table). The per-order-item "Get
help" link now points here and passes the order/item/status as search
params, which the page uses to show a short, state-specific note and
prefill the message field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 11:24:51 +02:00

216 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createFileRoute } from '@tanstack/react-router'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import { useState } from 'react'
import type { FormEvent } from 'react'
import { z } from 'zod'
import { orderItemStatusValues } from '#/db/schema.ts'
import type { OrderItemStatus } from '#/db/schema.ts'
import { submitContactMessage } from '#/server/contact.ts'
import styles from './help.module.scss'
const orderStatusHelp: Record<OrderItemStatus, string> = {
placed: 'Your order has just been placed. If you need to change or cancel it, do it soon — once the kitchen starts cooking, we can’t make changes.',
paid: 'Your order is already being prepared, so we can’t change or cancel it now. Tell us what’s wrong below and we’ll help.',
ready: 'Your order is ready for pickup. If something’s off with it, let us know below.',
picked_up: 'This order has already been picked up. If something was wrong with it, let us know below.',
cancelled: 'This order was cancelled. If that wasn’t expected, let us know below.',
}
const faqs = [
{
question: 'How do I track my order?',
answer:
'Once you place an order you’ll land on its status page, which updates live as each step of your order is prepared. You can get back to it any time from the order confirmation link.',
},
{
question: 'Can I change or cancel an order after placing it?',
answer:
'Orders start preparing right away, so we can’t guarantee changes or cancellations once it’s placed. Reach out through the form below as soon as possible and we’ll do what we can.',
},
{
question: 'Do I need an account to order?',
answer:
'No — you can check out as a guest with just a name and a way to reach you. Creating an account just lets you see your order history in one place.',
},
{
question: 'What payment methods are accepted?',
answer: 'We accept all major debit and credit cards at checkout.',
},
{
question: 'How do I get a refund?',
answer:
'Send us a message with your order details using the contact form below and we’ll sort it out with you.',
},
]
type FaqItemProps = {
question: string
answer: string
}
const FaqItem = ({ question, answer }: FaqItemProps) => {
const reduceMotion = useReducedMotion()
const [open, setOpen] = useState(false)
const transition = { duration: reduceMotion ? 0 : 0.2, ease: 'easeOut' as const }
return (
<div className={styles.faqItem}>
<button
type="button"
className={styles.faqQuestion}
aria-expanded={open}
onClick={() => setOpen((isOpen) => !isOpen)}
>
<span className={styles.faqMarker} aria-hidden="true">
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={open ? 'minus' : 'plus'}
className={styles.faqMarkerGlyph}
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={transition}
>
{open ? '−' : '+'}
</motion.span>
</AnimatePresence>
</span>
{question}
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
key="content"
className={styles.faqAnswerWrapper}
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={transition}
>
<p className={styles.faqAnswer}>{answer}</p>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
const HelpPage = () => {
const { orderId, item, status } = Route.useSearch()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [message, setMessage] = useState(() =>
orderId
? `Order #${orderId}${item ? ` — ${item}` : ''}\n\n`
: '',
)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [sent, setSent] = useState(false)
const submit = async (event: FormEvent) => {
event.preventDefault()
setSubmitting(true)
setError(null)
try {
await submitContactMessage({ data: { name, email, message } })
setSent(true)
setName('')
setEmail('')
setMessage('')
} catch (err) {
setError(
err instanceof Error
? err.message
: 'Could not send your message. Please try again.',
)
} finally {
setSubmitting(false)
}
}
return (
<main className={styles.page}>
<div>
<h1 className={styles.heading}>Help</h1>
{status && (
<div className={styles.orderContext}>
<h2 className={styles.sectionHeading}>
About order #{orderId}
{item ? ` — ${item}` : ''}
</h2>
<p className={styles.orderContextText}>{orderStatusHelp[status]}</p>
</div>
)}
<h2 className={styles.sectionHeading}>Frequently asked questions</h2>
<div className={styles.faq}>
{faqs.map(({ question, answer }) => (
<FaqItem key={question} question={question} answer={answer} />
))}
</div>
</div>
<div>
<h2 className={styles.sectionHeading}>Still need help?</h2>
{sent ? (
<p className={styles.success}>
Thanks for reaching out — we’ll get back to you soon.
</p>
) : (
<form className={styles.form} onSubmit={submit}>
<label className={styles.field}>
Name
<input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
required
/>
</label>
<label className={styles.field}>
Email
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
</label>
<label className={styles.field}>
Message
<textarea
value={message}
onChange={(event) => setMessage(event.target.value)}
required
/>
</label>
<button
type="submit"
className={styles.submitButton}
disabled={submitting}
>
{submitting ? 'Sending…' : 'Send message'}
</button>
{error && <p className={styles.error}>{error}</p>}
</form>
)}
</div>
</main>
)
}
const searchSchema = z.object({
orderId: z.number().optional(),
item: z.string().optional(),
status: z.enum(orderItemStatusValues).optional(),
})
export const Route = createFileRoute('/help')({
validateSearch: searchSchema,
component: HelpPage,
})