feat: Initialize backend with Express and MySQL, and restructure frontend with new routing and language support.

This commit is contained in:
Van Leemput Dayron
2025-12-15 17:03:03 +01:00
parent 56897a0c2d
commit 6c11cf5213
54 changed files with 2000 additions and 174 deletions

View File

@@ -0,0 +1,136 @@
import { motion } from 'framer-motion';
import { User, Heart, Target, Coffee } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const About = () => {
const { t } = useLanguage();
const stats = [
{ icon: <User size={24} />, value: t('about.stats.year'), label: t('about.stats.yearLabel') },
{ icon: <Heart size={24} />, value: t('about.stats.passion'), label: t('about.stats.passionLabel') },
{ icon: <Target size={24} />, value: t('about.stats.goals'), label: t('about.stats.goalsLabel') },
{ icon: <Coffee size={24} />, value: t('about.stats.fuel'), label: t('about.stats.fuelLabel') }
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="about" className="about">
<div className="container">
<motion.div
key="about-header"
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">{t('about.title')}</h2>
<p className="section-subtitle">
{t('about.subtitle')}
</p>
</motion.div>
<div className="about-content">
<motion.div
key="about-text"
className="about-text"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.div className="about-card" variants={itemVariants}>
<h3>{t('about.journey.title')}</h3>
<p>
{t('about.journey.content')}
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>{t('about.passion.title')}</h3>
<p>
{t('about.passion.content')}
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>{t('about.goals.title')}</h3>
<p>
{t('about.goals.content')}
</p>
</motion.div>
</motion.div>
<motion.div
key="about-stats"
className="about-stats"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{stats.map((stat, index) => (
<motion.div
key={`stat-${index}`}
className="stat-card"
variants={itemVariants}
whileHover={{
scale: 1.05,
y: -5,
transition: { duration: 0.3 }
}}
>
<div className="stat-icon">
{stat.icon}
</div>
<div className="stat-value">{stat.value}</div>
<div className="stat-label">{stat.label}</div>
</motion.div>
))}
</motion.div>
</div>
<motion.div
key="about-highlight"
className="about-highlight"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.5 }}
viewport={{ once: true }}
>
<div className="highlight-content">
<h3>{t('about.quote.title')}</h3>
<p>
"{t('about.quote.content')}"
</p>
<cite>- {t('about.quote.author')}</cite>
</div>
</motion.div>
</div>
</section>
);
};
export default About;

View File

