- Modify Dockerfile to install curl without recommended packages for a leaner image. - Update Next.js configuration to set outputFileTracingRoot for better Docker compatibility. - Revise contact components to improve messaging and clarity, changing "Get In Touch" to "Contact Me" and enhancing descriptions for collaboration opportunities. - Clean up Prisma schema by removing unnecessary comments and restructuring the Project model for clarity.
253 lines
8.7 KiB
TypeScript
253 lines
8.7 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 [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
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: '' });
|
|
} else {
|
|
const errorData = await response.json();
|
|
showEmailError(errorData.error || 'Unbekannter Fehler');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending email:', error);
|
|
showEmailError('Netzwerkfehler beim Senden der E-Mail');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
setFormData({
|
|
...formData,
|
|
[e.target.name]: e.target.value
|
|
});
|
|
};
|
|
|
|
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
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleChange}
|
|
required
|
|
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder="Your name"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
required
|
|
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder="your@email.com"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="subject" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Subject
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="subject"
|
|
name="subject"
|
|
value={formData.subject}
|
|
onChange={handleChange}
|
|
required
|
|
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder="What's this about?"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="message" className="block text-sm font-medium text-gray-300 mb-2">
|
|
Message
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
name="message"
|
|
value={formData.message}
|
|
onChange={handleChange}
|
|
required
|
|
rows={5}
|
|
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all resize-none"
|
|
placeholder="Tell me more about your project..."
|
|
/>
|
|
</div>
|
|
|
|
<motion.button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ 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"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
<span>Sending...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Send size={20} />
|
|
<span>Send Message</span>
|
|
</>
|
|
)}
|
|
</motion.button>
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Contact;
|