Section 13.4.5 : Projet frontend complet
🎯 Objectif pédagogique
Mettre en pratique HTML, CSS, JavaScript, Bootstrap et Stimulus en construisant un projet frontend complet — une application de gestion de tâches avec interface responsive et interactions dynamiques.
Le projet : TaskMaster
Vous allez construire une application de gestion de tâches avec les fonctionnalités suivantes :
- →Interface responsive (mobile-first)
- →Ajout, suppression, et marquage de tâches
- →Filtrage (toutes, actives, complétées)
- →Persistance locale (localStorage)
- →Animations fluides
Structure HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskMaster</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container py-5" data-controller="tasks">
<header class="text-center mb-4">
<h1>TaskMaster</h1>
<p class="text-muted">Gérez vos tâches efficacement</p>
</header>
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<form data-action="submit->tasks#add" class="mb-4">
<div class="input-group">
<input type="text" class="form-control"
data-tasks-target="input"
placeholder="Nouvelle tâche...">
<button class="btn btn-primary" type="submit">Ajouter</button>
</div>
</form>
<div class="btn-group w-100 mb-3">
<button class="btn btn-outline-secondary active"
data-action="click->tasks#filterAll">Toutes</button>
<button class="btn btn-outline-secondary"
data-action="click->tasks#filterActive">Actives</button>
<button class="btn btn-outline-secondary"
data-action="click->tasks#filterCompleted">Complétées</button>
</div>
<ul class="list-group" data-tasks-target="list"></ul>
<div class="d-flex justify-content-between mt-3">
<span data-tasks-target="counter" class="text-muted">0 tâche(s)</span>
<button class="btn btn-sm btn-outline-danger"
data-action="click->tasks#clearCompleted">
Supprimer complétées
</button>
</div>
</div>
</div>
</div>
</body>
</html>
Stimulus Controller
// controllers/tasks_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['input', 'list', 'counter']
connect() {
this.tasks = JSON.parse(localStorage.getItem('tasks') || '[]')
this.filter = 'all'
this.render()
}
add(event) {
event.preventDefault()
const text = this.inputTarget.value.trim()
if (!text) return
this.tasks.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
})
this.inputTarget.value = ''
this.save()
this.render()
}
toggle(event) {
const id = Number(event.currentTarget.dataset.id)
const task = this.tasks.find(t => t.id === id)
if (task) task.completed = !task.completed
this.save()
this.render()
}
delete(event) {
const id = Number(event.currentTarget.dataset.id)
this.tasks = this.tasks.filter(t => t.id !== id)
this.save()
this.render()
}
filterAll() { this.filter = 'all'; this.render() }
filterActive() { this.filter = 'active'; this.render() }
filterCompleted() { this.filter = 'completed'; this.render() }
clearCompleted() {
this.tasks = this.tasks.filter(t => !t.completed)
this.save()
this.render()
}
save() {
localStorage.setItem('tasks', JSON.stringify(this.tasks))
}
render() {
const filtered = this.tasks.filter(t => {
if (this.filter === 'active') return !t.completed
if (this.filter === 'completed') return t.completed
return true
})
this.listTarget.innerHTML = filtered.map(task => `
<li class="list-group-item d-flex align-items-center ${task.completed ? 'completed' : ''}">
<input type="checkbox" class="form-check-input me-3"
${task.completed ? 'checked' : ''}
data-id="${task.id}"
data-action="change->tasks#toggle">
<span class="flex-grow-1 ${task.completed ? 'text-decoration-line-through text-muted' : ''}">
${task.text}
</span>
<button class="btn btn-sm btn-outline-danger"
data-id="${task.id}"
data-action="click->tasks#delete">X</button>
</li>
`).join('')
const active = this.tasks.filter(t => !t.completed).length
this.counterTarget.textContent = `${active} tâche(s) restante(s)`
}
}
Styles CSS
body {
background: #f8f9fa;
font-family: 'Inter', sans-serif;
}
h1 {
background: linear-gradient(135deg, #7C3AED, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
}
.list-group-item {
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.list-group-item:hover {
border-left-color: #7C3AED;
background: #faf5ff;
}
.list-group-item.completed {
opacity: 0.6;
}
/* Animation d apparition */
.list-group-item {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
Pourquoi utiliser localStorage plutôt qu'une variable JavaScript pour stocker les tâches ?
Apprendre l’IA, du premier prompt aux agents