Files
thrilltrack-explorer/src-old/components/contact/ContactForm.tsx

317 lines
8.7 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Send } from 'lucide-react';
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,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha';
import { supabase } from '@/lib/supabaseClient';
import { handleError, handleSuccess } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
import { contactFormSchema, contactCategories, type ContactFormData } from '@/lib/contactValidation';
import { useAuth } from '@/hooks/useAuth';
export function ContactForm() {
const { user } = useAuth();
const [isSubmitting, setIsSubmitting] = useState(false);
const [captchaToken, setCaptchaToken] = useState<string>('');
const [captchaKey, setCaptchaKey] = useState(0);
const [userName, setUserName] = useState('');
const [userEmail, setUserEmail] = useState('');
// Fetch user profile if logged in
useEffect(() => {
const fetchUserProfile = async () => {
if (user) {
setUserEmail(user.email || '');
const { data: profile } = await supabase
.from('profiles')
.select('display_name, username')
.eq('user_id', user.id)
.single();
if (profile) {
setUserName(profile.display_name || profile.username || '');
}
}
};
fetchUserProfile();
}, [user]);
const {
register,
handleSubmit,
setValue,
watch,
reset,
formState: { errors },
} = useForm<ContactFormData>({
resolver: zodResolver(contactFormSchema),
defaultValues: {
name: userName,
email: userEmail,
subject: '',
category: 'general',
message: '',
captchaToken: '',
},
});
// Update form when user data loads
useEffect(() => {
if (userName) {
setValue('name', userName);
}
if (userEmail) {
setValue('email', userEmail);
}
}, [userName, userEmail, setValue]);
const onCaptchaSuccess = (token: string) => {
setCaptchaToken(token);
setValue('captchaToken', token);
};
const onCaptchaError = () => {
setCaptchaToken('');
setValue('captchaToken', '');
handleError(
new Error('CAPTCHA verification failed'),
{ action: 'verify_captcha' }
);
};
const onCaptchaExpire = () => {
setCaptchaToken('');
setValue('captchaToken', '');
};
const onSubmit = async (data: ContactFormData) => {
if (!captchaToken) {
handleError(
new Error('Please complete the CAPTCHA'),
{ action: 'submit_contact_form' }
);
return;
}
setIsSubmitting(true);
try {
logger.info('Submitting contact form', { category: data.category });
const { data: result, error } = await supabase.functions.invoke(
'send-contact-message',
{
body: {
name: data.name,
email: data.email,
subject: data.subject,
message: data.message,
category: data.category,
captchaToken: data.captchaToken,
},
}
);
if (error) {
throw error;
}
handleSuccess(
'Message Sent!',
"Thank you for contacting us. We'll respond within 24-48 hours."
);
// Reset form
reset({
name: userName,
email: userEmail,
subject: '',
category: 'general',
message: '',
captchaToken: '',
});
// Reset CAPTCHA
setCaptchaToken('');
setCaptchaKey((prev) => prev + 1);
} catch (error) {
handleError(error, {
action: 'submit_contact_form',
metadata: { category: data.category },
});
// Reset CAPTCHA on error
setCaptchaToken('');
setCaptchaKey((prev) => prev + 1);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Name */}
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
{...register('name')}
placeholder="Your name"
disabled={isSubmitting}
className={errors.name ? 'border-destructive' : ''}
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name.message}</p>
)}
</div>
{/* Email */}
<div className="space-y-2">
<Label htmlFor="email">Email *</Label>
<Input
id="email"
type="email"
{...register('email')}
placeholder="your.email@example.com"
disabled={isSubmitting}
className={errors.email ? 'border-destructive' : ''}
/>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>
{/* Category */}
<div className="space-y-2">
<Label htmlFor="category">Category *</Label>
<Select
value={watch('category')}
onValueChange={(value) =>
setValue('category', value as ContactFormData['category'])
}
disabled={isSubmitting}
>
<SelectTrigger className={errors.category ? 'border-destructive' : ''}>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{contactCategories.map((cat) => (
<SelectItem key={cat.value} value={cat.value}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.category && (
<p className="text-sm text-destructive">{errors.category.message}</p>
)}
</div>
{/* Subject */}
<div className="space-y-2">
<Label htmlFor="subject">Subject *</Label>
<Input
id="subject"
{...register('subject')}
placeholder="Brief description of your inquiry"
disabled={isSubmitting}
className={errors.subject ? 'border-destructive' : ''}
/>
{errors.subject && (
<p className="text-sm text-destructive">{errors.subject.message}</p>
)}
</div>
{/* Message */}
<div className="space-y-2">
<Label htmlFor="message">Message * (minimum 20 characters)</Label>
<Textarea
id="message"
{...register('message')}
placeholder="Please provide details about your inquiry (minimum 20 characters)..."
rows={6}
disabled={isSubmitting}
className={errors.message ? 'border-destructive' : ''}
/>
<div className="flex justify-between items-center">
{errors.message && (
<p className="text-sm text-destructive">{errors.message.message}</p>
)}
<p className={`text-sm ml-auto ${
(watch('message')?.length || 0) < 20
? 'text-destructive font-medium'
: 'text-muted-foreground'
}`}>
{watch('message')?.length || 0} / 2000
{(watch('message')?.length || 0) < 20 &&
` (${20 - (watch('message')?.length || 0)} more needed)`
}
</p>
</div>
</div>
{/* CAPTCHA */}
<div className="space-y-2">
<TurnstileCaptcha
key={captchaKey}
onSuccess={onCaptchaSuccess}
onError={onCaptchaError}
onExpire={onCaptchaExpire}
theme="auto"
size="normal"
/>
{errors.captchaToken && (
<p className="text-sm text-destructive">
{errors.captchaToken.message}
</p>
)}
</div>
{/* Submit Button */}
<Button
type="submit"
size="lg"
disabled={isSubmitting || !captchaToken || (watch('message')?.length || 0) < 20}
className="w-full"
title={
!captchaToken
? 'Please complete the CAPTCHA'
: (watch('message')?.length || 0) < 20
? `Message must be at least 20 characters (currently ${watch('message')?.length || 0})`
: ''
}
>
{isSubmitting ? (
<>
<span className="animate-spin mr-2"></span>
Sending...
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
Send Message
</>
)}
</Button>
<p className="text-sm text-muted-foreground text-center">
We typically respond within 24-48 hours
</p>
</form>
);
}