Étape 23 — Thème personnalisé et page détail enrichie
Objectifs
- Personnaliser le thème Vuetify avec des couleurs Pokémon
- Créer deux composants réutilisables :
PokemonTypesChipsetPokemonStats - Enrichir la page détail avec les types et les statistiques
- Enrichir
PokemonCardavec l'affichage des types - Personnaliser le favicon et le titre de l'application
Tâches
1. Personnaliser le thème Vuetify
Dans src/plugins/vuetify.js, ajoutez un thème personnalisé avec des couleurs inspirées de l'univers Pokémon :
| Couleur | Valeur | Inspiration |
|---|---|---|
primary | #E53935 | Rouge Pokéball |
secondary | #1A237E | Bleu nuit |
accent | #FFD600 | Jaune Pikachu |
error | #FF5252 | Rouge vif |
success | #4CAF50 | Vert Bulbizarre |
warning | #FB8C00 | Orange |
info | #2196F3 | Bleu info |
export default createVuetify({
theme: {
defaultTheme: 'dark',
themes: {
dark: {
colors: {
primary: '#E53935',
secondary: '#1A237E',
accent: '#FFD600',
error: '#FF5252',
success: '#4CAF50',
warning: '#FB8C00',
info: '#2196F3',
},
},
},
},
})Points importants :
defaultTheme: 'dark': le thème sombre est activé par défaut- Les couleurs sont utilisées automatiquement par Vuetify (boutons, alertes, barres, etc.)
- Vous pouvez utiliser ces couleurs avec l'attribut
color="primary"sur n'importe quel composant
2. Créer le composant PokemonTypesChips
Créez src/components/PokemonTypesChips.vue. Ce composant affiche les types d'un Pokémon sous forme de puces colorées (v-chip).
| Composant | Rôle | Documentation |
|---|---|---|
<v-chip> | Puce compacte avec couleur et texte | Chips |
Le composant reçoit une prop types (tableau d'IDs de types) et utilise le store pour récupérer le nom et la couleur de chaque type :
<template>
<!--
Puces colorées des types d'un Pokémon
* Composant réutilisable qui affiche les types sous forme de chips
* Chaque chip a la couleur associée au type
-->
<div class="d-inline-flex ga-1 flex-wrap">
<v-chip
v-for="typeId in types"
:key="typeId"
:color="pokemonStore.getTypeById(typeId)?.color"
size="small"
>
{{ pokemonStore.getTypeById(typeId)?.name || 'Inconnu' }}
</v-chip>
</div>
</template>
<script setup>
import { usePokemonStore } from '@/stores/pokemonStore'
/**
* Props du composant PokemonTypesChips
* @property {Array<number>} types - Liste des IDs de types du Pokémon
*/
defineProps({
types: {
type: Array,
required: true,
},
})
const pokemonStore = usePokemonStore()
</script>Points importants :
ga-1: gap de 1 unité entre les chips (espacement)flex-wrap: les chips passent à la ligne si l'espace manquegetTypeById()est un getter du store qui retourne{ name, color }pour un ID de type- L'opérateur
?.(optional chaining) évite une erreur si le type n'est pas trouvé
Getter getTypeById requis
Ce composant suppose que votre pokemonStore a un getter getTypeById(id) qui retourne un objet { name, color }. Si ce n'est pas le cas, ajoutez-le dans votre store :
getTypeById: (state) => (id) => {
return state.types.find(type => type.id === id)
}Et chargez les types depuis l'API (http://localhost:3535/types) dans une action fetchTypes().
3. Créer le composant PokemonStats
Créez src/components/PokemonStats.vue. Ce composant affiche les statistiques d'un Pokémon avec des barres de progression colorées.
| Composant | Rôle | Documentation |
|---|---|---|
<v-progress-linear> | Barre de progression horizontale | Progress linear |
<template>
<!--
Barres de progression pour les statistiques d'un Pokémon
* Affiche HP, Attaque, Défense et Vitesse avec des couleurs dynamiques
* La couleur change selon la valeur de la statistique
-->
<div>
<div
v-for="(value, key) in stats"
:key="key"
class="mb-2"
>
<!--
Label et valeur de la statistique
* justify-space-between place le nom à gauche et la valeur à droite
-->
<div class="d-flex justify-space-between text-caption mb-1">
<span>{{ labels[key] || key }}</span>
<span>{{ value }}</span>
</div>
<!--
Barre de progression
* :model-value définit la valeur actuelle
* :max="150" définit la valeur maximale (les stats Pokémon montent rarement au-dessus)
* :color change selon la valeur
* height="8" et rounded pour un style arrondi
-->
<v-progress-linear
:model-value="value"
:max="150"
:color="getStatColor(value)"
height="8"
rounded
/>
</div>
</div>
</template>
<script setup>
/**
* Props du composant PokemonStats
* @property {Object} stats - Objet { hp, attack, defense, speed }
*/
defineProps({
stats: {
type: Object,
required: true,
},
})
/**
* Labels lisibles pour chaque statistique
*/
const labels = {
hp: 'Points de vie',
attack: 'Attaque',
defense: 'Défense',
speed: 'Vitesse',
}
/**
* Retourne une couleur selon la valeur de la statistique
* - Vert si >= 100 (excellent)
* - Orange si >= 60 (moyen)
* - Rouge si < 60 (faible)
* @param {number} value - Valeur de la statistique
* @returns {string} Nom de couleur Vuetify
*/
function getStatColor (value) {
if (value >= 100) return 'green'
if (value >= 60) return 'orange'
return 'red'
}
</script>Points importants :
v-for="(value, key) in stats": itère sur les propriétés d'un objet (valeur + clé)labels[key]: transforme les clés techniques (hp,attack) en labels lisiblesgetStatColor(): donne un retour visuel immédiat sur la qualité de la statistiquemax="150": les statistiques Pokémon dépassent rarement 150
4. Enrichir la page détail
Dans src/pages/pokemon/[id].vue, importez les deux composants et utilisez-les dans le template :
import PokemonTypesChips from '@/components/PokemonTypesChips.vue'
import PokemonStats from '@/components/PokemonStats.vue'Dans le <v-card-text>, ajoutez les types et les statistiques après la description :
<v-card-text>
<!-- Description du Pokémon -->
<p
v-if="pokemon.description"
class="text-body-1 mb-4"
>
{{ pokemon.description }}
</p>
<!-- Types du Pokémon (chips colorés) -->
<div
v-if="pokemon.types && pokemon.types.length"
class="mb-4"
>
<strong class="mr-2">Types :</strong>
<pokemon-types-chips :types="pokemon.types" />
</div>
<!-- Statistiques du Pokémon (barres de progression) -->
<div v-if="pokemon.stats">
<strong class="d-block mb-2">Statistiques :</strong>
<pokemon-stats :stats="pokemon.stats" />
</div>
</v-card-text>5. Enrichir PokemonCard avec les types
Dans src/components/PokemonCard.vue, ajoutez l'affichage des types sous le sous-titre en utilisant le composant PokemonTypesChips :
<!-- Types du Pokémon (chips colorés) -->
<v-card-text
v-if="pokemon.types && pokemon.types.length"
class="pb-0"
>
<pokemon-types-chips :types="pokemon.types" />
</v-card-text>N'oubliez pas d'importer le composant dans le <script setup> :
import PokemonTypesChips from '@/components/PokemonTypesChips.vue'6. Personnaliser le favicon et le titre
Dans index.html à la racine du projet :
- Remplacez le titre par « Pokédex » :
<title>Pokédex</title>- Ajoutez un favicon personnalisé. Téléchargez une icône Pokéball (format
.icoou.png) et placez-la dans le dossierpublic/:
<link rel="icon" href="/favicon.ico" />- Ajoutez
lang="fr"sur la balise<html>:
<html lang="fr">Tests
- Le thème sombre est appliqué avec les couleurs Pokémon (rouge, bleu nuit, jaune)
- La page détail affiche les types sous forme de chips colorés
- La page détail affiche les statistiques avec des barres de progression
- Les barres changent de couleur selon la valeur (vert >= 100, orange >= 60, rouge < 60)
- Les cartes Pokémon affichent les types sous forme de chips colorés
- Le titre de l'onglet affiche « Pokédex »
- Le favicon est personnalisé
- 0 erreurs dans la console
Solution
src/plugins/vuetify.js
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
/**
* Configuration Vuetify avec thème personnalisé Pokémon
*
* Le thème 'dark' est défini par défaut avec des couleurs inspirées de l'univers Pokémon :
* - primary : rouge Pokéball
* - secondary : bleu nuit
* - accent : jaune Pikachu
* - error : rouge vif
* - success : vert Bulbizarre
*/
export default createVuetify({
theme: {
defaultTheme: 'dark',
themes: {
dark: {
colors: {
primary: '#E53935',
secondary: '#1A237E',
accent: '#FFD600',
error: '#FF5252',
success: '#4CAF50',
warning: '#FB8C00',
info: '#2196F3',
},
},
},
},
})src/components/PokemonTypesChips.vue
<template>
<!--
Puces colorées des types d'un Pokémon
* Composant réutilisable qui affiche les types sous forme de chips
* Chaque chip a la couleur associée au type
-->
<div class="d-inline-flex ga-1 flex-wrap">
<v-chip
v-for="typeId in types"
:key="typeId"
:color="pokemonStore.getTypeById(typeId)?.color"
size="small"
>
{{ pokemonStore.getTypeById(typeId)?.name || 'Inconnu' }}
</v-chip>
</div>
</template>
<script setup>
import { usePokemonStore } from '@/stores/pokemonStore'
/**
* Props du composant PokemonTypesChips
* @property {Array<number>} types - Liste des IDs de types du Pokémon
*/
defineProps({
types: {
type: Array,
required: true,
},
})
const pokemonStore = usePokemonStore()
</script>src/components/PokemonCard.vue (version enrichie avec types)
<template>
<v-card
class="pokemon-card"
:to="`/pokemon/${pokemon.id}`"
hover
>
<v-img
:src="getImageUrl(pokemon.img)"
:alt="pokemon.name"
height="200"
cover
>
<template #placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-progress-circular
indeterminate
color="grey-lighten-4"
/>
</div>
</template>
</v-img>
<v-card-title>{{ pokemon.name }}</v-card-title>
<v-card-subtitle>Niveau {{ pokemon.level }}</v-card-subtitle>
<!-- Types du Pokémon (chips colorés) -->
<v-card-text
v-if="pokemon.types && pokemon.types.length"
class="pb-0"
>
<pokemon-types-chips :types="pokemon.types" />
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
:icon="pokemonStore.isFavorite(pokemon) ? 'mdi-heart' : 'mdi-heart-outline'"
:color="pokemonStore.isFavorite(pokemon) ? 'red' : ''"
:class="{ 'favorite-active': pokemonStore.isFavorite(pokemon) }"
variant="text"
@click.stop.prevent="pokemonStore.toggleFavorite(pokemon)"
/>
</v-card-actions>
</v-card>
</template>
<script setup>
import { getImageUrl } from '@/utils/imageUrl'
import { usePokemonStore } from '@/stores/pokemonStore'
import PokemonTypesChips from '@/components/PokemonTypesChips.vue'
defineProps({
pokemon: {
type: Object,
required: true,
},
})
const pokemonStore = usePokemonStore()
</script>
<style scoped>
.favorite-active {
animation: heartbeat 0.6s ease-in-out;
}
</style>src/components/PokemonStats.vue
<template>
<!--
Barres de progression pour les statistiques d'un Pokémon
* Affiche HP, Attaque, Défense et Vitesse avec des couleurs dynamiques
* La couleur change selon la valeur de la statistique
-->
<div>
<div
v-for="(value, key) in stats"
:key="key"
class="mb-2"
>
<!--
Label et valeur de la statistique
* justify-space-between place le nom à gauche et la valeur à droite
-->
<div class="d-flex justify-space-between text-caption mb-1">
<span>{{ labels[key] || key }}</span>
<span>{{ value }}</span>
</div>
<!--
Barre de progression
* :model-value définit la valeur actuelle
* :max="150" définit la valeur maximale
* :color change selon la valeur
* height="8" et rounded pour un style arrondi
-->
<v-progress-linear
:model-value="value"
:max="150"
:color="getStatColor(value)"
height="8"
rounded
/>
</div>
</div>
</template>
<script setup>
/**
* Props du composant PokemonStats
* @property {Object} stats - Objet { hp, attack, defense, speed }
*/
defineProps({
stats: {
type: Object,
required: true,
},
})
/**
* Labels lisibles pour chaque statistique
*/
const labels = {
hp: 'Points de vie',
attack: 'Attaque',
defense: 'Défense',
speed: 'Vitesse',
}
/**
* Retourne une couleur selon la valeur de la statistique
* - Vert si >= 100 (excellent)
* - Orange si >= 60 (moyen)
* - Rouge si < 60 (faible)
* @param {number} value - Valeur de la statistique
* @returns {string} Nom de couleur Vuetify
*/
function getStatColor (value) {
if (value >= 100) return 'green'
if (value >= 60) return 'orange'
return 'red'
}
</script>Commit
git add -A
git commit -m "feat: thème Pokémon, composants types/stats, snackbar favoris"Projet perso — À faire en parallèle
Dans votre projet individuel :
- Personnalisez le thème Vuetify avec vos propres couleurs
- Créez au moins un composant réutilisable avec des props
- Ajoutez un
v-snackbarpour confirmer une action utilisateur - Personnalisez le favicon et le titre dans
index.html