130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
type Status = 'idle' | 'loading' | 'done' | 'error'
|
|
|
|
const CATEGORIES = [
|
|
{ value: 'BUG', label: 'Bug report' },
|
|
{ value: 'FEATURE_REQUEST', label: 'Feature request' },
|
|
{ value: 'GENERAL', label: 'General feedback' },
|
|
]
|
|
|
|
export function FeedbackForm({ onSuccess }: { onSuccess?: () => void }) {
|
|
const [category, setCategory] = useState('BUG')
|
|
const [message, setMessage] = useState('')
|
|
const [email, setEmail] = useState('')
|
|
const [status, setStatus] = useState<Status>('idle')
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
if (!message.trim()) return
|
|
setStatus('loading')
|
|
try {
|
|
const res = await fetch('/api/feedback', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
category,
|
|
message: message.trim(),
|
|
email: email.trim() || undefined,
|
|
}),
|
|
})
|
|
if (res.ok) {
|
|
setStatus('done')
|
|
onSuccess?.()
|
|
} else {
|
|
setStatus('error')
|
|
}
|
|
} catch {
|
|
setStatus('error')
|
|
}
|
|
}
|
|
|
|
if (status === 'done') {
|
|
return (
|
|
<div className="py-6 text-center">
|
|
<p className="text-sm font-semibold text-amber-400">Thanks — we got it.</p>
|
|
<p className="mt-1 text-xs text-zinc-500">
|
|
We read everything. If you left an email we'll follow up.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
|
<div>
|
|
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
|
|
Category
|
|
</label>
|
|
<select
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value)}
|
|
disabled={status === 'loading'}
|
|
className="w-full appearance-none rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2.5 text-sm text-zinc-200 focus:border-amber-500/40 focus:outline-none disabled:opacity-50"
|
|
>
|
|
{CATEGORIES.map((c) => (
|
|
<option key={c.value} value={c.value}>
|
|
{c.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
|
|
Email{' '}
|
|
<span className="font-normal normal-case tracking-normal text-zinc-600">
|
|
(optional — if you want a reply)
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="you@email.com"
|
|
disabled={status === 'loading'}
|
|
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-amber-500/40 focus:outline-none disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
|
|
Message
|
|
</label>
|
|
<textarea
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value.slice(0, 1000))}
|
|
placeholder="What's on your mind?"
|
|
rows={5}
|
|
required
|
|
disabled={status === 'loading'}
|
|
className="w-full resize-y rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2.5 text-sm leading-relaxed text-zinc-200 placeholder-zinc-600 focus:border-amber-500/40 focus:outline-none disabled:opacity-50"
|
|
/>
|
|
<p className="mt-1.5 text-right text-[11px] text-zinc-600">
|
|
{message.length} / 1000
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
<p className="text-[11px] text-zinc-600">
|
|
We'll only use your email to follow up on your feedback.
|
|
</p>
|
|
<button
|
|
type="submit"
|
|
disabled={status === 'loading' || !message.trim()}
|
|
className="shrink-0 rounded-lg bg-amber-500/90 px-6 py-2.5 text-sm font-bold text-black hover:bg-amber-400 disabled:opacity-50"
|
|
>
|
|
{status === 'loading' ? 'Sending…' : 'Send Feedback →'}
|
|
</button>
|
|
</div>
|
|
|
|
{status === 'error' && (
|
|
<p className="text-xs text-red-400">Something went wrong. Try again.</p>
|
|
)}
|
|
</form>
|
|
)
|
|
}
|