Skip to content

Étape 20 — Suppression avec confirmation

Séquence démo — pas requis dans le projet personnel

La suppression avec dialogue de confirmation est présentée en démonstration. Ce concept sera abordé à l'oral mais ne fait pas partie des livrables du projet individuel.

Objectifs

  • Ajouter un bouton de suppression visible uniquement si l'utilisateur est connecté
  • Demander confirmation avec v-dialog avant de supprimer
  • Appeler deletePokemon() du store (→ axios.delete)
  • Afficher un v-snackbar de confirmation et rediriger vers l'accueil

Nouveau composant Vuetify

ComposantRôleDocumentation
v-dialogBoîte de dialogue modale (bloque l'interaction)Dialogs

Pourquoi confirmer les actions destructives ?

Toujours confirmer avant de supprimer

Une suppression est irréversible. On ne supprime jamais un élément sur un simple clic. Le pattern standard est :

  1. L'utilisateur clique sur « Supprimer »
  2. Un dialogue de confirmation s'ouvre avec le nom de l'élément
  3. L'utilisateur confirme ou annule
  4. Si confirmé, l'action est exécutée et un message de succès s'affiche

v-dialog est le composant Vuetify prévu pour cela.

Tâches

1. Ajouter l'action dans le store

Dans src/stores/pokemonStore.js, ajoutez l'action deletePokemon qui supprime un Pokémon via l'API :

js
async deletePokemon (pokemonId) {
  this.isLoading = true

  try {
    // Supprimer le Pokémon via l'API
    await api.delete(`/pokemons/${pokemonId}`)

    // Supprimer le Pokémon de la liste locale
    this.pokemons = this.pokemons.filter(pokemon => pokemon.id !== pokemonId)

    // Supprimer le Pokémon des favoris s'il y était
    this.favorites = this.favorites.filter(favoriteId => favoriteId !== pokemonId)
    this.saveFavorites()

    return {
      success: true,
      message: 'Pokémon supprimé avec succès !',
    }
  } catch (error) {
    console.error('Erreur lors de la suppression du Pokémon:', error.message)

    return {
      success: false,
      message: 'Erreur lors de la suppression du Pokémon',
    }
  } finally {
    this.isLoading = false
  }
},

Cette action :

  • Fait un DELETE vers /pokemons/:id via Axios
  • Retire le Pokémon de la liste locale et des favoris
  • Retourne un objet { success, message } utilisé par la page détail

2. Ajouter le bouton de suppression

Dans src/pages/pokemon/[id].vue, ajoutez un bouton dans v-card-actions. Ce bouton n'est visible que si l'utilisateur est connecté :

vue
<v-btn
  v-if="authStore.isAuthenticated"
  color="error"
  variant="text"
  prepend-icon="mdi-delete"
  @click="showDeleteDialog = true"
>
  Supprimer
</v-btn>

La directive v-if="authStore.isAuthenticated" masque complètement le bouton pour les visiteurs non connectés.

3. Ajouter le dialogue de confirmation

Le v-dialog est contrôlé par une variable réactive showDeleteDialog :

vue
<v-dialog v-model="showDeleteDialog" max-width="400">
  <v-card>
    <v-card-title>Confirmer la suppression</v-card-title>
    <v-card-text>
      Êtes-vous sûr de vouloir supprimer <strong>{{ pokemon?.name }}</strong> ?
      Cette action est irréversible.
    </v-card-text>
    <v-card-actions>
      <v-spacer />
      <v-btn variant="text" @click="showDeleteDialog = false">
        Annuler
      </v-btn>
      <v-btn
        color="error"
        variant="flat"
        :loading="pokemonStore.isLoading"
        @click="handleDelete"
      >
        Supprimer
      </v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>

4. Écrire la fonction de suppression

La fonction handleDelete appelle le store, ferme le dialogue, affiche le résultat et redirige :

js
async function handleDelete () {
  // Appeler le store (qui fait axios.delete)
  const result = await pokemonStore.deletePokemon(route.params.id)

  // Fermer le dialogue
  showDeleteDialog.value = false

  // Afficher le résultat dans le snackbar
  snackbar.value = {
    show: true,
    message: result.message,
    color: result.success ? 'success' : 'error',
  }

  // Rediriger vers l'accueil si succès
  if (result.success) {
    setTimeout(() => router.push('/'), 1000)
  }
}

5. Ajouter le snackbar

Même principe que dans l'étape 18 :

vue
<v-snackbar
  v-model="snackbar.show"
  :color="snackbar.color"
  :timeout="3000"
>
  {{ snackbar.message }}
</v-snackbar>

Tests

  • Le bouton « Supprimer » est invisible quand on n'est pas connecté
  • Le bouton « Supprimer » apparaît après connexion
  • Cliquer sur « Supprimer » ouvre un dialogue de confirmation
  • Cliquer sur « Annuler » ferme le dialogue sans rien supprimer
  • Cliquer sur « Supprimer » dans le dialogue supprime le Pokémon
  • Le snackbar affiche « Pokémon supprimé avec succès ! »
  • La page redirige vers l'accueil après 1 seconde

Solution

src/pages/pokemon/[id].vue (modifications)
vue
<template>
  <v-container>
    <!--
    Bouton retour
      * Permet de revenir à la page précédente
    -->
    <v-btn
      variant="text"
      prepend-icon="mdi-arrow-left"
      class="mb-4"
      @click="$router.back()"
    >
      Retour
    </v-btn>

    <!-- Skeleton pendant le chargement -->
    <v-skeleton-loader
      v-if="pokemonStore.isLoading"
      type="card, article"
      max-width="800"
      class="mx-auto"
    />

    <!-- Message si le Pokémon n'est pas trouvé -->
    <v-alert
      v-else-if="!pokemon"
      type="error"
      variant="tonal"
    >
      Pokémon non trouvé.
    </v-alert>

    <!--
    Affichage détaillé du Pokémon
    -->
    <v-card
      v-else-if="pokemon"
      max-width="800"
      class="mx-auto"
    >
      <v-img
        :src="getImageUrl(pokemon.img)"
        :alt="pokemon.name"
        height="300"
        cover
      />

      <v-card-title class="text-h4">
        {{ pokemon.name }}
      </v-card-title>

      <v-card-subtitle>
        Niveau {{ pokemon.level }}
      </v-card-subtitle>

      <v-card-text>
        <p
          v-if="pokemon.description"
          class="text-body-1 mb-4"
        >
          {{ pokemon.description }}
        </p>

        <div
          v-if="pokemon.types && pokemon.types.length"
          class="mb-4"
        >
          <strong class="mr-2">Types :</strong>
          <pokemon-types-chips :types="pokemon.types" />
        </div>

        <div v-if="pokemon.stats">
          <strong class="d-block mb-2">Statistiques :</strong>
          <pokemon-stats :stats="pokemon.stats" />
        </div>
      </v-card-text>

      <!--
      Actions : favori et supprimer
      -->
      <v-card-actions>
        <!-- Bouton favori -->
        <v-btn
          :icon="pokemonStore.isFavorite(pokemon) ? 'mdi-heart' : 'mdi-heart-outline'"
          :color="pokemonStore.isFavorite(pokemon) ? 'red' : ''"
          variant="text"
          @click="pokemonStore.toggleFavorite(pokemon)"
        />

        <v-spacer />

        <!--
        Bouton supprimer (visible uniquement si connecté)
          * Ouvre un dialogue de confirmation avant la suppression
        -->
        <v-btn
          v-if="authStore.isAuthenticated"
          color="error"
          variant="text"
          prepend-icon="mdi-delete"
          @click="showDeleteDialog = true"
        >
          Supprimer
        </v-btn>
      </v-card-actions>
    </v-card>

    <!--
    Dialogue de confirmation de suppression
      * v-model contrôle l'ouverture/fermeture
      * max-width="400" limite la largeur
    -->
    <v-dialog
      v-model="showDeleteDialog"
      max-width="400"
    >
      <v-card>
        <v-card-title>Confirmer la suppression</v-card-title>
        <v-card-text>
          Êtes-vous sûr de vouloir supprimer <strong>{{ pokemon?.name }}</strong> ?
          Cette action est irréversible.
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn
            variant="text"
            @click="showDeleteDialog = false"
          >
            Annuler
          </v-btn>
          <v-btn
            color="error"
            variant="flat"
            :loading="pokemonStore.isLoading"
            @click="handleDelete"
          >
            Supprimer
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!--
    Snackbar pour les messages de confirmation
    -->
    <v-snackbar
      v-model="snackbar.show"
      :color="snackbar.color"
      :timeout="3000"
    >
      {{ snackbar.message }}
    </v-snackbar>
  </v-container>
</template>

<script setup>
import { usePokemonStore } from '@/stores/pokemonStore'
import { useAuthStore } from '@/stores/authStore'
import { getImageUrl } from '@/utils/imageUrl'
import PokemonTypesChips from '@/components/PokemonTypesChips.vue'
import PokemonStats from '@/components/PokemonStats.vue'

const route = useRoute()
const router = useRouter()
const pokemonStore = usePokemonStore()
const authStore = useAuthStore()

/**
 * Computed qui récupère le Pokémon correspondant à l'ID de la route
 * Se met à jour automatiquement si l'ID change
 */
const pokemon = computed(() => {
  return pokemonStore.getPokemonById(route.params.id)
})

/**
 * État du dialogue de suppression
 */
const showDeleteDialog = ref(false)

/**
 * État du snackbar
 */
const snackbar = ref({
  show: false,
  message: '',
  color: 'success',
})

/**
 * Gère la suppression du Pokémon
 * 1. Appelle le store pour supprimer via l'API
 * 2. Affiche un message de confirmation
 * 3. Redirige vers l'accueil si succès
 */
async function handleDelete () {
  const result = await pokemonStore.deletePokemon(route.params.id)

  showDeleteDialog.value = false

  snackbar.value = {
    show: true,
    message: result.message,
    color: result.success ? 'success' : 'error',
  }

  if (result.success) {
    setTimeout(() => {
      router.push('/')
    }, 1000)
  }
}
</script>

Commit

bash
git add -A
git commit -m "feat: suppression de Pokémon avec dialogue de confirmation"

Documentation pour les cours de développement web