### Sommaire:
* [Nuxt](#nuxt)
* [Plugin Bootstrap](#bootstrapPlugin)
* [Server Directory](#server)
* [Difference entre $fetch et useFetch()](#fetch)
* [La gestion des liens](#handlingLinks)
* [Adieu defineComponent](#defineComponent)
* [Définir les props](#props)
* [Définir les emits](#emits)
* [Le composable UseAssets()](#useAssets)
* [Mise en place des images](#images)
* [La gestion des hovers sur les NuxtLink](#nuxtLink)
* [Définir les layouts](#layout)
* [Le module i18n](#traduction)
* [Le composable useFetchWithCache()](#cache)
* [Content Directory](#contentDir)
* [Differences bootstrap et headless UI](#headlessUI)
* [La gestion des images avec le module NuxtImage](#moduleImage)
* [Mise en place du UnoCSS](#unocss)
* [Mise en place de Storyblok](#storyblok)
* [Bootstrap](#bootstrap)
<a id="nuxt"></a>
## Nuxt
<a id="bootstrapPlugin"></a>
- Le fichier `bootstrap.client.ts` dans les [**plugins**](https://nuxt.com/docs/guide/directory-structure/plugins):
J'ai décidé de mettre le fichier dans les plugins car c'est ce qui me paraissait le plus adapté. Je ne l'ai pas mis dans les composables car les composables sont là pour stocker de la data ou du code que l'ont peut appeler dans les pages ou les composants de notre choix. Les plugins sont chargés avant l'instanciation de l'application donc cela me paraissait plus adapté à notre problématique.
```
├── plugins
│ ├── bootstrap.client.ts
```
On utilise `defineNuxtPlugin` afin de définir que c'est un plugin et ce qu'il va devoir exécuter. Le seul argument que l'on peut passé dans un plugin est `nuxtApp`. Voici un exemple:
```j
import Dropdown from "bootstrap/js/dist/dropdown"
import Offcanvas from "bootstrap/js/dist/offcanvas"
import { defineNuxtPlugin } from "#app"
export default defineNuxtPlugin((nuxtApp) => {
// Provide dropdown everywhere to nuxt app
nuxtApp.vueApp.provide("Dropdown", Dropdown)
nuxtApp.vueApp.provide("Offcanvas", Offcanvas)
})
```
Pour finir, nous n'avons plus besoin en Nuxt 3, d'importer nos plugins dans `nuxt.config.ts` car ils sont maintenant auto-engistrés.
<a id="server"></a>
- Mise en place du dossier `server`:
Le dossier `server` permet de gérer plus facilement une partie du backend grâce à Nuxt. Chaque fichier est gérer grâce à `defineEventHandler(`) et cela permet de retourner des données au format JSON. Voici un exemple:
```typescript=
import { IOfferProperties } from "~/types/interfaces/IOfferProperties"
import offerData from "~/server/offerData"
export default defineEventHandler((): IOfferProperties[] => {
return offerData.offers
})
```
Le code ci-dessus permet de récupérer les données des offres qui sont au format JSON et sont stockés dans un fichier qui est appelé `offerData.ts`. Voici un extrait du fichier:
```json=
export default {
offers: [
{
id: 7,
name: "BattleHack",
monthly_price: 14.99,
months_of_covenant: 1,
months_of_covenant_text: "1 mois",
},
],
},
```
Pour plus d'informations complémentaires, vous pouvez consulter ces deux pages: [**ici**](https://nuxt.com/docs/guide/directory-structure/server) et [**ici**](https://masteringnuxt.com/blog/server-routes-in-nuxt-3)
<a id="fetch"></a>
- Difference entre `useFetch()` ou `$fetch` pour les requêtes API:
La methode ci-dessous, nous permet d'avoir accès juste au tableau d'objet qui se trouve dans `offerData.ts`. Avec `$fetch`, durant le SRR, la data est fetch deux fois, une fois par le server et l'autre par le client.
```typescript=
const offer = await $fetch(`/api/offers`);
```
La méthode `useFetch()` ci-dessous est un composable proposé par Nuxt et nous permet d'avoir accès au tableau mais aussi à d'autres options tel que la valeur **data** qui le corps de la réponse à notre requête à l'API, la valeur **pending** qui est un booléan qui indique si la requête est entrain de charger et **error** qui est un object contenant les informations concernant la requète d'erreur. `useFetch` permet de combiner l'utilisation de `$fetch` et de `useAsyncData` (= permet de fetch du côté server et transférer les infos aux clients)
```typescript=
const { data: offer } = await useFetch(`/api/offers`, {
method: "GET"
})
```
Dans le cas du hub, j'ai décidé de faire un composable nommé `useOffer.ts` utilisant le code ci-dessus et en gérant aussi les erreurs afin de pouvoir appeler mon API dans n'importe quel page sans avoir à réécrire du code.
Pour plus d'informations complémentaires, voici la doc de Nuxt juste [**ici**](https://nuxt.com/docs/getting-started/data-fetching) et [**ici**](https://nuxt.com/docs/api/composables/use-fetch).
<a id="handlingLinks"></a>
- La gestion des liens dans `nuxt.config.ts`:
La configuaration `profilRuntimeConfig` devient `runtimeConfig` avec Nuxt 3 et il est importé grâce à la configuration `defineNuxtConfig`. De plus, les liens utilisés sont récupérés directement de notre `.env` voici un exemple:
``` typescript=
export default defineNuxtConfig({
runtimeConfig: {
public: {
sites: {
cybertraining: process.env.CYBERTRAINING_URL,
battle: process.env.BATTLE_URL,
cyberunity: process.env.CYBERUNITY_URL,
showcaseURL: process.env.SHOWCASE_URL,
demoURL: process.env.REQUEST_DEMO,
},
},
},
})
```
Par ailleurs, les liens maintenant peuvent être gérés soit du coté serveur grace à `private`, soit du coté client grâce à `public` comme on peut le constater ci-dessus.
:::danger
**Note:** il faut faire très *attention* à bien les paramètrer car cela peut créer des problèmes à d'autres endroits dans le code comme par exemple désactiver tous les events dans le projet.
:::
Pour appeler runtimeConfig dans les pages ou les composants de notre projet, il faut maintenant appeler une constante dans le script de type `const config = useRuntimeConfig()` et cette technique ne fonctionne que si le script est *setup*: `<script setup></script>` voici un exemple:
```typescript=
<script lang='ts' setup>
const config = useRuntimeConfig()
const getUrl = () => {
return window.open(config.public.sites.battle)
}
</script>
```
Si vous voulez encore plus de précisions au sujet des `runtimeConfig`, vous pouvez consulter la doc de Nuxt juste [**ici**](https://nuxt.com/docs/guide/going-further/runtime-config).
<a id="defineComponent"></a>
- Plus besoin d'utiliser `defineComponent`:
On peut maintenant utiliser la fonctionnalité *setup*, comme dans dans l'exemple ci-dessous:
```typescript=
<script lang="ts" setup>
//le code sera écrit juste ici
</script>
```
La fonction *setup* permet d'avoir une code qui est plus succinte et moins lourd mais facilite aussi l'utilisation de TypeScript afin de déclaré des props ou encore des emits. Le code qui est contenu en setup, est compilé comme s'il se trouvait dans une fonctione `setup()`.
Pour plus d'explication, vous pouvez regarder [**ici**](https://vuejs.org/api/sfc-script-setup.html)
<a id="props"></a>
- Définir des *props*:
N'ayant plus besoin d'utiliser `defineComponent()`, nous pouvons maintenant utiliser l'API `defineProps()` qui est compilée grâce à la fonction *setup*. Cette nouvelle manière de déclarer les `props` accepte les mêmes valeurs que props en Nuxt 2. Voici un exemple:
```typescript=
<script lang="ts" setup>
const props = defineProps({
password: {
type: String,
required: true,
},
})
</script>
```
<a id="emits"></a>
- Définir des *emits*:
Comme pour les props, les emits ne sont plus appelés et utilisés de la même manière. Nous allons maintenant pouvoir utiliser l'API `defineEmits()` ce qui permet de définir quel type d'émit nous voulons pour ensuite l'appeler dans une fonction qui acceptera les mêmes valeurs que `$emits` en Nuxt 2. Tout comme les props, cette nouvelle manière de faire est complilé grâce à la fonction *setup*. Voici un exemple:
```typescript=
<script lang="ts" setup>
const emit = defineEmits(["click-button"])
const redirect = () => {
emit("click-button")
}
</script>
```
<a id="useAssets"></a>
- Mise en place d'un composable `useAssets()`:
Je l'ai crée à l'aide de cette ressource [**ici**](https://github.com/nuxt/nuxt/issues/14766) car avec vite `require()` ne fonctionne pas alors il a fallu contourner le problème afin d'appeler mes images de manière dynamique. La manière que j'ai utilisé n'est pas la seule manière de faire, on aurait aussi pu le faire comme [**ici**](https://stackoverflow.com/questions/73497705/nuxt-3-images-not-rendered-when-set-the-src-dynamically-on-build-process) cependant, cette technique là serait plus pertinente quand on a beaucoup de contenu visiuel sur le site, ce qui n'est pas vraiment le cas sur le hub. Mon composable vient alors récupéré le dossier assets afin de récupérer les images que je veux importer dynamiquement dans mon `JavaScript`. Voici à quoi ressemble le composable:
```typescript=
export default function useAssets(path: string): string {
const assets = import.meta.glob("~/assets/img/**/*", {
eager: true,
import: "default",
})
// @ts-expect-error: wrong type info
return assets["/assets/img/" + path]
}
```
Et voici comment on doit appeler le composable dans les pages ou les composants. On doit juste spécifier la suite de la route après `~/asset/img/`:
```typescript=
const logo = computed(() => {
if (
props.properties?.productName === ProductNameEnum.CYBERTRAINING ||
props.properties?.productName === ProductNameEnum.CYBERUNITY ||
props.properties?.productName === ProductNameEnum.BATTLEHACK
) {
return useAssets("logos/" + props.properties.productName.toLowerCase() + "_logo_text.png")
} else {
return ""
}
})
```
<a id="images"></a>
- Mise en place des images:
Elles sont dans le dossier [**public**](https://nuxt.com/docs/guide/directory-structure/public): le dossier public est le dossier qui remplace le static en nuxt 3, c'est pour cela que j'ai décidé de mettre les images que j'utilise de manière statique (importées uniquement dans mon *HTML*) dedans.
```
├── public
│ ├── flags
│ ├── logos
│ ├── video
```
Les images que l'on utilisera de manière dynamique dans le *JavaScript*, seront quant à elles stocker dans les `assets` dans un dossier appeler `img`.
```
├── assets
│ ├── img
│ │ ├── bg-cards
│ │ ├── logos
```
<a id="nuxtLink"></a>
- Le hover sur les [**NuxtLink**](https://nuxt.com/docs/api/components/nuxt-link#props):
J'ai utilisé la classe `.router-link-exact-active` qui est lié directement au `NuxtLink` afin de changer la couleur des icones et ajouter la ligne qui se trouve à gauche de l'écran quand l'élément est actif. Voici un example:
``` css=
.router-link-exact-active {
position: relative;
&:before {
content: "";
position: absolute;
left: -48px;
height: 100%;
width: 3px;
background-color: $blue;
}
i {
color: $green;
}
span {
font-weight: bolder;
}
}
```
<a id="layout"></a>
- Définir le `layout` utilisé:
Afin de faire ce choix, on utilisera: `definePageMeta` qui permet de paramétrer les meta-datas de la page et on lui donnera alors comme paramètre `layout` avec soit le nom de la layout, soit false si on ne veut pas de layout. Voici un exemple:
```typescript=
<script lang="ts" setup>
definePageMeta({
layout: "login",
})
</script>
```
Vous pouvez retrouver plus d'informations juste [**ici**](https://nuxt.com/docs/api/utils/define-page-meta) afin d'en apprendre sur quel metadata peut être paramétré avec `definePageMeta`.
<a id="traduction"></a>
- Mise en place de la traduction avec [**le module I18n**](https://v8.i18n.nuxtjs.org/):
Ce module permet la gestion de la traduction de notre site web. La seule version compatible avec Nuxt 3 est la v8, qui est une version encore beta et qui n'est pas encore totalement terminé à l'heure où j'écris.
La première étape est la mise en place des *fichiers JSON* qui contiennent et stockent les traductions. Dans mon cas, la version française et la version anglais dans un dossier que j'ai renommé `locales`:
```
├── locales
│ ├── en.json
│ ├── fr.json
```
Le module `i18n` a besoin d'être paramétré dans le fichier `nuxt.config.ts` afin de le faire fonctionner et pouvoir personnaliser ses fonctions:
```typescript=
modules: [
[
"@nuxtjs/i18n",
{
vueI18nLoader: true,
detectBrowserLanguage: {
useCookie: true,
cookieKey: "i18n_redirected",
redirectOn: "root", // recommended
},
},
],
],
i18n: {
locales: [
{ code: "fr", iso: "fr-FR", name: "Français", file: "fr.json" },
{ code: "en", iso: "en-US", name: "English", file: "en.json" },
],
defaultLocale: "en",
strategy: "no_prefix",
langDir: "locales/",
lazy: true,
},
```
Les fonctionnalités que j'ai utilisé ci-dessus:
- `detectBrowserLanguage`: cette option permet de détecté la langue du navigateur du visiteur et de le rediriger automatiquement sur leur langue de prédilection.
- `locales`: cette option permet de listé tous les langues qui sont disponibles sur notre application. On peut avoir soit un tableau de code soit un tableau d'objet contenant des configurations plus complexe en fonction de chaque langue.
- `defaultLocale` : cette option permet de déterminer quelle est la langue par défault de notre application. Il est recommandé de le mettre en place car si notre navigateur n'arrive pas à détecté la langue qu'on utilise, cela permet d'afficher le site dans notre langue par défault.
- `strategy` : cette option permet de gérer les routes présentes sur notre application. Il y a quatre stratégies différentes:
- `no_prefix` : la route n'aura pas de préfixe le code locale et c'est celle que j'ai choisi pour le site, car cela me parait le moins lourd à supporter et on aura pas à devoir faire des routes customisées.
- `prefix_except_default` : un préfixe sera ajouté sur chaque route sauf pour la langue par défault
- `prefix` : un préfixe sera ajouté à toutes nos routes peu importe la langue
- `prefix_and_default` : un préfixe sera ajouté à toutes nos routes même celle par défault
- `langDir`: cette option permet de déterminé où se trouve les fichiers avec la traduction.
- `lazy`: cette option permet de déterminé que seuls les traductions dont on se sert sont chargés durant le chargement de la page.
La gestion du changement de langue est relativement simple car cela est géré directement par notre module et son language switcher. Il y a plusieurs manières de faire, ici c'est la manière que j'ai utilisé car on utilise le détecteur de langue en fonction de la langue du navigateur:
```typescript=
<ul id="dropdownMenu" aria-labelledby="dropdownMenu" class="dropdown-menu dropdown-menu-end p-0 mt-2">
<li class="d-flex justify-content-between px-5 py-3 gap-3 lang">
<a :class="locale === 'fr' ? 'active' : ''" class="d-flex gap-1 align-items-center text-white text-decoration-none" @click.prevent.stop="setLocale('fr')" >
<img src="/flags/fr.png" alt="Drapeau de la France" width="20" height="20" />
<span>FR</span>
</a>
<a :class="locale === 'en' ? 'active' : ''" class="d-flex gap-1 align-items-center text-white text-decoration-none" @click.prevent.stop="setLocale('en')" >
<img src="/flags/en.png" alt="Drapeau du Royaume-Unis" width="20" height="20" />
<span>EN</span>
</a>
</li>
</ul>
<script setup>
const { locale, setLocale } = useI18n()
</script>
```
La technique ci-dessus doit être utilisé quand on utilise detectBrowserLanguage ce qui est notre cas et qu'on souhaite conserver les paramètres régionaux lors qu'un changement de route `setLocale(locale)` permet donc de mettre à jour le cookie de paramètre régionaux stocké et basculé sur la route rouge vers les paramètre régionaux spécifiés. Si on utilisait pas cette méthode, les paramètres régionaux pourraient revenir à ceux enregistrés pendant la navigation.
:::danger
**Note:** il faut bien faire *attention* que le `setLocale(locale)` soit dans un événement `@click.prevent.stop`. Si ce n'est pas le cas, cela peut entrainer des erreurs et des bugs dans le projet tel que ne plus avoir accès à son environnement local ( localhost).
:::
Pour appeler nos traductions dans nos templates, c'est assez simple car cela est géré directement par I18n comme ceci:
```htmlembedded=
<h2 class="text-white fs-20 fw-medium mb-3">{{ $t("title.service") }}</h2>
//...
<h2 class="text-white fs-20 fw-medium mb-3">{{ $t("title.overview") }}</h2>
```
Si nous voulons faire appelle à nos traductions de manière dynamique dans nos scripts, la manière de l'appeler est légèrement différentes:
```typescript=
const configs = useRuntimeConfig()
const { t } = useI18n()
const getProperties = computed((): ILoginCardProperties => {
return {
backLink: configs.public.sites.showcaseURL,
title: t("title.connect"),
subtitle: t("title.welcome"),
link: t("subtext.return"),
underlineWord: "Seela.io",
buttonLabel: t("button.to_connect"),
inputs: [
{ name: t("input.email"), type: "email" },
{ name: t("input.password"), type: "password", comment: t("subtext.forgot") },
],
}
})
```
<a id="cache"></a>
- Mise en place d'un composable pour gérer le cache de nos [**requêtes**](https://masteringnuxt.com/blog/writing-a-cache-composable-in-nuxt-3):
*Tout d'abord, pourquoi mettre nos requêtes en cache ?* La mise en cache permet d'améliorer l'expérience utilisateur en réduisant le temps de chargement d'une page web et la consommation de bande passante.
Pour la mise en place du composable, j'ai dû installer `@vueuse/core` au projet. `VueUse` est un ensemble de fonctions utilitaires.
Voici le code du composable que j'ai appelé `useFetchWithCache`:
```typescript=
import { StorageSerializers, useLocalStorage } from "@vueuse/core"
export default async <T>(url: string) => {
// Use sessionStorage to cache data
const cached = useLocalStorage<T>(url, null, {
serializer: StorageSerializers.object,
})
if (!cached.value) {
const { data, error } = await useFetch<T>(url)
if (error.value) {
throw createError({
...error.value,
statusMessage: `Could not fetch data from ${url}`,
})
}
// Update the cache
cached.value = data.value as T
}
return cached
}
```
Notre cache va être alors stocker dans notre `localStorage`. Il nous permet de stocker des informations en locale sans durée limitée. Le composable va permettre d'améliorer `useFetch` qui ne gère pas le cache.
<a id="contentDir"></a>
- Mise en place du [**content directory**](https://content.nuxtjs.org/):
Le module content permet de parser des fichiers en `markdown`, `yalm`, `json` ou `csv` dans le content directory et et fournit des chemins en fonction de la structure du répertoire.
On peut donc alors stocker des informations avec la même structure de dosier que les pages, voici la structure de mon content directory:
```
├── content
│ ├── pages
│ │ ├── confidentiality.md
│ │ ├── generalConditions.md
│ │ ├── knowledgeCenter.md
│ │ ├── legalMentions.md
```
Voici maintenant la structure de mon dossier pages:
```
├── pages
│ ├── Confidentiality.vue
│ ├── GeneralConditions.vue
│ ├── KnowledgeCenter.vue
│ ├── LegalMentions.vue
```
Pour appeler le content directory dans une page, c'est plutôt simple, voici un extrait d'une de mes pages:
```htmlmixed=
<template>
<div id="generalCondtions">
<ContentDoc path="/pages/generalconditions" />
</div>
</template>
```
La balise `<ContentDoc/>` est la manière la plus rapide pour faire une requête pour afficher le contenu de nos markdown. Il permet d'afficher un seul document à la fois. Le `path` permet de récuperer la route qu'on a besoin afin d'afficher notre contenu.
Le module doit être configuré dans `nuxt.config.ts`:
```typescript=
export default defineNuxtConfig({
modules: ["@nuxt/content"],
content: {
// https://content.nuxtjs.org/api/configuration
documentDriven: true,
}
})
```
:::info
***Remarques :***
La mise en place du style de notre contenu est beaucoup plus compliqué avec le `content directory`, car le module permet de nous envoyé des données d'un ficher markdown qui va être interprété par `Nuxt` qui va généré le `HTML`. Par conséquent, la gestion du style peut être un peu compliqué à mettre en place et cela peut donner un code qui n'est pas très propre car on se retrouve à manipuler des id générées par `Nuxt`.
Le `content directory` sera alors plus adapté pour du contenu statique tel que des conditions générales, les mentions légales ou la politique de confidentialité. Pour du contenu dynamique, je pense qu'il est préférable d'utiliser le `server directory` car on peut récupérer qu'une partie d'un fichier et ensuite le manipuler comme on le souhaite.
:::
<a id="headlessUI"></a>
- Difference entre les composants proposés par [bootstrap](https://getbootstrap.com/docs/5.3/getting-started/introduction/) et ceux de [headless UI](https://headlessui.com/) :
Pour faire la comparaison entre les deux, j'ai décidé de mettre en place un popover en bootstrap et un avec Headless UI afin de voir les avantages et les inconvénients des deux méthodes sur un même composant.
La mise en place [d'un popover avec Bootstrap](https://getbootstrap.com/docs/5.2/components/popovers/) est plutôt simple car ils proposent un composant "prêt à l'emploi":
```htmlmixed=
<template>
<div class="mb-2">
<button
id="popover"
type="button"
class="btn btn-lg btn-success"
data-bs-placement="left"
data-bs-toggle="popover"
data-bs-content="Bienvenue sur le hub de Seela"
data-bs-trigger="focus"
>
Click me
</button>
</div>
</template>
```
Le code ci-dessus est *la stucture HTML*, on utilise des classes bootstraps et autres fonctionnalités pour faire fonctionner le popover:
=> `data-bs-placement='left'` permet de choisir la position dans laquelle va s'ouvrir le popover;
=> `data-bs-toggle='toggle'` gère l'ouverture du popover;
=> `data-bs-content="..."` permet de stocker le contenu de la popover;
=> `data-bs-trigger="focus"` permet de paramétrer la popover va s'ouvrir et se fermer.
```typescript=
<script setup>
import { onMounted } from "vue"
import Popover from "bootstrap/js/dist/popover"
onMounted(() => {
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
popoverTriggerList.map(function (popoverTriggerEl) {
return new Popover(popoverTriggerEl)
})
})
</script>
```
Le code ci-dessus permet d'activer la popover grâce à un hook proposé par `vue` qui s'appelle `onMounted()`. Ce hook permet d'enregistrer une fonction de rappel qui sera appelée après le montage du composant.Ce hook est généralement utilisé pour effectuer des effets secondaires qui nécessitent un accès au DOM rendu du composant et c'est ce dont on a besoin afin de modifier le DOM afin de faire apparaitre le popover.
Ensuite pour le styliser comme on le souhaite, on peut soit jouer avec les classes soit override les bootstrap, comme ceci:
```sass=
// POPOVER
$popover-bg: $bright-black;
$popover-box-shadow: 0px 0px 10px black;
$popover-body-color: white;
```
Maintenant, voici comment on mets en place [une popover avec Headless UI](https://headlessui.com/vue/popover):
Tout d'abord, il faut installer le framework CSS tailwind car Headless UI ne fonctionne qu'avec cela. Voici la documentation pour installer tailwind, juste [ici](https://tailwindcss.com/docs/installation). Dans un second temps, il faut installer la librairie Headless UI avec cette ligne dans notre terminal: `npm install @headlessui/vue`
Voici maintenant à quoi ressemble un composant construit avec le librairie:
```htmlmixed=
<template>
<Popover class="relative" v-slot="{open}">
<PopoverButton class="p-3 m-2 rounded-full bg-purple-400 focus:ring-1 focus:ring-purple-600 focus:outline-none text-white hover:bg-purple-300">
Solutions
</PopoverButton>
<PopoverPanel class="absolute z-10 ml-2">
<div class="p-3 shadow-xl rounded border border-gray-100">
<p>Bienvenue sur l'essai de HEADLESS UI</p>
</div>
</PopoverPanel>
</Popover>
</template>
```
```typescript=
<script setup>
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
</script>
```
Comme on peut le voir ci-dessus, headless UI génère ses propres composants réutilisables, il suffit juste les importer dans le JS et ils sont directement opérationels.
On peut maintenant se poser la questions qu'elles sont les avantages et inconvéniants des deux. Voici tout d'abord **les avantages**:
*Les avantages de bootstrap:*
- L’esthétique/visuel est déjà presque prêt à l’emploi;
- On peut toujours faire nos propres personnalisations et on peut override les classes déjà existantes.
*Les avantages de Headless UI:*
- Propose des composants qui sont prêts à l’emploi sans avoir à ajouter de JS à notre composant;
- Beaucoup plus rapide à mettre en place;
- Le potentiel de personnalisation est assez vaste, on peut faire n’importe quel style à notre popover en passant par les classes de tailwind;
- La modification du contenu donne de plus grandes libertés, on peut ajotuer des images, des icones et même des vidéos dans nos popover sans aucun problème et avec une certaine facilité.
Voyons maintenant **les incovéniants**:
*Les inconvénients de bootstrap:*
- Long et parfois compliqué à faire fonctionner correctement;
- Obligé de passer par du JS afin de faire fonctionner certains composants proposés par bootstrap;
- La modification du contenu est plus restreint car on rempli juste une balise dans le bouton de la popover.
*Les inconvénients de headless UI:*
- Le style des composants n'est pas du tout prêt à l'emploi, on doit générer nous même tout le style avec beaucoup des classes différentes, cependant, cette inconvéniants est plus rattaché à l'utilisation de tailwind que par la librairie en elle-même;
- Pour changer la direction d'affichage de nos popover, il faut le faire à la fin dans nos classes.
:::info
**Remarques:**
D'un point de vue technique, je pense que je choisirais plus` Headless UI` par sa facilité d'installation et d'utilisation. Il n'y pas besoin d'ajouter de JS, ni d'autre fiotures. J'ai mis plus d'une journée à trouver comme parametrer correctement les popover sur `bootstrap` et faire en sorte qu'elle s'ouvrir. Tandis qu'avec `Headless UI`, en quelques clics, tout était fonctionnel.
Cependant, d'un point de vue esthétique/style, je pense que ça dépend des besoins du projet, `bootstrap` est pratique car esthétiquement les composants sont presque prêts à l'emploi, tandis qu'avec `Headless UI` il faut mettre en place. Cependant, une fois le style du composant `Headless UI` mis en place, on peut les réutiliser aussi à l'infini.
:::
<a id="moduleImage"></a>
- La gestion des images avec le module [NuxtImage](https://image.nuxtjs.org/):
Ce module permet d'optimiser les images utilisées dans un projet Nuxt et propose ces propres composants pour gérer les images:
- `<nuxt-img>` vient remplacer la balise native `<img> `que l'on utilise habituellement:
- Cette balise donne une meilleure optimisation des images,
- Elle converti l'attribut `src` pour fourni des URLs qui seront optimisés,
- Elle redimensionne les images en se basant sur la `width` et la `height`,
- Elle génère aussi le reponsive des images grâce à l'attribut `sizes` qui se base sur les tailles d'écran founies dans `nuxt.config.ts` grâce à `screens`,
- Elle permet aussi le chargement différé qui est une technique qui permet de charger les ressources d'un site web en différant le chargement des ressources qu'on considère non essentielles au début du temps de chargement.
- `<nuxt-picture>` vient remplacer la balise native `<picture>`. L'usage de cette balise est censément la même chose mais elle permet d'utiliser des formats comme webp quand cela est possible.
Les fonctionnalités intéréssantes de *NuxtImage*:
- Dans notre `nuxt.config.ts`, on peut venir paramétrer des tailles d'images en fonction de la catégorie grâce à la fonction `presets` et l'attribut `preset` comme ceci:
```typescript=
// à mettre dans nuxt.config.js
export default defineNuxtConfig({
image: {
presets: {
avatar: {
modifiers: {
format: 'jpg',
width: 50,
height: 50
}
}
}
}
})
```
```htmlmixed=
// la configuration dans le HTML
<template>
<nuxt-img preset="avatar" src="/nuxt-icon.png" />
</template>
```
- Par défaut, les images sont récupérées dans le dossier `public`, cependant, on peut toujours modifier la localisation des images. On peut par exemple définir que la source des images est le dossier assets. Comme ceci:
```typescript=
export default defineNuxtConfig({
image: {
dir: 'assets/images'
}
})
```
- *NuxtImage* propose des [providers](https://image.nuxtjs.org/providers/introduction) pour encore une fois, nous aider à améliorer nos images. Le provider par défaut est `ipx`.
:::info
**Remarques:**
On ne pourra pas agrandir une image au dessus de sa taille d'origine car le but de ce module est l'optimisation des images.
D'un point de vue de l'utilisation et de l'installation, ça a été plutot simple à mettre en place. Selon moi, la force de NuxtImage réside dans la possibilité de gérer la taille de nos images en fonction de la taille de l'écran un peu comme on pourrait voir avec bootstrap. De plus, la possibilité de mettre en place des tailles standards d'image en fonction du type d'image qu'on utilise, cela peut être pratique comme par exemple donner une taille standard à des avatars.
:::
<a id="unocss"></a>
- Mise en place de [UnoCSS](https://unocss.dev/):
Ce plugin nous permet de faire de **l'atomic CSS** qui est l'approche de l'architecture CSS qui privilégie les petites classes à usage unique avec des noms basés sur la fonction visuelle. Les classes utilisées par `UnoCSS` sont basé sur [**Tailwind CSS**](https://tailwindcss.com/) qui est un framework CSS open source. Contrairement à d'autres framework CSS, il ne procure pas une série de classes prédéfinies pour des éléments tels que des boutons ou des tables.
Cependant, c'est là que UnoCSS va nous être très utile. Ce plugin va nous permettre de mettre du CSS en place sans pour autant trop charger le projet et en n'ecrivant au final très peu voir presque pas du tout du "pure css".
D'un point de vue de l'installation c'est plutot simple: `npm install -D @unocss/nuxt` et il faut ensuite l'ajouter à notre fichier `nuxt.config.ts`:
```typescript=
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@unocss/nuxt',
],
})
```
On peut ensuite créer un fichier de configuration qui s'appelle `uno.config.ts`:
```typescript=
// uno.config.ts
import { defineConfig } from 'unocss'
export default defineConfig({
// ...UnoCSS options
})
```
Tout ce qui va être configuré dans le fichier ci-dessus sera injecté dans un fichier uno.css quelque part dans le projet. Le grand avantage est que seule les classes utilisées dans le projet seront injecté dans ce fichier, on ne stockera pas des classes qui ne sont pas utilisé.
Voici quelques fonctionnalités qui sont proposées par UnoCSS:
→ La première fonctionnalité intéréssante de *UnoCSS*, c'est la possibilité de créer des [règles](https://unocss.dev/config/rules) ou encore d'override des classes déjà existante chez *Tailwind*. Cela se présente comme ceci:
```typescript=
export default defineConfig({
rules: [
//Me permet de customiser ma width et les box-shadow
["custom-width", { width: "calc(100% - 40px)" }],
["custom-shadow", { "box-shadow": "0px 0px 40px 0px rgba(41, 127, 255, 0.70)" }],
],
})
```
→ La deuxième fonctionnalité est assez proche de ce qu'on peut faire avec *bootstrap*, nous pouvons spécifier des propriétés dans notre configuration grâce à la fonctionnalité de [thème](https://unocss.dev/config/theme) et ces propriétés seront fusionnés en profondeur avec le thème par défaut. Par conséquent, dans cette partie, nous allons pouvoir y définir, les couleurs de notre de projet ou encore les breakpoints que nous voulons utilisé pour générer notre responsive design. Cela ressembre à ceci:
```typescript=
export default defineConfig({
theme: {
colors: {
"dark-grey": "#2A2F34",
"light-grey": "#2C313C",
grey: "#636975",
"light-blue": "#4CBFE3",
blue: "#297FFF",
red: "#FC6076",
"light-black": "#242830",
},
},
})
```
→ La troisième fonctionnalité est celle, qui est selon moi, nous donne le plus de flexibilité. C'est la fonction pour créer des [shortcuts](https://unocss.dev/config/shortcuts) et qui va nous aider à créer nos propres classes et composant comme avec bootstrap qui seront personnaliser avec nos propres besoins. Cela ressemble à cela:
```typescript=
export default defineConfig({
shortcuts: {
"password-input": "bg-light-black border border-solid border-light-black",
btn: "px-4 py-2 border-none",
"gradiant-blue":
"bg-gradient-to-r from-light-blue to-blue text-white font-bold outline outline-1 outline-light-blue/30 outline-offset-4",
"form-input": "bg-transparent focus:bg-white border-solid border-light-grey border-1 font-bold",
card: "bg-dark-grey/25 border-light-grey border-1 border-solid max-w-[400px]",
"full-bg": "w-[100vw] h-[100vh] object-cover opacity-10 fixed",
"small-title": "text-grey text-sm mb-2 mt-0",
},
})
```
:::info
**Remarques:**
*Uno CSS* présente de grands avantages et permet de grandement d'améliorer l'utilisation de *Tailwind CSS*.
La possibilité de pouvoir créer ses propres composants à l'aide des classes de Tailwind peut paraitre parfois un peu long à mettre en place mais cependant, cela fait gagner beaucoup de temps par la suite quand on veut les réutiliser.
De plus, la possibilité de créer des classes personnalisées nous permet de ne plus écrire de *CSS* et d'aller un peu plus vite dans notre développement. Grâce à *Uno CSS*, seules les classes utilisées dans le projet sont importer dans le fichier `uno.css` et cela évite d'alourdir notre projet avec du code qui n'est pas utilisé.
Cependant, la mise en place de shortcuts peut très vite devenir illisible et cela est difficile à organiser ou à se retrouver.
Par ailleurs, ce plugin présente beaucoup de facilité d'installation et je pense que couplé avec *Headless UI* cela peut potentiellement être une bonne alternative à *Bootstrap* qui nous pose parfois problème quand il s'agit d'utiliser certains des composants prêt à l'emploi qu'ils proposent.
:::
<a id="s"></a>
- Mise en place de [**Storyblok**](https://www.storyblok.com/tc/nuxtjs?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-nuxt):
*Qu'est ce que Storyblok ?*
C'est un **Headless Content Management System** comme wordpress avec Elementor par exemple mais celui-ci peut s'adapter le framkework que l'on utilise, c'est à dire, Nuxt. Un CMS headless est un système qui sépare le contenu du code et qui, par défaut, ne comporte jamais de couche de présentation frontale.
Ce CMS va pouvoir nous permetttre de modifier une partie de nos contenus, sans avoir à entrer dans le code source du site. Par conséquent, nous pourrons faire en sorte que n'importe quel user (*par ex: les gens du pole marketing*) puisse modifier certaines parties de nos sites sans avoir à faire du code.
Par conséquent, Storyblok peut nous donner un bel éventail d'opportunités. Avant de parler d'installation, voyons ensemble *quel genre de contenu on peut gérer ?* :
- Du contenu textuel
- Les assets tel que les images
- On peut gerer le nombre de bouton et autre contenu dans les bloks
- On peut modifier les liens ou actions dans les boutons
Il y a quelques étapes à suivre afin de le mettre en place sur un projet:
- Faire un compte sur storyblok et créer un espace de travail afin de récupérer un token qui nous sera très utilse par la suite
- On viendra ensuite installer storyblok sur le projet en faisant `npm i @storyblok/nuxt`
- Maintenant installé, il faut le configurer dans `nuxt.config.ts` de cette manière:
```javascript=
modules: [
['@storyblok/nuxt',
{ accessToken: '<your-access-token-here>' }
]],
```
Comme vous pouvez le voir ci-dessus, le token qui est demandé est le même token qui vous avez récupéré lors de la création votre espace de travail.
- Une autre étape qui est assez importante afin que votre environnement fonctionne avec storyblok, on doit set up notre `localhost` en HTTPS, c'est plutôt simple et vous pouvez retrouver toutes les étapes sur ce lien **[ici](https://www.storyblok.com/faq/setting-up-https-on-localhost-in-nuxt-3)**
- Une fois toutes ces étapes faites, on peut enfin rentrer dans le vif du sujet pour créer nos composants storyblok. Il suffit de créer un dossier storyblok dans notre projet afin d'y stocker mes composants réutilisables. Voici un exemple de composant:
```javascript=
<template>
<div v-editable="blok" class="fixed top-0 z-10 w-full flex items-center justify-center gap-3 bg-[#FFF29C] py-3">
<div>{{ blok.title }}</div>
<a class="border rounded-md bg-white p-2 no-underline shadow-lg" :href="blok.url.url" target="_blank">{{
blok.redirection
}}</a>
</div>
</template>
<script setup>
defineProps({ blok: { type: Object } });
</script>
```
Afin de construire un composant storyblok et le lier à notre CMS, c'est plutot simple, il faut déjà appeler `v-editable="blok"` dans votre div ceci va déterminer et identifier que c'est bien un composant qu'on veut utiliser dans storyblok.
Ensuite, our gérer les champs modifiables, il suffit de définir en props `blok` qui a pour type d'être un objet. On peut ensuite y associer tous les champs que l'on souhaite comme par exemple: `blok.title`. Ces champs devront ensuite être paramétré dans l'interface de storyblok en associant les bons champs au bon composant. Pour récupérer les composants que vous avez créé, c'est comme pour les champs modifiables, il suffit juste d'ajouter un blok dans la partie `blok library` de votre espace de travail.
Afin d'activer storyblok sur une page, il faut au préable l'appeler dans la page en question, comme ceci:
```javascript=
<script setup>
const story = await useAsyncStoryblok('home', { version: 'draft' })
</script>
<template>
<StoryblokComponent v-if="story" :blok="story.content" />
</template>
```
A partir du moment, où vous avez activé storyblok vous pourrez ensuite ajouter les composants storyblok que vous souhaitez. Toutefois, l faut faire attention où vous insérez votre `StoryblokComponent` car cela déterminera quelle zone de la page est modifiable.
Pour finir, afin de modifier le style des composants dont on a besoin, Storyblok est compatible avec **Tailwind**. Par conséquent, cela peut très bien fonctionner avec **Unocss** et **Headless UI**.
:::info
**Remarques:**
Tout d'abord, de prime abord, storyblok semble peut être compliqué à installer et parametrer, mais une fois que cela est fait, c'est finalement plutôt simple d'utilisation et assez intuitif.
Storyblok est vraiment un outil très pratique afin d'implémenter des composants prêt à emploi et directement modifiable dans l'interface sans avoir à manipuler du code. Cela peut devenir un très bon atout afin de par exemple créer un composant bannière afin qu'on puisse ajouter une banière quand on a besoin de faire une annonce de promotion ou autre annonce importante.
Cependant, je pense qu'il est important de mettre en lumière qu'un bon parametrage est essentiel et nécéssaire pour un bon fonctionnement de ce CMS. Je pense qu'il faut tout d'abord faire attention au placement de nos balises `StoryblokComponent` dans nos pages parce je pense que si on ne paramètre par très bien tout, il est possible de "*casser nos pages*" et les designs.
**A noter aussi:** Storyblok est un module payant si on veut plus de fonctionnalités.
:::
<a id="bootstrap"></a>
## Bootstrap et CSS
- La fonction offcanvas est implémenter pour les petites résolutions d'écran ( ≤ 768px ). Quand on est sur une plus grande résolution ( > 768px ) tel que desktop, le menu est visible et en position fixed sur la gauche. Pour ne pas que le reste du contenu passe sous la navbar, j'ai decidé de mettre une marge sur la balise main de mon layout default et de le mettre en place de la manière la plus restrictive possible. Pour m'aider, j'ai utilisé ces deux exemples: [**exemple 1**](https://github.com/mladenplavsic/bootstrap-navbar-sidebar) et [**exemple 2**](https://www.w3schools.com/howto/howto_css_fixed_sidebar.asp). La marge ne sera présente que si on utilise le layout `defaut.vue`.