look good
@ -127,6 +127,8 @@ Les traductions sont centralisées dans `src/utils/translations.ts`. Pour ajoute
|
||||
3. **Cloud-1** - Infrastructure automatisée avec Docker/Ansible
|
||||
4. **Minishell** - Réimplémentation d'un shell bash en C
|
||||
5. **Cube3D** - Moteur 3D RayCaster
|
||||
6. **etsidemain.com** - Site vitrine pour conseil en transformation régénérative
|
||||
7. **avopieces.fr** - Plateforme juridique IA pour procédures de divorce
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
|
||||
13
index.html
@ -15,9 +15,22 @@
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@lovable_dev" />
|
||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-5V6TCG4C');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5V6TCG4C"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
10
public/images/projects/avopieces.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#1f2937" rx="8"/>
|
||||
<rect x="8" y="8" width="48" height="32" fill="#374151" rx="4"/>
|
||||
<polygon points="20,16 36,16 40,24 32,24 28,32 24,24 16,24" fill="#fbbf24"/>
|
||||
<rect x="8" y="44" width="48" height="12" fill="#4b5563" rx="2"/>
|
||||
<rect x="12" y="48" width="8" height="4" fill="#9ca3af" rx="1"/>
|
||||
<rect x="24" y="48" width="8" height="4" fill="#9ca3af" rx="1"/>
|
||||
<rect x="36" y="48" width="8" height="4" fill="#9ca3af" rx="1"/>
|
||||
<rect x="48" y="48" width="8" height="4" fill="#9ca3af" rx="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 627 B |
12
public/images/projects/cloud.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#1e40af" rx="8"/>
|
||||
<rect x="8" y="8" width="48" height="12" fill="#3b82f6" rx="2"/>
|
||||
<rect x="8" y="24" width="48" height="12" fill="#60a5fa" rx="2"/>
|
||||
<rect x="8" y="40" width="48" height="12" fill="#93c5fd" rx="2"/>
|
||||
<circle cx="12" cy="14" r="2" fill="#ffffff"/>
|
||||
<circle cx="12" cy="30" r="2" fill="#ffffff"/>
|
||||
<circle cx="12" cy="46" r="2" fill="#ffffff"/>
|
||||
<rect x="20" y="12" width="20" height="4" fill="#ffffff" rx="1"/>
|
||||
<rect x="20" y="28" width="20" height="4" fill="#ffffff" rx="1"/>
|
||||
<rect x="20" y="44" width="20" height="4" fill="#ffffff" rx="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
8
public/images/projects/cube3d.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#7c3aed" rx="8"/>
|
||||
<polygon points="32,8 52,24 52,40 32,56 12,40 12,24" fill="#a855f7"/>
|
||||
<polygon points="32,16 44,26 44,38 32,48 20,38 20,26" fill="#c084fc"/>
|
||||
<polygon points="32,24 36,27 36,37 32,40 28,37 28,27" fill="#ddd6fe"/>
|
||||
<rect x="30" y="20" width="4" height="24" fill="#8b5cf6"/>
|
||||
<rect x="20" y="30" width="24" height="4" fill="#8b5cf6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 485 B |
9
public/images/projects/etsidemain.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#059669" rx="8"/>
|
||||
<circle cx="32" cy="32" r="24" fill="#10b981"/>
|
||||
<path d="M20 32 Q32 20 44 32 Q32 44 20 32" fill="#34d399"/>
|
||||
<circle cx="24" cy="28" r="3" fill="#ffffff"/>
|
||||
<circle cx="40" cy="28" r="3" fill="#ffffff"/>
|
||||
<path d="M24 40 Q32 36 40 40" stroke="#ffffff" stroke-width="2" fill="none"/>
|
||||
<rect x="28" y="12" width="8" height="4" fill="#065f46" rx="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
11
public/images/projects/minishell.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#0c0c0c" rx="8"/>
|
||||
<rect x="4" y="8" width="56" height="48" fill="#1a1a1a" rx="2"/>
|
||||
<text x="8" y="20" font-family="monospace" font-size="6" fill="#10b981">$</text>
|
||||
<rect x="12" y="16" width="24" height="2" fill="#10b981" rx="1"/>
|
||||
<text x="8" y="28" font-family="monospace" font-size="6" fill="#10b981">$</text>
|
||||
<rect x="12" y="24" width="36" height="2" fill="#10b981" rx="1"/>
|
||||
<text x="8" y="36" font-family="monospace" font-size="6" fill="#10b981">$</text>
|
||||
<rect x="12" y="32" width="20" height="2" fill="#10b981" rx="1"/>
|
||||
<rect x="8" y="44" width="2" height="2" fill="#10b981"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
12
public/images/projects/nas.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#1e293b" rx="8"/>
|
||||
<rect x="8" y="8" width="48" height="48" fill="#334155" rx="4"/>
|
||||
<rect x="12" y="12" width="8" height="8" fill="#06b6d4" rx="2"/>
|
||||
<rect x="24" y="12" width="8" height="8" fill="#06b6d4" rx="2"/>
|
||||
<rect x="36" y="12" width="8" height="8" fill="#06b6d4" rx="2"/>
|
||||
<rect x="48" y="12" width="8" height="8" fill="#06b6d4" rx="2"/>
|
||||
<rect x="12" y="24" width="44" height="4" fill="#475569" rx="2"/>
|
||||
<rect x="12" y="32" width="44" height="4" fill="#475569" rx="2"/>
|
||||
<rect x="12" y="40" width="32" height="4" fill="#475569" rx="2"/>
|
||||
<circle cx="48" cy="48" r="8" fill="#10b981"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 733 B |
8
public/images/projects/transcendence.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" fill="#0f172a" rx="8"/>
|
||||
<rect x="8" y="16" width="48" height="32" fill="#1e293b" rx="4"/>
|
||||
<circle cx="20" cy="32" r="8" fill="#f59e0b"/>
|
||||
<circle cx="44" cy="32" r="8" fill="#ef4444"/>
|
||||
<rect x="16" y="52" width="32" height="4" fill="#334155" rx="2"/>
|
||||
<rect x="24" y="8" width="16" height="4" fill="#334155" rx="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 446 B |
173
src/components/CookieBanner.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Cookie, X, Settings } from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useCookieBanner } from "@/contexts/CookieBannerContext";
|
||||
|
||||
export const CookieBanner = () => {
|
||||
const { t } = useLanguage();
|
||||
const { isVisible, hideBanner } = useCookieBanner();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
const acceptAllCookies = () => {
|
||||
localStorage.setItem("cookieConsent", "accepted");
|
||||
localStorage.setItem("analyticsEnabled", "true");
|
||||
hideBanner();
|
||||
|
||||
// Activer Google Tag Manager
|
||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||
(window as any).gtag('consent', 'update', {
|
||||
'analytics_storage': 'granted'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const acceptNecessaryOnly = () => {
|
||||
localStorage.setItem("cookieConsent", "necessary");
|
||||
localStorage.setItem("analyticsEnabled", "false");
|
||||
hideBanner();
|
||||
|
||||
// Désactiver les cookies analytiques
|
||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||
(window as any).gtag('consent', 'update', {
|
||||
'analytics_storage': 'denied'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const rejectAll = () => {
|
||||
localStorage.setItem("cookieConsent", "rejected");
|
||||
localStorage.setItem("analyticsEnabled", "false");
|
||||
hideBanner();
|
||||
|
||||
// Désactiver tous les cookies non nécessaires
|
||||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||||
(window as any).gtag('consent', 'update', {
|
||||
'analytics_storage': 'denied'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<>
|
||||
{/* Overlay pour mobile */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-background/50 backdrop-blur-sm z-40 md:hidden"
|
||||
/>
|
||||
|
||||
{/* Bannière de cookies */}
|
||||
<motion.div
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 200 }}
|
||||
className="fixed bottom-0 left-0 right-0 z-50 p-4 md:p-6"
|
||||
>
|
||||
<Card className="mx-auto max-w-4xl border-border/50 bg-card/95 backdrop-blur-lg shadow-2xl">
|
||||
<div className="p-4 md:p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Cookie className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Gestion des cookies</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nous utilisons des cookies pour améliorer votre expérience
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => hideBanner()}
|
||||
className="shrink-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Contenu */}
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Ce site utilise des cookies pour analyser le trafic et personnaliser le contenu.
|
||||
Les cookies analytiques nous aident à comprendre comment vous utilisez notre site.
|
||||
</p>
|
||||
|
||||
{showDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="space-y-3 mb-4"
|
||||
>
|
||||
<div className="p-3 bg-muted/50 rounded-lg">
|
||||
<h4 className="font-medium text-sm mb-1">🍪 Cookies nécessaires</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Requis pour le fonctionnement de base du site (préférences, sécurité)
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-muted/50 rounded-lg">
|
||||
<h4 className="font-medium text-sm mb-1">📊 Cookies analytiques</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Google Analytics pour mesurer l'audience et améliorer le site
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-2 sm:justify-between sm:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="text-xs sm:order-first"
|
||||
>
|
||||
<Settings className="w-3 h-3 mr-1" />
|
||||
{showDetails ? "Masquer les détails" : "Voir les détails"}
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={rejectAll}
|
||||
className="text-xs"
|
||||
>
|
||||
Tout refuser
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={acceptNecessaryOnly}
|
||||
className="text-xs"
|
||||
>
|
||||
Nécessaires seulement
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={acceptAllCookies}
|
||||
className="text-xs"
|
||||
>
|
||||
Tout accepter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
179
src/components/Footer.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useCookieBanner } from "@/contexts/CookieBannerContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { LegalNoticeModal } from "@/components/LegalNoticeModal";
|
||||
import { Github, Mail, ExternalLink, Cookie, FileText } from "lucide-react";
|
||||
|
||||
export const Footer = () => {
|
||||
const { t } = useLanguage();
|
||||
const { showBanner } = useCookieBanner();
|
||||
const [showLegalNotice, setShowLegalNotice] = useState(false);
|
||||
|
||||
const reopenCookiePreferences = () => {
|
||||
// Simplement rouvrir la bannière sans recharger
|
||||
showBanner();
|
||||
};
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const footerLinks = [
|
||||
{
|
||||
icon: <FileText className="w-4 h-4" />,
|
||||
label: "Mentions légales",
|
||||
onClick: () => setShowLegalNotice(true)
|
||||
},
|
||||
{
|
||||
icon: <Cookie className="w-4 h-4" />,
|
||||
label: "Gestion des cookies",
|
||||
onClick: reopenCookiePreferences
|
||||
}
|
||||
];
|
||||
|
||||
const socialLinks = [
|
||||
{
|
||||
icon: <Github className="w-5 h-5" />,
|
||||
label: "GitHub",
|
||||
href: "https://github.com/kinou-p"
|
||||
},
|
||||
{
|
||||
icon: <Mail className="w-5 h-5" />,
|
||||
label: "Email",
|
||||
href: "mailto:contact@apommier.com"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-background/50 backdrop-blur-sm border-t border-border/50">
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
|
||||
{/* Colonne 1: Logo et description */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="text-2xl font-bold text-gradient">
|
||||
Alexandre Pommier
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Étudiant développeur à 42, passionné par les technologies web et systèmes.
|
||||
Créateur de solutions innovantes pour un avenir numérique.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{socialLinks.map((link, index) => (
|
||||
<motion.a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary hover:bg-primary/20 transition-colors"
|
||||
title={link.label}
|
||||
>
|
||||
{link.icon}
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Colonne 2: Navigation rapide */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="font-semibold text-lg">Navigation</h3>
|
||||
<nav className="space-y-2">
|
||||
{["home", "projects", "skills", "contact"].map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const element = document.getElementById(item);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}}
|
||||
className="justify-start h-auto p-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{t(`nav.${item}`)}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</motion.div>
|
||||
|
||||
{/* Colonne 3: Informations légales */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h3 className="font-semibold text-lg">Informations</h3>
|
||||
<nav className="space-y-2">
|
||||
{footerLinks.map((link, index) => (
|
||||
<Button
|
||||
key={link.label}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={link.onClick}
|
||||
className="justify-start h-auto p-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<span className="mr-2">{link.icon}</span>
|
||||
{link.label}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-6" />
|
||||
|
||||
{/* Bas du footer */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
className="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-muted-foreground"
|
||||
>
|
||||
<div className="text-center md:text-left">
|
||||
© {currentYear} Alexandre Pommier. Tous droits réservés.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
Construit avec
|
||||
<a
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 hover:text-foreground transition-colors"
|
||||
>
|
||||
React <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Modal mentions légales */}
|
||||
<LegalNoticeModal
|
||||
isOpen={showLegalNotice}
|
||||
onClose={() => setShowLegalNotice(false)}
|
||||
/>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@ -15,6 +15,10 @@ export const Header = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ y: -100 }}
|
||||
@ -22,14 +26,16 @@ export const Header = () => {
|
||||
className="fixed top-0 left-0 right-0 z-50 border-b border-border/40 bg-background/80 backdrop-blur-lg"
|
||||
>
|
||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<motion.div
|
||||
<motion.button
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-2xl font-bold text-gradient"
|
||||
onClick={scrollToTop}
|
||||
className="text-2xl font-bold text-gradient hover:scale-105 transition-transform duration-200 cursor-pointer"
|
||||
aria-label="Retour en haut de page"
|
||||
>
|
||||
AP
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{["home", "projects", "skills", "contact"].map((item, i) => (
|
||||
|
||||
169
src/components/LegalNoticeModal.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface LegalNoticeModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const LegalNoticeModal = ({ isOpen, onClose }: LegalNoticeModalProps) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">Mentions Légales</DialogTitle>
|
||||
<DialogDescription>
|
||||
Informations légales et conditions d'utilisation du site
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-full max-h-[60vh]">
|
||||
<div className="space-y-6 pr-4">
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h3 className="font-semibold text-lg mb-3 flex items-center gap-2">
|
||||
🏢 Éditeur du site
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p><strong>Nom :</strong> Alexandre Pommier</p>
|
||||
<p><strong>Statut :</strong> Étudiant à l'École 42</p>
|
||||
<p><strong>Email :</strong> contact@apommier.com</p>
|
||||
<p><strong>GitHub :</strong> @kinou-p</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<h3 className="font-semibold text-lg mb-3 flex items-center gap-2">
|
||||
🌐 Hébergement
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p><strong>Hébergeur :</strong> Serveur personnel d'Alexandre Pommier</p>
|
||||
<p><strong>Type :</strong> Hébergement privé</p>
|
||||
<p><strong>Responsable :</strong> Alexandre Pommier</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<h3 className="font-semibold text-lg mb-3 flex items-center gap-2">
|
||||
📝 Propriété intellectuelle
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Le contenu de ce site portfolio (textes, images, design, code source)
|
||||
est la propriété exclusive d'Alexandre Pommier et est protégé par le droit d'auteur.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Reproduction interdite :</strong> Toute reproduction, distribution,
|
||||
modification ou utilisation du contenu sans autorisation expresse est interdite.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Code source :</strong> Le code source de ce site est propriétaire
|
||||
et non distribué sous licence libre.
|
||||
</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<h3 className="font-semibold text-lg mb-3 flex items-center gap-2">
|
||||
🔒 Données personnelles et cookies
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<strong>Collecte de données :</strong> Ce site ne collecte aucune donnée
|
||||
personnelle directement. Seules les données de navigation anonymisées
|
||||
peuvent être collectées via Google Analytics (avec votre consentement).
|
||||
</p>
|
||||
<p>
|
||||
<strong>Cookies :</strong> Utilisation de cookies techniques nécessaires
|
||||
au fonctionnement du site et de cookies analytiques (Google Analytics)
|
||||
soumis à votre consentement.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Vos droits :</strong> Vous pouvez à tout moment modifier vos
|
||||
préférences de cookies via le bouton dédié dans l'en-tête du site.
|
||||
</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<h3 className="font-semibold text-lg mb-3 flex items-center gap-2">
|
||||
⚖️ Responsabilité
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Les informations diffusées sur ce site sont présentées à titre informatif.
|
||||
Alexandre Pommier s'efforce d'assurer l'exactitude et la mise à jour des
|
||||
informations, mais ne peut garantir l'exactitude, la précision ou
|
||||
l'exhaustivité des informations.
|
||||
</p>
|
||||
<p>
|
||||
En conséquence, l'utilisateur reconnaît utiliser ces informations sous
|
||||
sa responsabilité exclusive.
|
||||
</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<h3 className="font-semibold text-lg mb-3 flex items-center gap-2">
|
||||
📧 Contact
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Pour toute question relative aux présentes mentions légales ou
|
||||
à l'utilisation du site, vous pouvez me contacter :
|
||||
</p>
|
||||
<p><strong>Email :</strong> contact@apommier.com</p>
|
||||
<p><strong>GitHub :</strong> github.com/kinou-p</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<div className="text-xs text-muted-foreground mt-8 pt-4 border-t">
|
||||
<p>Dernière mise à jour : Octobre 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -5,10 +5,11 @@ interface ProjectCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
image?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const ProjectCard = ({ title, description, icon, delay = 0 }: ProjectCardProps) => {
|
||||
export const ProjectCard = ({ title, description, icon, image, delay = 0 }: ProjectCardProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@ -17,12 +18,21 @@ export const ProjectCard = ({ title, description, icon, delay = 0 }: ProjectCard
|
||||
transition={{ duration: 0.5, delay }}
|
||||
whileHover={{ y: -5 }}
|
||||
>
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur">
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur relative overflow-hidden">
|
||||
{image && (
|
||||
<div className="absolute top-4 right-4 w-16 h-16 rounded-lg overflow-hidden border-2 border-background/20 shadow-lg">
|
||||
<img
|
||||
src={image}
|
||||
alt={`${title} preview`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||
{icon}
|
||||
</div>
|
||||
<CardTitle className="text-xl">{title}</CardTitle>
|
||||
<CardTitle className="text-xl pr-20">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-base leading-relaxed">
|
||||
|
||||
@ -1,23 +1,27 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Mail, Phone, Github } from "lucide-react";
|
||||
import { Mail, Github, Send, Share2 } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useState } from "react";
|
||||
|
||||
export const ContactSection = () => {
|
||||
const { t } = useLanguage();
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
const contacts = [
|
||||
{
|
||||
icon: <Phone className="w-6 h-6" />,
|
||||
label: t("contact.phone"),
|
||||
value: "06.52.40.38.30",
|
||||
href: "tel:+33652403830",
|
||||
},
|
||||
{
|
||||
icon: <Mail className="w-6 h-6" />,
|
||||
label: t("contact.email"),
|
||||
value: "apommier@student.42.fr",
|
||||
href: "mailto:apommier@student.42.fr",
|
||||
value: "contact@apommier.com",
|
||||
href: "mailto:contact@apommier.com",
|
||||
},
|
||||
{
|
||||
icon: <Github className="w-6 h-6" />,
|
||||
@ -27,6 +31,42 @@ export const ContactSection = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Créer un lien mailto avec les données du formulaire
|
||||
const subject = encodeURIComponent(formData.subject || "Contact depuis le portfolio");
|
||||
const body = encodeURIComponent(
|
||||
`Nom: ${formData.name}\nEmail: ${formData.email}\n\nMessage:\n${formData.message}`
|
||||
);
|
||||
window.location.href = `mailto:contact@apommier.com?subject=${subject}&body=${body}`;
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
const shareData = {
|
||||
title: "Portfolio d'Alexandre Pommier",
|
||||
text: "Découvrez le portfolio d'Alexandre Pommier, étudiant développeur à 42",
|
||||
url: window.location.href,
|
||||
};
|
||||
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share(shareData);
|
||||
} else {
|
||||
// Fallback : copier l'URL dans le presse-papiers
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
// Vous pouvez ajouter ici une notification toast pour informer l'utilisateur
|
||||
alert("Lien copié dans le presse-papiers !");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Erreur lors du partage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-20 md:py-32">
|
||||
<div className="container mx-auto px-4">
|
||||
@ -45,34 +85,130 @@ export const ContactSection = () => {
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="max-w-3xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{contacts.map((contact, index) => (
|
||||
<motion.a
|
||||
key={contact.label}
|
||||
href={contact.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ y: -5 }}
|
||||
>
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur cursor-pointer">
|
||||
<CardContent className="pt-6 text-center space-y-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mx-auto text-primary">
|
||||
{contact.icon}
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Informations de contact */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h3 className="text-2xl font-semibold mb-6">Restons en contact</h3>
|
||||
<div className="space-y-4">
|
||||
{contacts.map((contact, index) => (
|
||||
<motion.a
|
||||
key={contact.label}
|
||||
href={contact.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
<Card className="hover:shadow-md transition-all duration-300 border-border/50 bg-card/50 backdrop-blur cursor-pointer">
|
||||
<CardContent className="p-4 flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary flex-shrink-0">
|
||||
{contact.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{contact.label}
|
||||
</p>
|
||||
<p className="font-medium">{contact.value}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.a>
|
||||
))}
|
||||
|
||||
{/* Bouton de partage */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
variant="outline"
|
||||
className="w-full p-4 h-auto justify-start space-x-4 hover:shadow-md transition-all duration-300 border-border/50 bg-card/50 backdrop-blur"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary flex-shrink-0">
|
||||
<Share2 className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Partager
|
||||
</p>
|
||||
<p className="font-medium">Partager mon portfolio</p>
|
||||
</div>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Formulaire de contact */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<Card className="border-border/50 bg-card/50 backdrop-blur">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-2xl font-semibold mb-6">Envoyez-moi un message</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
name="name"
|
||||
placeholder="Votre nom"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Votre email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">
|
||||
{contact.label}
|
||||
</p>
|
||||
<p className="font-medium">{contact.value}</p>
|
||||
<Input
|
||||
name="subject"
|
||||
placeholder="Sujet"
|
||||
value={formData.subject}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.a>
|
||||
))}
|
||||
<div>
|
||||
<Textarea
|
||||
name="message"
|
||||
placeholder="Votre message..."
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" size="lg">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Envoyer le message
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
84
src/components/sections/ContactSection_old.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Mail, Github, Send } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useState } from "react";
|
||||
|
||||
export const ContactSection = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const contacts = [
|
||||
{
|
||||
icon: <Phone className="w-6 h-6" />,
|
||||
label: t("contact.phone"),
|
||||
value: "06.52.40.38.30",
|
||||
href: "tel:+33652403830",
|
||||
},
|
||||
{
|
||||
icon: <Mail className="w-6 h-6" />,
|
||||
label: t("contact.email"),
|
||||
value: "contact@apommier.com",
|
||||
href: "mailto:contact@apommier.com",
|
||||
},
|
||||
{
|
||||
icon: <Github className="w-6 h-6" />,
|
||||
label: t("contact.github"),
|
||||
value: "kinou-p",
|
||||
href: "https://github.com/kinou-p",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-20 md:py-32">
|
||||
<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="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
{t("contact.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("contact.subtitle")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="max-w-3xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{contacts.map((contact, index) => (
|
||||
<motion.a
|
||||
key={contact.label}
|
||||
href={contact.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ y: -5 }}
|
||||
>
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur cursor-pointer">
|
||||
<CardContent className="pt-6 text-center space-y-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mx-auto text-primary">
|
||||
{contact.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">
|
||||
{contact.label}
|
||||
</p>
|
||||
<p className="font-medium">{contact.value}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@ -70,12 +70,15 @@ export const HeroSection = () => {
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
className="pt-16"
|
||||
>
|
||||
<motion.div
|
||||
<motion.button
|
||||
animate={{ y: [0, 10, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
onClick={() => scrollToSection("projects")}
|
||||
className="p-2 rounded-full hover:bg-primary/10 transition-colors duration-200 cursor-pointer"
|
||||
aria-label="Scroll to projects"
|
||||
>
|
||||
<ArrowDown className="h-6 w-6 mx-auto text-muted-foreground" />
|
||||
</motion.div>
|
||||
<ArrowDown className="h-8 w-8 mx-auto text-muted-foreground hover:text-primary transition-colors duration-200" />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { ProjectCard } from "@/components/ProjectCard";
|
||||
import { Server, Gamepad2, Cloud, Terminal, Box } from "lucide-react";
|
||||
import { Server, Gamepad2, Cloud, Terminal, Box, Globe, Scale } from "lucide-react";
|
||||
|
||||
export const ProjectsSection = () => {
|
||||
const { t } = useLanguage();
|
||||
@ -10,22 +10,37 @@ export const ProjectsSection = () => {
|
||||
{
|
||||
key: "nas",
|
||||
icon: <Server className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/nas.svg",
|
||||
},
|
||||
{
|
||||
key: "transcendence",
|
||||
icon: <Gamepad2 className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/transcendence.svg",
|
||||
},
|
||||
{
|
||||
key: "cloud",
|
||||
icon: <Cloud className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/cloud.svg",
|
||||
},
|
||||
{
|
||||
key: "minishell",
|
||||
icon: <Terminal className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/minishell.svg",
|
||||
},
|
||||
{
|
||||
key: "cube3d",
|
||||
icon: <Box className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/cube3d.svg",
|
||||
},
|
||||
{
|
||||
key: "etsidemain",
|
||||
icon: <Globe className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/etsidemain.svg",
|
||||
},
|
||||
{
|
||||
key: "avopieces",
|
||||
icon: <Scale className="w-6 h-6 text-primary" />,
|
||||
image: "/images/projects/avopieces.svg",
|
||||
},
|
||||
];
|
||||
|
||||
@ -54,6 +69,7 @@ export const ProjectsSection = () => {
|
||||
title={t(`projects.items.${project.key}.title`)}
|
||||
description={t(`projects.items.${project.key}.description`)}
|
||||
icon={project.icon}
|
||||
image={project.image}
|
||||
delay={index * 0.1}
|
||||
/>
|
||||
))}
|
||||
|
||||
51
src/contexts/CookieBannerContext.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||
|
||||
interface CookieBannerContextType {
|
||||
isVisible: boolean;
|
||||
showBanner: () => void;
|
||||
hideBanner: () => void;
|
||||
}
|
||||
|
||||
const CookieBannerContext = createContext<CookieBannerContextType | undefined>(undefined);
|
||||
|
||||
export const useCookieBanner = () => {
|
||||
const context = useContext(CookieBannerContext);
|
||||
if (!context) {
|
||||
throw new Error("useCookieBanner must be used within a CookieBannerProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface CookieBannerProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const CookieBannerProvider = ({ children }: CookieBannerProviderProps) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier si l'utilisateur a déjà fait un choix
|
||||
const cookieConsent = localStorage.getItem("cookieConsent");
|
||||
if (!cookieConsent) {
|
||||
// Délai de 2 secondes avant d'afficher la bannière
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showBanner = () => {
|
||||
setIsVisible(true);
|
||||
};
|
||||
|
||||
const hideBanner = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<CookieBannerContext.Provider value={{ isVisible, showBanner, hideBanner }}>
|
||||
{children}
|
||||
</CookieBannerContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,9 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { initializeGTM } from "./utils/gtm.ts";
|
||||
|
||||
// Initialiser Google Tag Manager
|
||||
initializeGTM();
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
@ -1,26 +1,33 @@
|
||||
import { Header } from "@/components/Header";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { ScrollProgress } from "@/components/ScrollProgress";
|
||||
import { CookieBanner } from "@/components/CookieBanner";
|
||||
import { HeroSection } from "@/components/sections/HeroSection";
|
||||
import { ProjectsSection } from "@/components/sections/ProjectsSection";
|
||||
import { SkillsSection } from "@/components/sections/SkillsSection";
|
||||
import { ContactSection } from "@/components/sections/ContactSection";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import { LanguageProvider } from "@/contexts/LanguageContext";
|
||||
import { CookieBannerProvider } from "@/contexts/CookieBannerContext";
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<div className="min-h-screen">
|
||||
<ScrollProgress />
|
||||
<Header />
|
||||
<main>
|
||||
<HeroSection />
|
||||
<ProjectsSection />
|
||||
<SkillsSection />
|
||||
<ContactSection />
|
||||
</main>
|
||||
</div>
|
||||
<CookieBannerProvider>
|
||||
<div className="min-h-screen">
|
||||
<ScrollProgress />
|
||||
<Header />
|
||||
<main>
|
||||
<HeroSection />
|
||||
<ProjectsSection />
|
||||
<SkillsSection />
|
||||
<ContactSection />
|
||||
</main>
|
||||
<Footer />
|
||||
<CookieBanner />
|
||||
</div>
|
||||
</CookieBannerProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
51
src/utils/gtm.ts
Normal file
@ -0,0 +1,51 @@
|
||||
// Configuration et initialisation de Google Tag Manager avec gestion du consentement
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag: (...args: any[]) => void;
|
||||
dataLayer: any[];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialiser le dataLayer si il n'existe pas
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
// Configuration du consentement par défaut (refusé jusqu'à acceptation)
|
||||
if (typeof window !== 'undefined' && window.gtag) {
|
||||
window.gtag('consent', 'default', {
|
||||
'analytics_storage': 'denied',
|
||||
'ad_storage': 'denied',
|
||||
'functionality_storage': 'granted',
|
||||
'security_storage': 'granted',
|
||||
});
|
||||
|
||||
// Vérifier si l'utilisateur a déjà donné son consentement
|
||||
const cookieConsent = localStorage.getItem('cookieConsent');
|
||||
const analyticsEnabled = localStorage.getItem('analyticsEnabled');
|
||||
|
||||
if (cookieConsent === 'accepted' && analyticsEnabled === 'true') {
|
||||
window.gtag('consent', 'update', {
|
||||
'analytics_storage': 'granted'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const initializeGTM = () => {
|
||||
// Cette fonction peut être utilisée pour des initialisations supplémentaires si nécessaire
|
||||
console.log('Google Tag Manager initialized');
|
||||
};
|
||||
|
||||
export const trackEvent = (eventName: string, parameters?: Record<string, any>) => {
|
||||
if (typeof window !== 'undefined' && window.gtag) {
|
||||
window.gtag('event', eventName, parameters);
|
||||
}
|
||||
};
|
||||
|
||||
export const trackPageView = (pageName: string) => {
|
||||
if (typeof window !== 'undefined' && window.gtag) {
|
||||
window.gtag('config', 'GTM-5V6TCG4C', {
|
||||
page_title: pageName,
|
||||
page_location: window.location.href,
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -36,6 +36,14 @@ export const translations = {
|
||||
title: "Cube3D",
|
||||
description: "Moteur RayCaster 3D inspiré de Wolfenstein 3D, développé avec MiniLibX et algorithmes graphiques avancés.",
|
||||
},
|
||||
etsidemain: {
|
||||
title: "etsidemain.com",
|
||||
description: "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.",
|
||||
},
|
||||
avopieces: {
|
||||
title: "avopieces.fr",
|
||||
description: "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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
@ -47,7 +55,6 @@ export const translations = {
|
||||
contact: {
|
||||
title: "Contact",
|
||||
subtitle: "N'hésitez pas à me contacter",
|
||||
phone: "Téléphone",
|
||||
email: "Email",
|
||||
github: "GitHub",
|
||||
},
|
||||
@ -89,6 +96,14 @@ export const translations = {
|
||||
title: "Cube3D",
|
||||
description: "3D RayCaster engine inspired by Wolfenstein 3D, developed with MiniLibX and advanced graphics algorithms.",
|
||||
},
|
||||
etsidemain: {
|
||||
title: "etsidemain.com",
|
||||
description: "Showcase website for regenerative transformation consulting firm. Modern responsive design with CSS animations, contact form and SEO optimizations.",
|
||||
},
|
||||
avopieces: {
|
||||
title: "avopieces.fr",
|
||||
description: "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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
@ -100,7 +115,6 @@ export const translations = {
|
||||
contact: {
|
||||
title: "Contact",
|
||||
subtitle: "Feel free to reach out",
|
||||
phone: "Phone",
|
||||
email: "Email",
|
||||
github: "GitHub",
|
||||
},
|
||||
|
||||