🚀 Feature: Router PHP + Pages projets (Stories 3.2 & 3.3)

Story 3.2 - Router PHP et URLs propres:
- Router PHP léger (43 lignes) avec support {slug}
- Front controller index.php
- .htaccess pour Apache
- Pages: home, projects, project-single, skills, about, contact, 404

Story 3.3 - Page liste projets vedettes:
- Grille responsive (1→2→3 colonnes)
- Template project-card.php réutilisable
- Badges technologies (max 4 + compteur)
- Lazy loading images avec fallback SVG

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 00:07:23 +01:00
parent a4a41933c4
commit 2c2b893558
12 changed files with 432 additions and 105 deletions

9
.htaccess Normal file
View File

@@ -0,0 +1,9 @@
RewriteEngine On
RewriteBase /
# Ne pas réécrire les fichiers et dossiers existants
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Rediriger tout vers index.php
RewriteRule ^(.*)$ index.php [QSA,L]

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="225" viewBox="0 0 400 225">
<rect width="400" height="225" fill="#1e293b"/>
<text x="200" y="112" text-anchor="middle" dominant-baseline="middle" fill="#64748b" font-family="system-ui, sans-serif" font-size="16">Image à venir</text>
</svg>

After

Width:  |  Height:  |  Size: 305 B

45
includes/router.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/**
* Router simple pour URLs propres
* < 50 lignes de code
*/
class Router
{
private array $routes = [];
public function add(string $pattern, string $handler): self
{
// Convertit {param} en regex ([^/]+)
$regex = preg_replace('/\{(\w+)\}/', '([^/]+)', $pattern);
$regex = '#^' . $regex . '$#';
$this->routes[$regex] = $handler;
return $this;
}
public function resolve(string $uri): array
{
$uri = parse_url($uri, PHP_URL_PATH);
$uri = rtrim($uri, '/') ?: '/';
foreach ($this->routes as $regex => $handler) {
if (preg_match($regex, $uri, $matches)) {
array_shift($matches); // Enlève le match complet
return [$handler, $matches];
}
}
return ['pages/404.php', []];
}
public function dispatch(): void
{
$uri = $_SERVER['REQUEST_URI'] ?? '/';
[$handler, $params] = $this->resolve($uri);
// Rend les paramètres accessibles
$GLOBALS['routeParams'] = $params;
require __DIR__ . '/../' . $handler;
}
}

116
index.php
View File

@@ -1,113 +1,19 @@
<?php <?php
/** /**
* Page d'accueil * Front Controller - Point d'entrée unique
*/ */
require_once __DIR__ . '/includes/functions.php'; require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/router.php';
$pageTitle = 'Accueil'; $router = new Router();
$pageDescription = 'Portfolio de développeur web full-stack. Découvrez mes projets, compétences et parcours.';
$currentPage = 'home';
include_template('header', compact('pageTitle', 'pageDescription')); $router
include_template('navbar', compact('currentPage')); ->add('/', 'pages/home.php')
?> ->add('/projets', 'pages/projects.php')
->add('/projet/{slug}', 'pages/project-single.php')
->add('/competences', 'pages/skills.php')
->add('/a-propos', 'pages/about.php')
->add('/contact', 'pages/contact.php');
<main> $router->dispatch();
<!-- Hero Section -->
<section class="min-h-[calc(100vh-5rem)] flex items-center justify-center">
<div class="container-content text-center py-20">
<!-- Intro -->
<p class="text-primary font-medium mb-4 animate-fade-in">
Bonjour, je suis
</p>
<!-- Nom -->
<h1 class="text-4xl sm:text-5xl lg:text-display font-bold text-text-primary mb-6 animate-fade-in animation-delay-100">
Prénom <span class="text-primary">NOM</span>
</h1>
<!-- Titre -->
<p class="text-xl sm:text-2xl lg:text-heading text-text-secondary mb-6 animate-fade-in animation-delay-200">
Développeur Web Full-Stack
</p>
<!-- Accroche -->
<p class="text-lg text-text-secondary max-w-2xl mx-auto mb-10 animate-fade-in animation-delay-300">
Je crée des expériences web modernes, performantes et accessibles.
<br class="hidden sm:block">Chaque projet est une opportunité de montrer plutôt que de dire.
</p>
<!-- CTA -->
<div class="flex flex-col sm:flex-row gap-4 justify-center animate-fade-in animation-delay-300">
<a href="/projets" class="btn-primary">
Découvrir mes projets
</a>
<a href="/a-propos" class="btn-secondary">
En savoir plus
</a>
</div>
</div>
</section>
<!-- Section Navigation Rapide -->
<section class="section bg-surface">
<div class="container-content">
<div class="section-header">
<h2 class="section-title">Explorez mon portfolio</h2>
<p class="section-subtitle">
Découvrez mes réalisations, compétences et parcours
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
<!-- Carte Projets -->
<a href="/projets" class="card-interactive group">
<div class="card-body text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
</div>
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Projets</h3>
<p class="text-text-secondary">
Découvrez mes réalisations web avec démonstrations et explications techniques.
</p>
</div>
</a>
<!-- Carte Compétences -->
<a href="/competences" class="card-interactive group">
<div class="card-body text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
</div>
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Compétences</h3>
<p class="text-text-secondary">
Technologies maîtrisées et outils utilisés, avec preuves à l'appui.
</p>
</div>
</a>
<!-- Carte Me Découvrir -->
<a href="/a-propos" class="card-interactive group">
<div class="card-body text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Me Découvrir</h3>
<p class="text-text-secondary">
Mon parcours, mes motivations et ce qui me passionne au-delà du code.
</p>
</div>
</a>
</div>
</div>
</section>
</main>
<?php include_template('footer'); ?>

