mirror of
https://github.com/arthur-pbty/portfolio.git
synced 2026-08-01 20:29:43 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { Code2, Server, Network, Container } from "lucide-react";
|
||||
|
||||
const skills = [
|
||||
{
|
||||
icon: <Code2 size={20} className="text-blue-400" />,
|
||||
title: "Développement",
|
||||
desc: "Next.js, React, TypeScript, Python, Node.js",
|
||||
},
|
||||
{
|
||||
icon: <Server size={20} className="text-purple-400" />,
|
||||
title: "Virtualisation",
|
||||
desc: "Proxmox, machines virtuelles, gestion de ressources",
|
||||
},
|
||||
{
|
||||
icon: <Container size={20} className="text-cyan-400" />,
|
||||
title: "Conteneurisation",
|
||||
desc: "Docker, déploiement automatisé, orchestration",
|
||||
},
|
||||
{
|
||||
icon: <Network size={20} className="text-green-400" />,
|
||||
title: "Réseau",
|
||||
desc: "VLAN, firewall, bridges, DNS, reverse proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<section id="a-propos" className="py-16 sm:py-20 md:py-24 px-4 sm:px-6" aria-labelledby="about-heading">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-10 sm:mb-16">
|
||||
<h2 id="about-heading" className="text-2xl sm:text-3xl md:text-4xl font-bold mb-3 sm:mb-4">
|
||||
À propos de <span className="gradient-text">moi</span>
|
||||
</h2>
|
||||
<p className="text-muted max-w-2xl mx-auto">
|
||||
Passionné par la programmation, l'infrastructure réseau, la
|
||||
virtualisation et l'auto-hébergement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 md:gap-12 items-start">
|
||||
{/* Text */}
|
||||
<div className="space-y-5">
|
||||
<p className="text-secondary leading-relaxed">
|
||||
J'aime concevoir des projets concrets, comprendre en
|
||||
profondeur le fonctionnement des systèmes et créer mes propres
|
||||
outils. Mon homelab basé sur <strong className="text-emphasis">Proxmox</strong> fonctionne
|
||||
en continu et me sert d'environnement principal pour
|
||||
héberger mes sites, gérer des machines virtuelles, déployer des
|
||||
conteneurs Docker et expérimenter avec le réseau.
|
||||
</p>
|
||||
<p className="text-secondary leading-relaxed">
|
||||
Mon objectif à long terme est de créer des systèmes complets,
|
||||
des plateformes et des projets ambitieux qui combinent{" "}
|
||||
<strong className="text-emphasis">développement logiciel</strong> et{" "}
|
||||
<strong className="text-emphasis">infrastructure</strong>.
|
||||
</p>
|
||||
<p className="text-secondary leading-relaxed">
|
||||
En parallèle, je suis{" "}
|
||||
<strong className="text-emphasis">sportif de haut niveau en voile</strong>,
|
||||
ce qui m'a appris la rigueur, la discipline et la
|
||||
persévérance — des valeurs que j'applique aussi dans mes
|
||||
projets techniques.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Skills grid */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4">
|
||||
{skills.map((skill) => (
|
||||
<div
|
||||
key={skill.title}
|
||||
className="p-4 sm:p-5 rounded-2xl border border-border bg-card glow-hover"
|
||||
>
|
||||
<div className="mb-3">{skill.icon}</div>
|
||||
<h3 className="font-semibold text-sm mb-1">{skill.title}</h3>
|
||||
<p className="text-xs text-muted leading-relaxed">
|
||||
{skill.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Github, Mail, Globe, Heart } from "lucide-react";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer id="contact" className="py-10 sm:py-16 px-4 sm:px-6 border-t border-border" role="contentinfo">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid sm:grid-cols-3 gap-8 sm:gap-12 mb-8 sm:mb-12">
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold gradient-text mb-3">
|
||||
arthurp.fr
|
||||
</h3>
|
||||
<p className="text-sm text-muted leading-relaxed">
|
||||
Développeur passionné par l'auto-hébergement et navigateur
|
||||
de haut niveau.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-4">Navigation</h4>
|
||||
<div className="space-y-2.5">
|
||||
{[
|
||||
{ href: "#accueil", label: "Accueil" },
|
||||
{ href: "#projets", label: "Projets" },
|
||||
{ href: "#homelab", label: "Homelab" },
|
||||
{ href: "#voile", label: "Voile" },
|
||||
].map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="block text-sm text-muted hover:text-emphasis transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-4">Contact</h4>
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href="https://arthurp.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-muted hover:text-emphasis transition-colors"
|
||||
>
|
||||
<Globe size={16} />
|
||||
arthurp.fr
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-muted hover:text-emphasis transition-colors"
|
||||
>
|
||||
<Github size={16} />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="mailto:arthur.puechberty@outlook.com"
|
||||
className="flex items-center gap-2 text-sm text-muted hover:text-emphasis transition-colors"
|
||||
>
|
||||
<Mail size={16} />
|
||||
arthur.puechberty@outlook.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section-divider mb-8" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p className="text-xs text-dim">
|
||||
© {new Date().getFullYear()} Arthur P. Tous droits réservés.
|
||||
</p>
|
||||
<p className="text-xs text-dim flex items-center gap-1">
|
||||
Fait avec <Heart size={12} className="text-red-400" /> et
|
||||
auto-hébergé sur Proxmox
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ArrowDown, Terminal, Sailboat } from "lucide-react";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section
|
||||
id="accueil"
|
||||
className="relative min-h-screen flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
{/* Background grid */}
|
||||
<div className="absolute inset-0 opacity-[0.03]" aria-hidden="true">
|
||||
<div className="w-full h-full hero-grid" />
|
||||
</div>
|
||||
|
||||
{/* Gradient orbs */}
|
||||
<div className="absolute top-1/4 -left-32 w-64 sm:w-96 h-64 sm:h-96 bg-blue-500/10 rounded-full blur-[80px] sm:blur-[120px]" aria-hidden="true" />
|
||||
<div className="absolute bottom-1/4 -right-32 w-64 sm:w-96 h-64 sm:h-96 bg-purple-500/10 rounded-full blur-[80px] sm:blur-[120px]" aria-hidden="true" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 text-center pt-16 sm:pt-0">
|
||||
<div className="flex items-center justify-center gap-3 mb-4 sm:mb-6 animate-fade-in-up">
|
||||
<span className="inline-flex items-center gap-2 px-3 sm:px-4 py-1.5 sm:py-2 rounded-full border border-border bg-card/80 text-xs sm:text-sm text-muted">
|
||||
<Terminal size={14} className="text-blue-400" />
|
||||
Développeur
|
||||
<span className="w-1 h-1 rounded-full bg-subtle" />
|
||||
<Sailboat size={14} className="text-cyan-400" />
|
||||
Navigateur
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight mb-4 sm:mb-6">
|
||||
Salut, je suis{" "}
|
||||
<span className="gradient-text">Arthur</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-base sm:text-lg md:text-xl text-muted max-w-2xl mx-auto mb-3 sm:mb-4 animate-fade-in-up animate-delay-200">
|
||||
18 ans · Développeur passionné par l'auto-hébergement et
|
||||
l'infrastructure · Sportif de haut niveau en voile
|
||||
</p>
|
||||
|
||||
<p className="text-sm sm:text-base text-dim max-w-xl mx-auto mb-8 sm:mb-10 animate-fade-in-up animate-delay-300">
|
||||
Je conçois mes propres outils, héberge mes services et repousse
|
||||
mes limites — sur l'eau comme dans le code.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center animate-fade-in-up animate-delay-400">
|
||||
<a
|
||||
href="#projets"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-2.5 sm:py-3 rounded-full bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm sm:text-base"
|
||||
>
|
||||
Voir mes projets
|
||||
</a>
|
||||
<a
|
||||
href="#voile"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-2.5 sm:py-3 rounded-full border border-border hover:border-subtle-hover text-muted hover:text-emphasis font-medium transition-colors text-sm sm:text-base"
|
||||
>
|
||||
Mon parcours voile
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#a-propos"
|
||||
className="inline-block mt-10 sm:mt-16 text-subtle-hover hover:text-muted transition-colors animate-bounce"
|
||||
aria-label="Défiler vers le bas"
|
||||
>
|
||||
<ArrowDown size={24} />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
Server,
|
||||
HardDrive,
|
||||
Container,
|
||||
Network,
|
||||
Shield,
|
||||
Image,
|
||||
Tv,
|
||||
Monitor,
|
||||
} from "lucide-react";
|
||||
|
||||
const infra = [
|
||||
{
|
||||
icon: <Server size={20} className="text-blue-400" />,
|
||||
title: "Proxmox VE",
|
||||
desc: "Hyperviseur principal pour la virtualisation de toutes mes machines et conteneurs.",
|
||||
},
|
||||
{
|
||||
icon: <Container size={20} className="text-cyan-400" />,
|
||||
title: "Docker",
|
||||
desc: "Déploiement conteneurisé de l'ensemble de mes applications et services.",
|
||||
},
|
||||
{
|
||||
icon: <HardDrive size={20} className="text-purple-400" />,
|
||||
title: "Machines virtuelles",
|
||||
desc: "Isolation des environnements : production, test, développement.",
|
||||
},
|
||||
{
|
||||
icon: <Network size={20} className="text-green-400" />,
|
||||
title: "Réseau avancé",
|
||||
desc: "VLAN, bridges, DNS personnalisé, reverse proxy Nginx.",
|
||||
},
|
||||
{
|
||||
icon: <Shield size={20} className="text-red-400" />,
|
||||
title: "Sécurité",
|
||||
desc: "Firewall, certificats SSL, accès sécurisés et segmentation réseau.",
|
||||
},
|
||||
{
|
||||
icon: <Monitor size={20} className="text-amber-400" />,
|
||||
title: "Déploiement automatisé",
|
||||
desc: "CI/CD, scripts de déploiement, gestion via Git.",
|
||||
},
|
||||
];
|
||||
|
||||
const personalServices = [
|
||||
{
|
||||
icon: <Image size={20} className="text-pink-400" />,
|
||||
name: "Immich",
|
||||
url: "immich.arthurp.fr",
|
||||
desc: "Gestionnaire de photos auto-hébergé — alternative à Google Photos.",
|
||||
},
|
||||
{
|
||||
icon: <Tv size={20} className="text-indigo-400" />,
|
||||
name: "Jellyfin",
|
||||
url: "jellyfin.arthurp.fr",
|
||||
desc: "Serveur multimédia auto-hébergé — films, séries, musique.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Homelab() {
|
||||
return (
|
||||
<section id="homelab" className="py-16 sm:py-20 md:py-24 px-4 sm:px-6" aria-labelledby="homelab-heading">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-10 sm:mb-16">
|
||||
<h2 id="homelab-heading" className="text-2xl sm:text-3xl md:text-4xl font-bold mb-3 sm:mb-4">
|
||||
Mon <span className="gradient-text">Homelab</span>
|
||||
</h2>
|
||||
<p className="text-muted max-w-2xl mx-auto">
|
||||
Une infrastructure personnelle qui tourne 24/7 — mon terrain de
|
||||
jeu pour l'auto-hébergement, la virtualisation et le réseau.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Architecture diagram */}
|
||||
<div className="mb-10 sm:mb-16 p-4 sm:p-6 md:p-8 rounded-2xl border border-border bg-card">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
||||
<span className="text-sm text-green-400 font-mono">
|
||||
Infrastructure en ligne
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
{infra.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="p-4 sm:p-5 rounded-xl border border-border bg-deep hover:border-subtle transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{item.icon}
|
||||
<h3 className="font-semibold text-sm">{item.title}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-muted leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Personal services */}
|
||||
<div>
|
||||
<h3 className="text-lg sm:text-xl font-bold mb-4 sm:mb-6 text-center">
|
||||
Exemples de services personnels hébergés
|
||||
</h3>
|
||||
<div className="grid sm:grid-cols-2 gap-4 sm:gap-5 max-w-2xl mx-auto">
|
||||
{personalServices.map((svc) => (
|
||||
<div
|
||||
key={svc.name}
|
||||
className="p-4 sm:p-6 rounded-2xl border border-border bg-card glow-hover"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{svc.icon}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm">{svc.name}</h4>
|
||||
<span className="text-[10px] text-dim font-mono">
|
||||
{svc.url}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted leading-relaxed">
|
||||
{svc.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-xs text-dim mt-6">
|
||||
Ces services tournent sur mon infrastructure Proxmox avec
|
||||
virtualisation et conteneurs Docker — et bien d'autres encore.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Menu, X, Sun, Moon } from "lucide-react";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
|
||||
const navLinks = [
|
||||
{ href: "#accueil", label: "Accueil" },
|
||||
{ href: "#a-propos", label: "À propos" },
|
||||
{ href: "#projets", label: "Projets" },
|
||||
{ href: "#homelab", label: "Homelab" },
|
||||
{ href: "#voile", label: "Voile" },
|
||||
{ href: "/photos", label: "Galerie" },
|
||||
{ href: "#contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 50);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Navigation principale"
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-background/80 backdrop-blur-xl border-b border-border"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between">
|
||||
<a
|
||||
href="#accueil"
|
||||
className="text-lg sm:text-xl font-bold gradient-text tracking-tight"
|
||||
>
|
||||
arthurp.fr
|
||||
</a>
|
||||
|
||||
{/* Desktop */}
|
||||
<div className="hidden lg:flex items-center gap-6 xl:gap-8">
|
||||
{navLinks.map((link) =>
|
||||
link.href.startsWith("#") ? (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-muted hover:text-emphasis transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-muted hover:text-emphasis transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-xl border border-border bg-card hover:bg-card-hover text-muted hover:text-emphasis transition-all duration-200"
|
||||
aria-label={
|
||||
theme === "dark"
|
||||
? "Passer en mode clair"
|
||||
: "Passer en mode sombre"
|
||||
}
|
||||
>
|
||||
{theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile toggle */}
|
||||
<div className="flex lg:hidden items-center gap-2 sm:gap-3">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-xl border border-border bg-card text-muted hover:text-emphasis transition-colors"
|
||||
aria-label={
|
||||
theme === "dark"
|
||||
? "Passer en mode clair"
|
||||
: "Passer en mode sombre"
|
||||
}
|
||||
>
|
||||
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="text-muted hover:text-emphasis"
|
||||
aria-label="Menu"
|
||||
>
|
||||
{mobileOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileOpen && (
|
||||
<div className="lg:hidden bg-background/95 backdrop-blur-xl border-b border-border px-4 sm:px-6 pb-6">
|
||||
{navLinks.map((link) =>
|
||||
link.href.startsWith("#") ? (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block py-3 text-sm text-muted hover:text-emphasis transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block py-3 text-sm text-muted hover:text-emphasis transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { ExternalLink, Timer, FileQuestion, Grid3X3, Video, QrCode, BookOpen, Link2, FileText, Bot, LinkIcon, Clock } from "lucide-react";
|
||||
|
||||
const projects = [
|
||||
{
|
||||
name: "arthurp.fr",
|
||||
url: "https://arthurp.fr",
|
||||
desc: "Site principal qui centralise mon écosystème de projets et services.",
|
||||
icon: <LinkIcon size={20} className="text-blue-400" />,
|
||||
tags: ["Site vitrine", "Hub"],
|
||||
},
|
||||
{
|
||||
name: "pomodoro.arthurp.fr",
|
||||
url: "https://pomodoro.arthurp.fr",
|
||||
desc: "Application de gestion du temps basée sur la méthode Pomodoro.",
|
||||
icon: <Timer size={20} className="text-red-400" />,
|
||||
tags: ["Productivité", "Web App"],
|
||||
},
|
||||
{
|
||||
name: "qcu.arthurp.fr",
|
||||
url: "https://qcu.arthurp.fr",
|
||||
desc: "Site de QCU pour réviser la physique.",
|
||||
icon: <FileQuestion size={20} className="text-yellow-400" />,
|
||||
tags: ["Éducation", "Physique"],
|
||||
},
|
||||
{
|
||||
name: "sudoku.arthurp.fr",
|
||||
url: "https://sudoku.arthurp.fr",
|
||||
desc: "Générateur et jeu de Sudoku en ligne.",
|
||||
icon: <Grid3X3 size={20} className="text-green-400" />,
|
||||
tags: ["Jeu", "Logique"],
|
||||
},
|
||||
{
|
||||
name: "visio.arthurp.fr",
|
||||
url: "https://visio.arthurp.fr",
|
||||
desc: "Solution de visioconférence auto-hébergée.",
|
||||
icon: <Video size={20} className="text-purple-400" />,
|
||||
tags: ["Communication", "Auto-hébergé"],
|
||||
},
|
||||
{
|
||||
name: "qrcode.arthurp.fr",
|
||||
url: "https://qrcode.arthurp.fr",
|
||||
desc: "Générateur de QR codes personnalisés.",
|
||||
icon: <QrCode size={20} className="text-cyan-400" />,
|
||||
tags: ["Outil", "Utilitaire"],
|
||||
},
|
||||
{
|
||||
name: "learn.arthurp.fr",
|
||||
url: "https://learn.arthurp.fr",
|
||||
desc: "Plateforme d'apprentissage et de documentation personnelle.",
|
||||
icon: <BookOpen size={20} className="text-orange-400" />,
|
||||
tags: ["Apprentissage", "Documentation"],
|
||||
},
|
||||
{
|
||||
name: "links.arthurp.fr",
|
||||
url: "https://links.arthurp.fr",
|
||||
desc: "Page de centralisation de liens.",
|
||||
icon: <Link2 size={20} className="text-pink-400" />,
|
||||
tags: ["Liens", "Hub"],
|
||||
},
|
||||
{
|
||||
name: "form.arthurp.fr",
|
||||
url: "https://form.arthurp.fr",
|
||||
desc: "Système de formulaires auto-hébergé.",
|
||||
icon: <FileText size={20} className="text-teal-400" />,
|
||||
tags: ["Formulaires", "Auto-hébergé"],
|
||||
},
|
||||
{
|
||||
name: "lazybot.arthurp.fr",
|
||||
url: "https://lazybot.arthurp.fr",
|
||||
desc: "Bot Discord contrôlé via un dashboard web, développé et hébergé par moi.",
|
||||
icon: <Bot size={20} className="text-indigo-400" />,
|
||||
tags: ["Discord", "Bot", "Dashboard"],
|
||||
},
|
||||
{
|
||||
name: "reducelink.arthurp.fr",
|
||||
url: "https://reducelink.arthurp.fr",
|
||||
desc: "Service de réduction de liens.",
|
||||
icon: <LinkIcon size={20} className="text-amber-400" />,
|
||||
tags: ["Outil", "Raccourcisseur"],
|
||||
},
|
||||
{
|
||||
name: "clock.arthurp.fr",
|
||||
url: "https://clock.arthurp.fr",
|
||||
desc: "Application d'affichage et de gestion du temps.",
|
||||
icon: <Clock size={20} className="text-sky-400" />,
|
||||
tags: ["Temps", "Utilitaire"],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<section id="projets" className="py-16 sm:py-20 md:py-24 px-4 sm:px-6" aria-labelledby="projects-heading">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-10 sm:mb-16">
|
||||
<h2 id="projects-heading" className="text-2xl sm:text-3xl md:text-4xl font-bold mb-3 sm:mb-4">
|
||||
Mes <span className="gradient-text">projets</span>
|
||||
</h2>
|
||||
<p className="text-muted max-w-2xl mx-auto">
|
||||
Tous hébergés sur mon infrastructure personnelle — chaque
|
||||
sous-domaine est un projet conçu, développé et maintenu par moi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
|
||||
{projects.map((project) => (
|
||||
<a
|
||||
key={project.name}
|
||||
href={project.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group p-4 sm:p-6 rounded-2xl border border-border bg-card glow-hover block"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="p-2.5 rounded-xl bg-card-hover border border-border">
|
||||
{project.icon}
|
||||
</div>
|
||||
<ExternalLink
|
||||
size={16}
|
||||
className="text-subtle-hover group-hover:text-blue-400 transition-colors mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="font-semibold text-sm mb-2 group-hover:text-blue-400 transition-colors">
|
||||
{project.name}
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-muted leading-relaxed mb-4">
|
||||
{project.desc}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-[10px] px-2.5 py-1 rounded-full bg-card-hover border border-border text-muted"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Trophy, Medal, Anchor, Wind, Award } from "lucide-react";
|
||||
|
||||
const palmares = [
|
||||
{
|
||||
year: "2025",
|
||||
results: [
|
||||
{ rank: "1er", event: "Spi Ouest France", boat: "Open 5.70" },
|
||||
{ rank: "1er", event: "GPEN", boat: "Open 5.70" },
|
||||
{ rank: "3e", event: "18h d'Arcachon", boat: "Open 5.70" },
|
||||
{ rank: "2e", event: "Championnat de France Espoirs", boat: "" },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: "2024",
|
||||
results: [
|
||||
{ rank: "1er Espoir", event: "National 5.90", boat: "5.90" },
|
||||
{ rank: "2e", event: "Coupe de France", boat: "Open 5.70" },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: "2023",
|
||||
results: [
|
||||
{ rank: "9e", event: "Championnat du monde", boat: "Open Skiff" },
|
||||
],
|
||||
},
|
||||
{
|
||||
year: "2022",
|
||||
results: [
|
||||
{ rank: "1er", event: "Open Tour", boat: "Open Skiff" },
|
||||
{ rank: "2e", event: "Championnat de France", boat: "Birdyfish" },
|
||||
{ rank: "9e", event: "Championnat du monde", boat: "Open Skiff" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const qualities = [
|
||||
{ icon: <Award size={18} />, label: "Rigueur" },
|
||||
{ icon: <Wind size={18} />, label: "Persévérance" },
|
||||
{ icon: <Anchor size={18} />, label: "Discipline" },
|
||||
{ icon: <Trophy size={18} />, label: "Gestion du stress" },
|
||||
];
|
||||
|
||||
function getRankColor(rank: string) {
|
||||
if (rank.startsWith("1er")) return "text-yellow-400 bg-yellow-400/10 border-yellow-400/20";
|
||||
if (rank.startsWith("2e")) return "text-gray-300 bg-gray-300/10 border-gray-300/20";
|
||||
if (rank.startsWith("3e")) return "text-amber-600 bg-amber-600/10 border-amber-600/20";
|
||||
return "text-blue-400 bg-blue-400/10 border-blue-400/20";
|
||||
}
|
||||
|
||||
export default function Sailing() {
|
||||
return (
|
||||
<section id="voile" className="py-16 sm:py-20 md:py-24 px-4 sm:px-6" aria-labelledby="sailing-heading">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-10 sm:mb-16">
|
||||
<h2 id="sailing-heading" className="text-2xl sm:text-3xl md:text-4xl font-bold mb-3 sm:mb-4">
|
||||
Parcours <span className="gradient-text">voile</span>
|
||||
</h2>
|
||||
<p className="text-muted max-w-2xl mx-auto">
|
||||
Sportif de haut niveau — la voile fait partie intégrante de mon
|
||||
identité depuis l'âge de 7 ans.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-5 gap-6 sm:gap-8 lg:gap-12">
|
||||
{/* Story */}
|
||||
<div className="lg:col-span-2 space-y-4 sm:space-y-6">
|
||||
<div className="p-4 sm:p-6 rounded-2xl border border-border bg-card">
|
||||
<h3 className="font-semibold mb-4 flex items-center gap-2">
|
||||
<Anchor size={18} className="text-cyan-400" />
|
||||
Mon histoire
|
||||
</h3>
|
||||
<div className="space-y-4 text-sm text-secondary leading-relaxed">
|
||||
<p>
|
||||
J'ai commencé la voile à <strong className="text-emphasis">7 ans</strong> pendant
|
||||
des stages d'été, influencé par ma mère qui pratiquait
|
||||
déjà ce sport.
|
||||
</p>
|
||||
<p>
|
||||
Lors de mon premier entraînement sur un nouveau bateau, il
|
||||
y avait beaucoup de vent. J'ai chaviré plus de{" "}
|
||||
<strong className="text-emphasis">vingt fois</strong>. Malgré
|
||||
cela, j'ai adoré — j'avais enfin l'impression
|
||||
de vraiment apprendre.
|
||||
</p>
|
||||
<p>
|
||||
Depuis, j'ai remporté plusieurs compétitions nationales,
|
||||
participé à{" "}
|
||||
<strong className="text-emphasis">
|
||||
deux championnats du monde
|
||||
</strong>{" "}
|
||||
en Open Skiff et obtenu le statut de{" "}
|
||||
<strong className="text-emphasis">sportif de haut niveau</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Qualities */}
|
||||
<div className="p-4 sm:p-6 rounded-2xl border border-border bg-card">
|
||||
<h3 className="font-semibold mb-3 sm:mb-4">
|
||||
Ce que la voile m'a appris
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{qualities.map((q) => (
|
||||
<div
|
||||
key={q.label}
|
||||
className="flex items-center gap-2 text-sm text-muted"
|
||||
>
|
||||
<span className="text-cyan-400">{q.icon}</span>
|
||||
{q.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Highlights */}
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-3">
|
||||
<div className="p-3 sm:p-4 rounded-xl border border-border bg-card text-center">
|
||||
<div className="text-2xl font-bold text-emphasis">2</div>
|
||||
<div className="text-[10px] text-muted mt-1">
|
||||
Championnats du monde
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 rounded-xl border border-border bg-card text-center">
|
||||
<div className="text-2xl font-bold text-emphasis">9e</div>
|
||||
<div className="text-[10px] text-muted mt-1">
|
||||
Mondial (×2)
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 rounded-xl border border-border bg-card text-center">
|
||||
<div className="text-2xl font-bold text-emphasis">SHN</div>
|
||||
<div className="text-[10px] text-muted mt-1">
|
||||
Sportif de haut niveau
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Palmarès */}
|
||||
<div className="lg:col-span-3">
|
||||
<div className="p-4 sm:p-6 rounded-2xl border border-border bg-card">
|
||||
<h3 className="font-semibold mb-4 sm:mb-6 flex items-center gap-2">
|
||||
<Trophy size={18} className="text-yellow-400" />
|
||||
Palmarès
|
||||
</h3>
|
||||
|
||||
<div className="space-y-8">
|
||||
{palmares.map((year) => (
|
||||
<div key={year.year}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-2xl font-bold gradient-text">
|
||||
{year.year}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{year.results.map((result, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-3 sm:gap-4 p-2 sm:p-3 rounded-xl hover:bg-card-hover transition-colors"
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center justify-center min-w-15 sm:min-w-18 px-2 sm:px-3 py-1 sm:py-1.5 rounded-lg border text-[10px] sm:text-xs font-bold shrink-0 ${getRankColor(result.rank)}`}
|
||||
>
|
||||
<Medal size={12} className="mr-1 sm:mr-1.5" />
|
||||
{result.rank}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-xs sm:text-sm font-medium">
|
||||
{result.event}
|
||||
</span>
|
||||
{result.boat && (
|
||||
<span className="text-[10px] sm:text-xs text-dim ml-1 sm:ml-2">
|
||||
— {result.boat}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
const ThemeContext = createContext<{
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
}>({
|
||||
theme: "dark",
|
||||
toggleTheme: () => {},
|
||||
});
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
|
||||
export default function ThemeProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [theme, setTheme] = useState<Theme>("dark");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const stored =
|
||||
(document.documentElement.getAttribute("data-theme") as Theme) || "dark";
|
||||
setTheme(stored);
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const next = theme === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
localStorage.setItem("theme", next);
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
+153
@@ -0,0 +1,153 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ─── Theme variables ─── */
|
||||
:root,
|
||||
[data-theme="dark"] {
|
||||
--theme-bg: #0a0a0f;
|
||||
--theme-fg: #f0f0f5;
|
||||
--theme-card: #12121a;
|
||||
--theme-card-hover: #1a1a25;
|
||||
--theme-border: #1e1e2e;
|
||||
--theme-muted: #8888a0;
|
||||
--theme-secondary: #b0b0c0;
|
||||
--theme-dim: #5a5a70;
|
||||
--theme-emphasis: #ffffff;
|
||||
--theme-subtle: #2e2e3e;
|
||||
--theme-subtle-hover: #3e3e4e;
|
||||
--theme-deep: #0e0e16;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--theme-bg: #f8f9fc;
|
||||
--theme-fg: #1a1a2e;
|
||||
--theme-card: #ffffff;
|
||||
--theme-card-hover: #f0f1f5;
|
||||
--theme-border: #dfe1e8;
|
||||
--theme-muted: #6b7084;
|
||||
--theme-secondary: #4a4d60;
|
||||
--theme-dim: #9098a8;
|
||||
--theme-emphasis: #0a0a1a;
|
||||
--theme-subtle: #d0d3dc;
|
||||
--theme-subtle-hover: #b8bcc8;
|
||||
--theme-deep: #f0f1f5;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-background: var(--theme-bg);
|
||||
--color-foreground: var(--theme-fg);
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-light: #60a5fa;
|
||||
--color-card: var(--theme-card);
|
||||
--color-card-hover: var(--theme-card-hover);
|
||||
--color-border: var(--theme-border);
|
||||
--color-muted: var(--theme-muted);
|
||||
--color-secondary: var(--theme-secondary);
|
||||
--color-dim: var(--theme-dim);
|
||||
--color-emphasis: var(--theme-emphasis);
|
||||
--color-subtle: var(--theme-subtle);
|
||||
--color-subtle-hover: var(--theme-subtle-hover);
|
||||
--color-deep: var(--theme-deep);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
* {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-fg);
|
||||
font-family: var(--font-sans), system-ui, sans-serif;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
overflow-x: hidden;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--theme-bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--theme-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--theme-muted);
|
||||
}
|
||||
|
||||
/* Fade-in animation */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.5s ease-out both;
|
||||
}
|
||||
|
||||
.animate-delay-100 { animation-delay: 0.05s; }
|
||||
.animate-delay-200 { animation-delay: 0.1s; }
|
||||
.animate-delay-300 { animation-delay: 0.15s; }
|
||||
.animate-delay-400 { animation-delay: 0.2s; }
|
||||
|
||||
/* Reduce motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in-up {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient text */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6, #06b6d4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Glow effect on hover */
|
||||
.glow-hover {
|
||||
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.glow-hover:hover {
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
[data-theme="light"] .glow-hover:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 0 20px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
/* Section separator */
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--theme-border), transparent);
|
||||
}
|
||||
|
||||
/* Hero grid background */
|
||||
.hero-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
[data-theme="light"] .hero-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import ThemeProvider from "./components/ThemeProvider";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#0a0a0f" },
|
||||
{ media: "(prefers-color-scheme: light)", color: "#f8f9fc" },
|
||||
],
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Arthur P. — Développeur & Navigateur",
|
||||
template: "%s | Arthur P.",
|
||||
},
|
||||
description:
|
||||
"Portfolio d'Arthur P. — développeur passionné par l'auto-hébergement, l'infrastructure Proxmox, Docker et la voile de haut niveau. Découvrez mes projets et mon homelab.",
|
||||
keywords: [
|
||||
"portfolio",
|
||||
"développeur",
|
||||
"homelab",
|
||||
"auto-hébergement",
|
||||
"voile",
|
||||
"Next.js",
|
||||
"Proxmox",
|
||||
"Docker",
|
||||
"infrastructure",
|
||||
"sportif de haut niveau",
|
||||
"Arthur P",
|
||||
"arthurp.fr",
|
||||
],
|
||||
authors: [{ name: "Arthur P.", url: "https://arthurp.fr" }],
|
||||
creator: "Arthur P.",
|
||||
publisher: "Arthur P.",
|
||||
metadataBase: new URL("https://arthurp.fr"),
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "fr_FR",
|
||||
url: "https://arthurp.fr",
|
||||
siteName: "Arthur P. — Portfolio",
|
||||
title: "Arthur P. — Développeur & Navigateur",
|
||||
description:
|
||||
"Développeur passionné par l'auto-hébergement, l'infrastructure et la voile de haut niveau. Découvrez mes projets auto-hébergés et mon homelab Proxmox.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Arthur P. — Développeur & Navigateur",
|
||||
description:
|
||||
"Développeur passionné par l'auto-hébergement, l'infrastructure et la voile de haut niveau.",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
category: "technology",
|
||||
};
|
||||
|
||||
const themeScript = `
|
||||
(function() {
|
||||
try {
|
||||
var t = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
} catch(e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
name: "Arthur P.",
|
||||
url: "https://arthurp.fr",
|
||||
email: "contact@arthurp.fr",
|
||||
jobTitle: "Développeur",
|
||||
description:
|
||||
"Développeur passionné par l'auto-hébergement, l'infrastructure et la voile de haut niveau.",
|
||||
knowsAbout: [
|
||||
"Next.js",
|
||||
"React",
|
||||
"TypeScript",
|
||||
"Docker",
|
||||
"Proxmox",
|
||||
"Auto-hébergement",
|
||||
"Infrastructure réseau",
|
||||
],
|
||||
sameAs: ["https://github.com"],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr" className="scroll-smooth" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<a
|
||||
href="#contenu-principal"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:rounded-lg focus:bg-blue-600 focus:text-white focus:outline-none"
|
||||
>
|
||||
Aller au contenu principal
|
||||
</a>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Navbar from "./components/Navbar";
|
||||
import Hero from "./components/Hero";
|
||||
import About from "./components/About";
|
||||
import Projects from "./components/Projects";
|
||||
import Homelab from "./components/Homelab";
|
||||
import Sailing from "./components/Sailing";
|
||||
import Footer from "./components/Footer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main id="contenu-principal">
|
||||
<Hero />
|
||||
<div className="section-divider max-w-6xl mx-auto px-4 sm:px-6" role="separator" aria-hidden="true" />
|
||||
<About />
|
||||
<div className="section-divider max-w-6xl mx-auto px-4 sm:px-6" role="separator" aria-hidden="true" />
|
||||
<Projects />
|
||||
<div className="section-divider max-w-6xl mx-auto px-4 sm:px-6" role="separator" aria-hidden="true" />
|
||||
<Homelab />
|
||||
<div className="section-divider max-w-6xl mx-auto px-4 sm:px-6" role="separator" aria-hidden="true" />
|
||||
<Sailing />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Galerie Photos",
|
||||
description:
|
||||
"Galerie photo d'Arthur P. — moments de vie, voile et aventures.",
|
||||
};
|
||||
|
||||
const photos = Array.from({ length: 68 }, (_, i) => {
|
||||
const num = String(i + 1).padStart(3, "0");
|
||||
return { src: `/pictures/photo_${num}.webp`, alt: `Photo ${i + 1}` };
|
||||
});
|
||||
|
||||
export default function PhotosPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Back navigation */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-xl border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 text-sm text-muted hover:text-emphasis transition-colors duration-200"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<span>Retour</span>
|
||||
</Link>
|
||||
<h1 className="text-lg sm:text-xl font-bold gradient-text tracking-tight">
|
||||
Galerie
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="pt-20 pb-16 px-4 sm:px-6 max-w-7xl mx-auto">
|
||||
{/* Mosaic grid */}
|
||||
<div className="columns-2 sm:columns-3 md:columns-4 lg:columns-5 gap-3 sm:gap-4">
|
||||
{photos.map((photo, index) => (
|
||||
<div
|
||||
key={photo.src}
|
||||
className="mb-3 sm:mb-4 break-inside-avoid group"
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-xl border border-border bg-card glow-hover">
|
||||
<Image
|
||||
src={photo.src}
|
||||
alt={photo.alt}
|
||||
width={400}
|
||||
height={500}
|
||||
className="w-full h-auto object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
|
||||
loading={index < 10 ? "eager" : "lazy"}
|
||||
/>
|
||||
{/* Subtle overlay on hover */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Back to top */}
|
||||
<div className="mt-12 text-center">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl border border-border bg-card hover:bg-card-hover text-sm text-muted hover:text-emphasis transition-all duration-200 glow-hover"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Retour au portfolio
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: "https://portfolio.arthurp.fr/sitemap.xml",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: "https://portfolio.arthurp.fr",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: "https://portfolio.arthurp.fr/photos",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user