feat: Website-Rework mit verbessertem Design, Sicherheit und Deployment
- 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
This commit is contained in:
190
app/components/About.tsx
Normal file
190
app/components/About.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Code, Database, Cloud, Smartphone, Globe, Zap, Brain, Rocket } from 'lucide-react';
|
||||
|
||||
const About = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const skills = [
|
||||
{
|
||||
category: 'Frontend',
|
||||
icon: Code,
|
||||
technologies: ['React', 'Next.js', 'TypeScript', 'Tailwind CSS', 'Framer Motion'],
|
||||
color: 'from-blue-500 to-cyan-500'
|
||||
},
|
||||
{
|
||||
category: 'Backend',
|
||||
icon: Database,
|
||||
technologies: ['Node.js', 'PostgreSQL', 'Prisma', 'REST APIs', 'GraphQL'],
|
||||
color: 'from-purple-500 to-pink-500'
|
||||
},
|
||||
{
|
||||
category: 'DevOps',
|
||||
icon: Cloud,
|
||||
technologies: ['Docker', 'CI/CD', 'Nginx', 'Redis', 'AWS'],
|
||||
color: 'from-green-500 to-emerald-500'
|
||||
},
|
||||
{
|
||||
category: 'Mobile',
|
||||
icon: Smartphone,
|
||||
technologies: ['React Native', 'Expo', 'iOS', 'Android'],
|
||||
color: 'from-orange-500 to-red-500'
|
||||
},
|
||||
];
|
||||
|
||||
const values = [
|
||||
{
|
||||
icon: Brain,
|
||||
title: 'Problem Solving',
|
||||
description: 'I love tackling complex challenges and finding elegant solutions.'
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Performance',
|
||||
description: 'Building fast, efficient applications that scale with your needs.'
|
||||
},
|
||||
{
|
||||
icon: Rocket,
|
||||
title: 'Innovation',
|
||||
description: 'Always exploring new technologies and best practices.'
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'User Experience',
|
||||
description: 'Creating intuitive interfaces that users love to interact with.'
|
||||
},
|
||||
];
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="about" className="py-20 px-4 relative overflow-hidden">
|
||||
<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">
|
||||
About Me
|
||||
</h2>
|
||||
<p className="text-xl text-gray-400 max-w-3xl mx-auto leading-relaxed">
|
||||
I'm a passionate software engineer with a love for creating beautiful,
|
||||
functional applications. I enjoy working with modern technologies and
|
||||
turning ideas into reality.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* About Content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h3 className="text-3xl font-bold text-white mb-4">My Journey</h3>
|
||||
<p className="text-gray-300 leading-relaxed text-lg">
|
||||
I'm a student and software engineer based in Osnabrück, Germany.
|
||||
My passion for technology started early, and I've been building
|
||||
applications ever since.
|
||||
</p>
|
||||
<p className="text-gray-300 leading-relaxed text-lg">
|
||||
I specialize in full-stack development, with a focus on creating
|
||||
modern, performant web applications. I'm always learning new
|
||||
technologies and improving my skills.
|
||||
</p>
|
||||
<p className="text-gray-300 leading-relaxed text-lg">
|
||||
When I'm not coding, I enjoy exploring new technologies, contributing
|
||||
to open-source projects, and sharing knowledge with the developer community.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h3 className="text-3xl font-bold text-white mb-4">What I Do</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{values.map((value, index) => (
|
||||
<motion.div
|
||||
key={value.title}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ y: -5, scale: 1.02 }}
|
||||
className="p-6 rounded-xl glass-card"
|
||||
>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center mb-4">
|
||||
<value.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-white mb-2">{value.title}</h4>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">{value.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Skills Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="mb-16"
|
||||
>
|
||||
<h3 className="text-3xl font-bold text-white mb-8 text-center">Skills & Technologies</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{skills.map((skill, index) => (
|
||||
<motion.div
|
||||
key={skill.category}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
whileHover={{ y: -8, scale: 1.02 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<div className={`w-14 h-14 bg-gradient-to-br ${skill.color} rounded-xl flex items-center justify-center mb-4`}>
|
||||
<skill.icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white mb-4">{skill.category}</h4>
|
||||
<div className="space-y-2">
|
||||
{skill.technologies.map((tech) => (
|
||||
<div
|
||||
key={tech}
|
||||
className="px-3 py-1.5 bg-gray-800/50 rounded-lg text-sm text-gray-300 border border-gray-700/50"
|
||||
>
|
||||
{tech}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
|
||||
|
||||
@@ -20,10 +20,48 @@ const Contact = () => {
|
||||
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 {
|
||||
@@ -43,23 +81,42 @@ const Contact = () => {
|
||||
if (response.ok) {
|
||||
showEmailSent(formData.email);
|
||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
||||
setTouched({});
|
||||
setErrors({});
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
showEmailError(errorData.error || 'Unbekannter Fehler');
|
||||
showEmailError(errorData.error || 'Failed to send message. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
showEmailError('Netzwerkfehler beim Senden der E-Mail');
|
||||
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,
|
||||
[e.target.name]: e.target.value
|
||||
[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 = [
|
||||
@@ -159,7 +216,7 @@ const Contact = () => {
|
||||
<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
|
||||
Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -167,15 +224,25 @@ const Contact = () => {
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
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"
|
||||
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
|
||||
Email <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -183,16 +250,26 @@ const Contact = () => {
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
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"
|
||||
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
|
||||
Subject <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -200,39 +277,66 @@ const Contact = () => {
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
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"
|
||||
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
|
||||
Message <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
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..."
|
||||
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={{ 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"
|
||||
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...</span>
|
||||
<span>Sending Message...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -25,7 +25,7 @@ const Footer = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className="relative py-12 px-4 bg-black border-t border-gray-800/50">
|
||||
<footer className="relative py-12 px-4 bg-black/95 backdrop-blur-sm border-t border-gray-800/50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center space-y-6 md:space-y-0">
|
||||
{/* Brand */}
|
||||
@@ -36,11 +36,15 @@ const Footer = () => {
|
||||
transition={{ duration: 0.6 }}
|
||||
className="flex items-center space-x-3"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg flex items-center justify-center">
|
||||
<Code className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360, scale: 1.1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-500 rounded-xl flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<Code className="w-6 h-6 text-white" />
|
||||
</motion.div>
|
||||
<div>
|
||||
<Link href="/" className="text-xl font-bold font-mono text-white">
|
||||
<Link href="/" className="text-xl font-bold font-mono text-white hover:text-blue-400 transition-colors">
|
||||
dk<span className="text-red-500">0</span>
|
||||
</Link>
|
||||
<p className="text-xs text-gray-500">Software Engineer</p>
|
||||
@@ -53,7 +57,7 @@ const Footer = () => {
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="flex space-x-4"
|
||||
className="flex space-x-3"
|
||||
>
|
||||
{socialLinks.map((social) => (
|
||||
<motion.a
|
||||
@@ -61,9 +65,10 @@ const Footer = () => {
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.1, y: -2 }}
|
||||
whileHover={{ scale: 1.15, y: -3 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-3 bg-gray-800/50 hover:bg-gray-700/50 rounded-lg text-gray-300 hover:text-white transition-all duration-200"
|
||||
className="p-3 bg-gray-800/60 backdrop-blur-sm hover:bg-gray-700/60 rounded-xl text-gray-300 hover:text-white transition-all duration-200 border border-gray-700/50 hover:border-gray-600 shadow-lg"
|
||||
aria-label={social.label}
|
||||
>
|
||||
<social.icon size={18} />
|
||||
</motion.a>
|
||||
@@ -112,8 +117,13 @@ const Footer = () => {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-600">
|
||||
Built with Next.js, TypeScript & Tailwind CSS
|
||||
<div className="text-xs text-gray-500 flex items-center space-x-1">
|
||||
<span>Built with</span>
|
||||
<span className="text-blue-400 font-semibold">Next.js</span>
|
||||
<span className="text-gray-600">•</span>
|
||||
<span className="text-blue-400 font-semibold">TypeScript</span>
|
||||
<span className="text-gray-600">•</span>
|
||||
<span className="text-blue-400 font-semibold">Tailwind CSS</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -26,8 +26,8 @@ const Header = () => {
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Home', href: '/' },
|
||||
{ name: 'Projects', href: '/projects' },
|
||||
{ name: 'About', href: '#about' },
|
||||
{ name: 'Projects', href: '#projects' },
|
||||
{ name: 'Contact', href: '#contact' },
|
||||
];
|
||||
|
||||
@@ -85,10 +85,19 @@ const Header = () => {
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="text-gray-300 hover:text-white transition-colors duration-200 font-medium relative group"
|
||||
className="text-gray-300 hover:text-white transition-colors duration-200 font-medium relative group px-2 py-1"
|
||||
onClick={(e) => {
|
||||
if (item.href.startsWith('#')) {
|
||||
e.preventDefault();
|
||||
const element = document.querySelector(item.href);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-300 group-hover:w-full"></span>
|
||||
<span className="absolute -bottom-1 left-2 right-2 h-0.5 bg-gradient-to-r from-blue-500 to-purple-500 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left"></span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -122,50 +131,77 @@ const Header = () => {
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="md:hidden glass"
|
||||
>
|
||||
<div className="px-4 py-6 space-y-4">
|
||||
{navItems.map((item) => (
|
||||
<motion.div
|
||||
key={item.name}
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: navItems.indexOf(item) * 0.1 }}
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="block text-gray-300 hover:text-white transition-colors duration-200 font-medium py-2"
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 md:hidden"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="md:hidden glass border-t border-gray-800/50 z-50 relative"
|
||||
>
|
||||
<div className="px-4 py-6 space-y-2">
|
||||
{navItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.name}
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -20, opacity: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
<div className="flex space-x-4">
|
||||
{socialLinks.map((social) => (
|
||||
<motion.a
|
||||
key={social.label}
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-3 rounded-lg bg-gray-800/50 hover:bg-gray-700/50 transition-colors duration-200 text-gray-300 hover:text-white"
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={(e) => {
|
||||
setIsOpen(false);
|
||||
if (item.href.startsWith('#')) {
|
||||
e.preventDefault();
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(item.href);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="block text-gray-300 hover:text-white transition-all duration-200 font-medium py-3 px-4 rounded-lg hover:bg-gray-800/50 border-l-2 border-transparent hover:border-blue-500"
|
||||
>
|
||||
<social.icon size={20} />
|
||||
</motion.a>
|
||||
))}
|
||||
{item.name}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<div className="pt-4 mt-4 border-t border-gray-700/50">
|
||||
<p className="text-xs text-gray-500 mb-3 px-4">Connect with me</p>
|
||||
<div className="flex space-x-3 px-4">
|
||||
{socialLinks.map((social, index) => (
|
||||
<motion.a
|
||||
key={social.label}
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: (navItems.length + index) * 0.05 }}
|
||||
whileHover={{ scale: 1.1, y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-3 rounded-xl bg-gray-800/50 hover:bg-gray-700/50 transition-all duration-200 text-gray-300 hover:text-white"
|
||||
aria-label={social.label}
|
||||
>
|
||||
<social.icon size={20} />
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.header>
|
||||
|
||||
@@ -203,20 +203,21 @@ const Hero = () => {
|
||||
>
|
||||
<motion.a
|
||||
href="#projects"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileHover={{ scale: 1.05, y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="btn-primary px-8 py-4 text-lg font-semibold"
|
||||
className="btn-primary px-8 py-4 text-lg font-semibold inline-flex items-center space-x-2"
|
||||
>
|
||||
View My Work
|
||||
<span>View My Work</span>
|
||||
<ArrowDown className="w-5 h-5" />
|
||||
</motion.a>
|
||||
|
||||
<motion.a
|
||||
href="#contact"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileHover={{ scale: 1.05, y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="px-8 py-4 text-lg font-semibold border-2 border-gray-600 text-gray-300 hover:text-white hover:border-gray-500 rounded-lg transition-all duration-200"
|
||||
className="btn-secondary px-8 py-4 text-lg font-semibold inline-flex items-center space-x-2"
|
||||
>
|
||||
Contact Me
|
||||
<span>Contact Me</span>
|
||||
</motion.a>
|
||||
</motion.div>
|
||||
|
||||
@@ -224,17 +225,18 @@ const Hero = () => {
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1, delay: 1.5 }}
|
||||
transition={{ duration: 1, delay: 2 }}
|
||||
className="mt-12 md:mt-16 text-center relative z-20"
|
||||
>
|
||||
<motion.div
|
||||
<motion.a
|
||||
href="#about"
|
||||
animate={{ y: [0, 10, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="flex flex-col items-center text-white/90 bg-black/30 backdrop-blur-md px-6 py-3 rounded-full border border-white/20 shadow-lg"
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="inline-flex flex-col items-center text-white/90 bg-black/30 backdrop-blur-md px-6 py-3 rounded-full border border-white/20 shadow-lg hover:bg-black/50 hover:border-white/30 transition-all cursor-pointer group"
|
||||
>
|
||||
<span className="text-sm md:text-base mb-2 font-medium">Scroll Down</span>
|
||||
<ArrowDown className="w-5 h-5 md:w-6 md:h-6" />
|
||||
</motion.div>
|
||||
<span className="text-sm md:text-base mb-2 font-medium group-hover:text-white transition-colors">Scroll Down</span>
|
||||
<ArrowDown className="w-5 h-5 md:w-6 md:h-6 group-hover:translate-y-1 transition-transform" />
|
||||
</motion.a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -76,39 +76,47 @@ const Projects = () => {
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
whileHover={{ y: -10 }}
|
||||
className={`group relative overflow-hidden rounded-2xl glass-card card-hover ${
|
||||
project.featured ? 'ring-2 ring-blue-500/50' : ''
|
||||
whileHover={{ y: -12, scale: 1.02 }}
|
||||
className={`group relative overflow-hidden rounded-2xl glass-card card-hover border border-gray-800/50 hover:border-gray-700/50 transition-all ${
|
||||
project.featured ? 'ring-2 ring-blue-500/30 shadow-lg shadow-blue-500/10' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-purple-500/20" />
|
||||
<div className="absolute inset-0 bg-gray-800/50 flex flex-col items-center justify-center p-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center mb-2">
|
||||
<div className="relative h-48 overflow-hidden bg-gradient-to-br from-gray-900 to-gray-800">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 via-purple-500/10 to-pink-500/10" />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center p-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
className="w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-500 rounded-2xl flex items-center justify-center mb-3 shadow-lg"
|
||||
>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{project.title.split(' ').map(word => word[0]).join('').toUpperCase()}
|
||||
{project.title.split(' ').map(word => word[0]).join('').toUpperCase().slice(0, 2)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-400 text-center leading-tight">
|
||||
</motion.div>
|
||||
<span className="text-sm font-semibold text-gray-300 text-center leading-tight px-2">
|
||||
{project.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{project.featured && (
|
||||
<div className="absolute top-4 right-4 px-3 py-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-xs font-semibold rounded-full">
|
||||
Featured
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute top-4 right-4 px-3 py-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-xs font-semibold rounded-full shadow-lg"
|
||||
>
|
||||
⭐ Featured
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center space-x-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end justify-center pb-4 space-x-3">
|
||||
{project.github && project.github.trim() !== '' && project.github !== '#' && (
|
||||
<motion.a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileHover={{ scale: 1.15, y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-3 bg-gray-800/80 rounded-lg text-white hover:bg-gray-700/80 transition-colors"
|
||||
className="p-3 bg-gray-800/90 backdrop-blur-sm rounded-xl text-white hover:bg-gray-700/90 transition-all shadow-lg border border-gray-700/50"
|
||||
aria-label="View on GitHub"
|
||||
>
|
||||
<Github size={20} />
|
||||
</motion.a>
|
||||
@@ -118,9 +126,10 @@ const Projects = () => {
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileHover={{ scale: 1.15, y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-3 bg-blue-600/80 rounded-lg text-white hover:bg-blue-500/80 transition-colors"
|
||||
className="p-3 bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl text-white hover:from-blue-500 hover:to-purple-500 transition-all shadow-lg"
|
||||
aria-label="View live site"
|
||||
>
|
||||
<ExternalLink size={20} />
|
||||
</motion.a>
|
||||
@@ -129,37 +138,42 @@ const Projects = () => {
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-blue-400 transition-colors">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-blue-400 transition-colors flex-1 pr-2">
|
||||
{project.title}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2 text-gray-400">
|
||||
<Calendar size={16} />
|
||||
<span className="text-sm">{project.date}</span>
|
||||
<div className="flex items-center space-x-1.5 text-gray-400 flex-shrink-0">
|
||||
<Calendar size={14} />
|
||||
<span className="text-xs">{new Date(project.date).getFullYear()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 mb-4 leading-relaxed">
|
||||
<p className="text-gray-300 mb-4 leading-relaxed line-clamp-3">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{project.tags.map((tag) => (
|
||||
<div className="flex flex-wrap gap-2 mb-5">
|
||||
{project.tags.slice(0, 4).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 bg-gray-800/50 text-gray-300 text-sm rounded-full border border-gray-700"
|
||||
className="px-3 py-1 bg-gray-800/60 backdrop-blur-sm text-gray-300 text-xs rounded-lg border border-gray-700/50 hover:border-gray-600 transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{project.tags.length > 4 && (
|
||||
<span className="px-3 py-1 bg-gray-800/60 backdrop-blur-sm text-gray-400 text-xs rounded-lg border border-gray-700/50">
|
||||
+{project.tags.length - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
||||
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors font-medium"
|
||||
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-all font-semibold group/link"
|
||||
>
|
||||
<span>View Project</span>
|
||||
<ExternalLink size={16} />
|
||||
<span>View Details</span>
|
||||
<ExternalLink size={16} className="group-hover/link:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user