Jelajahi Sumber

Merge branch 'feature/cookies-vue3js' into release/1.0

Olivier Massot 1 tahun lalu
induk
melakukan
67953133ff

+ 2 - 0
.eslintrc.cjs

@@ -53,5 +53,7 @@ module.exports = {
     useHead: 'readonly',
     useError: 'readonly',
     Ref: 'readonly',
+    watch: 'readonly',
+    useGtag: 'readonly',
   },
 }

+ 2 - 0
app.vue

@@ -1,5 +1,7 @@
 <template>
   <Html :lang="i18nHead.htmlAttrs.lang" :dir="i18nHead.htmlAttrs.dir">
+    <CommonCookiesConsent />
+
     <div id="top" />
 
     <LayoutNavigation />

+ 2 - 0
assets/style/theme.scss

@@ -37,6 +37,8 @@ body {
   --artist-color-light: #fef3ce;
   --school-color-light: #a5d4e5;
   --manager-color-light: #f7cdce;
+
+  --warning-color: #D8050B;
 }
 
 body {

+ 598 - 0
components/Common/CookiesConsent.vue

@@ -0,0 +1,598 @@
+<template>
+  <div v-if="layoutStore.isCookieConsentDialogVisible">
+    <div v-if="!showCustomizationOptions" class="cookie-consent-banner">
+      <div class="continue-wrapper">
+        <a class="continue" href="#" @click.prevent="continueWithoutAccepting">
+          Continuer sans accepter
+        </a>
+        <v-icon
+          size="20"
+          class="fa-solid fa-arrow-right ml-6"
+          @click="closePopup()"
+        />
+      </div>
+      <v-row justify="center">
+        <v-col cols="12">
+          <img
+            src="/images/components/cookie-consent/A_cute_and_beautiful_illustration_of_a_cookie_list-removebg-preview.png"
+            alt="Cookie"
+            class="cookie-image"
+          />
+        </v-col>
+      </v-row>
+      <v-row no-gutters>
+        <v-col cols="12">
+          <div class="text">
+            <p>GESTION DES COOKIES</p>
+          </div>
+        </v-col>
+      </v-row>
+      <p class="details-cookies" style="padding-left: 20px">
+        Le site Opentalent.fr utilise des cookies fonctionnels nécessaires à la
+        navigation du site et d'autres technologies similaire pour plusieurs
+        objectifs : des cookies d'analyse de l'audience du site, des cookies de
+        personnalisation de contenu et des cookies publicitaires. Pour plus de
+        détail, veuillez consulter notre
+        <NuxtLink to="/politique-de-confidentialite#cookie-policy">
+          Politique de confidentialité</NuxtLink
+        >. Vous pouvez ajuster vos préférences en matière de cookies à tout
+        moment en cliquant sur le bouton "Gérer mes préférences"
+      </p>
+      <div class="horizontal-line"></div>
+      <div class="actions">
+        <button
+          class="customize-button"
+          @click="showCustomizationOptions = true"
+        >
+          Gérer mes préférences
+        </button>
+        <button class="accept-button" @click="acceptAllCookies">
+          Tout accepter
+        </button>
+        <!-- <button class="decline-button" @click="declineCookies">Refuser</button> -->
+      </div>
+    </div>
+
+    <v-dialog v-model="showCustomizationOptions" persistent max-width="600px">
+      <v-card>
+        <v-row class="headline">
+          <v-btn
+            class="close-dialog"
+            :icon="true"
+            @click="showCustomizationOptions = false"
+          >
+            <v-icon size="20" class="fas fa-times" />
+          </v-btn>
+
+          <v-card-title>Gérer mes préferences</v-card-title>
+        </v-row>
+
+        <p class="gestion-preferences">
+          Vous pouvez définir vos préférences sur la manière dont vous souhaitez
+          que vos données soient utilisées en fonction des finalités et des
+          entreprises tierces ci-dessous. Certains tiers peuvent traiter des
+          données sur la base d'un intérêt légitime et vous pouvez choisir de
+          vous désinscrire.
+        </p>
+
+        <v-container class="preferences-actions text-end">
+          <button class="decline-button" @click="declineCookies">
+            Tout refuser
+          </button>
+          <button class="accept-button" @click="acceptAllCookies">
+            Tout accepter
+          </button>
+        </v-container>
+
+        <v-card-text>
+          <h4>Des cookies tiers permettant de réaliser des statistiques</h4>
+
+          <v-row align="center">
+            <v-col cols="auto">
+              <v-switch
+                v-model="cookiesPreferences.analyticsConsent"
+                label="Mesure d'audience"
+                hide-details
+                color="green"
+                inset
+              />
+            </v-col>
+            <v-col>{{
+              cookiesPreferences.analyticsConsent ? 'Autorisé' : 'Non-autorisé'
+            }}</v-col>
+          </v-row>
+          <p>
+            Ces cookies nous permettent d'établir des statistiques, des volumes
+            de fréquentation et d'utilisation des divers éléments de notre site,
+            nous permettant d’optimiser son fonctionnement.
+          </p>
+
+          <h4 class="mt-6">Des cookies tiers à visée publicitaire</h4>
+
+          <v-row align="center">
+            <v-col cols="auto">
+              <v-switch
+                v-model="cookiesPreferences.advertisingConsent"
+                label="Suivi publicitaire"
+                hide-details
+                color="green"
+                inset
+              />
+            </v-col>
+            <v-col>{{
+              cookiesPreferences.advertisingConsent
+                ? 'Autorisé'
+                : 'Non-autorisé'
+            }}</v-col>
+          </v-row>
+          <p>
+            Autoriser le stockage et l'accès aux cookies pour diffuser des
+            annonces publicitaires personnalisées. Cela permet de vous montrer
+            des publicités plus pertinentes.
+          </p>
+
+          <v-row align="center">
+            <v-col cols="auto">
+              <v-switch
+                v-model="cookiesPreferences.adUserDataConsent"
+                label="Données Utilisateur pour la Publicité"
+                hide-details
+                color="green"
+                inset
+              />
+            </v-col>
+            <v-col>{{
+              cookiesPreferences.advertisingConsent
+                ? 'Autorisé'
+                : 'Non-autorisé'
+            }}</v-col>
+          </v-row>
+          <p>
+            Autoriser la collecte et l'utilisation de vos données personnelles
+            (comme votre adresse e-mail ou numéro de téléphone) pour créer des
+            profils de publicité personnalisés et améliorer le ciblage des
+            annonces.
+          </p>
+
+          <v-row align="center">
+            <v-col cols="auto">
+              <v-switch
+                v-model="cookiesPreferences.adPersonalizationConsent"
+                label="Personnalisation des annonces"
+                hide-details
+                color="green"
+                inset
+              />
+            </v-col>
+            <v-col>{{
+              cookiesPreferences.advertisingConsent
+                ? 'Autorisé'
+                : 'Non-autorisé'
+            }}</v-col>
+          </v-row>
+          <p>
+            Autoriser la personnalisation des annonces en fonction de vos
+            préférences et de votre comportement de navigation pour vous offrir
+            des publicités plus pertinentes et ciblées.
+          </p>
+        </v-card-text>
+
+        <v-card-actions>
+          <v-btn color="secondary" @click="showCustomizationOptions = false">
+            Fermer
+          </v-btn>
+
+          <v-spacer />
+
+          <button class="accept-button" @click="saveCookiesPreferences">
+            Sauvegarder
+          </button>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </div>
+  <v-alert
+    v-model="showNotification"
+    title="Confirmation"
+    type="warning"
+    width="400"
+    closable
+    transition="fade-transition"
+    density="compact"
+    class="alert"
+  >
+    Vous avez refusé nos cookies. Si vous le souhaitez, vous pouvez encore
+    modifier votre décision en
+    <a href="#" @click="showPopup()">cliquant ici</a>.
+  </v-alert>
+</template>
+
+<script setup lang="ts">
+import { onMounted, type Ref, ref } from 'vue'
+import { useCookies } from 'vue3-cookies'
+import { COOKIE_CONSENT_CHOICE } from '~/types/enum/enums'
+import type { CookiesPreferences } from '~/types/interface'
+
+const layoutStore = useLayoutStore()
+
+const { cookies } = useCookies()
+const showCustomizationOptions = ref(false)
+const showNotification = ref(false)
+
+const { gtag, initialize: initializeGTag } = useGtag()
+
+/**
+ * Cookies options
+ */
+const cookiesPreferences: Ref<CookiesPreferences> = ref({
+  consent: COOKIE_CONSENT_CHOICE.NONE,
+  analyticsConsent: true,
+  advertisingConsent: true,
+  adUserDataConsent: true,
+  adPersonalizationConsent: true,
+})
+
+/**
+ * Affiche la popup de choix de consentement aux cookies
+ */
+const showPopup = () => {
+  layoutStore.setIsCookieConsentDialogVisible(true)
+}
+
+/**
+ * Ferme la popup de choix de consentement aux cookies
+ */
+const closePopup = () => {
+  layoutStore.setIsCookieConsentDialogVisible(false)
+  showCustomizationOptions.value = false
+  showNotification.value = false
+}
+
+/**
+ * Affiche une notification d'une minute pour permettre à l'utilisateur qui
+ * a refusé les cookies de changer d'avis
+ */
+const notify = () => {
+  showNotification.value = true
+
+  // Hide the notification after 1 minute
+  setTimeout(() => {
+    showNotification.value = false
+  }, 60000)
+}
+
+/**
+ * Créé ou supprime les cookies selon les préférences en cours
+ *
+ * @param duration Durée de vie des cookies (en jours, défaut : 365)
+ */
+const setupCookies = (duration: number = 365) => {
+  // Enregistre les préférences actuelles dans 2 cookies
+  cookies.set(
+    'cookie_consent',
+    cookiesPreferences.value.consent,
+    duration + 'd'
+  )
+
+  cookies.set(
+    'cookie_preferences',
+    JSON.stringify({
+      analyticsConsent: cookiesPreferences.value.analyticsConsent,
+      advertisingConsent: cookiesPreferences.value.advertisingConsent,
+      adUserDataConsent: cookiesPreferences.value.adUserDataConsent,
+      adPersonalizationConsent:
+        cookiesPreferences.value.adPersonalizationConsent,
+    }),
+    duration + 'd'
+  )
+
+  // Initialise et paramètre google tag manager
+  initializeGTag()
+
+  gtag('consent', 'update', {
+    analytics_storage: cookiesPreferences.value.analyticsConsent
+      ? 'granted'
+      : 'denied',
+    ad_storage: cookiesPreferences.value.advertisingConsent
+      ? 'granted'
+      : 'denied',
+    ad_user_data: cookiesPreferences.value.adUserDataConsent
+      ? 'granted'
+      : 'denied',
+    ad_personalization: cookiesPreferences.value.adPersonalizationConsent
+      ? 'granted'
+      : 'denied',
+  })
+
+  // Nettoie les cookies si ceux ci ne sont plus les bienvenus
+  // TODO: voir si ce nettoyage manuel est nécessaire, ou si google tag manager ne peut pas s'en occuper
+  if (!cookiesPreferences.value.analyticsConsent) {
+    purgeAnalyticsCookies()
+  }
+  if (!cookiesPreferences.value.advertisingConsent) {
+    purgeAdvertisingCookies()
+  }
+
+  // Enregistre la date et le contenu de la dernière acceptation (comme trace)
+  localStorage.setItem(
+    'cookie_consent',
+    JSON.stringify({
+      date: new Date(),
+      consent: cookiesPreferences.value,
+    })
+  )
+}
+
+/**
+ * Nettoie les cookies google analytics
+ */
+const purgeAnalyticsCookies = () => {
+  cookies.remove('_ga')
+  cookies.remove('_gat')
+  cookies.remove('_gid')
+  cookies.keys().forEach((name) => {
+    if (/_ga_\w{10}/.test(name)) {
+      cookies.remove(name)
+    }
+  })
+}
+
+/**
+ * Nettoie les cookies meta pixel
+ */
+const purgeAdvertisingCookies = () => {
+  cookies.remove('_fbp')
+}
+
+/**
+ * Accept and setup all the cookies and close the popup
+ */
+const acceptAllCookies = () => {
+  cookiesPreferences.value = {
+    consent: COOKIE_CONSENT_CHOICE.ACCEPTED,
+    analyticsConsent: true,
+    advertisingConsent: true,
+    adUserDataConsent: true,
+    adPersonalizationConsent: true,
+  }
+
+  setupCookies()
+  closePopup()
+}
+
+/**
+ * Refuse all the cookies, set up the cookie_consent cookie and close the popup
+ */
+const declineCookies = () => {
+  cookiesPreferences.value = {
+    consent: COOKIE_CONSENT_CHOICE.DECLINED,
+    analyticsConsent: false,
+    advertisingConsent: false,
+    adUserDataConsent: false,
+    adPersonalizationConsent: false,
+  }
+
+  setupCookies(7)
+  notify()
+  closePopup()
+}
+
+/**
+ * Set up the cookies following user preferences and close the popup
+ */
+const saveCookiesPreferences = () => {
+  cookiesPreferences.value.consent = COOKIE_CONSENT_CHOICE.CUSTOMIZED
+  setupCookies()
+  closePopup()
+}
+
+/**
+ * Continue without accepting cookies
+ */
+const continueWithoutAccepting = () => {
+  closePopup()
+}
+
+/**
+ * Charge les préférences depuis les cookies
+ */
+const loadActivePreferences = () => {
+  const cookieConsentVal = cookies.get('cookie_consent')
+  const cookiePreferencesVal = cookies.get('cookie_preferences')
+
+  if (cookieConsentVal && cookiePreferencesVal) {
+    cookiesPreferences.value = {
+      consent: cookieConsentVal,
+      // @ts-ignore
+      ...cookiePreferencesVal,
+    }
+  }
+}
+
+/**
+ * Check if the user has already accepted the cookies when page is mounted
+ */
+onMounted(() => {
+  loadActivePreferences()
+  if (cookiesPreferences.value.consent === COOKIE_CONSENT_CHOICE.NONE) {
+    showPopup()
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.gestion-preferences {
+  font-size: 1.2rem;
+  font-weight: 500;
+  margin-bottom: 10px;
+  padding: 20px;
+  text-align: justify;
+}
+
+.preferences-actions {
+  overflow: visible;
+}
+
+.headline {
+  font-size: 1.5rem;
+  font-weight: 500;
+  margin: 0 0 10px 0;
+  background-color: var(--secondary-color);
+  color: var(--on-secondary-color);
+  text-transform: uppercase;
+  padding: 15px;
+}
+
+.cookie-consent-banner {
+  background: var(--neutral-color);
+  position: fixed;
+  bottom: 10px;
+  left: 15px;
+  max-width: 550px;
+  z-index: 1000 !important;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
+  border-radius: 15px;
+  padding: 20px;
+}
+
+.cookie-image {
+  width: 150px;
+  margin: 0;
+}
+
+.text {
+  padding-top: 10px;
+  padding-bottom: 0;
+  font-weight: 600;
+  font-size: 1.3rem;
+  text-align: center;
+}
+
+.details-cookies {
+  padding: 10px;
+  text-align: justify;
+}
+
+.horizontal-line {
+  width: 90%;
+  height: 1px;
+  background-color: var(--neutral-color-alt-strong);
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+.actions {
+  margin-top: 10px;
+  margin-bottom: 10px;
+  display: flex;
+  justify-content: center;
+}
+
+.accept-button,
+.customize-button {
+  background-color: var(--on-primary-color-alt);
+  border: none;
+  padding: 10px 20px;
+  margin: 5px;
+  cursor: pointer;
+}
+
+.decline-button {
+  border: 1px solid var(--on-neutral-color);
+  padding: 10px 20px;
+  margin: 5px;
+  cursor: pointer;
+}
+
+.accept-button:hover,
+.customize-button:hover {
+  background-color: var(--on-primary-color-alt);
+}
+
+.cookie-description {
+  margin: 0;
+  font-size: 0.875rem;
+  color: var(--on-neutral-color-light);
+}
+
+.custom-switch .v-input--selection-controls__ripple .v-ripple__container {
+  background-color: var(--v-primary-color);
+}
+
+.custom-switch,
+.v-input--selection-controls__ripple--active,
+.v-ripple__container {
+  background-color: var(--v-primary-darken4);
+}
+
+.custom-switch .v-input--selection-controls__input {
+  --v-theme-primary: var(--v-primary-base);
+  --v-theme-primary-lighten4: var(--v-primary-lighten4);
+  --v-theme-primary-darken4: var(--v-primary-darken4);
+}
+
+.custom-switch,
+.v-input--selection-controls__input,
+input:checked,
+.v-input--selection-controls__ripple,
+.v-ripple__container {
+  background-color: var(--v-primary-darken4);
+}
+
+.custom-switch,
+.v-input--selection-controls__input,
+input:checked,
+.v-input--selection-controls__ripple,
+.v-ripple__container,
+.v-ripple__animation {
+  background-color: var(--v-primary-darken4);
+}
+
+:deep(.v-switch__track) {
+  background-color: var(--warning-color);
+}
+
+.close-dialog {
+  background: none !important;
+  box-shadow: none !important;
+}
+
+.continue {
+  font-size: 0.9rem;
+  font-weight: 500;
+  cursor: pointer;
+  text-decoration: none !important;
+  color: var(--on-neutral-color);
+}
+
+.continue-wrapper {
+  display: flex;
+  justify-content: end;
+  align-items: center;
+  margin-left: auto;
+}
+
+:deep(.v-switch .v-label) {
+  opacity: 0.8;
+}
+
+.alert {
+  position: fixed;
+  bottom: 20px;
+  right: 20px;
+  z-index: 1000;
+
+  a {
+    color: var(--on-primary-color);
+    font-weight: 700;
+    text-decoration: none;
+  }
+
+  a:hover {
+    text-decoration: underline;
+  }
+}
+</style>

+ 5 - 33
nuxt.config.ts

@@ -68,39 +68,6 @@ export default defineNuxtConfig({
           href: 'https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;700&display=swap',
         },
       ],
-      script: [
-        // Google Analytics
-        {
-          src: 'https://www.googletagmanager.com/gtag/js?id=G-L8PZ9TEFNX',
-          async: true,
-        },
-        {
-          innerHTML: `
-            window.dataLayer = window.dataLayer || [];
-            function gtag(){dataLayer.push(arguments);}
-            gtag('js', new Date());
-            gtag('config', 'G-L8PZ9TEFNX');
-          `,
-          type: 'text/javascript',
-        },
-        // Meta Pixel
-        {
-          src: 'https://connect.facebook.net/en_US/fbevents.js',
-          async: true,
-        },
-        {
-          innerHTML: `
-            !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
-            n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
-            n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
-            t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
-            document,'script','https://connect.facebook.net/en_US/fbevents.js');
-            fbq('init', '1045498113172655');
-            fbq('track', 'PageView');
-          `,
-          type: 'text/javascript',
-        },
-      ],
     },
   },
   typescript: {
@@ -140,6 +107,7 @@ export default defineNuxtConfig({
     'nuxt3-leaflet',
     '@nuxtjs/google-fonts',
     '@nuxtjs/sitemap',
+    'nuxt-gtag',
   ],
   router: {
     options: {
@@ -203,4 +171,8 @@ export default defineNuxtConfig({
     },
     display: 'block',
   },
+  gtag: {
+    id: 'G-L8PZ9TEFNX',
+    enabled: false,
+  },
 })

+ 2 - 0
package.json

@@ -41,6 +41,7 @@
     "leaflet": "^1.9.3",
     "libphonenumber-js": "^1.10.55",
     "nuxt": "^3.11.2",
+    "nuxt-gtag": "^2.0.6",
     "nuxt-lodash": "^2.5.3",
     "nuxt3-leaflet": "^1.0.12",
     "ofetch": "^1.3.3",
@@ -51,6 +52,7 @@
     "uuid": "^9.0.1",
     "vite-plugin-vuetify": "^2.0.3",
     "vue3-carousel": "^0.3.1",
+    "vue3-cookies": "^1.0.6",
     "vuetify": "^3.6.7"
   },
   "devDependencies": {

File diff ditekan karena terlalu besar
+ 733 - 603
pages/politique-de-confidentialite-et-protection-des-donnees-personnelles.vue


+ 6 - 0
plugins/vue3-cookies.ts

@@ -0,0 +1,6 @@
+import { defineNuxtPlugin } from '#app'
+import VueCookies from 'vue3-cookies'
+
+export default defineNuxtPlugin((nuxtApp) => {
+  nuxtApp.vueApp.use(VueCookies)
+})

TEMPAT SAMPAH
public/images/components/cookie-consent/A_cute_and_beautiful_illustration_of_a_cookie_list-removebg-preview.png


+ 7 - 0
stores/layoutStore.ts

@@ -29,6 +29,11 @@ export const useLayoutStore = defineStore('layout', () => {
     isAnchoredSectionOnScreen.value[sectionId] = value
   }
 
+  const isCookieConsentDialogVisible: Ref<boolean> = ref(false)
+  const setIsCookieConsentDialogVisible = (value: boolean) => {
+    isCookieConsentDialogVisible.value = value
+  }
+
   return {
     isHeaderVisible,
     setIsHeaderVisible,
@@ -39,5 +44,7 @@ export const useLayoutStore = defineStore('layout', () => {
     isAnchoredSectionOnScreen,
     resetAnchoredSections,
     setIsAnchoredSectionOnScreen,
+    isCookieConsentDialogVisible,
+    setIsCookieConsentDialogVisible
   }
 })

+ 7 - 0
types/enum/enums.ts

@@ -28,3 +28,10 @@ export const enum QUERY_TYPE {
   IMAGE,
   FILE,
 }
+
+export const enum COOKIE_CONSENT_CHOICE {
+  ACCEPTED = 'accepted',
+  DECLINED = 'declined',
+  CUSTOMIZED = 'customized',
+  NONE = 'none',
+}

+ 9 - 0
types/interface.d.ts

@@ -1,4 +1,5 @@
 import { ActionMenuItemType } from "~/types/enum/layout";
+import { COOKIE_CONSENT_CHOICE } from '~/types/enum/enums'
 
 interface ActionMenuItem {
   type: ActionMenuItemType
@@ -164,3 +165,11 @@ interface ContactFormData {
   concernedProduct: string | null
   newsletterSubscription: boolean
 }
+
+interface CookiesPreferences {
+  consent: COOKIE_CONSENT_CHOICE,
+  analyticsConsent: boolean,
+  advertisingConsent: boolean,
+  adUserDataConsent: boolean,
+  adPersonalizationConsent: boolean,
+}

+ 140 - 0
yarn.lock

@@ -3702,6 +3702,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/compiler-core@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/compiler-core@npm:3.4.27"
+  dependencies:
+    "@babel/parser": "npm:^7.24.4"
+    "@vue/shared": "npm:3.4.27"
+    entities: "npm:^4.5.0"
+    estree-walker: "npm:^2.0.2"
+    source-map-js: "npm:^1.2.0"
+  checksum: 10c0/fbc9a4a6c467fa47609df3337c1b2012a55e3b07adbffc45a31435237ec1169d0a4ece22f3538607364427b779ce04154b86a0e8dd40d3bd4aa03358d4db136d
+  languageName: node
+  linkType: hard
+
 "@vue/compiler-dom@npm:3.4.21, @vue/compiler-dom@npm:^3.3.4":
   version: 3.4.21
   resolution: "@vue/compiler-dom@npm:3.4.21"
@@ -3712,6 +3725,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/compiler-dom@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/compiler-dom@npm:3.4.27"
+  dependencies:
+    "@vue/compiler-core": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+  checksum: 10c0/ceb8aef314b6b7df1ab6cd3c7c1290e5b60363a6092bbffc3ee6aca42f6f5247a070b0dcbe71530751e840d01beec00a6268e3663abcf4a6ac297a32bfb90e49
+  languageName: node
+  linkType: hard
+
 "@vue/compiler-sfc@npm:3.4.21, @vue/compiler-sfc@npm:^3.2.47, @vue/compiler-sfc@npm:^3.3.4, @vue/compiler-sfc@npm:^3.4.15, @vue/compiler-sfc@npm:^3.4.21":
   version: 3.4.21
   resolution: "@vue/compiler-sfc@npm:3.4.21"
@@ -3729,6 +3752,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/compiler-sfc@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/compiler-sfc@npm:3.4.27"
+  dependencies:
+    "@babel/parser": "npm:^7.24.4"
+    "@vue/compiler-core": "npm:3.4.27"
+    "@vue/compiler-dom": "npm:3.4.27"
+    "@vue/compiler-ssr": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+    estree-walker: "npm:^2.0.2"
+    magic-string: "npm:^0.30.10"
+    postcss: "npm:^8.4.38"
+    source-map-js: "npm:^1.2.0"
+  checksum: 10c0/2ccb852c521bf799cf2b118ee8d2aa0eeaaaab1a2e8d3a4a0bd9db5aaccb6224d6673c0c8e39ff8a04e3a99b21128bdaa6ee643e08562af36d75803801cfd641
+  languageName: node
+  linkType: hard
+
 "@vue/compiler-ssr@npm:3.4.21":
   version: 3.4.21
   resolution: "@vue/compiler-ssr@npm:3.4.21"
@@ -3739,6 +3779,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/compiler-ssr@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/compiler-ssr@npm:3.4.27"
+  dependencies:
+    "@vue/compiler-dom": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+  checksum: 10c0/5c51a43481e5faa3f4e66a01a19a5de8a0c25db5df25183d7f9227853740d8ea75c12b1b89f47198f840de852d2e4c258be114528c0c322aff50c5982a973e1f
+  languageName: node
+  linkType: hard
+
 "@vue/devtools-api@npm:^6.5.0, @vue/devtools-api@npm:^6.5.1":
   version: 6.6.1
   resolution: "@vue/devtools-api@npm:6.6.1"
@@ -3845,6 +3895,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/reactivity@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/reactivity@npm:3.4.27"
+  dependencies:
+    "@vue/shared": "npm:3.4.27"
+  checksum: 10c0/5a30fa92cb1b467f56c467d9851a9d594475c80952a600db444c38a8fe2dfc53e4aa09fed6b0e6074eca667c915c730d02b386be26d5f7a0565e70ae04fe92b7
+  languageName: node
+  linkType: hard
+
 "@vue/runtime-core@npm:3.4.21":
   version: 3.4.21
   resolution: "@vue/runtime-core@npm:3.4.21"
@@ -3855,6 +3914,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/runtime-core@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/runtime-core@npm:3.4.27"
+  dependencies:
+    "@vue/reactivity": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+  checksum: 10c0/dc02dfefebeec49c6b8aab9e133551b6cedef3c55e7441732a696aba66b865945549ba0f92a97a0f4ab080b828bca2cc2ce669ad7c6d2ee129d5050948f03817
+  languageName: node
+  linkType: hard
+
 "@vue/runtime-dom@npm:3.4.21":
   version: 3.4.21
   resolution: "@vue/runtime-dom@npm:3.4.21"
@@ -3866,6 +3935,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/runtime-dom@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/runtime-dom@npm:3.4.27"
+  dependencies:
+    "@vue/runtime-core": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+    csstype: "npm:^3.1.3"
+  checksum: 10c0/2ace60cab29400c4d466b6743552ae3af360f908d7716316c23a641bd5adce7aa05d2b4522ecf3b6b2f912bb525c8e055708db11791e50aea24ff6b2a71e0a8e
+  languageName: node
+  linkType: hard
+
 "@vue/server-renderer@npm:3.4.21":
   version: 3.4.21
   resolution: "@vue/server-renderer@npm:3.4.21"
@@ -3878,6 +3958,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/server-renderer@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/server-renderer@npm:3.4.27"
+  dependencies:
+    "@vue/compiler-ssr": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+  peerDependencies:
+    vue: 3.4.27
+  checksum: 10c0/5e6761ecd74c0a9ca9fd991f7a980140d2e09427712dbdc74b536bc5a9b97c06825ca4fa006b4a7cd6ba224fdb13c1c6a600e7d039d2a40f036b13ed611aa20f
+  languageName: node
+  linkType: hard
+
 "@vue/shared@npm:3.4.21, @vue/shared@npm:^3.4.21":
   version: 3.4.21
   resolution: "@vue/shared@npm:3.4.21"
@@ -3885,6 +3977,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vue/shared@npm:3.4.27":
+  version: 3.4.27
+  resolution: "@vue/shared@npm:3.4.27"
+  checksum: 10c0/4a21918858270bcc654bb94b3429d9acbe95af097ea3063e192b36bd502dc896ca47778fa74a863b01f677ec271b189eb90f8b372943c10e52725a6bdc7f6cd5
+  languageName: node
+  linkType: hard
+
 "@vuelidate/core@npm:^2.0.3":
   version: 2.0.3
   resolution: "@vuelidate/core@npm:2.0.3"
@@ -9344,6 +9443,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"nuxt-gtag@npm:^2.0.6":
+  version: 2.0.6
+  resolution: "nuxt-gtag@npm:2.0.6"
+  dependencies:
+    "@nuxt/kit": "npm:^3.11.2"
+    defu: "npm:^6.1.4"
+    pathe: "npm:^1.1.2"
+    ufo: "npm:^1.5.3"
+  checksum: 10c0/584aa2399b4be9f8ce95b8f456eef8d2ca405caf0e10abc53a29826e39371cdc2f6bff24534179df81a6f437babc4994c4abbc3ffbaa6412908f33fb52355701
+  languageName: node
+  linkType: hard
+
 "nuxt-lodash@npm:^2.5.3":
   version: 2.5.3
   resolution: "nuxt-lodash@npm:2.5.3"
@@ -11316,6 +11427,7 @@ __metadata:
     leaflet: "npm:^1.9.3"
     libphonenumber-js: "npm:^1.10.55"
     nuxt: "npm:^3.11.2"
+    nuxt-gtag: "npm:^2.0.6"
     nuxt-lodash: "npm:^2.5.3"
     nuxt3-leaflet: "npm:^1.0.12"
     ofetch: "npm:^1.3.3"
@@ -11329,6 +11441,7 @@ __metadata:
     uuid: "npm:^9.0.1"
     vite-plugin-vuetify: "npm:^2.0.3"
     vue3-carousel: "npm:^0.3.1"
+    vue3-cookies: "npm:^1.0.6"
     vuetify: "npm:^3.6.7"
   languageName: unknown
   linkType: soft
@@ -12950,6 +13063,33 @@ __metadata:
   languageName: node
   linkType: hard
 
+"vue3-cookies@npm:^1.0.6":
+  version: 1.0.6
+  resolution: "vue3-cookies@npm:1.0.6"
+  dependencies:
+    vue: "npm:^3.0.0"
+  checksum: 10c0/f304253553944d0cab94bbcadb27da0a4212e19d5392ab1df8fb531f77bd8ced0cb46aacbc8d019694677b7c368b51f2e9bec009b3a8f4325d7383e7574394dc
+  languageName: node
+  linkType: hard
+
+"vue@npm:^3.0.0":
+  version: 3.4.27
+  resolution: "vue@npm:3.4.27"
+  dependencies:
+    "@vue/compiler-dom": "npm:3.4.27"
+    "@vue/compiler-sfc": "npm:3.4.27"
+    "@vue/runtime-dom": "npm:3.4.27"
+    "@vue/server-renderer": "npm:3.4.27"
+    "@vue/shared": "npm:3.4.27"
+  peerDependencies:
+    typescript: "*"
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 10c0/73349e05cf554891d5e0076be10083150c92831c1cffdeee1e25c2222a8a4d8291630825a897049add753c4925e1c916c3614fe8d9c0392d9ff0186e553fe24b
+  languageName: node
+  linkType: hard
+
 "vue@npm:^3.2.19, vue@npm:^3.2.25, vue@npm:^3.4.21":
   version: 3.4.21
   resolution: "vue@npm:3.4.21"

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini