É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-dialogavant de supprimer - Appeler
deletePokemon()du store (→axios.delete) - Afficher un
v-snackbarde confirmation et rediriger vers l'accueil
Nouveau composant Vuetify
| Composant | Rôle | Documentation |
|---|---|---|
v-dialog | Boî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 :
- L'utilisateur clique sur « Supprimer »
- Un dialogue de confirmation s'ouvre avec le nom de l'élément
- L'utilisateur confirme ou annule
- 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 :
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
DELETEvers/pokemons/:idvia 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é :
<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 :
<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 :
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 :
<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)
<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
git add -A
git commit -m "feat: suppression de Pokémon avec dialogue de confirmation"