'use client'; import { useState } from 'react'; import { useTranslations } from 'next-intl'; import { motion } from 'framer-motion'; import { MapPin, Phone, Mail, Send, Loader2, CheckCircle2, AlertTriangle } from 'lucide-react'; import Image from 'next/image'; interface FormData { name: string; email: string; message: string; } export function ContactForm() { const t = useTranslations('pages.contact'); const MAX_MESSAGE_LENGTH = 1000; const [isSubmitting, setIsSubmitting] = useState(false); const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [errorMessage, setErrorMessage] = useState(''); const [formData, setFormData] = useState({ name: '', email: '', message: '' }); const [errors, setErrors] = useState>({}); const [remainingAttempts, setRemainingAttempts] = useState(null); const messageLength = formData.message.length; const validateForm = () => { const newErrors: Partial = {}; if (!formData.name.trim()) { newErrors.name = t('contactForm.errors.nameRequired'); } if (!formData.email.trim()) { newErrors.email = t('contactForm.errors.emailRequired'); } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { newErrors.email = t('contactForm.errors.emailInvalid'); } if (!formData.message.trim()) { newErrors.message = t('contactForm.errors.messageRequired'); } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); if (errors[name as keyof FormData]) { setErrors(prev => ({ ...prev, [name]: undefined })); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) return; setIsSubmitting(true); setSubmitStatus('idle'); setErrorMessage(''); try { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData), }); const data = await response.json(); // Parse rate limit headers const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining'); if (rateLimitRemaining !== null) { setRemainingAttempts(parseInt(rateLimitRemaining, 10)); } if (response.ok) { setSubmitStatus('success'); setFormData({ name: '', email: '', message: '' }); } else if (response.status === 429) { // Rate limit exceeded setSubmitStatus('error'); setRemainingAttempts(0); const retryAfter = data.retryAfter || 0; const minutes = Math.ceil(retryAfter / 60); const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; let timeMessage = ''; if (hours > 0) { timeMessage = `${hours} hour${hours > 1 ? 's' : ''}${remainingMinutes > 0 ? ` and ${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''}` : ''}`; } else { timeMessage = `${minutes} minute${minutes > 1 ? 's' : ''}`; } setErrorMessage( t('contactForm.rateLimit.error', { time: timeMessage }) ); } else { // Other errors (400, 500, etc.) setSubmitStatus('error'); setErrorMessage(data.error || t('contactForm.errorMessage')); } } catch { setSubmitStatus('error'); setErrorMessage(t('contactForm.errorMessage')); } finally { setIsSubmitting(false); } }; return (
{/* Hero Section */}
{t('hero.title')} {t('hero.subtitle')}
{/* Main Content */}
{/* Contact Info */} {/* Headshot */}
Damjan Savić - Full-Stack Web Developer

{t('contactInfo.title')}

{t('contactInfo.description')}

{t('contactInfo.address.label')}

{t('contactInfo.address.value')}

{t('contactInfo.phone.label')}

{t('contactInfo.phone.value')}

{t('contactInfo.email.label')}

{t('contactInfo.email.value')}
{t('contactInfo.availabilityNote')}
{/* Contact Form */}

{t('contactForm.description')}

{errors.name && ( )}
{errors.email && ( )}