@@ -0,0 +1,381 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Mail, Phone, MapPin, Send, Github, Linkedin, MessageCircle, CheckCircle, AlertCircle } from 'lucide-react';
import { sendContactEmail } from '../services/emailService';
import type { ContactFormData } from '../services/emailService';
import { useLanguage } from '../contexts/LanguageContext';
const Contact = () => {
const { t } = useLanguage();
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Vérification du délai de 15 minutes par email
const storageKey = `lastMessageTime_${formData.email}`;
const lastMessageTime = localStorage.getItem(storageKey);
if (lastMessageTime) {
const timeSinceLastMessage = Date.now() - parseInt(lastMessageTime, 10);
const fifteenMinutes = 15 * 60 * 1000;
if (timeSinceLastMessage < fifteenMinutes) {
const remainingMinutes = Math.ceil((fifteenMinutes - timeSinceLastMessage) / 60000);
setError(`Veuillez attendre ${remainingMinutes} minutes avant d'envoyer un nouveau message avec cette adresse email.`);
return;
}
}
setIsSubmitting(true);
setError(null);
try {
const contactData: ContactFormData = {
name: formData.name,
email: formData.email,
subject: formData.subject,
message: formData.message
};
const result = await sendContactEmail(contactData);
if (result.success) {
localStorage.setItem(`lastMessageTime_${formData.email}`, Date.now().toString());
setIsSubmitted(true);
setFormData({ name: '', email: '', subject: '', message: '' });
// Reset du message de succès après 5 secondes
setTimeout(() => {
setIsSubmitted(false);
}, 5000);
} else {
setError(result.message);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Une erreur est survenue lors de l\'envoi du message.';
setError(errorMessage);
console.error('Erreur lors de l\'envoi de l\'email:', err);
} finally {
setIsSubmitting(false);
}
};
const contactInfo = [
{
icon: <Mail size={24} />,
title: "Email",
content: "dayronvanleemput@gmail.com",
link: "mailto:dayronvanleemput@gmail.com",
color: "#EA4335"
},
{
icon: <Phone size={24} />,
title: "Téléphone",
content: "+32 455 19 47 62",
link: "tel:+32455194762",
color: "#34A853"
},
{
icon: <MapPin size={24} />,
title: "Localisation",
content: "Ath, Belgique",
link: "https://maps.google.com/?q=Ath,Belgium",
color: "#4285F4"
}
];
const socialLinks = [
{
icon: <Github size={24} />,
name: "GitHub",
url: "https://git.xeewy.be/Xeewy", // Remplacez par votre profil
color: "#333"
},
{
icon: <Linkedin size={24} />,
name: "LinkedIn",
url: "https://www.linkedin.com/in/dayron-van-leemput-992a94398", // Remplacez par votre profil
color: "#0077B5"
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="contact" className="contact">
<div className="container">
<motion.div
key="contact-header"
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">{t('contact.title')}</h2>
<p className="section-subtitle">
{t('contact.subtitle')}
</p>
</motion.div>
<motion.div
key="contact-content"
className="contact-content"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{/* Informations de contact */}
<motion.div className="contact-info" variants={itemVariants}>
<div className="contact-intro">
<h3>
<MessageCircle size={24} />
{t('contact.stayInTouch')}
</h3>
<p>
{t('contact.intro')}
</p>
</div>
<div className="contact-methods">
{contactInfo.map((contact, index) => (
<motion.a
key={index}
href={contact.link}
className="contact-method"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.05,
x: 10,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
<div
className="contact-icon"
style={{ backgroundColor: `${contact.color}20`, color: contact.color }}
>
{contact.icon}
</div>
<div className="contact-details">
<h4>{contact.title}</h4>
<span>{contact.content}</span>
</div>
</motion.a>
))}
</div>
<div className="social-links">
<h4>{t('contact.findMeOn')}</h4>
<div className="social-grid">
{socialLinks.map((social, index) => (
<motion.a
key={index}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="social-link"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.1,
rotate: 5,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
<div
className="social-icon"
style={{ backgroundColor: `${social.color}20`, color: social.color }}
>
{social.icon}
</div>
<span>{social.name}</span>
</motion.a>
))}
</div>
</div>
</motion.div>
{/* Formulaire de contact */}
<motion.div className="contact-form-container" variants={itemVariants}>
<h3>{t('contact.sendMessage')}</h3>
{isSubmitted && (
<motion.div
className="success-message"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
>
<CheckCircle size={20} />
{t('contact.success')}
</motion.div>
)}
{error && (
<motion.div
className="error-message"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
>
<AlertCircle size={20} />
{error}
</motion.div>
)}
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-row">
<motion.div
className="form-group"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
viewport={{ once: true }}
>
<label htmlFor="name">{t('contact.form.name')}</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
placeholder={t('contact.form.name')}
/>
</motion.div>
<motion.div
className="form-group"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<label htmlFor="email">{t('contact.form.email')}</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
placeholder="votre.email@example.com"
/>
</motion.div>
</div>
<motion.div
className="form-group"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
viewport={{ once: true }}
>
<label htmlFor="subject">{t('contact.form.subject')}</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
required
placeholder={t('contact.form.subject')}
/>
</motion.div>
<motion.div
className="form-group"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
viewport={{ once: true }}
>
<label htmlFor="message">{t('contact.form.message')}</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
required
rows={6}
placeholder={t('contact.form.message')}
/>
</motion.div>
<motion.button
type="submit"
className={`submit-btn ${isSubmitting ? 'submitting' : ''}`}
disabled={isSubmitting}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
whileHover={!isSubmitting ? { scale: 1.05 } : {}}
whileTap={!isSubmitting ? { scale: 0.95 } : {}}
viewport={{ once: true }}
>
{isSubmitting ? (
<>
<div className="loading-spinner" />
{t('contact.form.sending')}
</>
) : (
<>
<Send size={20} />
{t('contact.form.send')}
</>
)}
</motion.button>
</form>
</motion.div>
</motion.div>
</div>
</section>
);
};
export default Contact;

View File

@@ -0,0 +1,291 @@
import { motion } from 'framer-motion';
import { GraduationCap, Calendar, MapPin, Award, BookOpen, Target } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const Education = () => {
const { t } = useLanguage();
const education = [
{
id: 1,
degree: t('education.degree'),
school: t('education.school'),
location: t('education.location'),
period: t('education.period'),
currentYear: t('education.currentYear'),
status: t('education.status'),
description: t('education.description'),
highlights: [
t('education.highlights.0'),
t('education.highlights.1'),
t('education.highlights.2'),
t('education.highlights.3'),
t('education.highlights.4'),
t('education.highlights.5')
],
color: "#4CAF50",
icon: <GraduationCap size={24} />
},
{
id: 2,
degree: t('education.highschool.degree'),
school: t('education.highschool.school'),
location: t('education.highschool.location'),
period: t('education.highschool.period'),
currentYear: "",
status: t('education.highschool.status'),
description: t('education.highschool.description'),
highlights: [
t('education.highschool.highlights.0'),
t('education.highschool.highlights.1'),
t('education.highschool.highlights.2')
],
color: "#3F51B5",
icon: <GraduationCap size={24} />
}
];
const certifications = [
{
title: t('education.cert1.title'),
provider: t('education.cert1.provider'),
date: t('education.cert1.date'),
skills: ["Dart", "Flutter", "Firebase", "API REST"],
color: "#2196F3"
},
{
title: t('education.cert2.title'),
provider: t('education.cert2.provider'),
date: t('education.cert2.date'),
skills: ["React", "TypeScript", "Hooks", "Context API"],
color: "#FF9800"
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, x: -50 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="education" className="education">
<div className="container">
<motion.div
key="education-header"
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">{t('education.title')}</h2>
<p className="section-subtitle">
{t('education.subtitle')}
</p>
</motion.div>
<div className="education-content">
{/* Formation principale */}
<motion.div
key="education-main"
className="education-main"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{education.map((edu) => (
<motion.div
key={edu.id}
className="education-card main-education"
variants={itemVariants}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.3 }
}}
>
<div className="education-timeline">
<div
className="timeline-dot"
style={{ backgroundColor: edu.color }}
>
{edu.icon}
</div>
<div className="timeline-line"></div>
</div>
<div className="education-content-card">
<div className="education-header">
<div className="education-title">
<h3>{edu.degree}</h3>
<div className="education-meta">
<span className="school-name">
<BookOpen size={16} />
{edu.school}
</span>
<span className="location">
<MapPin size={16} />
{edu.location}
</span>
<span className="period">
<Calendar size={16} />
{edu.period}
</span>
</div>
</div>
<div className="education-status">
<span
className="status-badge"
style={{ backgroundColor: `${edu.color}20`, color: edu.color }}
>
{edu.status}
</span>
<span className="current-year">{edu.currentYear}</span>
</div>
</div>
<p className="education-description">{edu.description}</p>
<div className="education-highlights">
<h4>
<Target size={16} />
Matières principales
</h4>
<div className="highlights-grid">
{edu.highlights.map((highlight, index) => (
<motion.span
key={index}
className="highlight-tag"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
viewport={{ once: true }}
>
{highlight}
</motion.span>
))}
</div>
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Certifications et formations complémentaires */}
<motion.div
className="certifications"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<h3 className="certifications-title">
<Award size={24} />
{t('education.certifications.title')}
</h3>
<div className="certifications-grid">
{certifications.map((cert, index) => (
<motion.div
key={index}
className="certification-card"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.03,
y: -3,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
<div className="cert-header">
<h4>{cert.title}</h4>
<span className="cert-provider">{cert.provider}</span>
<span className="cert-date">{cert.date}</span>
</div>
<div className="cert-skills">
{cert.skills.map((skill, skillIndex) => (
<span
key={skillIndex}
className="cert-skill-tag"
style={{ backgroundColor: `${cert.color}20`, color: cert.color }}
>
{skill}
</span>
))}
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Objectifs d'apprentissage */}
<motion.div
className="learning-goals"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
>
<h3>{t('education.learningGoals2025')}</h3>
<div className="goals-grid">
{[
{ goal: t('education.goal1'), progress: 60 },
{ goal: t('education.goal2'), progress: 30 },
{ goal: t('education.goal3'), progress: 20 },
{ goal: t('education.goal4'), progress: 45 }
].map((item, index) => (
<motion.div
key={index}
className="goal-item"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="goal-header">
<span className="goal-text">{item.goal}</span>
<span className="goal-percentage">{item.progress}%</span>
</div>
<div className="goal-progress">
<motion.div
className="goal-progress-bar"
initial={{ width: 0 }}
whileInView={{ width: `${item.progress}%` }}
transition={{
duration: 1.5,
delay: index * 0.1 + 0.5,
ease: [0.25, 0.1, 0.25, 1] as const
}}
viewport={{ once: true }}
/>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
};
export default Education;

View File

@@ -0,0 +1,28 @@
import { motion } from 'framer-motion';
const Footer = () => {
return (
<motion.footer
className="footer"
style={{
padding: '2rem 0',
textAlign: 'center',
borderTop: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))',
background: 'var(--bg-secondary, rgba(0, 0, 0, 0.2))',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
>
<p style={{ opacity: 0.7, margin: 0 }}>
© {new Date().getFullYear()} Dayron Van Leemput.
</p>
</motion.footer>
);
};
export default Footer;

View File

@@ -0,0 +1,159 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Menu, X, Sun, Moon, Globe } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { useNavigate, useLocation, Link } from 'react-router-dom';
interface HeaderProps {
darkMode: boolean;
toggleDarkMode: () => void;
}
const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { language, setLanguage, t } = useLanguage();
const navigate = useNavigate();
const location = useLocation();
const menuItems = [
{ id: 'home', name: t('nav.home'), href: '#hero' },
{ id: 'about', name: t('nav.about'), href: '#about' },
{ id: 'skills', name: t('nav.skills'), href: '#skills' },
{ id: 'projects', name: t('nav.projects'), href: '#projects' },
{ id: 'education', name: t('nav.education'), href: '#education' },
{ id: 'contact', name: t('nav.contact'), href: '#contact' }
];
const handleNavigation = (href: string) => {
setIsMenuOpen(false);
if (location.pathname === `/${language}` || location.pathname === `/${language}/`) {
// Already on home, just scroll
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
} else {
// Not on home, navigate then scroll (using a simple timeout for simplicity or hash)
navigate(`/${language}`);
// Small timeout to allow navigation to render Home before scrolling
setTimeout(() => {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
};
const toggleLanguage = () => {
setLanguage(language === 'fr' ? 'en' : 'fr');
};
return (
<motion.header
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5 }}
className="header"
>
<nav className="nav">
<motion.div
className="nav-brand"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Link to={`/${language}`} onClick={(e) => {
e.preventDefault();
handleNavigation('#hero');
}}>
Dayron Van Leemput
</Link>
</motion.div>
{/* Navigation desktop */}
<ul className="nav-menu desktop-menu">
{menuItems.map((item, index) => (
<motion.li
key={item.id}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavigation(item.href);
}}
>
{item.name}
</a>
</motion.li>
))}
</ul>
<div className="nav-controls">
{/* Toggle langue */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleLanguage}
className="language-toggle"
aria-label={t('btn.changeLang')}
title={t('btn.changeLang')}
>
<Globe size={18} />
<span className="language-text">{language === 'fr' ? 'EN' : 'FR'}</span>
</motion.button>
{/* Toggle thème */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleDarkMode}
className="theme-toggle"
aria-label={t('btn.changeTheme')}
>
{darkMode ? <Sun size={20} /> : <Moon size={20} />}
</motion.button>
{/* Menu hamburger mobile */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="menu-toggle"
aria-label={t('btn.menu')}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
</div>
{/* Navigation mobile */}
<motion.ul
className={`nav-menu mobile-menu ${isMenuOpen ? 'open' : ''}`}
initial={false}
animate={isMenuOpen ? { opacity: 1, x: 0 } : { opacity: 0, x: '100%' }}
transition={{ duration: 0.3 }}
>
{menuItems.map((item) => (
<li key={item.id}>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavigation(item.href);
}}
>
{item.name}
</a>
</li>
))}
</motion.ul>
</nav>
</motion.header>
);
};
export default Header;

View File

@@ -0,0 +1,165 @@
import { motion } from 'framer-motion';
import { Download, Github, Linkedin, Mail } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import dvlPhoto from '../assets/dvl.jpg';
const Hero = () => {
const { t } = useLanguage();
const handleDownloadCV = () => {
// Ici, vous pouvez ajouter le lien vers votre CV
const link = document.createElement('a');
link.href = '/Dayron_Van_Leemput_CV.pdf'; // Ajoutez votre CV dans le dossier public
link.download = 'Dayron_Van_Leemput_CV.pdf';
link.click();
};
return (
<section id="hero" className="hero">
<div className="hero-content">
<motion.div
key="hero-text"
className="hero-text"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
<motion.h1
className="hero-title"
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
{t('hero.title')}
</motion.h1>
<motion.h2
className="hero-subtitle"
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
>
{t('hero.subtitle')}
</motion.h2>
<motion.p
className="hero-description"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
>
{t('hero.description')}
</motion.p>
<motion.div
className="hero-buttons"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1 }}
>
<motion.button
onClick={handleDownloadCV}
className="btn btn-primary"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Download size={20} />
{t('btn.downloadCV')}
</motion.button>
<motion.a
href="#contact"
onClick={(e) => {
e.preventDefault();
document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' });
}}
className="btn btn-secondary"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Mail size={20} />
{t('btn.contactMe')}
</motion.a>
</motion.div>
<motion.div
className="hero-social"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.2 }}
>
<motion.a
href="https://git.xeewy.be/Xeewy" // Remplacez par votre profil GitHub
target="_blank"
rel="noopener noreferrer"
className="social-link"
whileHover={{ scale: 1.2, rotate: 5 }}
whileTap={{ scale: 0.9 }}
>
<Github size={24} />
</motion.a>
<motion.a
href="https://www.linkedin.com/in/dayron-van-leemput-992a94398" // Remplacez par votre profil LinkedIn
target="_blank"
rel="noopener noreferrer"
className="social-link"
whileHover={{ scale: 1.2, rotate: -5 }}
whileTap={{ scale: 0.9 }}
>
<Linkedin size={24} />
</motion.a>
</motion.div>
</motion.div>
<motion.div
key="hero-image"
className="hero-image"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, delay: 0.5 }}
>
<motion.div
className="hero-avatar"
whileHover={{ scale: 1.05, rotate: 5 }}
transition={{ type: "spring", stiffness: 300, damping: 10 }}
>
<img
src={dvlPhoto}
alt="Dayron Van Leemput - Portrait"
className="avatar-image"
/>
</motion.div>
</motion.div>
</div>
{/* Particules d'animation en arrière-plan */}
<div className="hero-particles">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="particle"
initial={{
opacity: 0,
scale: 0,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
}}
animate={{
opacity: [0, 1, 0],
scale: [0, 1, 0],
y: [null, -100],
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 2,
}}
/>
))}
</div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,26 @@
import { useEffect } from 'react';
import Hero from './Hero';
import About from './About';
import Skills from './Skills';
import Projects from './Projects';
import Education from './Education';
import Contact from './Contact';
const Home = () => {
useEffect(() => {
document.title = "Portfolio - Dayron Van Leemput";
}, []);
return (
<>
<Hero />
<About />
<Skills />
<Projects />
<Education />
<Contact />
</>
);
};
export default Home;

