full upgrade to dev
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Mail, MapPin, Send } from 'lucide-react';
|
||||
import { useToast } from '@/components/Toast';
|
||||
import { LiquidHeading } from '@/components/LiquidHeading';
|
||||
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);
|
||||
@@ -15,10 +14,10 @@ const Contact = () => {
|
||||
}, []);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
name: "",
|
||||
email: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
@@ -29,27 +28,27 @@ const Contact = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Name is required';
|
||||
newErrors.name = "Name is required";
|
||||
} else if (formData.name.trim().length < 2) {
|
||||
newErrors.name = 'Name must be at least 2 characters';
|
||||
newErrors.name = "Name must be at least 2 characters";
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required';
|
||||
newErrors.email = "Email is required";
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email address';
|
||||
newErrors.email = "Please enter a valid email address";
|
||||
}
|
||||
|
||||
if (!formData.subject.trim()) {
|
||||
newErrors.subject = 'Subject is required';
|
||||
newErrors.subject = "Subject is required";
|
||||
} else if (formData.subject.trim().length < 3) {
|
||||
newErrors.subject = 'Subject must be at least 3 characters';
|
||||
newErrors.subject = "Subject must be at least 3 characters";
|
||||
}
|
||||
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = 'Message is required';
|
||||
newErrors.message = "Message is required";
|
||||
} else if (formData.message.trim().length < 10) {
|
||||
newErrors.message = 'Message must be at least 10 characters';
|
||||
newErrors.message = "Message must be at least 10 characters";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@@ -58,18 +57,18 @@ const Contact = () => {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/email', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/api/email", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
@@ -81,41 +80,49 @@ const Contact = () => {
|
||||
|
||||
if (response.ok) {
|
||||
showEmailSent(formData.email);
|
||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
||||
setFormData({ name: "", email: "", subject: "", message: "" });
|
||||
setTouched({});
|
||||
setErrors({});
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
showEmailError(errorData.error || 'Failed to send message. Please try again.');
|
||||
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.');
|
||||
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 handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value
|
||||
[name]: value,
|
||||
});
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[name]) {
|
||||
setErrors({
|
||||
...errors,
|
||||
[name]: ''
|
||||
[name]: "",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const handleBlur = (
|
||||
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setTouched({
|
||||
...touched,
|
||||
[e.target.name]: true
|
||||
[e.target.name]: true,
|
||||
});
|
||||
validateForm();
|
||||
};
|
||||
@@ -123,53 +130,61 @@ const Contact = () => {
|
||||
const contactInfo = [
|
||||
{
|
||||
icon: Mail,
|
||||
title: 'Email',
|
||||
value: 'contact@dk0.dev',
|
||||
href: 'mailto:contact@dk0.dev'
|
||||
title: "Email",
|
||||
value: "contact@dk0.dev",
|
||||
href: "mailto:contact@dk0.dev",
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
title: 'Location',
|
||||
value: 'Osnabrück, Germany',
|
||||
}
|
||||
title: "Location",
|
||||
value: "Osnabrück, Germany",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-24 px-4 relative bg-cream">
|
||||
<section
|
||||
id="contact"
|
||||
className="py-24 px-4 relative bg-gradient-to-br from-liquid-teal/15 via-liquid-mint/10 to-liquid-lime/15"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<LiquidHeading
|
||||
text="Contact Me"
|
||||
level={2}
|
||||
className="text-4xl md:text-5xl font-bold mb-6 text-stone-800"
|
||||
/>
|
||||
<p className="text-xl text-stone-500 max-w-2xl mx-auto mt-4">
|
||||
Interested in working together or have questions about my projects? Feel free to reach out!
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6 text-stone-900">
|
||||
Contact Me
|
||||
</h2>
|
||||
<p className="text-xl text-stone-700 max-w-2xl mx-auto mt-4">
|
||||
Interested in working together or have questions about my projects?
|
||||
Feel free to reach out!
|
||||
</p>
|
||||
</div>
|
||||
</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 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-stone-800 mb-6">
|
||||
<h3 className="text-2xl font-bold text-stone-900 mb-6">
|
||||
Get In Touch
|
||||
</h3>
|
||||
<p className="text-stone-600 leading-relaxed">
|
||||
I'm always available to discuss new opportunities, interesting projects,
|
||||
or simply chat about technology and innovation.
|
||||
<p className="text-stone-700 leading-relaxed">
|
||||
I'm always available to discuss new opportunities,
|
||||
interesting projects, or simply chat about technology and
|
||||
innovation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -182,37 +197,50 @@ const Contact = () => {
|
||||
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-2xl glass-card hover:bg-white/60 transition-colors group border-transparent hover:border-white/60"
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: index * 0.15,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
}}
|
||||
whileHover={{
|
||||
x: 8,
|
||||
transition: { duration: 0.4, ease: "easeOut" },
|
||||
}}
|
||||
className="flex items-center space-x-4 p-4 rounded-2xl glass-card hover:bg-white/80 transition-all duration-500 ease-out group border-transparent hover:border-white/70"
|
||||
>
|
||||
<div className="p-3 bg-white rounded-xl shadow-sm group-hover:shadow-md transition-all">
|
||||
<info.icon className="w-6 h-6 text-stone-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-stone-800">{info.title}</h4>
|
||||
<h4 className="font-semibold text-stone-800">
|
||||
{info.title}
|
||||
</h4>
|
||||
<p className="text-stone-500">{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-3xl bg-white/40 border border-white/60"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="glass-card p-8 rounded-3xl bg-white/50 border border-white/70"
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-stone-800 mb-6">Send Message</h3>
|
||||
|
||||
<h3 className="text-2xl font-bold text-stone-800 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-stone-600 mb-2">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-stone-600 mb-2"
|
||||
>
|
||||
Name <span className="text-liquid-rose">*</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -225,20 +253,29 @@ const Contact = () => {
|
||||
required
|
||||
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all ${
|
||||
errors.name && touched.name
|
||||
? 'border-red-400 focus:ring-red-400'
|
||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
||||
? "border-red-400 focus:ring-red-400"
|
||||
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
|
||||
}`}
|
||||
placeholder="Your name"
|
||||
aria-invalid={errors.name && touched.name ? 'true' : 'false'}
|
||||
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
|
||||
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-500">{errors.name}</p>
|
||||
<p id="name-error" className="mt-1 text-sm text-red-500">
|
||||
{errors.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-stone-600 mb-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-stone-600 mb-2"
|
||||
>
|
||||
Email <span className="text-liquid-rose">*</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -251,21 +288,30 @@ const Contact = () => {
|
||||
required
|
||||
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all ${
|
||||
errors.email && touched.email
|
||||
? 'border-red-400 focus:ring-red-400'
|
||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
||||
? "border-red-400 focus:ring-red-400"
|
||||
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
|
||||
}`}
|
||||
placeholder="your@email.com"
|
||||
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
|
||||
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
|
||||
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-500">{errors.email}</p>
|
||||
<p id="email-error" className="mt-1 text-sm text-red-500">
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-sm font-medium text-stone-600 mb-2">
|
||||
<label
|
||||
htmlFor="subject"
|
||||
className="block text-sm font-medium text-stone-600 mb-2"
|
||||
>
|
||||
Subject <span className="text-liquid-rose">*</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -278,20 +324,31 @@ const Contact = () => {
|
||||
required
|
||||
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all ${
|
||||
errors.subject && touched.subject
|
||||
? 'border-red-400 focus:ring-red-400'
|
||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
||||
? "border-red-400 focus:ring-red-400"
|
||||
: "border-white/60 focus:ring-liquid-blue 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}
|
||||
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-500">{errors.subject}</p>
|
||||
<p id="subject-error" className="mt-1 text-sm text-red-500">
|
||||
{errors.subject}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-stone-600 mb-2">
|
||||
<label
|
||||
htmlFor="message"
|
||||
className="block text-sm font-medium text-stone-600 mb-2"
|
||||
>
|
||||
Message <span className="text-liquid-rose">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
@@ -304,16 +361,24 @@ const Contact = () => {
|
||||
rows={6}
|
||||
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all resize-none ${
|
||||
errors.message && touched.message
|
||||
? 'border-red-400 focus:ring-red-400'
|
||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
||||
? "border-red-400 focus:ring-red-400"
|
||||
: "border-white/60 focus:ring-liquid-blue 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}
|
||||
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-500">{errors.message}</p>
|
||||
<p id="message-error" className="text-sm text-red-500">
|
||||
{errors.message}
|
||||
</p>
|
||||
) : (
|
||||
<span></span>
|
||||
)}
|
||||
@@ -328,7 +393,8 @@ const Contact = () => {
|
||||
disabled={isSubmitting}
|
||||
whileHover={!isSubmitting ? { scale: 1.02, y: -2 } : {}}
|
||||
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
|
||||
className="w-full 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 bg-stone-900 text-white rounded-xl hover:bg-black transition-all shadow-lg"
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="w-full 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 bg-stone-900 text-white rounded-xl hover:bg-stone-950 transition-all duration-500 ease-out shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user