Tous les guides
Intermédiaire-AvancéÀ votre rythmeGuide gratuitSection 19/33

Projet frontend complet

Apprenez à construire et vérifier une application web avec des outils de développement et des assistants IA. Pratiquez les étapes de conception, code et validation.

💡

Contenu gratuit, accessible sans compte. Les outils et modèles évoluent : consultez le dossier actuel pour vérifier leurs versions. Modèles et sources

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);
  }
}
Vérification rapide

Pourquoi utiliser localStorage plutôt qu'une variable JavaScript pour stocker les tâches ?

Apprendre l’IA, du premier prompt aux agents

15 guides gratuitsQuiz, schémas et ateliersSans compte

Votre progression

Cette progression est déclarative et conservée dans votre navigateur. L’attestation finale ne constitue pas un diplôme ni une évaluation indépendante.

Voir ma progression et mon attestation

Apprendre l’IA, du premier prompt aux agents

15 guides gratuitsQuiz, schémas et ateliersSans compte

Un cas pratique IA dans votre boîte mail

Méthodes, ressources et nouveaux exercices. Gratuit, avec désinscription à tout moment.