feat: Initialize backend with Express and MySQL, and restructure frontend with new routing and language support.
This commit is contained in:
8
.vite/deps/_metadata.json
Normal file
8
.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "a5aca759",
|
||||||
|
"configHash": "b6b52121",
|
||||||
|
"lockfileHash": "e3b0c442",
|
||||||
|
"browserHash": "5526a657",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
3
.vite/deps/package.json
Normal file
3
.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
105
EMAILJS_SETUP.md
105
EMAILJS_SETUP.md
@@ -1,105 +0,0 @@
|
|||||||
# Configuration EmailJS pour le Portfolio
|
|
||||||
|
|
||||||
Ce guide vous explique comment configurer EmailJS pour que le formulaire de contact de votre portfolio fonctionne réellement.
|
|
||||||
|
|
||||||
## 1. Créer un compte EmailJS
|
|
||||||
|
|
||||||
1. Allez sur [EmailJS.com](https://www.emailjs.com/)
|
|
||||||
2. Créez un compte gratuit (jusqu'à 200 emails/mois)
|
|
||||||
3. Connectez-vous à votre dashboard
|
|
||||||
|
|
||||||
## 2. Configurer le service email
|
|
||||||
|
|
||||||
1. Dans votre dashboard EmailJS, allez dans **Email Services**
|
|
||||||
2. Cliquez sur **Add New Service**
|
|
||||||
3. Choisissez votre fournisseur email (Gmail, Outlook, etc.)
|
|
||||||
4. Suivez les instructions pour connecter votre compte email
|
|
||||||
5. Notez votre **Service ID** (ex: `service_xyz123`)
|
|
||||||
|
|
||||||
## 3. Créer un template d'email
|
|
||||||
|
|
||||||
1. Allez dans **Email Templates**
|
|
||||||
2. Cliquez sur **Create New Template**
|
|
||||||
3. Configurez votre template avec ces variables :
|
|
||||||
- `{{from_name}}` - Nom de l'expéditeur
|
|
||||||
- `{{from_email}}` - Email de l'expéditeur
|
|
||||||
- `{{subject}}` - Sujet du message
|
|
||||||
- `{{message}}` - Corps du message
|
|
||||||
|
|
||||||
Exemple de template :
|
|
||||||
```
|
|
||||||
Nouveau message depuis votre portfolio
|
|
||||||
|
|
||||||
De: {{from_name}} ({{from_email}})
|
|
||||||
Sujet: {{subject}}
|
|
||||||
|
|
||||||
Message:
|
|
||||||
{{message}}
|
|
||||||
|
|
||||||
---
|
|
||||||
Ce message a été envoyé depuis votre formulaire de contact.
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Notez votre **Template ID** (ex: `template_abc456`)
|
|
||||||
|
|
||||||
## 4. Obtenir votre clé publique
|
|
||||||
|
|
||||||
1. Allez dans **Account** > **General**
|
|
||||||
2. Trouvez votre **Public Key** (ex: `user_def789`)
|
|
||||||
|
|
||||||
## 5. Configurer les variables d'environnement
|
|
||||||
|
|
||||||
Créez un fichier `.env` à la racine de votre projet :
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_EMAILJS_SERVICE_ID=your_service_id_here
|
|
||||||
VITE_EMAILJS_TEMPLATE_ID=your_template_id_here
|
|
||||||
VITE_EMAILJS_PUBLIC_KEY=your_public_key_here
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Sécurité et limitations
|
|
||||||
|
|
||||||
### Version gratuite d'EmailJS :
|
|
||||||
- 200 emails par mois maximum
|
|
||||||
- Pas de limitation de domaine
|
|
||||||
- Support communautaire
|
|
||||||
|
|
||||||
### Pour la production :
|
|
||||||
- Considérez un plan payant pour plus d'emails
|
|
||||||
- Ajoutez une validation côté serveur si nécessaire
|
|
||||||
- Implémentez un captcha pour éviter le spam
|
|
||||||
|
|
||||||
## 7. Test du formulaire
|
|
||||||
|
|
||||||
1. Remplissez les variables d'environnement
|
|
||||||
2. Redémarrez votre serveur de développement
|
|
||||||
3. Testez le formulaire de contact
|
|
||||||
4. Vérifiez la réception de l'email
|
|
||||||
|
|
||||||
## 8. Dépannage
|
|
||||||
|
|
||||||
### Erreurs communes :
|
|
||||||
|
|
||||||
**"Invalid template ID"**
|
|
||||||
- Vérifiez que le Template ID est correct
|
|
||||||
- Assurez-vous que le template est publié
|
|
||||||
|
|
||||||
**"Forbidden"**
|
|
||||||
- Vérifiez votre Public Key
|
|
||||||
- Assurez-vous que le service est actif
|
|
||||||
|
|
||||||
**Email non reçu**
|
|
||||||
- Vérifiez vos spams
|
|
||||||
- Vérifiez la configuration du service email
|
|
||||||
- Testez depuis le dashboard EmailJS
|
|
||||||
|
|
||||||
### Fallback automatique
|
|
||||||
Si EmailJS ne fonctionne pas, le système bascule automatiquement vers `mailto:` pour ouvrir le client email par défaut de l'utilisateur.
|
|
||||||
|
|
||||||
## 9. Fichiers modifiés
|
|
||||||
|
|
||||||
- `src/services/emailService.ts` - Service d'envoi d'emails
|
|
||||||
- `src/components/Contact.tsx` - Intégration du service
|
|
||||||
- `src/styles/components/_contact.scss` - Styles des messages d'erreur
|
|
||||||
|
|
||||||
Le formulaire de contact est maintenant prêt à fonctionner avec une vraie logique d'envoi d'emails !
|
|
||||||
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
.env.local
|
||||||
1769
backend/package-lock.json
generated
Normal file
1769
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/package.json
Normal file
28
backend/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "xeewy-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Backend API for xeewy.be",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "nodemon src/index.ts",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"mysql2": "^3.11.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/node": "^22.8.6",
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/config/db.ts
Normal file
17
backend/src/config/db.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
port: Number(process.env.DB_PORT) || 3306,
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
export default pool;
|
||||||
32
backend/src/index.ts
Normal file
32
backend/src/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import pool from './config/db';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Basic health check
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Database connection test
|
||||||
|
app.get('/api/test-db', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT 1 + 1 AS solution');
|
||||||
|
res.json({ status: 'connected', result: rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Database connection error:', error);
|
||||||
|
res.status(500).json({ status: 'error', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running on port ${port}`);
|
||||||
|
});
|
||||||
18
backend/tsconfig.json
Normal file
18
backend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
0
.gitignore → frontend/.gitignore
vendored
0
.gitignore → frontend/.gitignore
vendored
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
72
frontend/src/App.tsx
Normal file
72
frontend/src/App.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom';
|
||||||
|
import { LanguageProvider } from './contexts/LanguageContext';
|
||||||
|
import Header from './components/Header';
|
||||||
|
import Footer from './components/Footer';
|
||||||
|
import Home from './components/Home';
|
||||||
|
import TravelMate from './components/TravelMate';
|
||||||
|
import Policies from './components/Policies';
|
||||||
|
import ScrollToTop from './components/ScrollToTop';
|
||||||
|
import './styles/main.scss';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/fr" replace />} />
|
||||||
|
<Route path="/:lang/*" element={
|
||||||
|
<LanguageProvider>
|
||||||
|
<AppContent />
|
||||||
|
</LanguageProvider>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const useParamsLang = useParams();
|
||||||
|
|
||||||
|
// Sync URL language with context if needed, or validate it
|
||||||
|
useEffect(() => {
|
||||||
|
// Optional: Check if useParamsLang.lang is valid, else redirect
|
||||||
|
}, [useParamsLang]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem('darkMode');
|
||||||
|
if (savedTheme) {
|
||||||
|
setDarkMode(JSON.parse(savedTheme));
|
||||||
|
} else {
|
||||||
|
setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
||||||
|
document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
|
||||||
|
}, [darkMode]);
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ScrollToTop />
|
||||||
|
<div className={`app ${darkMode ? 'dark' : 'light'}`} style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||||
|
<Header darkMode={darkMode} toggleDarkMode={toggleDarkMode} />
|
||||||
|
<main style={{ flex: 1 }}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="travelmate" element={<TravelMate />} />
|
||||||
|
<Route path="travelmate/policies" element={<Policies />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
Before Width: | Height: | Size: 694 KiB After Width: | Height: | Size: 694 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
@@ -27,7 +27,7 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
|
|||||||
const handleNavigation = (href: string) => {
|
const handleNavigation = (href: string) => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
|
|
||||||
if (location.pathname === '/') {
|
if (location.pathname === `/${language}` || location.pathname === `/${language}/`) {
|
||||||
// Already on home, just scroll
|
// Already on home, just scroll
|
||||||
const element = document.querySelector(href);
|
const element = document.querySelector(href);
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -35,7 +35,7 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not on home, navigate then scroll (using a simple timeout for simplicity or hash)
|
// Not on home, navigate then scroll (using a simple timeout for simplicity or hash)
|
||||||
navigate('/');
|
navigate(`/${language}`);
|
||||||
// Small timeout to allow navigation to render Home before scrolling
|
// Small timeout to allow navigation to render Home before scrolling
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const element = document.querySelector(href);
|
const element = document.querySelector(href);
|
||||||
@@ -63,7 +63,7 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
|
|||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
<Link to="/" onClick={(e) => {
|
<Link to={`/${language}`} onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleNavigation('#hero');
|
handleNavigation('#hero');
|
||||||
}}>
|
}}>
|
||||||
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { ArrowLeft, ExternalLink, Camera, MapPin, Bell } from 'lucide-react';
|
import { ArrowLeft, ExternalLink, Camera, MapPin, Bell } from 'lucide-react';
|
||||||
|
|
||||||
const Policies = () => {
|
const Policies = () => {
|
||||||
const { t } = useLanguage();
|
const { t, language } = useLanguage();
|
||||||
|
|
||||||
const sections = [2, 3, 4, 5, 6]; // Sections after the permission cards (1 is before)
|
const sections = [2, 3, 4, 5, 6]; // Sections after the permission cards (1 is before)
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const Policies = () => {
|
|||||||
<div className="policies-page" style={{ paddingTop: '100px', minHeight: '100vh', paddingBottom: '50px' }}>
|
<div className="policies-page" style={{ paddingTop: '100px', minHeight: '100vh', paddingBottom: '50px' }}>
|
||||||
<div className="container" style={{ maxWidth: '800px', margin: '0 auto', padding: '0 20px' }}>
|
<div className="container" style={{ maxWidth: '800px', margin: '0 auto', padding: '0 20px' }}>
|
||||||
<Link
|
<Link
|
||||||
to="/travelmate"
|
to={`/${language}/travelmate`}
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -4,7 +4,7 @@ import { useLanguage } from '../contexts/LanguageContext';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { t } = useLanguage();
|
const { t, language } = useLanguage();
|
||||||
const projects = [
|
const projects = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -184,7 +184,7 @@ const Projects = () => {
|
|||||||
|
|
||||||
{project.links.demo !== "#" && (
|
{project.links.demo !== "#" && (
|
||||||
project.links.demo.startsWith('/') ? (
|
project.links.demo.startsWith('/') ? (
|
||||||
<Link to={project.links.demo} style={{ textDecoration: 'none' }}>
|
<Link to={`/${language}${project.links.demo}`} style={{ textDecoration: 'none' }}>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="project-link primary"
|
className="project-link primary"
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
@@ -64,7 +64,7 @@ const FeatureCard = ({ title, icon: Icon, description }: { title: string, icon:
|
|||||||
);
|
);
|
||||||
|
|
||||||
const TravelMate = () => {
|
const TravelMate = () => {
|
||||||
const { t } = useLanguage();
|
const { t, language } = useLanguage();
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
@@ -248,7 +248,7 @@ const TravelMate = () => {
|
|||||||
{t('policies.title')}
|
{t('policies.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<Link
|
<Link
|
||||||
to="/travelmate/policies"
|
to={`/${language}/travelmate/policies`}
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// CONTEXTE DE LANGUE
|
// CONTEXTE DE LANGUE
|
||||||
// ========================
|
// ========================
|
||||||
|
|
||||||
import React, { createContext, useContext, useState } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
// Types pour les langues supportées
|
// Types pour les langues supportées
|
||||||
@@ -169,7 +169,7 @@ const translations = {
|
|||||||
'travelmate.tech.api': 'APIs & Outils',
|
'travelmate.tech.api': 'APIs & Outils',
|
||||||
'travelmate.tech.api.1': 'Google Maps API / Mapbox',
|
'travelmate.tech.api.1': 'Google Maps API / Mapbox',
|
||||||
'travelmate.tech.api.2': 'Stripe (tbc) pour les paiements',
|
'travelmate.tech.api.2': 'Stripe (tbc) pour les paiements',
|
||||||
'travelmate.tech.api.3': 'CI/CD avec GitHub Actions',
|
'travelmate.tech.api.3': 'CI/CD avec Gitea Actions',
|
||||||
'travelmate.viewCode': 'Voir le code',
|
'travelmate.viewCode': 'Voir le code',
|
||||||
|
|
||||||
'travelmate.policies.link': 'Voir les politiques de confidentialité',
|
'travelmate.policies.link': 'Voir les politiques de confidentialité',
|
||||||
@@ -370,11 +370,12 @@ const translations = {
|
|||||||
'travelmate.tech.backend.1': 'Authentication (Google, Apple, Email)',
|
'travelmate.tech.backend.1': 'Authentication (Google, Apple, Email)',
|
||||||
'travelmate.tech.backend.2': 'Firestore (Real-time Database)',
|
'travelmate.tech.backend.2': 'Firestore (Real-time Database)',
|
||||||
'travelmate.tech.backend.3': 'Cloud Functions (Server Logic)',
|
'travelmate.tech.backend.3': 'Cloud Functions (Server Logic)',
|
||||||
'travelmate.tech.backend.4': 'Storage (Media)',
|
'travelmate.tech.backend.4': 'Firebase Cloud Messaging - Notifications push',
|
||||||
|
'travelmate.tech.backend.5': 'Storage (Media)',
|
||||||
'travelmate.tech.api': 'APIs & Tools',
|
'travelmate.tech.api': 'APIs & Tools',
|
||||||
'travelmate.tech.api.1': 'Google Maps API / Mapbox',
|
'travelmate.tech.api.1': 'Google Maps API / Mapbox',
|
||||||
'travelmate.tech.api.2': 'Stripe (tbc) for payments',
|
'travelmate.tech.api.2': 'Stripe (tbc) for payments',
|
||||||
'travelmate.tech.api.3': 'CI/CD with GitHub Actions',
|
'travelmate.tech.api.3': 'CI/CD with Gitea Actions',
|
||||||
'travelmate.viewCode': 'View Code',
|
'travelmate.viewCode': 'View Code',
|
||||||
|
|
||||||
'travelmate.policies.link': 'View Privacy Policy',
|
'travelmate.policies.link': 'View Privacy Policy',
|
||||||
@@ -437,16 +438,48 @@ interface LanguageProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
|
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
|
||||||
const [language, setLanguage] = useState<Language>('fr');
|
const { lang } = useParams<{ lang: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Validate language, default to 'fr' if undefined or invalid (though App.tsx handles defaults)
|
||||||
|
const currentLanguage: Language = (lang === 'en' || lang === 'fr') ? lang : 'fr';
|
||||||
|
|
||||||
|
const setLanguage = (newLang: Language) => {
|
||||||
|
if (newLang === currentLanguage) return;
|
||||||
|
|
||||||
|
// Replace the language segment in the URL
|
||||||
|
// Assumes the first segment is the language
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
const segments = currentPath.split('/');
|
||||||
|
// segments[0] is empty string (before first slash)
|
||||||
|
// segments[1] is the language
|
||||||
|
if (segments[1] === 'fr' || segments[1] === 'en') {
|
||||||
|
segments[1] = newLang;
|
||||||
|
} else {
|
||||||
|
// Should not happen if routing is correct, but safe fallback
|
||||||
|
segments.splice(1, 0, newLang);
|
||||||
|
}
|
||||||
|
const newPath = segments.join('/');
|
||||||
|
|
||||||
|
navigate(newPath + location.search + location.hash);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update HTML lang attribute
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.documentElement.lang = currentLanguage;
|
||||||
|
}, [currentLanguage]);
|
||||||
|
|
||||||
// Fonction de traduction
|
// Fonction de traduction
|
||||||
const t = (key: string): string => {
|
const t = (key: string): string => {
|
||||||
return translations[language][key as keyof typeof translations['fr']] || key;
|
return translations[currentLanguage][key as keyof typeof translations['fr']] || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
<LanguageContext.Provider value={{ language: currentLanguage, setLanguage, t }}>
|
||||||
{children}
|
{children}
|
||||||
</LanguageContext.Provider>
|
</LanguageContext.Provider>
|
||||||
);
|
);
|
||||||
53
src/App.tsx
53
src/App.tsx
@@ -1,53 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
||||||
import { LanguageProvider } from './contexts/LanguageContext';
|
|
||||||
import Header from './components/Header';
|
|
||||||
import Footer from './components/Footer';
|
|
||||||
import Home from './components/Home';
|
|
||||||
import TravelMate from './components/TravelMate';
|
|
||||||
import Policies from './components/Policies';
|
|
||||||
import ScrollToTop from './components/ScrollToTop';
|
|
||||||
import './styles/main.scss';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [darkMode, setDarkMode] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const savedTheme = localStorage.getItem('darkMode');
|
|
||||||
if (savedTheme) {
|
|
||||||
setDarkMode(JSON.parse(savedTheme));
|
|
||||||
} else {
|
|
||||||
setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
|
||||||
document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
|
|
||||||
}, [darkMode]);
|
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
|
||||||
setDarkMode(!darkMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LanguageProvider>
|
|
||||||
<BrowserRouter>
|
|
||||||
<ScrollToTop />
|
|
||||||
<div className={`app ${darkMode ? 'dark' : 'light'}`} style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
|
||||||
<Header darkMode={darkMode} toggleDarkMode={toggleDarkMode} />
|
|
||||||
<main style={{ flex: 1 }}>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Home />} />
|
|
||||||
<Route path="/travelmate" element={<TravelMate />} />
|
|
||||||
<Route path="/travelmate/policies" element={<Policies />} />
|
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</BrowserRouter>
|
|
||||||
</LanguageProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
Reference in New Issue
Block a user