feat: add FeedbackForm, FeedbackModal, and Give Feedback tab
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user