All guides
Intermediate-AdvancedSelf-pacedFree GuideSection 19/33

Projet frontend complet

Learn to build and check a web application with development tools and AI assistants. Practise design, coding and validation.

The full lessons for this course are currently available in French. Use the section menu to navigate the French course.

💡

Free content, available without an account. Tools and models change: use the current model guide to check their versions. Models and 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);
  }
}
Quick Check

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

Learn AI, from prompts to agents

15 free guidesQuizzes, diagrams and workshopsNo account needed

Your progress

Progress is self-reported and stored in your browser. The final completion record is not a qualification or an independent assessment.

View progress and completion record

Learn AI, from prompts to agents

15 free guidesQuizzes, diagrams and workshopsNo account needed

One practical AI workflow in your inbox

Methods, resources and new exercises. Free, unsubscribe at any time.