27
pages/404.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
/**
* Page 404 - Page non trouvée
*/
http_response_code(404);
$pageTitle = 'Page non trouvée';
$currentPage = '';
include_template('header', compact('pageTitle'));
include_template('navbar', compact('currentPage'));
?>
<main class="min-h-screen flex items-center justify-center">
<div class="container-content text-center py-20">
<h1 class="text-display text-primary mb-4">404</h1>
<p class="text-xl text-text-secondary mb-8">
Oups ! Cette page n'existe pas.
</p>
<a href="/" class="btn-primary">
Retour à l'accueil
</a>
</div>
</main>
<?php include_template('footer'); ?>

31
pages/about.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
/**
* Page à propos
*/
$pageTitle = 'À propos';
$pageDescription = 'Découvrez mon parcours, mes motivations et ce qui me passionne.';
$currentPage = 'a-propos';
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main>
<section class="section">
<div class="container-content">
<div class="section-header">
<h1 class="section-title">À propos</h1>
<p class="section-subtitle">
Mon parcours et mes motivations
</p>
</div>
<p class="text-text-secondary text-center">
Page en construction - Epic 4
</p>
</div>
</section>
</main>
<?php include_template('footer'); ?>

31
pages/contact.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
/**
* Page contact
*/
$pageTitle = 'Contact';
$pageDescription = 'Contactez-moi pour discuter de votre projet web.';
$currentPage = 'contact';
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main>
<section class="section">
<div class="container-content">
<div class="section-header">
<h1 class="section-title">Contact</h1>
<p class="section-subtitle">
Discutons de votre projet
</p>
</div>
<p class="text-text-secondary text-center">
Page en construction - Epic 5
</p>
</div>
</section>
</main>
<?php include_template('footer'); ?>

111
pages/home.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
/**
* Page d'accueil
*/
$pageTitle = 'Accueil';
$pageDescription = 'Portfolio de développeur web full-stack. Découvrez mes projets, compétences et parcours.';
$currentPage = 'home';
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main>
<!-- Hero Section -->
<section class="min-h-[calc(100vh-5rem)] flex items-center justify-center">
<div class="container-content text-center py-20">
<!-- Intro -->
<p class="text-primary font-medium mb-4 animate-fade-in">
Bonjour, je suis
</p>
<!-- Nom -->
<h1 class="text-4xl sm:text-5xl lg:text-display font-bold text-text-primary mb-6 animate-fade-in animation-delay-100">
Prénom <span class="text-primary">NOM</span>
</h1>
<!-- Titre -->
<p class="text-xl sm:text-2xl lg:text-heading text-text-secondary mb-6 animate-fade-in animation-delay-200">
Développeur Web Full-Stack
</p>
<!-- Accroche -->
<p class="text-lg text-text-secondary max-w-2xl mx-auto mb-10 animate-fade-in animation-delay-300">
Je crée des expériences web modernes, performantes et accessibles.
<br class="hidden sm:block">Chaque projet est une opportunité de montrer plutôt que de dire.
</p>
<!-- CTA -->
<div class="flex flex-col sm:flex-row gap-4 justify-center animate-fade-in animation-delay-300">
<a href="/projets" class="btn-primary">
Découvrir mes projets
</a>
<a href="/a-propos" class="btn-secondary">
En savoir plus
</a>
</div>
</div>
</section>
<!-- Section Navigation Rapide -->
<section class="section bg-surface">
<div class="container-content">
<div class="section-header">
<h2 class="section-title">Explorez mon portfolio</h2>
<p class="section-subtitle">
Découvrez mes réalisations, compétences et parcours
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
<!-- Carte Projets -->
<a href="/projets" class="card-interactive group">
<div class="card-body text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
</div>
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Projets</h3>
<p class="text-text-secondary">
Découvrez mes réalisations web avec démonstrations et explications techniques.
</p>
</div>
</a>
<!-- Carte Compétences -->
<a href="/competences" class="card-interactive group">
<div class="card-body text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
</div>
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Compétences</h3>
<p class="text-text-secondary">
Technologies maîtrisées et outils utilisés, avec preuves à l'appui.
</p>
</div>
</a>
<!-- Carte Me Découvrir -->
<a href="/a-propos" class="card-interactive group">
<div class="card-body text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Me Découvrir</h3>
<p class="text-text-secondary">
Mon parcours, mes motivations et ce qui me passionne au-delà du code.
</p>
</div>
</a>
</div>
</div>
</section>
</main>
<?php include_template('footer'); ?>

