- Seller KYC via Stripe Identity (start verification, status check, webhook) - Support ticket system (backend handler + frontend form page) - E2E payout flow integration test (sale → payment → balance → payout) - Migrations: seller_kyc columns, support_tickets table - Frontend: SupportPage with SUMI design, lazy loading, routing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
8.5 KiB
TypeScript
225 lines
8.5 KiB
TypeScript
/**
|
|
* SupportPage — Accessible contact/support form
|
|
* v0.13.5 TASK-MKT-004: Page support accessible
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select } from '@/components/ui/select';
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
|
import { apiClient } from '@/services/api/client';
|
|
import { Mail, CheckCircle2, AlertCircle, Send, HelpCircle } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const CATEGORY_OPTIONS = [
|
|
{ value: 'general', label: 'General' },
|
|
{ value: 'payment', label: 'Payment' },
|
|
{ value: 'account', label: 'Account' },
|
|
{ value: 'bug', label: 'Bug Report' },
|
|
];
|
|
|
|
type SubmitState = 'idle' | 'submitting' | 'success' | 'error';
|
|
|
|
export function SupportPage() {
|
|
const { t } = useTranslation();
|
|
const { data: user } = useUser();
|
|
const [email, setEmail] = useState(user?.email || '');
|
|
const [subject, setSubject] = useState('');
|
|
const [message, setMessage] = useState('');
|
|
const [category, setCategory] = useState('general');
|
|
const [submitState, setSubmitState] = useState<SubmitState>('idle');
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSubmitState('submitting');
|
|
setErrorMessage('');
|
|
|
|
try {
|
|
await apiClient.post('/support/tickets', {
|
|
email: email.trim(),
|
|
subject: subject.trim(),
|
|
message: message.trim(),
|
|
category,
|
|
});
|
|
setSubmitState('success');
|
|
setSubject('');
|
|
setMessage('');
|
|
} catch (err: unknown) {
|
|
setSubmitState('error');
|
|
const msg = err instanceof Error ? err.message : 'An error occurred';
|
|
setErrorMessage(msg);
|
|
}
|
|
};
|
|
|
|
const isValid = email.includes('@') && subject.trim().length >= 3 && message.trim().length >= 10;
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="max-w-2xl mx-auto px-4 py-8 md:py-12">
|
|
<div className="space-y-2 mb-8">
|
|
<h1 className="text-3xl font-heading font-bold text-foreground">
|
|
{t('support.title', 'Support')}
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
{t('support.description', "Need help? Send us a message and we'll get back to you within 48 hours.")}
|
|
</p>
|
|
</div>
|
|
|
|
{submitState === 'success' ? (
|
|
<div
|
|
className="flex flex-col items-center justify-center gap-4 p-8 rounded-xl border border-[var(--sumi-sage)]/30 bg-[var(--sumi-sage)]/5"
|
|
role="status"
|
|
>
|
|
<CheckCircle2 className="w-12 h-12 text-[var(--sumi-sage)]" />
|
|
<h2 className="text-xl font-heading font-bold text-foreground">
|
|
{t('support.success.title', 'Message sent!')}
|
|
</h2>
|
|
<p className="text-muted-foreground text-center max-w-md">
|
|
{t('support.success.description', "We've received your message and will respond to your email within 48 hours.")}
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setSubmitState('idle')}
|
|
className="mt-4"
|
|
>
|
|
{t('support.success.sendAnother', 'Send another message')}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Email */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="support-email">
|
|
{t('support.form.email', 'Email address')}
|
|
</Label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
id="support-email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="your@email.com"
|
|
className="pl-10"
|
|
required
|
|
aria-describedby="email-hint"
|
|
/>
|
|
</div>
|
|
<p id="email-hint" className="text-xs text-muted-foreground">
|
|
{t('support.form.emailHint', "We'll respond to this address")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Category */}
|
|
<div className="space-y-2">
|
|
<Label id="support-category-label">
|
|
{t('support.form.category', 'Category')}
|
|
</Label>
|
|
<Select
|
|
options={CATEGORY_OPTIONS}
|
|
value={category}
|
|
onChange={(v) => setCategory(typeof v === 'string' ? v : v[0] || 'general')}
|
|
aria-labelledby="support-category-label"
|
|
/>
|
|
</div>
|
|
|
|
{/* Subject */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="support-subject">
|
|
{t('support.form.subject', 'Subject')}
|
|
</Label>
|
|
<Input
|
|
id="support-subject"
|
|
value={subject}
|
|
onChange={(e) => setSubject(e.target.value)}
|
|
placeholder={t('support.form.subjectPlaceholder', 'Brief description of your issue') as string}
|
|
minLength={3}
|
|
maxLength={500}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Message */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="support-message">
|
|
{t('support.form.message', 'Message')}
|
|
</Label>
|
|
<Textarea
|
|
id="support-message"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
placeholder={t('support.form.messagePlaceholder', 'Describe your issue in detail...') as string}
|
|
rows={6}
|
|
minLength={10}
|
|
maxLength={5000}
|
|
required
|
|
className="resize-y"
|
|
/>
|
|
<p className="text-xs text-muted-foreground text-right">
|
|
{message.length}/5000
|
|
</p>
|
|
</div>
|
|
|
|
{/* Error message */}
|
|
{submitState === 'error' && (
|
|
<div
|
|
className="flex items-center gap-2 p-3 rounded-lg bg-[var(--sumi-vermillion)]/10 border border-[var(--sumi-vermillion)]/30 text-sm"
|
|
role="alert"
|
|
>
|
|
<AlertCircle className="w-4 h-4 text-[var(--sumi-vermillion)] shrink-0" />
|
|
<span className="text-[var(--sumi-vermillion)]">
|
|
{errorMessage || t('support.error', 'Failed to send message. Please try again.')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Submit */}
|
|
<Button
|
|
type="submit"
|
|
disabled={!isValid || submitState === 'submitting'}
|
|
className={cn(
|
|
'w-full',
|
|
submitState === 'submitting' && 'opacity-70',
|
|
)}
|
|
>
|
|
<Send className="w-4 h-4 mr-2" />
|
|
{submitState === 'submitting'
|
|
? t('support.form.sending', 'Sending...')
|
|
: t('support.form.submit', 'Send message')}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
|
|
{/* FAQ / Quick Links */}
|
|
<div className="mt-12 pt-8 border-t border-border">
|
|
<h2 className="text-lg font-heading font-bold text-foreground mb-4">
|
|
{t('support.quickHelp', 'Quick help')}
|
|
</h2>
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
{[
|
|
{ label: t('support.faq.payments', 'Payment issues'), href: '/marketplace' },
|
|
{ label: t('support.faq.account', 'Account settings'), href: '/settings' },
|
|
{ label: t('support.faq.selling', 'Selling on Veza'), href: '/sell' },
|
|
{ label: t('support.faq.privacy', 'Privacy & data'), href: '/settings' },
|
|
].map(({ label, href }) => (
|
|
<a
|
|
key={href + String(label)}
|
|
href={href}
|
|
className="flex items-center gap-2 p-3 rounded-lg border border-border hover:bg-[var(--sumi-bg-hover)] transition-colors text-sm text-muted-foreground hover:text-foreground"
|
|
>
|
|
<HelpCircle className="w-4 h-4 shrink-0" />
|
|
{label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|