feat: Create individual project page template
This commit is contained in:
parent
38c4dd1b7d
commit
b56f422dd3
@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import ProjectPage from "./pages/ProjectPage";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@ -16,6 +17,7 @@ const App = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/project/:projectId" element={<ProjectPage />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
||||
113
src/components/ImageGallery.tsx
Normal file
113
src/components/ImageGallery.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface ImageGalleryProps {
|
||||
images: string[];
|
||||
projectTitle: string;
|
||||
}
|
||||
|
||||
export const ImageGallery = ({ images, projectTitle }: ImageGalleryProps) => {
|
||||
const [selectedImage, setSelectedImage] = useState<number | null>(null);
|
||||
|
||||
const openLightbox = (index: number) => {
|
||||
setSelectedImage(index);
|
||||
};
|
||||
|
||||
const closeLightbox = () => {
|
||||
setSelectedImage(null);
|
||||
};
|
||||
|
||||
const nextImage = () => {
|
||||
if (selectedImage !== null) {
|
||||
setSelectedImage((selectedImage + 1) % images.length);
|
||||
}
|
||||
};
|
||||
|
||||
const prevImage = () => {
|
||||
if (selectedImage !== null) {
|
||||
setSelectedImage((selectedImage - 1 + images.length) % images.length);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{images.map((image, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="cursor-pointer overflow-hidden rounded-lg border border-border/50 bg-card/50 backdrop-blur"
|
||||
onClick={() => openLightbox(index)}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`${projectTitle} - Image ${index + 1}`}
|
||||
className="w-full h-48 object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedImage !== null && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
onClick={closeLightbox}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-4 right-4 text-foreground"
|
||||
onClick={closeLightbox}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
prevImage();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="w-8 h-8" />
|
||||
</Button>
|
||||
|
||||
<motion.img
|
||||
key={selectedImage}
|
||||
src={images[selectedImage]}
|
||||
alt={`${projectTitle} - Image ${selectedImage + 1}`}
|
||||
className="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
nextImage();
|
||||
}}
|
||||
>
|
||||
<ChevronRight className="w-8 h-8" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface ProjectCardProps {
|
||||
title: string;
|
||||
@ -7,9 +8,18 @@ interface ProjectCardProps {
|
||||
icon: React.ReactNode;
|
||||
image?: string;
|
||||
delay?: number;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export const ProjectCard = ({ title, description, icon, image, delay = 0 }: ProjectCardProps) => {
|
||||
export const ProjectCard = ({ title, description, icon, image, delay = 0, projectId }: ProjectCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (projectId) {
|
||||
navigate(`/project/${projectId}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@ -17,6 +27,8 @@ export const ProjectCard = ({ title, description, icon, image, delay = 0 }: Proj
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay }}
|
||||
whileHover={{ y: -5 }}
|
||||
onClick={handleClick}
|
||||
className={projectId ? "cursor-pointer" : ""}
|
||||
>
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur relative overflow-hidden">
|
||||
{image && (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Mail, Github, Send } from "lucide-react";
|
||||
import { Mail, Github, Send, Phone } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@ -71,6 +71,7 @@ export const ProjectsSection = () => {
|
||||
icon={project.icon}
|
||||
image={project.image}
|
||||
delay={index * 0.1}
|
||||
projectId={project.key}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
391
src/data/projects.ts
Normal file
391
src/data/projects.ts
Normal file
@ -0,0 +1,391 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface ProjectData {
|
||||
id: string;
|
||||
title: { fr: string; en: string };
|
||||
shortDescription: { fr: string; en: string };
|
||||
detailedDescription: { fr: string; en: string };
|
||||
objectives: { fr: string[]; en: string[] };
|
||||
challenges: { fr: string[]; en: string[] };
|
||||
solution: { fr: string; en: string };
|
||||
images: string[];
|
||||
mainImage: string;
|
||||
technologies: { name: string; color?: string; icon?: string }[];
|
||||
githubUrl?: string;
|
||||
demoUrl?: string;
|
||||
}
|
||||
|
||||
export const projectsData: Record<string, ProjectData> = {
|
||||
nas: {
|
||||
id: "nas",
|
||||
title: { fr: "Homemade NAS", en: "Homemade NAS" },
|
||||
shortDescription: {
|
||||
fr: "Serveur personnel avec OpenMediaVault, password manager auto-hébergé, Home Assistant, Portainer, Plex, Traefik pour une infrastructure complète.",
|
||||
en: "Personal server with OpenMediaVault, self-hosted password manager, Home Assistant, Portainer, Plex, Traefik for a complete infrastructure.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Mise en place d'un serveur NAS complet avec OpenMediaVault comme système de base. Configuration d'un écosystème de services auto-hébergés incluant un gestionnaire de mots de passe sécurisé, Home Assistant pour la domotique, Portainer pour la gestion de conteneurs, Plex pour le streaming média, et Traefik comme reverse proxy pour sécuriser tous les accès.",
|
||||
en: "Setup of a complete NAS server with OpenMediaVault as base system. Configuration of a self-hosted services ecosystem including a secure password manager, Home Assistant for home automation, Portainer for container management, Plex for media streaming, and Traefik as reverse proxy to secure all accesses.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Créer une infrastructure personnelle sécurisée",
|
||||
"Auto-héberger des services critiques",
|
||||
"Apprendre l'administration système Linux",
|
||||
"Maîtriser Docker et la conteneurisation",
|
||||
],
|
||||
en: [
|
||||
"Create a secure personal infrastructure",
|
||||
"Self-host critical services",
|
||||
"Learn Linux system administration",
|
||||
"Master Docker and containerization",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Configuration de la sécurité réseau",
|
||||
"Gestion des certificats SSL/TLS",
|
||||
"Optimisation des performances",
|
||||
"Backup et redondance des données",
|
||||
],
|
||||
en: [
|
||||
"Network security configuration",
|
||||
"SSL/TLS certificate management",
|
||||
"Performance optimization",
|
||||
"Data backup and redundancy",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "Architecture microservices avec Docker Compose, reverse proxy Traefik automatisant les certificats Let's Encrypt, système de backup automatique avec rsync, et monitoring avec Prometheus.",
|
||||
en: "Microservices architecture with Docker Compose, Traefik reverse proxy automating Let's Encrypt certificates, automatic backup system with rsync, and monitoring with Prometheus.",
|
||||
},
|
||||
mainImage: "/images/projects/nas.svg",
|
||||
images: ["/images/projects/nas.svg"],
|
||||
technologies: [
|
||||
{ name: "OpenMediaVault" },
|
||||
{ name: "Docker" },
|
||||
{ name: "Traefik" },
|
||||
{ name: "Linux" },
|
||||
{ name: "Home Assistant" },
|
||||
{ name: "Portainer" },
|
||||
],
|
||||
},
|
||||
transcendence: {
|
||||
id: "transcendence",
|
||||
title: { fr: "Ft_Transcendence", en: "Ft_Transcendence" },
|
||||
shortDescription: {
|
||||
fr: "Application web Pong social en temps réel avec React, NestJS, PostgreSQL, WebSocket et authentification sécurisée.",
|
||||
en: "Real-time social Pong web application with React, NestJS, PostgreSQL, WebSocket and secure authentication.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Développement d'une application web full-stack moderne réinventant le jeu Pong classique avec des fonctionnalités sociales. Architecture client-serveur avec React pour le frontend, NestJS pour le backend, communication temps réel via WebSocket, base de données PostgreSQL, et système d'authentification OAuth2.",
|
||||
en: "Development of a modern full-stack web application reinventing the classic Pong game with social features. Client-server architecture with React for frontend, NestJS for backend, real-time communication via WebSocket, PostgreSQL database, and OAuth2 authentication system.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Créer une application web full-stack complète",
|
||||
"Implémenter la communication temps réel",
|
||||
"Développer un système de matchmaking",
|
||||
"Gérer l'authentification et la sécurité",
|
||||
],
|
||||
en: [
|
||||
"Create a complete full-stack web application",
|
||||
"Implement real-time communication",
|
||||
"Develop a matchmaking system",
|
||||
"Handle authentication and security",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Synchronisation du jeu en temps réel",
|
||||
"Gestion des collisions et physique",
|
||||
"Architecture scalable backend",
|
||||
"Sécurisation des communications WebSocket",
|
||||
],
|
||||
en: [
|
||||
"Real-time game synchronization",
|
||||
"Collision and physics management",
|
||||
"Scalable backend architecture",
|
||||
"WebSocket communication security",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "Architecture MVC avec NestJS, WebSocket rooms pour les matchs, système de queuing Redis pour le matchmaking, JWT pour l'authentification, et Canvas HTML5 pour le rendu du jeu.",
|
||||
en: "MVC architecture with NestJS, WebSocket rooms for matches, Redis queuing system for matchmaking, JWT for authentication, and HTML5 Canvas for game rendering.",
|
||||
},
|
||||
mainImage: "/images/projects/transcendence.svg",
|
||||
images: ["/images/projects/transcendence.svg"],
|
||||
technologies: [
|
||||
{ name: "React", icon: "⚛️" },
|
||||
{ name: "TypeScript" },
|
||||
{ name: "NestJS" },
|
||||
{ name: "PostgreSQL" },
|
||||
{ name: "WebSocket" },
|
||||
{ name: "Docker" },
|
||||
],
|
||||
},
|
||||
cloud: {
|
||||
id: "cloud",
|
||||
title: { fr: "Cloud-1", en: "Cloud-1" },
|
||||
shortDescription: {
|
||||
fr: "Infrastructure automatisée avec Docker et Ansible, incluant WordPress, PHPMyAdmin et MySQL containerisés.",
|
||||
en: "Automated infrastructure with Docker and Ansible, including containerized WordPress, PHPMyAdmin and MySQL.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Projet d'infrastructure cloud automatisée utilisant Docker pour la conteneurisation et Ansible pour l'orchestration. Déploiement automatique d'une stack LAMP complète avec WordPress, PHPMyAdmin et MySQL, configuration réseau sécurisée, et scripts d'automatisation pour le déploiement et la maintenance.",
|
||||
en: "Automated cloud infrastructure project using Docker for containerization and Ansible for orchestration. Automatic deployment of a complete LAMP stack with WordPress, PHPMyAdmin and MySQL, secure network configuration, and automation scripts for deployment and maintenance.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Automatiser le déploiement d'infrastructure",
|
||||
"Maîtriser Docker et la conteneurisation",
|
||||
"Apprendre Ansible et l'Infrastructure as Code",
|
||||
"Implémenter des bonnes pratiques DevOps",
|
||||
],
|
||||
en: [
|
||||
"Automate infrastructure deployment",
|
||||
"Master Docker and containerization",
|
||||
"Learn Ansible and Infrastructure as Code",
|
||||
"Implement DevOps best practices",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Orchestration multi-conteneurs",
|
||||
"Gestion des variables d'environnement",
|
||||
"Persistance des données",
|
||||
"Automatisation complète avec Ansible",
|
||||
],
|
||||
en: [
|
||||
"Multi-container orchestration",
|
||||
"Environment variables management",
|
||||
"Data persistence",
|
||||
"Complete automation with Ansible",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "Docker Compose pour l'orchestration, Ansible playbooks pour l'automatisation, volumes Docker pour la persistance, et réseau bridge personnalisé pour l'isolation.",
|
||||
en: "Docker Compose for orchestration, Ansible playbooks for automation, Docker volumes for persistence, and custom bridge network for isolation.",
|
||||
},
|
||||
mainImage: "/images/projects/cloud.svg",
|
||||
images: ["/images/projects/cloud.svg"],
|
||||
technologies: [
|
||||
{ name: "Docker", icon: "🐳" },
|
||||
{ name: "Ansible" },
|
||||
{ name: "WordPress" },
|
||||
{ name: "MySQL" },
|
||||
{ name: "Linux" },
|
||||
],
|
||||
},
|
||||
minishell: {
|
||||
id: "minishell",
|
||||
title: { fr: "Minishell", en: "Minishell" },
|
||||
shortDescription: {
|
||||
fr: "Réimplémentation d'un shell bash en C avec gestion de processus, pipes, redirections et variables d'environnement.",
|
||||
en: "Bash shell reimplementation in C with process management, pipes, redirections and environment variables.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Recréation d'un interpréteur de commandes shell en langage C, reproduisant le comportement de bash. Implémentation du parsing de commandes, gestion des processus avec fork/exec, pipes pour la communication inter-processus, redirections d'entrée/sortie, gestion des signaux, et variables d'environnement.",
|
||||
en: "Recreation of a shell command interpreter in C language, reproducing bash behavior. Implementation of command parsing, process management with fork/exec, pipes for inter-process communication, input/output redirections, signal handling, and environment variables.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Comprendre le fonctionnement d'un shell Unix",
|
||||
"Maîtriser les appels système POSIX",
|
||||
"Gérer les processus et la communication inter-processus",
|
||||
"Implémenter un parser robuste",
|
||||
],
|
||||
en: [
|
||||
"Understand Unix shell functioning",
|
||||
"Master POSIX system calls",
|
||||
"Handle processes and inter-process communication",
|
||||
"Implement a robust parser",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Parsing de commandes complexes",
|
||||
"Gestion mémoire sans fuites",
|
||||
"Gestion des signaux (Ctrl+C, Ctrl+D, Ctrl+\\)",
|
||||
"Reproduction exacte du comportement bash",
|
||||
],
|
||||
en: [
|
||||
"Complex command parsing",
|
||||
"Memory management without leaks",
|
||||
"Signal handling (Ctrl+C, Ctrl+D, Ctrl+\\)",
|
||||
"Exact bash behavior reproduction",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "Tokenizer/Lexer pour le parsing, AST pour représenter les commandes, gestion des descripteurs de fichiers pour les redirections, et table de hash pour les variables d'environnement.",
|
||||
en: "Tokenizer/Lexer for parsing, AST to represent commands, file descriptor management for redirections, and hash table for environment variables.",
|
||||
},
|
||||
mainImage: "/images/projects/minishell.svg",
|
||||
images: ["/images/projects/minishell.svg"],
|
||||
technologies: [
|
||||
{ name: "C" },
|
||||
{ name: "Linux" },
|
||||
{ name: "POSIX" },
|
||||
{ name: "Make" },
|
||||
],
|
||||
},
|
||||
cube3d: {
|
||||
id: "cube3d",
|
||||
title: { fr: "Cube3D", en: "Cube3D" },
|
||||
shortDescription: {
|
||||
fr: "Moteur RayCaster 3D inspiré de Wolfenstein 3D, développé avec MiniLibX et algorithmes graphiques avancés.",
|
||||
en: "3D RayCaster engine inspired by Wolfenstein 3D, developed with MiniLibX and advanced graphics algorithms.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Développement d'un moteur de rendu 3D utilisant la technique du raycasting, inspiré du jeu Wolfenstein 3D. Implémentation des algorithmes DDA pour le lancer de rayons, gestion des textures, systèmes de collisions, mini-map en temps réel, et optimisations de performance pour un rendu fluide.",
|
||||
en: "Development of a 3D rendering engine using raycasting technique, inspired by Wolfenstein 3D game. Implementation of DDA algorithms for ray casting, texture management, collision systems, real-time mini-map, and performance optimizations for smooth rendering.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Comprendre les algorithmes de rendu 3D",
|
||||
"Implémenter le raycasting depuis zéro",
|
||||
"Optimiser les performances graphiques",
|
||||
"Gérer les textures et sprites",
|
||||
],
|
||||
en: [
|
||||
"Understand 3D rendering algorithms",
|
||||
"Implement raycasting from scratch",
|
||||
"Optimize graphics performance",
|
||||
"Handle textures and sprites",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Calculs trigonométriques optimisés",
|
||||
"Gestion de la mémoire graphique",
|
||||
"Chargement et mapping des textures",
|
||||
"Détection de collisions précise",
|
||||
],
|
||||
en: [
|
||||
"Optimized trigonometric calculations",
|
||||
"Graphics memory management",
|
||||
"Texture loading and mapping",
|
||||
"Precise collision detection",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "Algorithme DDA pour le raycasting, lookup tables pour les calculs trigonométriques, buffer d'image pour le double buffering, et grille 2D pour la détection de collisions.",
|
||||
en: "DDA algorithm for raycasting, lookup tables for trigonometric calculations, image buffer for double buffering, and 2D grid for collision detection.",
|
||||
},
|
||||
mainImage: "/images/projects/cube3d.svg",
|
||||
images: ["/images/projects/cube3d.svg"],
|
||||
technologies: [
|
||||
{ name: "C" },
|
||||
{ name: "MiniLibX" },
|
||||
{ name: "Raycasting" },
|
||||
{ name: "Make" },
|
||||
],
|
||||
},
|
||||
etsidemain: {
|
||||
id: "etsidemain",
|
||||
title: { fr: "etsidemain.com", en: "etsidemain.com" },
|
||||
shortDescription: {
|
||||
fr: "Site web vitrine pour cabinet de conseil en transformation régénérative. Design moderne et responsive avec animations CSS, formulaire de contact et optimisations SEO.",
|
||||
en: "Showcase website for regenerative transformation consulting firm. Modern responsive design with CSS animations, contact form and SEO optimizations.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Création d'un site web vitrine professionnel pour un cabinet de conseil spécialisé dans la transformation régénérative. Design moderne et épuré avec animations CSS sophistiquées, formulaire de contact fonctionnel, optimisations SEO avancées, performance optimisée avec lazy loading, et déploiement avec CI/CD.",
|
||||
en: "Creation of a professional showcase website for a consulting firm specialized in regenerative transformation. Modern and clean design with sophisticated CSS animations, functional contact form, advanced SEO optimizations, optimized performance with lazy loading, and CI/CD deployment.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Créer une présence web professionnelle",
|
||||
"Optimiser le référencement naturel",
|
||||
"Assurer une expérience utilisateur fluide",
|
||||
"Garantir des performances optimales",
|
||||
],
|
||||
en: [
|
||||
"Create a professional web presence",
|
||||
"Optimize natural referencing",
|
||||
"Ensure smooth user experience",
|
||||
"Guarantee optimal performance",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Design moderne et élégant",
|
||||
"Animations fluides et performantes",
|
||||
"Optimisation SEO complète",
|
||||
"Compatibilité multi-navigateurs",
|
||||
],
|
||||
en: [
|
||||
"Modern and elegant design",
|
||||
"Smooth and performant animations",
|
||||
"Complete SEO optimization",
|
||||
"Multi-browser compatibility",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "HTML5/CSS3 sémantique, animations avec transitions CSS et Intersection Observer, balises meta optimisées, structure de données JSON-LD, et optimisation des assets.",
|
||||
en: "Semantic HTML5/CSS3, animations with CSS transitions and Intersection Observer, optimized meta tags, JSON-LD data structure, and asset optimization.",
|
||||
},
|
||||
mainImage: "/images/projects/etsidemain.svg",
|
||||
images: ["/images/projects/etsidemain.svg"],
|
||||
technologies: [
|
||||
{ name: "HTML5" },
|
||||
{ name: "CSS3" },
|
||||
{ name: "JavaScript" },
|
||||
{ name: "SEO" },
|
||||
],
|
||||
demoUrl: "https://etsidemain.com",
|
||||
},
|
||||
avopieces: {
|
||||
id: "avopieces",
|
||||
title: { fr: "avopieces.fr", en: "avopieces.fr" },
|
||||
shortDescription: {
|
||||
fr: "Plateforme juridique intelligente pour le cabinet AvoCab, spécialisée dans les procédures de divorce. Intègre un chatbot IA analysant les documents uploadés, système de gestion de comptes client/admin, prise de RDV en ligne et vitrine du cabinet.",
|
||||
en: "Intelligent legal platform for AvoCab law firm, specialized in divorce procedures. Features AI chatbot analyzing uploaded documents, client/admin account management system, online appointment booking and law firm showcase.",
|
||||
},
|
||||
detailedDescription: {
|
||||
fr: "Développement d'une plateforme web complète pour un cabinet d'avocats spécialisé en droit de la famille. Chatbot IA utilisant l'analyse documentaire pour fournir des réponses juridiques personnalisées, système de gestion de comptes avec authentification sécurisée, module de prise de rendez-vous intégré, interface admin pour la gestion des clients, et site vitrine présentant les services du cabinet.",
|
||||
en: "Development of a complete web platform for a law firm specialized in family law. AI chatbot using document analysis to provide personalized legal answers, account management system with secure authentication, integrated appointment booking module, admin interface for client management, and showcase website presenting the firm's services.",
|
||||
},
|
||||
objectives: {
|
||||
fr: [
|
||||
"Automatiser la première consultation juridique",
|
||||
"Faciliter la gestion des dossiers clients",
|
||||
"Moderniser la prise de rendez-vous",
|
||||
"Améliorer l'expérience client",
|
||||
],
|
||||
en: [
|
||||
"Automate initial legal consultation",
|
||||
"Facilitate client case management",
|
||||
"Modernize appointment booking",
|
||||
"Improve client experience",
|
||||
],
|
||||
},
|
||||
challenges: {
|
||||
fr: [
|
||||
"Intégration d'IA pour l'analyse documentaire",
|
||||
"Sécurité des données sensibles (RGPD)",
|
||||
"Architecture full-stack complexe",
|
||||
"Interface intuitive pour non-techniciens",
|
||||
],
|
||||
en: [
|
||||
"AI integration for document analysis",
|
||||
"Sensitive data security (GDPR)",
|
||||
"Complex full-stack architecture",
|
||||
"Intuitive interface for non-technical users",
|
||||
],
|
||||
},
|
||||
solution: {
|
||||
fr: "Architecture MERN stack, RAG (Retrieval Augmented Generation) pour le chatbot, chiffrement des données, système de roles et permissions, API RESTful, et design system cohérent.",
|
||||
en: "MERN stack architecture, RAG (Retrieval Augmented Generation) for chatbot, data encryption, roles and permissions system, RESTful API, and consistent design system.",
|
||||
},
|
||||
mainImage: "/images/projects/avopieces.svg",
|
||||
images: ["/images/projects/avopieces.svg"],
|
||||
technologies: [
|
||||
{ name: "React", icon: "⚛️" },
|
||||
{ name: "Node.js", icon: "📗" },
|
||||
{ name: "MongoDB" },
|
||||
{ name: "OpenAI" },
|
||||
{ name: "TypeScript" },
|
||||
],
|
||||
demoUrl: "https://avopieces.fr",
|
||||
},
|
||||
};
|
||||
300
src/pages/ProjectPage.tsx
Normal file
300
src/pages/ProjectPage.tsx
Normal file
@ -0,0 +1,300 @@
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowLeft, Github, ExternalLink, Mail } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ScrollProgress } from "@/components/ScrollProgress";
|
||||
import { ImageGallery } from "@/components/ImageGallery";
|
||||
import { SkillBadge } from "@/components/SkillBadge";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { projectsData } from "@/data/projects";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import { LanguageProvider } from "@/contexts/LanguageContext";
|
||||
|
||||
const ProjectPageContent = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { language, t } = useLanguage();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const project = projectId ? projectsData[projectId] : null;
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">{t("project.notFound") || "Project not found"}</h1>
|
||||
<Button onClick={() => navigate("/")}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
{t("project.backToHome") || "Back to Home"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<ScrollProgress />
|
||||
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Button variant="ghost" onClick={() => navigate("/")} className="gap-2">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
{t("project.back") || "Back"}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="py-20 md:py-32">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="max-w-4xl mx-auto text-center mb-12"
|
||||
>
|
||||
<h1 className="text-5xl md:text-7xl font-bold mb-6">
|
||||
{project.title[language]}
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-muted-foreground mb-8">
|
||||
{project.shortDescription[language]}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center mb-8">
|
||||
{project.technologies.map((tech, index) => (
|
||||
<SkillBadge
|
||||
key={tech.name}
|
||||
name={tech.name}
|
||||
icon={tech.icon}
|
||||
delay={index * 0.05}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="max-w-5xl mx-auto rounded-xl overflow-hidden border border-border/50 bg-card/50 backdrop-blur"
|
||||
>
|
||||
<img
|
||||
src={project.mainImage}
|
||||
alt={project.title[language]}
|
||||
className="w-full h-[400px] object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Description Section */}
|
||||
<section className="py-20 bg-muted/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||
{t("project.description") || "Description"}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-12">
|
||||
{project.detailedDescription[language]}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card className="h-full border-border/50 bg-card/50 backdrop-blur">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
{t("project.objectives") || "Objectives"}
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{project.objectives[language].map((objective, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<span className="text-primary mt-1">✓</span>
|
||||
<span className="text-muted-foreground">{objective}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card className="h-full border-border/50 bg-card/50 backdrop-blur">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
{t("project.challenges") || "Challenges"}
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{project.challenges[language].map((challenge, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<span className="text-primary mt-1">⚡</span>
|
||||
<span className="text-muted-foreground">{challenge}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="mt-8"
|
||||
>
|
||||
<Card className="border-border/50 bg-card/50 backdrop-blur">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
{t("project.solution") || "Solution"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{project.solution[language]}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gallery Section */}
|
||||
{project.images.length > 0 && (
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="max-w-6xl mx-auto"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
|
||||
{t("project.gallery") || "Gallery"}
|
||||
</h2>
|
||||
<ImageGallery images={project.images} projectTitle={project.title[language]} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Links Section */}
|
||||
{(project.githubUrl || project.demoUrl) && (
|
||||
<section className="py-20 bg-muted/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="max-w-4xl mx-auto text-center"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8">
|
||||
{t("project.links") || "Project Links"}
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
{project.githubUrl && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
asChild
|
||||
className="gap-2"
|
||||
>
|
||||
<a href={project.githubUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Github className="w-5 h-5" />
|
||||
{t("project.viewGithub") || "View on GitHub"}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{project.demoUrl && (
|
||||
<Button
|
||||
size="lg"
|
||||
asChild
|
||||
className="gap-2"
|
||||
>
|
||||
<a href={project.demoUrl} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="w-5 h-5" />
|
||||
{t("project.viewDemo") || "View Live Demo"}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="max-w-4xl mx-auto text-center"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||
{t("project.interested") || "Interested in this project?"}
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
{t("project.contactMe") || "Let's discuss how I can help with your project"}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => navigate("/#contact")}
|
||||
className="gap-2"
|
||||
>
|
||||
<Mail className="w-5 h-5" />
|
||||
{t("hero.cta2") || "Contact me"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => navigate("/")}
|
||||
className="gap-2"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
{t("project.backToHome") || "Back to Home"}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectPage = () => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<ProjectPageContent />
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectPage;
|
||||
@ -58,6 +58,21 @@ export const translations = {
|
||||
email: "Email",
|
||||
github: "GitHub",
|
||||
},
|
||||
project: {
|
||||
back: "Retour",
|
||||
backToHome: "Retour à l'accueil",
|
||||
notFound: "Projet non trouvé",
|
||||
description: "Description",
|
||||
objectives: "Objectifs",
|
||||
challenges: "Défis",
|
||||
solution: "Solution",
|
||||
gallery: "Galerie",
|
||||
links: "Liens du projet",
|
||||
viewGithub: "Voir sur GitHub",
|
||||
viewDemo: "Voir la démo",
|
||||
interested: "Intéressé par ce projet ?",
|
||||
contactMe: "Discutons de comment je peux vous aider avec votre projet",
|
||||
},
|
||||
},
|
||||
en: {
|
||||
nav: {
|
||||
@ -118,5 +133,20 @@ export const translations = {
|
||||
email: "Email",
|
||||
github: "GitHub",
|
||||
},
|
||||
project: {
|
||||
back: "Back",
|
||||
backToHome: "Back to Home",
|
||||
notFound: "Project not found",
|
||||
description: "Description",
|
||||
objectives: "Objectives",
|
||||
challenges: "Challenges",
|
||||
solution: "Solution",
|
||||
gallery: "Gallery",
|
||||
links: "Project Links",
|
||||
viewGithub: "View on GitHub",
|
||||
viewDemo: "View Live Demo",
|
||||
interested: "Interested in this project?",
|
||||
contactMe: "Let's discuss how I can help with your project",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user