- Neue About/Skills-Sektion hinzugefügt - Verbesserte UI/UX für alle Komponenten - Enhanced Contact Form mit Validierung - Verbesserte Security Headers und Middleware - Sichere Deployment-Skripte (safe-deploy.sh) - Zero-Downtime Deployment Support - Verbesserte Docker-Sicherheit - Umfassende Sicherheits-Dokumentation - Performance-Optimierungen - Accessibility-Verbesserungen
357 lines
13 KiB
TypeScript
357 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Mail, MapPin, Send } from 'lucide-react';
|
|
import { useToast } from '@/components/Toast';
|
|
|
|
const Contact = () => {
|
|
const [mounted, setMounted] = useState(false);
|
|
const { showEmailSent, showEmailError } = useToast();
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
subject: '',
|
|
message: ''
|
|
});
|
|
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
|
|
|
const validateForm = () => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
if (!formData.name.trim()) {
|
|
newErrors.name = 'Name is required';
|
|
} else if (formData.name.trim().length < 2) {
|
|
newErrors.name = 'Name must be at least 2 characters';
|
|
}
|
|
|
|
if (!formData.email.trim()) {
|
|
newErrors.email = 'Email is required';
|
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
newErrors.email = 'Please enter a valid email address';
|
|
}
|
|
|
|
if (!formData.subject.trim()) {
|
|
newErrors.subject = 'Subject is required';
|
|
} else if (formData.subject.trim().length < 3) {
|
|
newErrors.subject = 'Subject must be at least 3 characters';
|
|
}
|
|
|
|
if (!formData.message.trim()) {
|
|
newErrors.message = 'Message is required';
|
|
} else if (formData.message.trim().length < 10) {
|
|
newErrors.message = 'Message must be at least 10 characters';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/email', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: formData.name,
|
|
email: formData.email,
|
|
subject: formData.subject,
|
|
message: formData.message,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
showEmailSent(formData.email);
|
|
setFormData({ name: '', email: '', subject: '', message: '' });
|
|
setTouched({});
|
|
setErrors({});
|
|
} else {
|
|
const errorData = await response.json();
|
|
showEmailError(errorData.error || 'Failed to send message. Please try again.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending email:', error);
|
|
showEmailError('Network error. Please check your connection and try again.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData({
|
|
...formData,
|
|
[name]: value
|
|
});
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[name]) {
|
|
setErrors({
|
|
...errors,
|
|
[name]: ''
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
setTouched({
|
|
...touched,
|
|
[e.target.name]: true
|
|
});
|
|
validateForm();
|
|
};
|
|
|
|
const contactInfo = [
|
|
{
|
|
icon: Mail,
|
|
title: 'Email',
|
|
value: 'contact@dk0.dev',
|
|
href: 'mailto:contact@dk0.dev'
|
|
},
|
|
{
|
|
icon: MapPin,
|
|
title: 'Location',
|
|
value: 'Osnabrück, Germany',
|
|
}
|
|
];
|
|
|
|
|
|
if (!mounted) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<section id="contact" className="py-20 px-4 relative">
|
|
<div className="max-w-7xl mx-auto">
|
|
{/* Section Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8 }}
|
|
className="text-center mb-16"
|
|
>
|
|
<h2 className="text-4xl md:text-5xl font-bold mb-6 gradient-text">
|
|
Contact Me
|
|
</h2>
|
|
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
|
Interested in working together or have questions about my projects? Feel free to reach out!
|
|
</p>
|
|
</motion.div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
|
{/* Contact Information */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -30 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8 }}
|
|
className="space-y-8"
|
|
>
|
|
<div>
|
|
<h3 className="text-2xl font-bold text-white mb-6">
|
|
Get In Touch
|
|
</h3>
|
|
<p className="text-gray-400 leading-relaxed">
|
|
I'm always available to discuss new opportunities, interesting projects,
|
|
or simply chat about technology and innovation.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Contact Details */}
|
|
<div className="space-y-4">
|
|
{contactInfo.map((info, index) => (
|
|
<motion.a
|
|
key={info.title}
|
|
href={info.href}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
|
whileHover={{ x: 5 }}
|
|
className="flex items-center space-x-4 p-4 rounded-lg glass-card hover:bg-gray-800/30 transition-colors group"
|
|
>
|
|
<div className="p-3 bg-blue-500/20 rounded-lg group-hover:bg-blue-500/30 transition-colors">
|
|
<info.icon className="w-6 h-6 text-blue-400" />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-white">{info.title}</h4>
|
|
<p className="text-gray-400">{info.value}</p>
|
|
</div>
|
|
</motion.a>
|
|
))}
|
|
</div>
|
|
|
|
</motion.div>
|
|
|
|
{/* Contact Form */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 30 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8 }}
|
|
className="glass-card p-8 rounded-2xl"
|
|
>
|
|
<h3 className="text-2xl font-bold text-white mb-6">Send Message</h3>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Name <span className="text-red-400">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
required
|
|
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all ${
|
|
errors.name && touched.name
|
|
? 'border-red-500 focus:ring-red-500'
|
|
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
|
|
}`}
|
|
placeholder="Your name"
|
|
aria-invalid={errors.name && touched.name ? 'true' : 'false'}
|
|
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
|
|
/>
|
|
{errors.name && touched.name && (
|
|
<p id="name-error" className="mt-1 text-sm text-red-400">{errors.name}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Email <span className="text-red-400">*</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
required
|
|
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all ${
|
|
errors.email && touched.email
|
|
? 'border-red-500 focus:ring-red-500'
|
|
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
|
|
}`}
|
|
placeholder="your@email.com"
|
|
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
|
|
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
|
|
/>
|
|
{errors.email && touched.email && (
|
|
<p id="email-error" className="mt-1 text-sm text-red-400">{errors.email}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="subject" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Subject <span className="text-red-400">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="subject"
|
|
name="subject"
|
|
value={formData.subject}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
required
|
|
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all ${
|
|
errors.subject && touched.subject
|
|
? 'border-red-500 focus:ring-red-500'
|
|
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
|
|
}`}
|
|
placeholder="What's this about?"
|
|
aria-invalid={errors.subject && touched.subject ? 'true' : 'false'}
|
|
aria-describedby={errors.subject && touched.subject ? 'subject-error' : undefined}
|
|
/>
|
|
{errors.subject && touched.subject && (
|
|
<p id="subject-error" className="mt-1 text-sm text-red-400">{errors.subject}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="message" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Message <span className="text-red-400">*</span>
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
name="message"
|
|
value={formData.message}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
required
|
|
rows={6}
|
|
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all resize-none ${
|
|
errors.message && touched.message
|
|
? 'border-red-500 focus:ring-red-500'
|
|
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
|
|
}`}
|
|
placeholder="Tell me more about your project or question..."
|
|
aria-invalid={errors.message && touched.message ? 'true' : 'false'}
|
|
aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
|
|
/>
|
|
<div className="flex justify-between items-center mt-1">
|
|
{errors.message && touched.message ? (
|
|
<p id="message-error" className="text-sm text-red-400">{errors.message}</p>
|
|
) : (
|
|
<span></span>
|
|
)}
|
|
<span className="text-xs text-gray-500">
|
|
{formData.message.length} characters
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<motion.button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
whileHover={!isSubmitting ? { scale: 1.02, y: -2 } : {}}
|
|
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
|
|
className="w-full btn-primary py-4 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 disabled:hover:scale-100 disabled:hover:translate-y-0"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
<span>Sending Message...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Send size={20} />
|
|
<span>Send Message</span>
|
|
</>
|
|
)}
|
|
</motion.button>
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Contact;
|