Files
ragusaitweb/src/pages/Contact.tsx
2026-01-21 22:38:10 +01:00

245 lines
8.2 KiB
TypeScript

import { useState, type FormEvent } from 'react';
import { motion } from 'motion/react';
import emailjs from '@emailjs/browser';
import { useTranslation } from '../i18n';
import { Button, Input, Textarea } from '../components/ui';
import styles from './Contact.module.css';
// EmailJS configuration - replace these with your actual IDs
const EMAILJS_SERVICE_ID = 'YOUR_SERVICE_ID';
const EMAILJS_TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
const EMAILJS_PUBLIC_KEY = 'YOUR_PUBLIC_KEY';
interface FormData {
name: string;
email: string;
subject: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
subject?: string;
message?: string;
}
export function Contact() {
const { t } = useTranslation();
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Required';
}
if (!formData.email.trim()) {
newErrors.email = 'Required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email';
}
if (!formData.subject.trim()) {
newErrors.subject = 'Required';
}
if (!formData.message.trim()) {
newErrors.message = 'Required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
setSubmitStatus('idle');
try {
await emailjs.send(
EMAILJS_SERVICE_ID,
EMAILJS_TEMPLATE_ID,
{
from_name: formData.name,
from_email: formData.email,
subject: formData.subject,
message: formData.message,
},
EMAILJS_PUBLIC_KEY
);
setSubmitStatus('success');
setFormData({ name: '', email: '', subject: '', message: '' });
} catch {
setSubmitStatus('error');
} finally {
setIsSubmitting(false);
}
};
const handleChange = (field: keyof FormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
return (
<main className={styles.contact}>
{/* Hero */}
<section className={styles.hero}>
<div className="container">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className={styles.title}>{t.contact.title}</h1>
<p className={styles.subtitle}>{t.contact.subtitle}</p>
</motion.div>
</div>
</section>
{/* Content */}
<section className={styles.content}>
<div className="container">
<div className={styles.grid}>
{/* Form */}
<motion.div
className={styles.formSection}
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<p className={styles.intro}>{t.contact.intro}</p>
<form onSubmit={handleSubmit} className={styles.form}>
<Input
label={t.contact.form.name}
placeholder={t.contact.form.namePlaceholder}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
error={errors.name}
/>
<Input
label={t.contact.form.email}
type="email"
placeholder={t.contact.form.emailPlaceholder}
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
error={errors.email}
/>
<Input
label={t.contact.form.subject}
placeholder={t.contact.form.subjectPlaceholder}
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
error={errors.subject}
/>
<Textarea
label={t.contact.form.message}
placeholder={t.contact.form.messagePlaceholder}
value={formData.message}
onChange={(e) => handleChange('message', e.target.value)}
error={errors.message}
rows={6}
/>
<Button
type="submit"
variant="primary"
size="lg"
isLoading={isSubmitting}
disabled={isSubmitting}
>
{isSubmitting ? t.contact.form.sending : t.contact.form.submit}
</Button>
{submitStatus === 'success' && (
<motion.p
className={styles.success}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
{t.contact.form.success}
</motion.p>
)}
{submitStatus === 'error' && (
<motion.p
className={styles.error}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
{t.contact.form.error}
</motion.p>
)}
</form>
</motion.div>
{/* Info */}
<motion.div
className={styles.infoSection}
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<h2 className={styles.infoTitle}>{t.contact.info.title}</h2>
<div className={styles.infoList}>
<div className={styles.infoItem}>
<div className={styles.infoIcon}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
</div>
<div>
<h3>{t.contact.info.email}</h3>
<a href="mailto:info@ragusa-it.dev">info@ragusa-it.dev</a>
</div>
</div>
<div className={styles.infoItem}>
<div className={styles.infoIcon}>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
</div>
<div>
<h3>{t.contact.info.github}</h3>
<a
href="https://github.com/ragusa-it"
target="_blank"
rel="noopener noreferrer"
>
github.com/ragusa-it
</a>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
</main>
);
}