veza/apps/web/src/features/support/pages/SupportPage.tsx
senke 1ccaa03737 feat(v0.13.5): polish marketplace & compliance — KYC, support, payout E2E
- 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>
2026-03-13 14:57:19 +01:00

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>
);
}