Skip to content

É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 : PokemonTypesChips et PokemonStats
  • Enrichir la page détail avec les types et les statistiques
  • Enrichir PokemonCard avec 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 :

CouleurValeurInspiration
primary#E53935Rouge Pokéball
secondary#1A237EBleu nuit
accent#FFD600Jaune Pikachu
error#FF5252Rouge vif
success#4CAF50Vert Bulbizarre
warning#FB8C00Orange
info#2196F3Bleu info
js
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).

ComposantRôleDocumentation
<v-chip>Puce compacte avec couleur et texteChips

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 :

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>

Points importants :

  • ga-1 : gap de 1 unité entre les chips (espacement)
  • flex-wrap : les chips passent à la ligne si l'espace manque
  • getTypeById() 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 :

js
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.

ComposantRôleDocumentation
<v-progress-linear>Barre de progression horizontaleProgress linear
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 (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 lisibles
  • getStatColor() : donne un retour visuel immédiat sur la qualité de la statistique
  • max="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 :

js
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 :

vue
<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 :

vue
<!-- 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> :

js
import PokemonTypesChips from '@/components/PokemonTypesChips.vue'

6. Personnaliser le favicon et le titre

Dans index.html à la racine du projet :

  1. Remplacez le titre par « Pokédex » :
html
<title>Pokédex</title>
  1. Ajoutez un favicon personnalisé. Téléchargez une icône Pokéball (format .ico ou .png) et placez-la dans le dossier public/ :
html
<link rel="icon" href="/favicon.ico" />
  1. Ajoutez lang="fr" sur la balise <html> :
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
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
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)
vue
<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
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

bash
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 :

  1. Personnalisez le thème Vuetify avec vos propres couleurs
  2. Créez au moins un composant réutilisable avec des props
  3. Ajoutez un v-snackbar pour confirmer une action utilisateur
  4. Personnalisez le favicon et le titre dans index.html

Documentation utile

Documentation pour les cours de développement web