View File

@@ -0,0 +1,148 @@
import { motion } from 'framer-motion';
import { useLanguage } from '../contexts/LanguageContext';
import { Link } from 'react-router-dom';
import { ArrowLeft, ExternalLink, Camera, MapPin, Bell } from 'lucide-react';
const Policies = () => {
const { t, language } = useLanguage();
const sections = [2, 3, 4, 5, 6]; // Sections after the permission cards (1 is before)
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const sectionVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
return (
<div className="policies-page" style={{ paddingTop: '100px', minHeight: '100vh', paddingBottom: '50px' }}>
<div className="container" style={{ maxWidth: '800px', margin: '0 auto', padding: '0 20px' }}>
<Link
to={`/${language}/travelmate`}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
marginBottom: '40px',
color: 'var(--text-color)',
textDecoration: 'none',
fontSize: '1rem',
opacity: 0.8
}}
>
<ArrowLeft size={18} />
{t('policies.back')}
</Link>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Header */}
<motion.div variants={sectionVariants} style={{ marginBottom: '3rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '2.5rem', marginBottom: '1rem', fontWeight: 'bold' }}>{t('policies.title')}</h1>
<p style={{ fontSize: '1.2rem', opacity: 0.7 }}>{t('policies.intro')}</p>
</motion.div>
{/* Section 1: Collection (Text) */}
<motion.div variants={sectionVariants} className="policy-section" style={{ marginBottom: '2.5rem' }}>
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', fontWeight: '700' }}>
{t('policies.section.1.title')}
</h2>
<p style={{ lineHeight: '1.8', opacity: 0.9, fontSize: '1rem' }}>
{t('policies.section.1.content')}
</p>
</motion.div>
{/* Visual Permission Cards */}
<motion.div variants={sectionVariants} style={{ marginBottom: '4rem' }}>
<h3 style={{ fontSize: '1.2rem', marginBottom: '1.5rem', opacity: 0.8 }}>{t('policies.permissions.title')}</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1.5rem' }}>
{/* Camera */}
<div style={{ background: 'var(--card-bg, rgba(255,255,255,0.05))', padding: '1.5rem', borderRadius: '1rem', border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1rem', color: 'var(--primary-color)' }}>
<Camera size={24} />
<h4 style={{ margin: 0, fontSize: '1.1rem' }}>{t('policies.data.camera')}</h4>
</div>
<p style={{ opacity: 0.7, fontSize: '0.9rem', lineHeight: '1.5' }}>{t('policies.data.camera.desc')}</p>
</div>
{/* GPS */}
<div style={{ background: 'var(--card-bg, rgba(255,255,255,0.05))', padding: '1.5rem', borderRadius: '1rem', border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1rem', color: 'var(--primary-color)' }}>
<MapPin size={24} />
<h4 style={{ margin: 0, fontSize: '1.1rem' }}>{t('policies.data.gps')}</h4>
</div>
<p style={{ opacity: 0.7, fontSize: '0.9rem', lineHeight: '1.5' }}>{t('policies.data.gps.desc')}</p>
</div>
{/* Notifications */}
<div style={{ background: 'var(--card-bg, rgba(255,255,255,0.05))', padding: '1.5rem', borderRadius: '1rem', border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1rem', color: 'var(--primary-color)' }}>
<Bell size={24} />
<h4 style={{ margin: 0, fontSize: '1.1rem' }}>{t('policies.data.notif')}</h4>
</div>
<p style={{ opacity: 0.7, fontSize: '0.9rem', lineHeight: '1.5' }}>{t('policies.data.notif.desc')}</p>
</div>
</div>
</motion.div>
{/* Remaining Sections (Loop) */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2.5rem' }}>
{sections.map((num) => (
<motion.div key={num} variants={sectionVariants} className="policy-section">
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', fontWeight: '700' }}>
{t(`policies.section.${num}.title`)}
</h2>
<p style={{ lineHeight: '1.8', opacity: 0.9, fontSize: '1rem' }}>
{t(`policies.section.${num}.content`)}
</p>
</motion.div>
))}
</div>
{/* Google Policy Button */}
<motion.div variants={sectionVariants} style={{ marginTop: '5rem', textAlign: 'center' }}>
<a
href="https://policies.google.com/privacy"
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
background: 'transparent',
border: '1px solid var(--primary-color)',
color: 'var(--primary-color)',
padding: '12px 30px',
borderRadius: '50px',
textDecoration: 'none',
fontWeight: '600',
transition: 'all 0.3s ease'
}}
className="hover-scale"
>
<ExternalLink size={18} />
{t('policies.googleBtn')}
</a>
</motion.div>
</motion.div>
</div>
</div>
);
};
export default Policies;

View File

@@ -0,0 +1,221 @@
import { motion } from 'framer-motion';
import { ExternalLink, MapPin, Wine } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { Link } from 'react-router-dom';
const Projects = () => {
const { t, language } = useLanguage();
const projects = [
{
id: 1,
title: t('projects.travelMate.title'),
description: t('projects.travelMate.description'),
status: t('projects.status.available'),
technologies: ["Dart", "Flutter", "Firebase"],
features: [
t('projects.travelMate.feature1'),
t('projects.travelMate.feature2'),
t('projects.travelMate.feature3'),
t('projects.travelMate.feature4')
],
color: "#4CAF50",
icon: <MapPin size={24} />,
links: {
demo: "/travelmate"
},
image: "/travel-mate-preview.png" // Ajoutez votre image dans le dossier public
},
{
id: 3,
title: t('projects.shelbys.title'),
description: t('projects.shelbys.description'),
status: t('projects.status.online'),
technologies: ["React", "Vite", "TailwindCSS", "Framer Motion"], // Inferring stack based on modern standards and user's other projects
features: [
t('projects.shelbys.feature1'),
t('projects.shelbys.feature2'),
t('projects.shelbys.feature4')
],
color: "#E91E63", // A distinct color
icon: <Wine size={24} />,
links: {
demo: "https://shelbys.be"
},
image: "/shelbys-preview.png" // Placeholder, user might need to add this
},
{
id: 2,
title: t('projects.portfolio.title'),
description: t('projects.portfolio.description'),
status: t('projects.status.current'),
technologies: ["React", "TypeScript", "Framer Motion", "CSS3"],
features: [
t('projects.portfolio.feature1'),
t('projects.portfolio.feature2'),
t('projects.portfolio.feature3'),
t('projects.portfolio.feature4')
],
color: "#2196F3",
icon: <ExternalLink size={24} />,
links: {
demo: "https://xeewy.be" // Remplacez par votre lien
}
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.3,
delayChildren: 0.2
}
}
};
const projectVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.8,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="projects" className="projects">
<div className="container">
<motion.div
key="projects-header"
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">{t('projects.title')}</h2>
<p className="section-subtitle">
{t('projects.subtitle')}
</p>
</motion.div>
<motion.div
key="projects-grid"
className="projects-grid"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{projects.map((project, index) => (
<motion.div
key={project.id}
className="project-card"
variants={projectVariants}
whileHover={{
y: -10,
scale: 1.02,
transition: { duration: 0.3 }
}}
>
<div className="project-header">
<div
className="project-icon"
style={{ backgroundColor: `${project.color}20`, color: project.color }}
>
{project.icon}
</div>
<div className="project-status">
<span className="status-badge" style={{ backgroundColor: `${project.color}20`, color: project.color }}>
{project.status}
</span>
</div>
</div>
<div className="project-content">
<h3 className="project-title">{project.title}</h3>
<p className="project-description">{project.description}</p>
<div className="project-technologies">
{project.technologies.map((tech, techIndex) => (
<motion.span
key={tech}
className="tech-tag"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.3,
delay: (index * 0.1) + (techIndex * 0.05)
}}
viewport={{ once: true }}
>
{tech}
</motion.span>
))}
</div>
<div className="project-features">
<h4>{t('projects.features')}</h4>
<ul>
{project.features.map((feature, featureIndex) => (
<motion.li
key={featureIndex}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{
duration: 0.5,
delay: (index * 0.2) + (featureIndex * 0.1)
}}
viewport={{ once: true }}
>
{feature}
</motion.li>
))}
</ul>
</div>
</div>
<div className="project-footer">
<div className="project-links">
{project.links.demo !== "#" && (
project.links.demo.startsWith('/') ? (
<Link to={`/${language}${project.links.demo}`} style={{ textDecoration: 'none' }}>
<motion.div
className="project-link primary"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={20} />
{t('projects.btn.viewProject')}
</motion.div>
</Link>
) : (
<motion.a
href={project.links.demo}
target="_blank"
rel="noopener noreferrer"
className="project-link primary"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={20} />
{t('projects.btn.viewProject')}
</motion.a>
)
)}
</div>
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
};
export default Projects;

View File

@@ -0,0 +1,18 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant',
});
}, [pathname]);
return null;
};
export default ScrollToTop;

