J’ai effectué mon stage de 2e année en entreprise pendant 8 semaines chez le freelanceur Etienne Leriche. Cette expérience m’a permis de me former principalement au développement front-end avec le framework Nuxt. J’ai appris à maîtriser Nuxt en l’associant avec des outils de stylisation comme Bootstrap, SASS et Tailwind CSS. Au cours de mon stage, j’ai travaillé sur plusieurs projets, dont certains intégraient des requêtes vers des API basées sur des technologies comme WordPress ou Symfony. Un de mes camarades de classe, également en stage dans la même entreprise mais chargé du développement back-end, s’est occupé de cette partie que je n’ai pas directement traitée, sauf pour lui apporter mon aide pour le débugger.
Projet Multi-Step Form (formulaire à plusieurs étapes)
Lors des 8 semaines de stage, j'ai travaillé sur plusieurs petits projets et pas juste un seul gros projet. Le premier projet, d'une durée d'environ 1 semaine, est l'intégration d'un formulaire de plusieurs étapes du site Frontend Mentor pour m'entraîner à utiliser Nuxt, ne l'ayant jamais utilisé auparavant. Le but était de l'implémenter de la manière la plus autonome possible (sans utiliser trop les outils comme l'intelligence artificielle) mais notre maître de stage nous a cependant donné des pistes pour le réussir.
Le formulaire d'origine
Ma version
Le but de ce projet était de reproduire quasiment à l'identique le formulaire Multi-Step Form de Frontend Mentor avec 4 étapes et une page de remerciement.
La structure des fichiers et installation des dépendances nécessaires
La structure du projet est une structure typique d'un projet Nuxt :
assets/css : contient le fichier main.css qui importe Tailwind et qui permet de déclarer des styles personnalisés (boutons, textes, etc).
components : contient les composants Nuxt qui seront ensuite utilisés dans les pages.
docs : le dossier généré par NPM en compilant le projet (le dossier à héberger).
layouts : les formats de page.
pages : toutes les pages du projet.
public : les assets
server : configuration par défaut
stores : déclarations de Pinia
app.vue : point d'entrée de l'interface utilisateur. C'est le layout racine qui englobe toutes les pages. On y définit souvent le header, footer, et on y applique les styles ou composants globaux.
nuxt.config.ts : fichier de configuration principal de Nuxt 3. Il sert à configurer les modules, les plugins, les variables d’environnement, TailwindCSS, les polices, les routes, etc.
package.json : fichier de gestion du projet Node.js. Il contient les dépendances, les scripts (ex: dev, build, lint) et des infos sur ton projet (nom, version, etc.).
package-lock.json : fichier généré automatiquement, verrouillant les versions exactes des dépendances installées pour assurer la reproductibilité de l’environnement sur toutes les machines.
tsconfig.json : configuration de TypeScript définissant les règles du compilateur TypeScript pour ce projet Nuxt, comme les chemins d'importation, options strictes, types inclus, etc..
deploy.sh : script de déploiement automatique. pour compiler le projet (utilisé pour débugger lors des problèmes de déplioement sur Github Page mais n'est pas obligatoire pour le déploiement)
Pour créer un projet nuxt, il faut utiliser la commande npm create nuxt <project-name>. Le format à utiliser dans un composant Nuxt est le suivant :
<template>
//Contenu (principalement du HTML avec des boucles)
</template>
<script setup>
//Fonctions en JS ou TS
</script setup>
<style scoped>
//CSS complémentaire
</style>
Pour lancer un projet en local, il suffit d'utiliser la commande npm run dev et le projet s'ouvre en local, généralement sur localhost:3000 (voire localhost:3001 si on lance 2 projets en même temps).
Pour utiliser des plugins comme Pinia, il suffit de lancer la commande npm qui correspond. Dans le cas de Pinia c'est npm install @pinia/nuxt qui fait l'affaire. Ensuite, il faut aller dans le fichier nuxt.config.ts et ajouter @pinia/nuxt dans les modules :
L'option target signifie que lors de la compilation, on souhaite générer une page statique. L'option css fait référence au fichier main.css qui contient l'import de Tailwindcss et l'option router définit le chemin de base de l’application, nécessaire pour un déploiement sur GitHub Pages dans un sous-dossier. Dans ce cas, j'ai mis le nom du projet (Multi-Step-Form).
Les stores
stepStore.js
Ce fichier crée un store Pinia nommé step qui sert à gérer l’état d’une navigation par étapes dans une interface, par exemple un formulaire multi-étapes. Pinia est une bibliothèque officielle gérer la gestion d'états (state management) dans les applications Vue.js comme Vue 3 et Nuxt 3.
La variable currentStep est une variable réactive (ref) qui stocke l’étape courante. Initialisée à 1 par défaut.
Trois fonctions permettent de changer la valeur de cette variable :
goToStep(step) permet d’aller directement à une étape précise passée en argument.
nextStep() passe à l’étape suivante en incrémentant currentStep.
previousStep() revient à l’étape précédente en décrémentant currentStep.
fromStores.js
Ce store Pinia permet de gérer un formulaire d’abonnement avec sélection de plan, d’options, et de mode de facturation. Le store centralise les données utilisateur et calcule automatiquement le prix total en fonction des choix. Cette approche permet une logique claire, réactive et facilement testable dans une application Vue/Nuxt.
Le state formData contient le suivant :
name, email, phone : Champs utilisateur du formulaire.
plans : Liste des plans d’abonnement disponibles (Arcade, Advanced, Pro).
selectedPlan : Le plan actuellement sélectionné (nul au par défaut).
options : Options supplémentaires disponibles (stockage, services en ligne, personnalisation, nul par défaut).
selectedOptions : Tableau des options choisies par l’utilisateur.
isYearly : Booléen indiquant si la facturation est annuelle ou mensuelle : vrai pour abonnement annuel et faux pour abonnement mensuel.
Le getter totalPrice Calcule dynamiquement le prix total en fonction : du plan choisi (mensuel ou annuel), des options sélectionnées et du mode de facturation (isYearly).
Les actions permettent de mettre à jour les données du formulaire de manière centralisée et contrôlée. Elles sont appelées par les composants pour interagir avec le state :
setSelectedPlan(plan) définit le plan d’abonnement sélectionné.
toggleOption(option) ajoute ou retire une option du tableau selectedOptions, selon qu’elle soit déjà sélectionnée ou non.
setIsYearly(value) active ou désactive la facturation annuelle (vrai ou faux).
updatePersonalInfo({ name, email, phone }) Met à jour les champs personnels saisis par l’utilisateur (nom, email, téléphone).
Cette structure permet d'encapsuler toute la logique liée au formulaire : sélection du plan, gestion dynamique des options, bascule entre facturation mensuelle ou annuelle, et mise à jour des données personnelles. Cela garantit une séparation claire entre la logique et la présentation, tout en assurant une expérience utilisateur fluide et cohérente.
Affichage des étapes
Le composant SideBar.vue affiche la barre latérale de navigation d’un formulaire en plusieurs étapes définies par le composant Step. Il montre visuellement les nom des étapes selon le chiffre sur le step actuel (Your Info, Select Plan, etc.). La boucle v-for permet d'afficher dynamiquement sur un tableau steps et générer un composant <Step /> pour chaque étape.
Le composant Step représente une seule étape dans la barre de progression. Il est utilisé dans une boucle v-for pour afficher chaque étape dans le composant SidebarSteps. Il, reçoit les props step (le numéro de l’étape), content (le texte associé à l’étape) et active, un booléan permerttant de définir si le step est actif ou pas pour adapter son style : la pastille s'affiche avec un chiffre noir et arrière-plan bleu si actif et texte blanc sans arrière-plan si inactif. La ligne hidden md:flex md:flex-col signifie que le contenu ne s'affiche que sur la version bureau, sur mobile il n'y a que la pastille.
Dans le layout default (mise en page par défaut) le composant Sidebar est déclaré, signifiant qu'il s'afichera sur toutes les pages sur la gauche sur bureau et en haut sur mobile. La partie slot est l'endroit où le contenu des pages sera affiché.
Application dans une page du formulaire
Prenons pour exemple l'épate 2 du formulaire, permettant à l’utilisateur de sélectionner un plan d’abonnement avec une facturation mensuelle ou annuelle. J’ai utilisé des composants dynamiques et réactifs (Card, ButtonGroup) combinés avec un toggle interactif.
Les données Pinia utilisées sont plans, isYearly, selectedPlan et isPlanSelected. Lorsque l'utilisateur clique sur un plan (@update:modelValue="onSelectPlan"), selectedPlan du store Pinia est mis à jour.
Le toggle switch permet de basculer entre le plan mensuel et le plan annuel et le texte en gras varie selon l'option choisie. Un v-model est utilisé pour synchroniser le toggle avec isYearly du store Pinia.
Dans le script de la page, les plans et les états selectedPlan et isYearly sont récupérés depuis le store. Les refs sont utilisées pour permettre le binding avec les composants comme v-model.
Les deux watchers mettent à jour les données dans le store chaque fois que l’utilisateur change son choix: le premier vérifie si l'utilisateur change le plan et le deuxième vérifie si l'utilisateur change entre mensuel et annuel.
Lorsqu’un utilisateur sélectionne un plan via le composant Card, le selectedPlan est mis à jour.
La commande const isPlanSelected = computed(() => !!selectedPlan.value) permet de vérifier si un plan a été sélectionné pour activer ou désactiver les boutons, comme désactiver le bouton "next" si aucun plan n'a été choisi.
stepStore.goToStep(2) permet de forcer le store à rester ou revenir à l’étape 2 dans le cas où ce script est chargé à l’avance.
Déploiement sur une page Github
Pour compiler un projet Nuxt sur une page Github, il suffit d'utiliser la commande npm run generate. Ensuite, j'ai décidé de push le contenu du dossier dist fraîchement généré dans une branche gh-pages à part afin de garder une propreté dans ma branche master. Par défaut, Github ne reconnaît pas les dossiers comemnçant par "_", donc il fait ajouter un fichier .nojekyll vide afin d'éviter ce problème.
Ce premier projet m’a permis de découvrir et de me familiariser avec Nuxt, que je n’avais encore jamais utilisé auparavant. Il m’a fallu un temps d’adaptation d'environ une semaine, notamment pour comprendre le fonctionnement des stores. Ayant déjà travaillé avec React, j’ai retrouvé certaines similarités, bien que les approches restent différentes. Ce que j’apprécie particulièrement avec Nuxt, c’est sa simplicité d’utilisation. L’architecture claire et les conventions intégrées permettent de structurer rapidement un projet sans avoir à tout configurer manuellement, et il est également possible de mettre des balises HTML, du CSS et un script JS ou TS dans le même fichier, ce qui n'est pas le cas avec React sauf si on utilise du Tailwind CSS.
Landing page pour boutique e-commerce
J’ai participé à la réalisation d’une landing page pour une cliente de mon chef de stage. Cette page comportait un compteur dynamique jusqu'à la date d'ouverture du site officiel, ainsi qu’un formulaire d’inscription à une newsletter permettant aux utilisateurs de laisser leur adresse mail. Pour cela, j'ai utilisé Nuxt mais cette fois-ci j'ai utilisé SASS au lieu de Tailwind. Certaines parties sont confidentielles donc je ne montre que des parties qui ne poseront aucun soucis.
Affichage du menu "à propos"
Le bouton à propos contient un stroke qui fait un effet de rotation constant par rapport au centre. Cet effet est obtenu en utilisant un keyframe ayant une rotation de 360° et ensuite en le déclarant dans la propriété animation de la classe stroke.
Le composant Apropos.vue affiche le bouton permettant d’ouvrir un menu superposé ("overlay" en anglais) contenant le message de présentation de la marque SN Beauty. L’icône du bouton change selon l’état du menu (point d'interrogation quand le menu est fermé et une flèche vers la gauche quand le menu est ouvert), et l’animation de l’overlay est contrôlée par une variable isMenuOpen.
Ce script contrôle l'ouverture et la fermeture animée du menu superposé en manipulant un effet de clip path circulaire (masque SVG).
La fonction toggleMenu change l'état du menu selon s'il est ouvert ou fermé et appelle soit openMenu soit closeMenu.
La fonction openMenu ouvre le menu en animant le clip path circulaire en augmentant progressivement son rayon (r) jusqu’à 1500 pour couvrir tout l’écran. L’animation est faite à la main avec setInterval.
La fonction closeMenu lance l’animation de fermeture en diminuant progressivement le rayon du clip path (r) jusqu’à 0. Ensuite, elle réinitialise les états isMenuOpen et isClosing.
Le CSS de la classe overlay fixe le menu en haut à droite de la page en prenant toute la largeur et toute la hauteur. Les couleurs sont des variables que j'ai définies dans un fichier _variables.scss. La propriété z-index lui donne la priorité au-dessus des autres éléments.Le clip-path est le masque circulaire initial et en utilisant la propriété transition nous avons une transition fluide entre les deux états.
La classe open est une classe qui s'ajoute quand le menu est ouvert. Son CSS dévoile l’overlay avec un grand cercle avec la propriété clip-path en activant les interactions en mettant la propriété pointer-events à auto et en cachant la barre de scroll en mettant la propriété scrollbar-width à none.
Résultat final du menu "à propos"
Variables des couleurs
_variables.scss
Utiliser SASS plutôt que du CSS pur permet de définir des variables pour chaque couleur, signifiant que si par exemple on définit une couleur principale et on décide de changer d'avis après, il est possible de changer la couleur ici pour appliquer le changement partout où la variable est définie. Ici par exemple, j'ai défini des couleurs pour l'arrière-plan, le bouton "à propos", les couleurs primaires pour les boutons et titres, les couleurs pour le texte et les couleurs neutres, tout cela selon si le thème est clair ou sombre.
Le fichier global.scss fonctionne d'une manière identique au fichier main.css de Tailwind CSS. Le fichier global.scss permet de définir des thèmes et des classes utilitaires personnalisées, mais contrairement au main.css de Tailwind, il ne génère pas automatiquement les classes utilitaires. Celles-ci doivent être écrites manuellement.
Pour ce projet, j'ai défini toutes les variables sous root en utilisant les variables de light définies sur _variables.scss par défaut. J'ai ensuite mis une classe dark-mode en définissant les mêmes variables mais cette fois-ci en mettant les variables de dark. Cela signifie que si la classe dark-mode est présente sur la page HTML, toutes les couleurs seront celles du mode dark.
global.scss
Changement de thème (thème clair et thème sombre)
Extrait de ButtonGroup.vue
Pour permettre aux utilisateurs d'utiliser la page selon leurs préférences, il est important d'inclure un thème clair et un thème sombre avec la possibilité de changer ce thème sans passer par le code. Pour cela, j'ai implémenté un bouton permettant de changer le thème. C'est un bouton avec un soleil dessus.
La fonction toggleTheme alterne entre les deux thèmes. colorMode.value contient la valeur du thème actuel (dark ou light) et colorMode.preference définit la préférence de l'utilisateur. Si le thème actuel est sur "dark" il passe sur "light" et vice-versa.
Le colorMode vient de l'import @nuxtjs/color-mode et est défini dans les modules du fichier de configuration Nuxt.
Extrait de nuxt.config.ts
Compte à rebours dynamique
Le composant Counter.vue affiche 4 blocs : Jours, Heures, Minutes et Secondes. Il affiche un compte à rebours dynamique, actuellement programmé jusqu'au 14 juin 2025 mais avec la possibilité de changer cela dans le script. Chaque bloc contient une valeur numérique ({{ days }}, {{ hours }}, etc.) et un libellé conditionnel (jour / jours, heure / heures, etc.). Cela signifie que si une des valeurs est égale à 1, l'étiquette affichera le mot au singulier et pas au pluriel.
Les variables days, hours, minutes et seconds vont stocker les valeurs du compte à rebours.
La fonction updateCountdown calcule la différence en millisecondes entre maintenant et la date cible et ensuite convertit ce temps en jours, heures, minutes et secondes.
le hook onMounted appelle une première fois la fonction et la répète toutes les secondes (100ms).
Le hook onUnmounted arrête le setInterval pour éviter les fuites de mémoire quand le composant est supprimé.
Ce projet m'a permis d'apprendre à efficacement faire un bouton permettant de changer le thème d'une page en déclarant correctement les variables permettant son fonctionnement. Il m'a également appris à faire un compte à rebours dynamique tout en évitant les erreurs de syntaxe de langue (une étiquette au pluriel quand le compteur affiche 1 par exemple). J'ai aussi appris à faire des menus s'ouvrant de manière animée en utilisant des clip paths et un keyframe pour faire un effet de rotation. Mon chef de stage m'a bien aidé à donner des indications pour pouvoir terminer le projet et il a été à l'écoute pour débugger quand il y avait des problèmes.
Compte à rebours dynamique