first commit
@@ -0,0 +1,53 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# docker
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Portfolio Arthur P
|
||||||
|
|
||||||
|
Portfolio personnel développé avec Next.js 16 (App Router), React 19 et TypeScript.
|
||||||
|
|
||||||
|
Site en ligne: [portfolio.arthurp.fr](https://portfolio.arthurp.fr)
|
||||||
|
|
||||||
|
## Aperçu
|
||||||
|
|
||||||
|
Ce projet présente:
|
||||||
|
|
||||||
|
- Une page d'accueil structurée en sections (Hero, About, Projects, Homelab, Sailing)
|
||||||
|
- Une page dédiée aux photos
|
||||||
|
- Une base SEO (robots/sitemap)
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
- Next.js 16
|
||||||
|
- React 19
|
||||||
|
- TypeScript
|
||||||
|
- ESLint
|
||||||
|
|
||||||
|
## Lancer le projet en local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Application disponible sur `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Scripts utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # développement
|
||||||
|
npm run lint # vérification ESLint
|
||||||
|
npm run build # build production
|
||||||
|
npm run start # démarrage après build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
Le projet peut être déployé sur un serveur Node.js ou une plateforme compatible Next.js.
|
||||||
|
|
||||||
|
Exemple de flux:
|
||||||
|
|
||||||
|
1. `npm ci`
|
||||||
|
2. `npm run build`
|
||||||
|
3. `npm run start`
|
||||||
|
|
||||||
|
## Checklist avant push GitHub
|
||||||
|
|
||||||
|
- Vérifier que les secrets ne sont pas commités (`.env*`, clés privées, tokens)
|
||||||
|
- Vérifier que les fichiers locaux IDE sont ignorés (`.vscode/`, `.idea/`)
|
||||||
|
- Vérifier que les dossiers de build/deps sont ignorés (`.next/`, `node_modules/`)
|
||||||
|
|
||||||
|
## Backlink
|
||||||
|
|
||||||
|
Projet officiel: [portfolio.arthurp.fr](https://portfolio.arthurp.fr)
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 361 KiB |
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
image: node:22-alpine
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- node_modules:/app/node_modules
|
||||||
|
ports:
|
||||||
|
- "3012:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
command: sh -c "npm install && npm run build && cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/static && node .next/standalone/server.js"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node_modules:
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "portfolio",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.576.0",
|
||||||
|
"next": "16.1.6",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.6",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 1003 KiB |
|
After Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 698 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 722 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 1003 KiB |
|
After Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 698 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 722 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||