View File

@@ -0,0 +1,199 @@
import { motion } from 'framer-motion';
import { Code, Database, Smartphone, Globe, Server, Wrench } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const Skills = () => {
const { t } = useLanguage();
const skillCategories = [
{
id: 'mobile',
icon: <Smartphone size={32} />,
title: t('skills.category.mobile'),
color: "#4FC3F7",
skills: [
{ name: "Dart", level: 85, color: "#0175C2" },
{ name: "Flutter", level: 80, color: "#02569B" }
]
},
{
id: 'frontend',
icon: <Globe size={32} />,
title: t('skills.category.frontend'),
color: "#42A5F5",
skills: [
{ name: "React", level: 75, color: "#61DAFB" },
{ name: "TypeScript", level: 70, color: "#3178C6" },
{ name: "JavaScript", level: 80, color: "#F7DF1E" }
]
},
{
id: 'backend',
icon: <Server size={32} />,
title: t('skills.category.backend'),
color: "#66BB6A",
skills: [
{ name: "Java", level: 75, color: "#ED8B00" },
{ name: "C#", level: 65, color: "#239120" }
]
},
{
id: 'tools',
icon: <Wrench size={32} />,
title: t('skills.category.tools'),
color: "#AB47BC",
skills: [
{ name: "Git", level: 70, color: "#F05032" },
{ name: "VS Code", level: 90, color: "#007ACC" },
{ name: "Android Studio", level: 75, color: "#3DDC84" }
]
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const categoryVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="skills" className="skills">
<div className="container">
<motion.div
key="skills-header"
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">{t('skills.title')}</h2>
<p className="section-subtitle">
{t('skills.subtitle')}
</p>
</motion.div>
<motion.div
key="skills-grid"
className="skills-grid"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{skillCategories.map((category, categoryIndex) => (
<motion.div
key={category.id}
className="skill-category"
variants={categoryVariants}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.3 }
}}
>
<div className="category-header">
<div
className="category-icon"
style={{ backgroundColor: `${category.color}20`, color: category.color }}
>
{category.icon}
</div>
<h3 className="category-title">{category.title}</h3>
</div>
<div className="skills-list">
{category.skills.map((skill, skillIndex) => (
<motion.div
key={skill.name}
className="skill-item"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{
duration: 0.5,
delay: (categoryIndex * 0.2) + (skillIndex * 0.1)
}}
viewport={{ once: true }}
>
<div className="skill-header">
<span className="skill-name">{skill.name}</span>
<span className="skill-percentage">{skill.level}%</span>
</div>
<div className="skill-bar">
<motion.div
className="skill-progress"
style={{ backgroundColor: skill.color }}
initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }}
transition={{
duration: 1.5,
delay: (categoryIndex * 0.2) + (skillIndex * 0.1) + 0.5,
ease: [0.25, 0.1, 0.25, 1] as const
}}
viewport={{ once: true }}
/>
</div>
</motion.div>
))}
</div>
</motion.div>
))}
</motion.div>
{/* Section des soft skills */}
<motion.div
className="soft-skills"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<h3 className="soft-skills-title">{t('skills.otherSkills')}</h3>
<div className="soft-skills-grid">
{[
{ name: t('skills.problemSolving'), icon: <Wrench size={20} /> },
{ name: t('skills.teamwork'), icon: <Code size={20} /> },
{ name: t('skills.continuousLearning'), icon: <Database size={20} /> },
{ name: t('skills.communication'), icon: <Globe size={20} /> }
].map((softSkill, index) => (
<motion.div
key={softSkill.name}
className="soft-skill-tag"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.05,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
{softSkill.icon}
<span>{softSkill.name}</span>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default Skills;

View File

@@ -0,0 +1,271 @@
import { useEffect } from 'react';
import { motion } from 'framer-motion';
import { useLanguage } from '../contexts/LanguageContext';
import { Link, Outlet } from 'react-router-dom';
import { Shield, Smartphone, Map, DollarSign, Users, Globe, Code } from 'lucide-react';
import appIcon from '../assets/app_icon.png';
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
const FeatureCard = ({ title, icon: Icon, description }: { title: string, icon: any, description: string }) => (
<motion.div
variants={itemVariants}
className="feature-card"
style={{
background: 'var(--card-bg, rgba(255, 255, 255, 0.03))', // Slightly more transparent for glass effect
backdropFilter: 'blur(10px)', // Glassmorphism
padding: '2rem',
borderRadius: '1.5rem',
border: '1px solid var(--border-color, rgba(255, 255, 255, 0.08))',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center', // Center items horizontally
textAlign: 'center', // Center text
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'
}}
whileHover={{
y: -5,
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
borderColor: 'var(--primary-color, #4f46e5)'
}}
>
<div style={{
marginBottom: '1.5rem',
background: 'var(--primary-color-alpha, rgba(79, 70, 229, 0.1))',
width: 'fit-content',
padding: '12px',
borderRadius: '12px'
}}>
<Icon size={32} color="var(--primary-color)" />
</div>
<h3 style={{
marginBottom: '1rem',
fontSize: '1.5rem',
fontWeight: '600',
color: 'var(--text-color)' // Explicit color for dark mode
}}>
{title}
</h3>
<p style={{
opacity: 0.8,
lineHeight: '1.7',
flex: 1,
fontSize: '1rem',
color: 'var(--text-color)' // Explicit color
}}>
{description}
</p>
</motion.div>
);
const TravelMate = () => {
const { t, language } = useLanguage();
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2
}
}
};
useEffect(() => {
document.title = "Travel Mate";
}, []);
return (
<div className="travel-mate-page" style={{ paddingTop: '100px', minHeight: '100vh', paddingBottom: '50px' }}>
<div className="container" style={{ maxWidth: '1400px', margin: '0 auto', padding: '0 20px' }}>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Header Section */}
<motion.div variants={itemVariants} style={{ textAlign: 'center', marginBottom: '4rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<motion.img
src={appIcon}
alt="Travel Mate Icon"
style={{ width: '120px', height: '120px', borderRadius: '24px', marginBottom: '2rem', boxShadow: '0 10px 30px rgba(0,0,0,0.2)' }}
whileHover={{ scale: 1.05, rotate: 5 }}
transition={{ type: "spring", stiffness: 300 }}
/>
<h1 className="gradient-text" style={{ fontSize: '4rem', fontWeight: '800', marginBottom: '1rem' }}>
{t('travelmate.page.mainTitle')}
</h1>
<p style={{ fontSize: '1.5rem', opacity: 0.7, maxWidth: '600px', margin: '0 auto 2rem auto' }}>
{t('travelmate.page.subtitle')}
</p>
{/* View Code Button */}
<motion.a
href="https://git.xeewy.be/Xeewy/TravelMate"
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none',
fontSize: '1.1rem',
padding: '0.8rem 1.5rem',
borderRadius: '50px'
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Code size={20} />
{t('travelmate.viewCode') || "Voir le code"}
</motion.a>
</motion.div>
{/* Description as Intro */}
<motion.div variants={itemVariants} style={{ marginBottom: '5rem', maxWidth: '800px', margin: '0 auto 5rem auto', textAlign: 'center' }}>
<p style={{
lineHeight: '1.8',
fontSize: '1.4rem',
opacity: 0.9,
fontStyle: 'italic',
color: 'var(--text-color)'
}}>
"{t('travelmate.page.intro')}"
</p>
</motion.div>
{/* Highlights Sections */}
<motion.div variants={itemVariants} style={{ marginBottom: '6rem' }}>
<h2 style={{
textAlign: 'center',
marginBottom: '4rem',
fontSize: '2.5rem',
fontWeight: '700',
color: 'var(--text-color)'
}}>{t('travelmate.highlights.title')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '2.5rem' }}>
{[1, 2, 3, 4].map((num) => (
<FeatureCard
key={num}
title={t(`travelmate.highlight.${num}.title`)}
description={t(`travelmate.highlight.${num}.desc`)}
icon={
num === 1 ? Users :
num === 2 ? DollarSign :
num === 3 ? Map :
Smartphone
}
/>
))}
</div>
</motion.div>
{/* Conclusion */}
<motion.div variants={itemVariants} style={{ marginBottom: '6rem', textAlign: 'center', maxWidth: '800px', margin: '0 auto 6rem auto' }}>
<p style={{
fontSize: '1.8rem',
fontWeight: 'bold',
color: 'var(--primary-color)',
lineHeight: '1.4'
}}>
{t('travelmate.page.conclusion')}
</p>
</motion.div>
{/* Tech Stack */}
<motion.div variants={itemVariants} style={{ marginBottom: '5rem' }}>
<h2 style={{ textAlign: 'center', marginBottom: '3rem' }}>{t('travelmate.tech.title')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '2rem' }}>
{/* Frontend */}
<div style={{ padding: '1.5rem' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1.5rem', color: 'var(--primary-color)' }}>
<Smartphone /> {t('travelmate.tech.frontend')}
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[1, 2, 3].map(i => (
<li key={i} style={{ marginBottom: '0.8rem', paddingLeft: '1rem', borderLeft: '2px solid var(--primary-color)' }}>
{t(`travelmate.tech.frontend.${i}`)}
</li>
))}
</ul>
</div>
{/* Backend */}
<div style={{ padding: '1.5rem' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1.5rem', color: 'var(--primary-color)' }}>
<Globe /> {t('travelmate.tech.backend')}
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[1, 2, 3, 4].map(i => (
<li key={i} style={{ marginBottom: '0.8rem', paddingLeft: '1rem', borderLeft: '2px solid var(--primary-color)' }}>
{t(`travelmate.tech.backend.${i}`)}
</li>
))}
</ul>
</div>
{/* API */}
<div style={{ padding: '1.5rem' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1.5rem', color: 'var(--primary-color)' }}>
<Code /> {t('travelmate.tech.api')}
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[1, 2, 3].map(i => (
<li key={i} style={{ marginBottom: '0.8rem', paddingLeft: '1rem', borderLeft: '2px solid var(--primary-color)' }}>
{t(`travelmate.tech.api.${i}`)}
</li>
))}
</ul>
</div>
</div>
</motion.div>
{/* Policies CTA */}
<motion.div
variants={itemVariants}
style={{
padding: '2rem',
background: 'var(--card-bg, rgba(255, 255, 255, 0.05))',
borderRadius: '1rem',
border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))',
textAlign: 'center',
maxWidth: '600px',
margin: '0 auto'
}}
>
<Shield className="text-primary" size={48} style={{ marginBottom: '1rem', color: 'var(--primary-color)' }} />
<h3 style={{ marginBottom: '1.5rem' }}>
{t('policies.title')}
</h3>
<Link
to={`/${language}/travelmate/policies`}
className="btn btn-secondary"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none'
}}
>
{t('travelmate.policies.link')}
</Link>
</motion.div>
</motion.div>
<Outlet />
</div>
</div>
);
};
export default TravelMate;