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
|
3. **Cloud-1** - Infrastructure automatisée avec Docker/Ansible
|
||||||
4. **Minishell** - Réimplémentation d'un shell bash en C
|
4. **Minishell** - Réimplémentation d'un shell bash en C
|
||||||
5. **Cube3D** - Moteur 3D RayCaster
|
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
|
## 📱 Responsive Design
|
||||||
|
|
||||||
|
|||||||
13
index.html
@ -15,9 +15,22 @@
|
|||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@lovable_dev" />
|
<meta name="twitter:site" content="@lovable_dev" />
|
||||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</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 (
|
return (
|
||||||
<motion.header
|
<motion.header
|
||||||
initial={{ y: -100 }}
|
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"
|
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">
|
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
<motion.div
|
<motion.button
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.2 }}
|
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
|
AP
|
||||||
</motion.div>
|
</motion.button>
|
||||||
|
|
||||||
<nav className="hidden md:flex items-center gap-8">
|
<nav className="hidden md:flex items-center gap-8">
|
||||||
{["home", "projects", "skills", "contact"].map((item, i) => (
|
{["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;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
image?: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectCard = ({ title, description, icon, delay = 0 }: ProjectCardProps) => {
|
export const ProjectCard = ({ title, description, icon, image, delay = 0 }: ProjectCardProps) => {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@ -17,12 +18,21 @@ export const ProjectCard = ({ title, description, icon, delay = 0 }: ProjectCard
|
|||||||
transition={{ duration: 0.5, delay }}
|
transition={{ duration: 0.5, delay }}
|
||||||
whileHover={{ y: -5 }}
|
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>
|
<CardHeader>
|
||||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl">{title}</CardTitle>
|
<CardTitle className="text-xl pr-20">{title}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<CardDescription className="text-base leading-relaxed">
|
<CardDescription className="text-base leading-relaxed">
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
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 { 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 = () => {
|
export const ContactSection = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
subject: "",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
|
||||||
const contacts = [
|
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" />,
|
icon: <Mail className="w-6 h-6" />,
|
||||||
label: t("contact.email"),
|
label: t("contact.email"),
|
||||||
value: "apommier@student.42.fr",
|
value: "contact@apommier.com",
|
||||||
href: "mailto:apommier@student.42.fr",
|
href: "mailto:contact@apommier.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Github className="w-6 h-6" />,
|
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 (
|
return (
|
||||||
<section id="contact" className="py-20 md:py-32">
|
<section id="contact" className="py-20 md:py-32">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
@ -45,34 +85,130 @@ export const ContactSection = () => {
|
|||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="max-w-3xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="max-w-4xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||||
{contacts.map((contact, index) => (
|
{/* Informations de contact */}
|
||||||
<motion.a
|
<motion.div
|
||||||
key={contact.label}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
href={contact.href}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
target="_blank"
|
viewport={{ once: true }}
|
||||||
rel="noopener noreferrer"
|
transition={{ duration: 0.5 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="space-y-6"
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
>
|
||||||
viewport={{ once: true }}
|
<h3 className="text-2xl font-semibold mb-6">Restons en contact</h3>
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
<div className="space-y-4">
|
||||||
whileHover={{ y: -5 }}
|
{contacts.map((contact, index) => (
|
||||||
>
|
<motion.a
|
||||||
<Card className="h-full hover:shadow-lg transition-all duration-300 border-border/50 bg-card/50 backdrop-blur cursor-pointer">
|
key={contact.label}
|
||||||
<CardContent className="pt-6 text-center space-y-3">
|
href={contact.href}
|
||||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mx-auto text-primary">
|
target="_blank"
|
||||||
{contact.icon}
|
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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">
|
<Input
|
||||||
{contact.label}
|
name="subject"
|
||||||
</p>
|
placeholder="Sujet"
|
||||||
<p className="font-medium">{contact.value}</p>
|
value={formData.subject}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
<div>
|
||||||
</Card>
|
<Textarea
|
||||||
</motion.a>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 }}
|
transition={{ duration: 0.5, delay: 0.8 }}
|
||||||
className="pt-16"
|
className="pt-16"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.button
|
||||||
animate={{ y: [0, 10, 0] }}
|
animate={{ y: [0, 10, 0] }}
|
||||||
transition={{ duration: 1.5, repeat: Infinity }}
|
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" />
|
<ArrowDown className="h-8 w-8 mx-auto text-muted-foreground hover:text-primary transition-colors duration-200" />
|
||||||
</motion.div>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { ProjectCard } from "@/components/ProjectCard";
|
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 = () => {
|
export const ProjectsSection = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
@ -10,22 +10,37 @@ export const ProjectsSection = () => {
|
|||||||
{
|
{
|
||||||
key: "nas",
|
key: "nas",
|
||||||
icon: <Server className="w-6 h-6 text-primary" />,
|
icon: <Server className="w-6 h-6 text-primary" />,
|
||||||
|
image: "/images/projects/nas.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "transcendence",
|
key: "transcendence",
|
||||||
icon: <Gamepad2 className="w-6 h-6 text-primary" />,
|
icon: <Gamepad2 className="w-6 h-6 text-primary" />,
|
||||||
|
image: "/images/projects/transcendence.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "cloud",
|
key: "cloud",
|
||||||
icon: <Cloud className="w-6 h-6 text-primary" />,
|
icon: <Cloud className="w-6 h-6 text-primary" />,
|
||||||
|
image: "/images/projects/cloud.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "minishell",
|
key: "minishell",
|
||||||
icon: <Terminal className="w-6 h-6 text-primary" />,
|
icon: <Terminal className="w-6 h-6 text-primary" />,
|
||||||
|
image: "/images/projects/minishell.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "cube3d",
|
key: "cube3d",
|
||||||
icon: <Box className="w-6 h-6 text-primary" />,
|
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`)}
|
title={t(`projects.items.${project.key}.title`)}
|
||||||
description={t(`projects.items.${project.key}.description`)}
|
description={t(`projects.items.${project.key}.description`)}
|
||||||
icon={project.icon}
|
icon={project.icon}
|
||||||
|
image={project.image}
|
||||||
delay={index * 0.1}
|
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 { createRoot } from "react-dom/client";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { initializeGTM } from "./utils/gtm.ts";
|
||||||
|
|
||||||
|
// Initialiser Google Tag Manager
|
||||||
|
initializeGTM();
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(<App />);
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|||||||
@ -1,26 +1,33 @@
|
|||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
|
import { Footer } from "@/components/Footer";
|
||||||
import { ScrollProgress } from "@/components/ScrollProgress";
|
import { ScrollProgress } from "@/components/ScrollProgress";
|
||||||
|
import { CookieBanner } from "@/components/CookieBanner";
|
||||||
import { HeroSection } from "@/components/sections/HeroSection";
|
import { HeroSection } from "@/components/sections/HeroSection";
|
||||||
import { ProjectsSection } from "@/components/sections/ProjectsSection";
|
import { ProjectsSection } from "@/components/sections/ProjectsSection";
|
||||||
import { SkillsSection } from "@/components/sections/SkillsSection";
|
import { SkillsSection } from "@/components/sections/SkillsSection";
|
||||||
import { ContactSection } from "@/components/sections/ContactSection";
|
import { ContactSection } from "@/components/sections/ContactSection";
|
||||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||||
import { LanguageProvider } from "@/contexts/LanguageContext";
|
import { LanguageProvider } from "@/contexts/LanguageContext";
|
||||||
|
import { CookieBannerProvider } from "@/contexts/CookieBannerContext";
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<div className="min-h-screen">
|
<CookieBannerProvider>
|
||||||
<ScrollProgress />
|
<div className="min-h-screen">
|
||||||
<Header />
|
<ScrollProgress />
|
||||||
<main>
|
<Header />
|
||||||
<HeroSection />
|
<main>
|
||||||
<ProjectsSection />
|
<HeroSection />
|
||||||
<SkillsSection />
|
<ProjectsSection />
|
||||||
<ContactSection />
|
<SkillsSection />
|
||||||
</main>
|
<ContactSection />
|
||||||
</div>
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<CookieBanner />
|
||||||
|
</div>
|
||||||
|
</CookieBannerProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</ThemeProvider>
|
</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",
|
title: "Cube3D",
|
||||||
description: "Moteur RayCaster 3D inspiré de Wolfenstein 3D, développé avec MiniLibX et algorithmes graphiques avancés.",
|
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: {
|
skills: {
|
||||||
@ -47,7 +55,6 @@ export const translations = {
|
|||||||
contact: {
|
contact: {
|
||||||
title: "Contact",
|
title: "Contact",
|
||||||
subtitle: "N'hésitez pas à me contacter",
|
subtitle: "N'hésitez pas à me contacter",
|
||||||
phone: "Téléphone",
|
|
||||||
email: "Email",
|
email: "Email",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
},
|
},
|
||||||
@ -89,6 +96,14 @@ export const translations = {
|
|||||||
title: "Cube3D",
|
title: "Cube3D",
|
||||||
description: "3D RayCaster engine inspired by Wolfenstein 3D, developed with MiniLibX and advanced graphics algorithms.",
|
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: {
|
skills: {
|
||||||
@ -100,7 +115,6 @@ export const translations = {
|
|||||||
contact: {
|
contact: {
|
||||||
title: "Contact",
|
title: "Contact",
|
||||||
subtitle: "Feel free to reach out",
|
subtitle: "Feel free to reach out",
|
||||||
phone: "Phone",
|
|
||||||
email: "Email",
|
email: "Email",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
},
|
},
|
||||||
|
|||||||