182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
import { useState } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import { FaEnvelope, FaMapMarkerAlt } from 'react-icons/fa'
|
|
import { useNotification } from '../context/notification-context'
|
|
|
|
const Contact = () => {
|
|
const { showNotification } = useNotification()
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
message: '',
|
|
website: '' // Honeypot field
|
|
})
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (isSubmitting) return
|
|
setIsSubmitting(true)
|
|
|
|
try {
|
|
const response = await fetch('/api/contact', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(formData),
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
showNotification('Message sent successfully!', 'success')
|
|
setFormData({ name: '', email: '', message: '', website: '' })
|
|
} else {
|
|
showNotification(data.message || 'Failed to send message', 'error')
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'An error occurred. Please try again later.'
|
|
showNotification(errorMessage, 'error')
|
|
} finally {
|
|
setTimeout(() => {
|
|
setIsSubmitting(false)
|
|
}, 1000)
|
|
}
|
|
}
|
|
|
|
const contactInfo = [
|
|
{
|
|
icon: <FaEnvelope className="text-2xl sm:text-3xl" />,
|
|
title: 'Email',
|
|
content: 'contact@deafgain.com'
|
|
},
|
|
{
|
|
icon: <FaMapMarkerAlt className="text-2xl sm:text-3xl" />,
|
|
title: 'Location',
|
|
content: 'Montana, United States'
|
|
}
|
|
]
|
|
|
|
return (
|
|
<div className="min-h-screen bg-accent-snow">
|
|
<div className="max-w-7xl mx-auto px-4 py-12 sm:py-20">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center mb-8 sm:mb-16"
|
|
>
|
|
<h1 className="text-3xl sm:text-4xl font-bold text-secondary mb-4">Get in Touch</h1>
|
|
<p className="text-accent-mountain text-base sm:text-lg max-w-2xl mx-auto px-4">
|
|
We're here to help and answer any questions you might have
|
|
</p>
|
|
</motion.div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-6 mb-12 max-w-2xl mx-auto">
|
|
{contactInfo.map((info, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.2 }}
|
|
className="bg-white p-6 rounded-lg shadow-lg text-center"
|
|
>
|
|
<div className="text-primary mb-4">{info.icon}</div>
|
|
<h3 className="text-lg sm:text-xl font-semibold mb-2">{info.title}</h3>
|
|
<p className="text-accent-mountain">{info.content}</p>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.4 }}
|
|
className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6 sm:p-8"
|
|
>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div>
|
|
<label htmlFor="name" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
|
Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
|
text-base sm:text-lg"
|
|
required
|
|
minLength={2}
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="email" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
|
Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
|
text-base sm:text-lg"
|
|
required
|
|
pattern="[^@\s]+@[^@\s]+\.[^@\s]+"
|
|
/>
|
|
</div>
|
|
|
|
{/* Honeypot field - hidden from humans, visible to bots */}
|
|
<div className="absolute left-[-5000px] opacity-0" aria-hidden="true">
|
|
<label htmlFor="website">Website</label>
|
|
<input
|
|
type="text"
|
|
id="website"
|
|
name="website"
|
|
value={formData.website}
|
|
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
|
|
tabIndex={-1}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="message" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
|
Message
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
value={formData.message}
|
|
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
|
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
|
text-base sm:text-lg min-h-[120px] sm:min-h-[150px]"
|
|
required
|
|
minLength={10}
|
|
maxLength={1000}
|
|
/>
|
|
</div>
|
|
|
|
<motion.button
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className={`w-full py-4 rounded-lg font-semibold text-base sm:text-lg transition-colors
|
|
${isSubmitting
|
|
? 'bg-accent-mountain text-accent-snow cursor-not-allowed'
|
|
: 'bg-primary text-accent-snow hover:bg-secondary active:bg-secondary'
|
|
}`}
|
|
>
|
|
{isSubmitting ? 'Sending...' : 'Send Message'}
|
|
</motion.button>
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Contact
|