feat: add FeedbackPage with custom feedback type selector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-18 17:58:42 -05:00
parent 47c2ee42c6
commit 5672d9062d

View File

@@ -0,0 +1,198 @@
import { useState } from 'react'
import { MessageSquareText, Send, CheckCircle2, ChevronDown } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { feedbackApi } from '@/api'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
// TODO: Post-session contextual feedback prompt — after completing a troubleshooting
// session, show a subtle inline prompt like "How was this flow? Quick feedback →"
// that opens a lightweight version of this form pre-tagged with tree/session context.
const FEEDBACK_TYPES = [
{ value: 'Bug Report', description: 'Something is broken or not working as expected' },
{ value: 'Feature Request', description: "An idea for something new you'd like to see" },
{ value: 'Usability Issue', description: 'Something works but is confusing or hard to use' },
{ value: 'Documentation', description: 'Feedback on help docs, tooltips, or in-app guidance' },
{ value: 'General Feedback', description: 'Anything else — thoughts, impressions, suggestions' },
] as const
export function FeedbackPage() {
const user = useAuthStore(s => s.user)
const [email, setEmail] = useState(user?.email ?? '')
const [feedbackType, setFeedbackType] = useState<string>('')
const [message, setMessage] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false)
const canSubmit = email.trim() && feedbackType && message.trim().length >= 10
const selectedType = FEEDBACK_TYPES.find(t => t.value === feedbackType)
const handleSelectType = (value: string) => {
setFeedbackType(value)
setTypeDropdownOpen(false)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!canSubmit || isSubmitting) return
setIsSubmitting(true)
try {
const response = await feedbackApi.submit({
email: email.trim(),
feedback_type: feedbackType,
message: message.trim(),
})
if (response.success) {
setSubmitted(true)
setFeedbackType('')
setMessage('')
}
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
toast.error(error.response?.data?.detail || 'Failed to submit feedback. Please try again.')
} finally {
setIsSubmitting(false)
}
}
const handleNewFeedback = () => {
setSubmitted(false)
setEmail(user?.email ?? '')
}
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
{/* Page header */}
<div className="mb-8">
<div className="flex items-center gap-3">
<MessageSquareText className="h-8 w-8 text-muted-foreground" />
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Send Feedback</h1>
</div>
<p className="mt-2 text-muted-foreground">
Help us improve ResolutionFlow. Report bugs, request features, or share your thoughts.
</p>
</div>
<div className="max-w-2xl">
{submitted ? (
<div className="bg-card border border-border rounded-xl p-8 text-center">
<CheckCircle2 className="mx-auto h-12 w-12 text-green-500 mb-4" />
<h2 className="text-xl font-semibold text-foreground mb-2">Thank you for your feedback!</h2>
<p className="text-muted-foreground mb-6">
We've received your submission and will review it shortly. Check your email for a confirmation.
</p>
<button
onClick={handleNewFeedback}
className="bg-gradient-brand text-white font-medium px-6 py-2.5 rounded-lg shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity"
>
Send More Feedback
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="bg-card border border-border rounded-xl p-4 sm:p-6 space-y-5">
{/* Email */}
<div>
<label htmlFor="feedback-email" className="block text-sm font-medium text-foreground mb-1.5">
Email Address
</label>
<input
id="feedback-email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none"
/>
<p className="mt-1 text-xs text-muted-foreground">We'll reply to this address if we need more details.</p>
</div>
{/* Feedback Type — custom selector with descriptions */}
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">
Feedback Type
</label>
<div className="relative">
<button
type="button"
onClick={() => setTypeDropdownOpen(!typeDropdownOpen)}
className={cn(
"w-full rounded-lg border border-border bg-card px-3 py-2 text-left flex items-center justify-between focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none",
feedbackType ? "text-foreground" : "text-muted-foreground"
)}
>
<span>{selectedType?.value ?? 'Select a type...'}</span>
<ChevronDown size={16} className={cn("transition-transform", typeDropdownOpen && "rotate-180")} />
</button>
{typeDropdownOpen && (
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-lg overflow-hidden">
{FEEDBACK_TYPES.map(type => (
<button
key={type.value}
type="button"
onClick={() => handleSelectType(type.value)}
className={cn(
"w-full text-left px-3 py-2.5 hover:bg-accent transition-colors",
feedbackType === type.value && "bg-accent"
)}
>
<div className="text-sm font-medium text-foreground">{type.value}</div>
<div className="text-xs text-muted-foreground mt-0.5">{type.description}</div>
</button>
))}
</div>
)}
</div>
</div>
{/* Message */}
<div>
<label htmlFor="feedback-message" className="block text-sm font-medium text-foreground mb-1.5">
Your Feedback
</label>
<textarea
id="feedback-message"
value={message}
onChange={e => setMessage(e.target.value)}
placeholder="Describe your feedback in detail..."
required
minLength={10}
rows={6}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none resize-y"
/>
<p className="mt-1 text-xs text-muted-foreground">
{message.trim().length < 10
? `Minimum 10 characters (${message.trim().length}/10)`
: `${message.trim().length} characters`}
</p>
</div>
{/* Submit */}
<div className="pt-2">
<button
type="submit"
disabled={!canSubmit || isSubmitting}
className={cn(
"flex items-center gap-2 rounded-lg px-6 py-2.5 font-medium text-white shadow-lg shadow-primary/20 transition-opacity",
canSubmit && !isSubmitting
? "bg-gradient-brand hover:opacity-90"
: "bg-gradient-brand opacity-50 cursor-not-allowed"
)}
>
<Send size={16} />
{isSubmitting ? 'Sending...' : 'Submit Feedback'}
</button>
</div>
</form>
)}
</div>
</div>
)
}
export default FeedbackPage