mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 18:31:13 -05:00
317 lines
8.7 KiB
TypeScript
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>
|
|
);
|
|
}
|