🚀 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:
9
.htaccess
Normal file
9
.htaccess
Normal 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]
|
||||
4
assets/img/projects/default-project.svg
Normal file
4
assets/img/projects/default-project.svg
Normal 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
45
includes/router.php
Normal 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
116
index.php
@@ -1,113 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* Page d'accueil
|
||||
* Front Controller - Point d'entrée unique
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
require_once __DIR__ . '/includes/router.php';
|
||||
|
||||
$pageTitle = 'Accueil';
|
||||
$pageDescription = 'Portfolio de développeur web full-stack. Découvrez mes projets, compétences et parcours.';
|
||||
$currentPage = 'home';
|
||||
$router = new Router();
|
||||
|
||||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
$router
|
||||
->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>
|
||||
<!-- 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'); ?>
|
||||
$router->dispatch();
|
||||
|
||||
27
pages/404.php
Normal file
27
pages/404.php
Normal 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
31
pages/about.php
Normal 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
31
pages/contact.php
Normal 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
111
pages/home.php
Normal 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
40
pages/project-single.php
Normal 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
45
pages/projects.php
Normal 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
31
pages/skills.php
Normal 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'); ?>
|
||||
47
templates/project-card.php
Normal file
47
templates/project-card.php
Normal 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>
|
||||
Reference in New Issue
Block a user