first commit

This commit is contained in:
Puechberty Arthur
2026-03-30 22:47:25 +02:00
commit 4ca8848d09
92 changed files with 8139 additions and 0 deletions
+53
View File
@@ -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
+58
View File
@@ -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)
+85
View File
@@ -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&apos;infrastructure réseau, la
virtualisation et l&apos;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&apos;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&apos;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&apos;a appris la rigueur, la discipline et la
persévérance des valeurs que j&apos;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>
);
}
+87
View File
@@ -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&apos;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>
);
}
+69
View File
@@ -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&apos;auto-hébergement et
l&apos;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&apos;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>
);
}
+135
View File
@@ -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&apos;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&apos;autres encore.
</p>
</div>
</div>
</section>
);
}
+133
View File
@@ -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>
);
}
+147
View File
@@ -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>
);
}
+190
View File
@@ -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&apos;â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&apos;ai commencé la voile à <strong className="text-emphasis">7 ans</strong> pendant
des stages d&apos;é, 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&apos;ai chaviré plus de{" "}
<strong className="text-emphasis">vingt fois</strong>. Malgré
cela, j&apos;ai adoré j&apos;avais enfin l&apos;impression
de vraiment apprendre.
</p>
<p>
Depuis, j&apos;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&apos;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>
);
}
+46
View File
@@ -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>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

+153
View File
@@ -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
View File
@@ -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>
);
}
+27
View File
@@ -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 />
</>
);
}
+74
View File
@@ -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>
</>
);
}
+11
View File
@@ -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",
};
}
+18
View File
@@ -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,
},
];
}
+17
View File
@@ -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:
+18
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
+6602
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

+34
View File
@@ -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"]
}