40
pages/project-single.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
/**
* Page projet individuel
*/
$slug = $GLOBALS['routeParams'][0] ?? null;
$project = getProjectBySlug($slug);
if (!$project) {
http_response_code(404);
include __DIR__ . '/404.php';
exit;
}
$pageTitle = $project['title'];
$pageDescription = $project['context'];
$currentPage = 'projets';
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main>
<section class="section">
<div class="container-content">
<div class="section-header">
<h1 class="section-title"><?= htmlspecialchars($project['title']) ?></h1>
<p class="section-subtitle">
<?= htmlspecialchars($project['duration']) ?>
</p>
</div>
<p class="text-text-secondary text-center">
Page en construction - Story 3.4
</p>
</div>
</section>
</main>
<?php include_template('footer'); ?>

45
pages/projects.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/**
* Page liste des projets
*/
$pageTitle = 'Mes Projets';
$pageDescription = 'Découvrez mes réalisations web : sites vitrines, e-commerce, applications et plus encore.';
$currentPage = 'projets';
$featuredProjects = getProjectsByCategory('vedette');
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main>
<!-- Header de page -->
<section class="section">
<div class="container-content">
<div class="section-header">
<h1 class="section-title">Mes Projets</h1>
<p class="section-subtitle">
Découvrez les réalisations qui illustrent mon travail et mes compétences.
</p>
</div>
<!-- Grille des projets vedettes -->
<?php if (!empty($featuredProjects)): ?>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
<?php foreach ($featuredProjects as $project): ?>
<?php include_template('project-card', ['project' => $project]); ?>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-center text-text-secondary py-12">
Projets à venir...
</p>
<?php endif; ?>
</div>
</section>
<!-- Section projets secondaires (Story 3.5) -->
</main>
<?php include_template('footer'); ?>

31
pages/skills.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
/**
* Page compétences
*/
$pageTitle = 'Compétences';
$pageDescription = 'Mes compétences techniques : langages, frameworks et outils maîtrisés.';
$currentPage = 'competences';
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main>
<section class="section">
<div class="container-content">
<div class="section-header">
<h1 class="section-title">Compétences</h1>
<p class="section-subtitle">
Technologies et outils maîtrisés
</p>
</div>
<p class="text-text-secondary text-center">
Page en construction - Epic 4
</p>
</div>
</section>
</main>
<?php include_template('footer'); ?>

View File

@@ -0,0 +1,47 @@
<?php
/**
* Carte projet réutilisable
* @param array $project Données du projet
*/
$title = $project['title'] ?? 'Sans titre';
$slug = $project['slug'] ?? '#';
$thumbnail = $project['thumbnail'] ?? 'default-project.webp';
$technologies = $project['technologies'] ?? [];
$maxTechs = 4;
?>
<article class="card-interactive group">
<a href="/projet/<?= htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') ?>" class="block">
<!-- Thumbnail -->
<div class="aspect-video overflow-hidden rounded-t-lg bg-surface-alt">
<img
src="/assets/img/projects/<?= htmlspecialchars($thumbnail, ENT_QUOTES, 'UTF-8') ?>"
alt="Aperçu du projet <?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>"
width="400"
height="225"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
onerror="this.src='/assets/img/projects/default-project.webp'; this.onerror=null;"
>
</div>
<!-- Contenu -->
<div class="card-body">
<h3 class="text-lg font-semibold text-text-primary mb-3 group-hover:text-primary transition-colors">
<?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>
</h3>
<!-- Technologies (badges) -->
<div class="flex flex-wrap gap-2">
<?php foreach (array_slice($technologies, 0, $maxTechs) as $tech): ?>
<span class="badge"><?= htmlspecialchars($tech, ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
<?php if (count($technologies) > $maxTechs): ?>
<span class="badge badge-muted">+<?= count($technologies) - $maxTechs ?></span>
<?php endif; ?>
</div>
</div>
</a>
</article>