feat: Initialize backend with Express and MySQL, and restructure frontend with new routing and language support.
This commit is contained in:
136
frontend/src/components/About.tsx
Normal file
136
frontend/src/components/About.tsx
Normal 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;
|
||||
381
frontend/src/components/Contact.tsx
Normal file
381
frontend/src/components/Contact.tsx
Normal 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;
|
||||
291
frontend/src/components/Education.tsx
Normal file
291
frontend/src/components/Education.tsx
Normal 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;
|
||||
28
frontend/src/components/Footer.tsx
Normal file
28
frontend/src/components/Footer.tsx
Normal 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;
|
||||
159
frontend/src/components/Header.tsx
Normal file
159
frontend/src/components/Header.tsx
Normal 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;
|
||||
165
frontend/src/components/Hero.tsx
Normal file
165
frontend/src/components/Hero.tsx
Normal 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;
|
||||
26
frontend/src/components/Home.tsx
Normal file
26
frontend/src/components/Home.tsx
Normal 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;
|
||||
148
frontend/src/components/Policies.tsx
Normal file
148
frontend/src/components/Policies.tsx
Normal 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;
|
||||
221
frontend/src/components/Projects.tsx
Normal file
221
frontend/src/components/Projects.tsx
Normal 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;
|
||||
18
frontend/src/components/ScrollToTop.tsx
Normal file
18
frontend/src/components/ScrollToTop.tsx
Normal 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;
|
||||
199
frontend/src/components/Skills.tsx
Normal file
199
frontend/src/components/Skills.tsx
Normal 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;
|
||||
271
frontend/src/components/TravelMate.tsx
Normal file
271
frontend/src/components/TravelMate.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user