浏览代码

Merge branch 'V8-5732_finalize_contact_page' into feature/V8-5674-recette--refactoring-du-site-log

Olivier Massot 1 年之前
父节点
当前提交
45ce73c4c9

+ 92 - 0
components/Contact/AddressSection.vue

@@ -0,0 +1,92 @@
+<template>
+  <LayoutContainer>
+    <v-row>
+      <v-col cols="6" class="px-8">
+        <ContactMap class="contact-map" />
+      </v-col>
+      <v-col cols="6" class="address-col px-8">
+
+        <h4>
+          <v-icon icon="fas fa-envelope"/> Pour nous écrire
+        </h4>
+
+        <v-card>
+          <div>
+            2iOpenService
+          </div>
+          <div>
+            217, rue Raoul Follereau
+          </div>
+          <div>
+            74300 - Cluses
+          </div>
+        </v-card>
+
+        <div class="mb-6" />
+
+        <h4>
+          <v-icon icon="fas fa-phone"/>
+          Ou par téléphone
+        </h4>
+
+        <div>
+          <v-btn
+            v-if="!revealPhoneNumber"
+            @click="revealPhoneNumber = !revealPhoneNumber"
+          >
+            Cliquer pour afficher le numéro de téléphone
+          </v-btn>
+          <span v-else>
+            <v-card>
+              Contactez nous au
+              <a href="tel:+33972126017">
+                09 72 12 60 17
+              </a>
+            </v-card>
+          </span>
+        </div>
+      </v-col>
+    </v-row>
+  </LayoutContainer>
+</template>
+
+<script setup lang="ts">
+  const revealPhoneNumber = ref(false)
+</script>
+
+<style scoped lang="scss">
+  .container {
+    width: 70%;
+  }
+
+  h4 {
+    display: flex;
+    flex-direction: row;
+    color: var(--primary-color);
+    font-size: 20px;
+    font-weight: 500;
+    margin: 24px 0;
+  }
+
+  .v-icon {
+    margin-right: 16px;
+  }
+
+  .address-col > div, .address-col > .v-card > div {
+    padding: 8px 16px;
+  }
+
+  .v-card {
+    margin: 16px 64px;
+    padding: 12px;
+  }
+
+  .v-btn {
+    margin: 16px 64px;
+  }
+
+  a {
+    color: var(--on-neutral-color);
+    text-decoration: underline;
+  }
+</style>

+ 1 - 1
components/Contact/Banner.vue

@@ -2,7 +2,7 @@
   <LayoutContainer>
   <LayoutContainer>
     <LayoutUITitlePage>
     <LayoutUITitlePage>
       Besoin d'aide ?
       Besoin d'aide ?
-      <template #subtitle>Notre équipe est là pour vous. contactez-nous!. </template>
+      <template #subtitle>Notre équipe est là pour vous. Contactez-nous! </template>
     </LayoutUITitlePage>
     </LayoutUITitlePage>
 
 
     <v-row>
     <v-row>

+ 167 - 143
components/Contact/Form.vue

@@ -1,16 +1,19 @@
 <template>
 <template>
   <LayoutContainer>
   <LayoutContainer>
+    <div id="anchor" />
+
     <v-form
     <v-form
+      v-if="!contactRequestSent"
       ref="form"
       ref="form"
-      v-model="valid"
-      lazy-validation
+      validate-on="submit lazy"
+      @submit.prevent="submit"
     >
     >
       <v-container>
       <v-container>
         <h4>
         <h4>
-          Veuillez remplir le formulaire ci-dessous
+          <span class="line" /> Veuillez remplir le formulaire ci-dessous
         </h4>
         </h4>
 
 
-        <i>Les champs dont le nom est suivi d'une astérisque (*) sont obligatoires</i>
+        <i>Les champs dont le nom est suivi d'un astérisque (*) sont obligatoires</i>
 
 
         <h6>
         <h6>
           Vos coordonnées
           Vos coordonnées
@@ -20,7 +23,7 @@
         <v-row>
         <v-row>
           <v-col cols="12">
           <v-col cols="12">
             <v-radio-group
             <v-radio-group
-              v-model="gender"
+              v-model="contactRequest.gender"
               row
               row
               mandatory
               mandatory
               inline
               inline
@@ -35,7 +38,7 @@
         <v-row>
         <v-row>
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-text-field
             <v-text-field
-              v-model="name"
+              v-model="contactRequest.name"
               :rules="[validateName]"
               :rules="[validateName]"
               label="Nom*"
               label="Nom*"
               required
               required
@@ -44,7 +47,7 @@
 
 
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-text-field
             <v-text-field
-              v-model="surname"
+              v-model="contactRequest.surname"
               :rules="[validateSurname]"
               :rules="[validateSurname]"
               label="Prénom*"
               label="Prénom*"
               required
               required
@@ -56,7 +59,7 @@
         <v-row>
         <v-row>
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-text-field
             <v-text-field
-              v-model="postalCode"
+              v-model="contactRequest.postalCode"
               label="Code postal*"
               label="Code postal*"
               :rules="[validatePostalCode]"
               :rules="[validatePostalCode]"
             />
             />
@@ -64,7 +67,7 @@
 
 
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-text-field
             <v-text-field
-              v-model="city"
+              v-model="contactRequest.city"
               label="Ville"
               label="Ville"
             />
             />
           </v-col>
           </v-col>
@@ -74,7 +77,7 @@
         <v-row>
         <v-row>
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-text-field
             <v-text-field
-              v-model="email"
+              v-model="contactRequest.email"
               :rules="[validateEmail]"
               :rules="[validateEmail]"
               label="Email*"
               label="Email*"
               required
               required
@@ -83,9 +86,9 @@
           </v-col>
           </v-col>
 
 
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
-            <!-- TODO: remplacer par un input dédié aux nums de téléphone -->
             <v-text-field
             <v-text-field
-              v-model="phone"
+              v-model="contactRequest.phone"
+              :rules="[validatePhone]"
               label="Téléphone*"
               label="Téléphone*"
               type="tel"
               type="tel"
             />
             />
@@ -96,7 +99,7 @@
         <v-row>
         <v-row>
           <v-col cols="12">
           <v-col cols="12">
             <v-text-field
             <v-text-field
-              v-model="structureName"
+              v-model="contactRequest.structureName"
               :rules="[validateStructureName]"
               :rules="[validateStructureName]"
               label="Nom de la structure*"
               label="Nom de la structure*"
               required
               required
@@ -105,27 +108,25 @@
         </v-row>
         </v-row>
 
 
         <h6>
         <h6>
-          Votre demande concerne
+          Votre demande concerne *
         </h6>
         </h6>
 
 
         <!-- Request type and product concerned  -->
         <!-- Request type and product concerned  -->
         <v-row>
         <v-row>
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-select
             <v-select
-              v-model="requestType"
+              v-model="contactRequest.requestType"
               :items="requestTypes"
               :items="requestTypes"
-              label="Votre demande concerne*"
-              outlined
-              dense
+              item-value="id"
+              item-title="label"
+              variant="outlined"
             />
             />
           </v-col>
           </v-col>
 
 
           <v-col cols="12" md="6">
           <v-col cols="12" md="6">
             <v-text-field
             <v-text-field
-              v-model="concernedProduct"
-              label="Le produit concerné"
-              outlined
-              dense
+              v-model="contactRequest.concernedProduct"
+              label="Produit concerné (facultatif)"
             />
             />
           </v-col>
           </v-col>
         </v-row>
         </v-row>
@@ -135,110 +136,97 @@
         </h6>
         </h6>
 
 
         <!-- Message  -->
         <!-- Message  -->
-        <v-row>
+        <v-row class="mb-8">
           <v-col cols="12">
           <v-col cols="12">
             <v-textarea
             <v-textarea
-              v-model="message"
-              :rules="[validateMessageLength]"
+              v-model="contactRequest.message"
+              :rules="[validateNonEmptyMessage, validateMessageLength]"
               label="Votre message*"
               label="Votre message*"
               required
               required
-              outlined
-              dense
               maxlength="400"
               maxlength="400"
             />
             />
+            <span class="remaining-cars-notice">{{ leftCars }} caractères restants</span>
           </v-col>
           </v-col>
         </v-row>
         </v-row>
 
 
         <!-- Policy and  checkboxes -->
         <!-- Policy and  checkboxes -->
         <v-checkbox
         <v-checkbox
-          v-model="privacyPolicy"
-          :rules="[(v) => !!v || 'You must accept the privacy policy']"
+          v-model="contactRequest.privacyPolicyAccepted"
+          :rules="[(v) => !!v || 'Vous devez accepter la politique de confidentialité']"
           label="J'ai pris connaissance de la politique de confidentialité et j'accepte le traitement de mes données personnelles par Opentalent."
           label="J'ai pris connaissance de la politique de confidentialité et j'accepte le traitement de mes données personnelles par Opentalent."
         />
         />
 
 
         <v-checkbox
         <v-checkbox
-          v-model="newsletterSubscription"
+          v-model="contactRequest.newsletterSubscription"
           label="Je souhaite recevoir des communications d'Opentalent par email (promotions, informations logiciel…). Je pourrai me désinscrire à tout moment."
           label="Je souhaite recevoir des communications d'Opentalent par email (promotions, informations logiciel…). Je pourrai me désinscrire à tout moment."
         />
         />
 
 
-        <!-- TODO: Remplacer par un vrai captcha
-        voir:
-          - https://nuxt.com/modules/recaptcha
-          - https://nuxt.com/modules/turnstile
-        -->
-        <v-checkbox
-          v-model="captchaChecked"
-          :rules="[(v) => !!v || 'You must pass the captcha']"
-          label="Captcha"
-        />
+        <div class="d-flex flex-row justify-center">
+          <!-- @see https://github.com/hCaptcha/vue-hcaptcha -->
+          <LayoutCaptcha/>
+        </div>
 
 
         <!-- Submit Button -->
         <!-- Submit Button -->
-        <v-row>
-          <v-col cols="12">
-            <v-btn
-              :disabled="!valid"
-              @click="submitForm"
-            >
-              Envoyer
-            </v-btn>
-          </v-col>
-        </v-row>
+        <div class="d-flex flex-row justify-center my-10">
+          <v-btn
+            type="submit"
+            variant="outlined"
+            :height="54"
+            :width="180"
+            class="submit-btn"
+          >
+            Envoyer
+          </v-btn>
+        </div>
       </v-container>
       </v-container>
+
+      <div class="legal">
+        Les données recueillies par Opentalent sont utilisées pour le traitement de votre demande et pour vous informer sur nos offres.
+        Elles sont destinées aux services Opentalent et à ses sous-traitants pour l’exécution des contrats. Conformément à la loi
+        "Informatique et Libertés du 6 Janvier 1978", vous disposez d’un droit d’accès, de modifications, de rectification et de suppression
+        des données vous concernant. Pour toute demande, adressez-vous à : Opentalent, 217 rue Raoul Follereau, 74300 CLUSES,
+        opentalent.fr s’engage à la confidentialité et à la protection de vos données.
+      </div>
     </v-form>
     </v-form>
 
 
-    <div v-if="submissionStatus">
-      {{ submissionStatus }}
+    <div v-else class="confirmation-message d-flex flex-row justify-center">
+      <v-card>
+        <v-icon icon="fas fa-check mr-1"/>
+        Votre demande de contact a bien été enregistrée, nous reviendrons vers vous dès que possible.
+      </v-card>
     </div>
     </div>
-
   </LayoutContainer>
   </LayoutContainer>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ComputedRef } from "vue";
-import { ContactFormData } from "~/types/interface";
+import ContactRequest from "~/models/Maestro/ContactRequest";
+import { useEntityManager } from "~/composables/data/useEntityManager";
+import { useRouter } from "vue-router";
 
 
 const route = useRoute();
 const route = useRoute();
-const defaultRequestType = route.query.request;
-
-// --- Constants ---
-const requestTypes: Array<string> = [
-  "Demande d'information",
-  "Demande de devis",
-  "Demande de démonstration",
-  "Demande d'option supplémentaire",
-  "Autre",
-];
-
-
-// --- Refs ---
-const name: Ref<string | null> = ref(null);
-
-const surname: Ref<string | null> = ref(null);
-
-const email: Ref<string | null> = ref(null);
-
-const structureName: Ref<string | null> = ref(null);
-
-const message: Ref<string | null> = ref(null);
-
-const privacyPolicy: Ref<boolean> = ref(false);
-
-const gender: Ref<string | null> = ref(null);
-
-const postalCode: Ref<string | null> = ref(null);
-
-const city: Ref<string | null> = ref(null);
+const router = useRouter()
+const { em } = useEntityManager()
 
 
-const phone: Ref<string | null> = ref(null);
+const form: Ref<any | null> = ref(null)
 
 
-const concernedProduct: Ref<string | null> = ref(null);
+const requestTypes: Array<{id: string, label: string}> = [
+  { id: "CONTACT_REQUEST_INFORMATION", label: "Demande d'information"},
+  { id: "CONTACT_REQUEST_ESTIMATE", label: "Demande de devis"},
+  { id: "CONTACT_REQUEST_DEMO", label: "Demande de démonstration"},
+  { id: "CONTACT_REQUEST_OPTION", label: "Demande d'option supplémentaire"},
+  { id: "CONTACT_REQUEST_OTHER", label: "Autre"}
+]
 
 
-const newsletterSubscription: Ref<boolean> = ref(false);
+const defaultRequestType = route.query.request ?? 'CONTACT_REQUEST_INFORMATION'
 
 
-const captchaChecked: Ref<boolean> = ref(false);
+//@ts-ignore
+const contactRequest: ContactRequest = reactive(em.newInstance(ContactRequest, { requestType: defaultRequestType }))
 
 
-const submissionStatus: Ref<string | null> = ref(null);
+const maxMessageLength = 2000
 
 
+const leftCars: ComputedRef<number> = computed(() =>
+  maxMessageLength - (contactRequest.message ? contactRequest.message.length : 0)
+)
 
 
 // --- Validation ---
 // --- Validation ---
 const validateName = (name: string | null) => !!name || "Le nom est obligatoire";
 const validateName = (name: string | null) => !!name || "Le nom est obligatoire";
@@ -251,81 +239,117 @@ const validatePostalCode = (postalCode: string | null) =>
 const validateEmail = (email: string | null) =>
 const validateEmail = (email: string | null) =>
   (!!email && /.+@.+\..+/.test(email)) || "L'adresse e-mail doit être valide";
   (!!email && /.+@.+\..+/.test(email)) || "L'adresse e-mail doit être valide";
 
 
+const validatePhone = (email: string | null) =>
+  (!!email && /^((\+|00)33\s?|0)[1-7]([\s.]?\d{2}){4}$/.test(email)) || "Le numéro de téléphone doit être valide";
+
 const validateStructureName = (structureName: string | null) =>
 const validateStructureName = (structureName: string | null) =>
   !!structureName || "Le nom de la structure est requis";
   !!structureName || "Le nom de la structure est requis";
 
 
-const validateMessageLength = (message: string | null) =>
-  (!!message && message.length <= 400) ||
-  "Le message ne doit pas dépasser 400 caractères";
-
-// TODO: revoir la validation, ça devrait être géré directement dans le v-form
-const valid: ComputedRef<boolean> = computed(() => {
-  return (
-    validateName(name.value) === true &&
-    validateSurname(surname.value) === true &&
-    validatePostalCode(postalCode.value) === true &&
-    validateEmail(email.value) === true &&
-    validateStructureName(structureName.value) === true &&
-    validateMessageLength(message.value) === true &&
-    privacyPolicy.value === true &&
-    captchaChecked.value === true
-  );
-});
-
-// --- Form data (voir si utile) ---
-const formData: ContactFormData = reactive({
-  gender: null,
-  postalCode: null,
-  city: "",
-  phone: null,
-  requestType: null,
-  concernedProduct: "",
-  newsletterSubscription,
-});
-
-const formRefs = {
-  ...toRefs(formData),
-  name,
-  surname,
-  email,
-  structureName,
-  message,
-  privacyPolicy,
-  valid,
-};
+const validateNonEmptyMessage = (message: string | null) =>
+  (!!message && message.length > 0) ||
+  "Le message ne peut pas être vide";
 
 
-// --- Methods ---
-const submitForm = () => {
-  if (valid.value) {
-    // Logique d'envoi du formulaire
-    // TODO: implémenter
-    submissionStatus.value = "Mail envoyé à contact@opentalent.fr";
-  } else {
-    console.log("Validation failed!");
-    submissionStatus.value = "";
+const validateMessageLength = (message: string | null) =>
+  (!!message && message.length <= maxMessageLength) ||
+  "Le message ne doit pas dépasser " + maxMessageLength + " caractères";
+
+const contactRequestSent: Ref<boolean> = ref(false);
+
+/**
+ * Submits the contact form.
+ *
+ * This function validates the form and sets the value of a variable to indicate whether the form submission was successful.
+ *
+ * @function
+ *
+ * @returns {void}
+ */
+const submit = async (): Promise<void> => {
+  const { valid } = await form.value.validate()
+
+  if (!valid) {
+    contactRequestSent.value = false
+    return
   }
   }
-};
 
 
-const requestType: Ref<string | null> = ref(
-  defaultRequestType === "demo" ? "Demande de démonstration" : null
-);
+  await em.persist(ContactRequest, contactRequest)
+
+  contactRequestSent.value = true;
+
+  // Défile vers le début de page pour afficher le message de confirmation
+  setTimeout(
+    () => router.push({ path: '', hash: '#anchor' }),
+    30
+  )
+};
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
+
 .v-form {
 .v-form {
   max-width: 1400px;
   max-width: 1400px;
   margin: 0 auto;
   margin: 0 auto;
 
 
   h4 {
   h4 {
+    display: flex;
+    flex-direction: row;
     font-size: 40px;
     font-size: 40px;
     line-height: 95px;
     line-height: 95px;
     margin-bottom: 1rem;
     margin-bottom: 1rem;
+    align-items: center;
+    font-weight: 100;
+
+    .line {
+      display: block;
+      height: 1px;
+      width: 64px;
+      min-width: 64px;
+      border-top: solid 1px var(--on-neutral-color);
+      margin-right: 18px;
+    }
   }
   }
 
 
   h6 {
   h6 {
     margin-top: 32px;
     margin-top: 32px;
-    font-size: 20px;
+    font-size: 16px;
     margin-bottom: 1rem;
     margin-bottom: 1rem;
+    text-transform: uppercase;
+    font-weight: 600;
+    letter-spacing: 0.1em;
+  }
+
+  .v-select {
+    .v-field {
+      border-radius: 0;
+    }
+  }
+
+  .remaining-cars-notice {
+    font-size: 13px;
+    font-weight: 550;
+    opacity: 0.6;
+  }
+
+  .submit-btn {
+    border-radius: 0;
+    font-weight: 600;
+  }
+
+  .legal {
+    opacity: 0.7;
+  }
+}
+
+.confirmation-message {
+  .v-card {
+    .v-icon {
+      color: green;
+    }
+
+    max-width: 1200px;
+    padding: 24px;
+    margin: 128px 0;
+    font-weight: 500;
   }
   }
 }
 }
 </style>
 </style>

+ 124 - 14
components/Contact/Map.vue

@@ -1,28 +1,138 @@
 <template>
 <template>
-  <div id="map" style="height: 500px" />
+<LayoutContainer>
+    <h4>
+      <span class="line" /> Contactez-nous
+    </h4>
+
+    <v-row>
+      <v-col cols="6">
+        <div class="map-container">
+          <l-map
+            ref="map"
+            :zoom="15"
+            :center="location"
+            :options="{scrollWheelZoom: false}"
+          >
+            <l-tile-layer
+              url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
+              layer-type="base"
+              name="OpenStreetMap"
+              attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
+            />
+
+            <l-marker
+              :icon="icon"
+              :lat-lng="location"
+            />
+<!--              <l-popup>-->
+<!--                <v-row>-->
+<!--                  <v-col cols="4" class="d-flex align-center">-->
+<!--                    <v-img src="/images/Opentalent_Griffe.png"/>-->
+<!--                  </v-col>-->
+<!--                  <v-col cols="8">-->
+<!--                    <div>-->
+<!--                      OPENTALENT-->
+<!--                    </div>-->
+<!--                    <div>-->
+<!--                      217, rue Raoul Follereau-->
+<!--                    </div>-->
+<!--                    <div>-->
+<!--                      74300 - Cluses-->
+<!--                    </div>-->
+<!--                  </v-col>-->
+<!--                </v-row>-->
+<!--              </l-popup>-->
+          </l-map>
+        </div>
+      </v-col>
+
+      <v-col class="infos">
+        <div class="name mb-8">
+          Opentalent
+        </div>
+
+        <div class="d-flex flex-row align-center mb-8">
+          <v-icon icon="fa fa-map-marker-alt" />
+          <span>
+            217 rue Raoul Follereau<br/>
+            74300 CLUSES
+          </span>
+        </div>
+
+        <div class="d-flex flex-row mb-4">
+          <v-icon icon="fa fa-phone" />
+          09.72.12.60.17
+        </div>
+
+        <div class="d-flex flex-row mb-4">
+          <v-icon icon="far fa-envelope" />
+          contact@opentalent.fr
+        </div>
+      </v-col>
+    </v-row>
+  </LayoutContainer>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import L from "leaflet";
-import "leaflet/dist/leaflet.css";
+import 'leaflet/dist/leaflet.css'
+import { LMap, LTileLayer, LMarker, LIcon } from '@vue-leaflet/vue-leaflet'
+import L from 'leaflet';
 
 
-onMounted(() => {
-  const map = L.map("map").setView([46.075245, 6.570162], 16);
+const location = [46.075245, 6.570162]
 
 
-  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
-    maxZoom: 19,
-    attribution: "© OpenStreetMap contributors",
-  }).addTo(map);
+const icon = L.icon({
+  iconUrl: '/images/contact/Pointeur_Opentalent.png',
+  iconSize: [80, 80],
+  iconAnchor: [22, 94],
+  popupAnchor: [5, -100],
+})
 
 
-  const marker = L.marker([46.075245, 6.570162]).addTo(map);
-});
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
-#map {
-  height: 100%;
-  width: 70%;
+
+h4 {
+  display: flex;
+  flex-direction: row;
+  font-size: 40px;
+  line-height: 95px;
+  align-items: center;
+  font-weight: 100;
+  margin: 0 auto 1rem auto;
+  max-width: 1400px;
+
+  .line {
+    display: block;
+    height: 1px;
+    width: 64px;
+    min-width: 64px;
+    border-top: solid 1px var(--on-neutral-color);
+    margin-right: 18px;
+  }
+}
+
+
+.map-container {
+  height: 500px;
+  width: 100%;
   margin-left: auto;
   margin-left: auto;
   margin-right: auto;
   margin-right: auto;
 }
 }
+
+.infos {
+  padding: 48px 36px;
+  font-size: 21px;
+  font-weight: 300;
+
+  .name {
+    font-size: 36px;
+    text-transform: uppercase;
+    font-weight: 600;
+  }
+
+  .v-icon {
+    margin-right: 16px;
+    opacity: 0.6;
+  }
+}
 </style>
 </style>

+ 77 - 0
components/Layout/Captcha.vue

@@ -0,0 +1,77 @@
+<template>
+  <vue-hcaptcha
+    :sitekey="runtimeConfig.hCaptchaSiteKey"
+    @verify="onVerify"
+    @expired="onExpire"
+    @challenge-expired="onChallengeExpire"
+    @error="onError"
+  />
+  <v-checkbox
+    v-model="honeyPotChecked"
+    :rules="[validateCaptchaState]"
+    class="hidden-ctrl"
+  />
+</template>
+
+<script setup lang="ts">
+import VueHcaptcha from "@hcaptcha/vue3-hcaptcha";
+
+const runtimeConfig = useRuntimeConfig()
+
+const verified: Ref<boolean> = ref(false);
+const expired: Ref<boolean> = ref(false);
+const token: Ref<string> = ref("");
+const eKey: Ref<string> = ref("");
+const error: Ref<string> = ref("");
+
+// La case étant masquée, si elle est cochée, alors on peut en déduire
+// qu'on a affaire un robot (voir mécanisme du honey pot)
+const honeyPotChecked: Ref<boolean> = ref(false)
+
+const emit = defineEmits(['update'])
+
+const validateCaptchaState = () =>
+  verified.value && !honeyPotChecked.value ||
+  "Veuillez procédez à la vérification";
+
+function onVerify(tokenStr: string, ekey: string) {
+  verified.value = true;
+  token.value = tokenStr;
+  eKey.value = ekey;
+  emit('update', true)
+}
+
+function onExpire() {
+  verified.value = false;
+  token.value = '';
+  eKey.value = '';
+  expired.value = true;
+}
+
+function onChallengeExpire() {
+  verified.value = false;
+  token.value = '';
+  eKey.value = '';
+  expired.value = true;
+}
+
+function onError(err: string) {
+  token.value = '';
+  eKey.value = '';
+  error.value = err;
+  console.log(`Error: ${err}`);
+
+  if (process.dev) {
+    console.log('Dev mode: force captcha validation')
+    verified.value = true;
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.hidden-ctrl {
+  :deep(.v-input__control) {
+    display: none;
+  }
+}
+</style>

+ 53 - 0
models/Maestro/ContactRequest.ts

@@ -0,0 +1,53 @@
+import ApiModel from "~/models/ApiModel";
+import {Uid, Str, Bool, Attr} from "pinia-orm/dist/decorators";
+
+/**
+ * Maestro Model : JobPosting
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/maestro/-/blob/master/src/Entity/JobPosting/JobPosting.php?ref_type=heads
+ */
+export default class ContactRequest extends ApiModel {
+  static entity = 'contact-request'
+
+  @Uid()
+  declare id: number
+
+  @Str(null)
+  declare name: string | null
+
+  @Str(null)
+  declare surname: string | null
+
+  @Str(null)
+  declare email: string | null
+
+  @Str(null)
+  declare structureName: string | null
+
+  @Str(null)
+  declare message: string | null
+
+  @Bool(false)
+  declare privacyPolicyAccepted: boolean
+
+  @Str(null)
+  declare gender: string | null
+
+  @Str(null)
+  declare postalCode: string | null
+
+  @Str(null)
+  declare city: string | null
+
+  @Str(null)
+  declare phone: string | null
+
+  @Str(null)
+  declare requestType: string | null
+
+  @Str(null)
+  declare concernedProduct: string | null
+
+  @Bool(false)
+  declare newsletterSubscription: boolean
+}

+ 3 - 19
nuxt.config.ts

@@ -31,10 +31,12 @@ export default defineNuxtConfig({
     // Private config that is only available on the server
     // Private config that is only available on the server
     env: "",
     env: "",
     apiBaseUrl: "",
     apiBaseUrl: "",
+    hCaptchaSiteKey: "35360874-ebb1-4748-86e3-9b156d5bfc53",
     // Config within public will be also exposed to the client
     // Config within public will be also exposed to the client
     public: {
     public: {
       env: "",
       env: "",
       apiBaseUrl: "",
       apiBaseUrl: "",
+      hCaptchaSiteKey: "35360874-ebb1-4748-86e3-9b156d5bfc53",
     },
     },
   },
   },
   css: [
   css: [
@@ -94,6 +96,7 @@ export default defineNuxtConfig({
     "nuxt-lodash",
     "nuxt-lodash",
     "@nuxtjs/i18n",
     "@nuxtjs/i18n",
     "@nuxt/devtools",
     "@nuxt/devtools",
+    'nuxt3-leaflet'
   ],
   ],
   webfontloader: {
   webfontloader: {
     google: {
     google: {
@@ -137,25 +140,6 @@ export default defineNuxtConfig({
     ],
     ],
     defaultLocale: "fr",
     defaultLocale: "fr",
     detectBrowserLanguage: false,
     detectBrowserLanguage: false,
-    vueI18n: {
-      legacy: false,
-      datetimeFormats: {
-        "fr-FR": {
-          short: {
-            year: "numeric",
-            month: "numeric",
-            day: "numeric",
-          },
-          long: {
-            year: "numeric",
-            month: "numeric",
-            day: "numeric",
-            hour: "numeric",
-            minute: "numeric",
-          },
-        },
-      },
-    },
   } as NuxtI18nOptions,
   } as NuxtI18nOptions,
   build: {
   build: {
     transpile: transpile,
     transpile: transpile,

+ 3 - 2
package.json

@@ -21,6 +21,7 @@
     "@fortawesome/free-brands-svg-icons": "^6.4.0",
     "@fortawesome/free-brands-svg-icons": "^6.4.0",
     "@fortawesome/free-regular-svg-icons": "^6.4.0",
     "@fortawesome/free-regular-svg-icons": "^6.4.0",
     "@fortawesome/free-solid-svg-icons": "^6.4.0",
     "@fortawesome/free-solid-svg-icons": "^6.4.0",
+    "@hcaptcha/vue3-hcaptcha": "^1.3.0",
     "@mdi/font": "^7.2.96",
     "@mdi/font": "^7.2.96",
     "@nuxtjs/date-fns": "^1.5.0",
     "@nuxtjs/date-fns": "^1.5.0",
     "@nuxtjs/i18n": "^8.0.0-beta.9",
     "@nuxtjs/i18n": "^8.0.0-beta.9",
@@ -29,7 +30,6 @@
     "@splidejs/vue-splide": "^0.6.12",
     "@splidejs/vue-splide": "^0.6.12",
     "@syncfusion/ej2-vue-calendars": "^20.4.54",
     "@syncfusion/ej2-vue-calendars": "^20.4.54",
     "@turf/turf": "^6.5.0",
     "@turf/turf": "^6.5.0",
-    "@vue-leaflet/vue-leaflet": "^0.10.1",
     "@vuelidate/core": "^2.0.3",
     "@vuelidate/core": "^2.0.3",
     "@vuelidate/validators": "^2.0.4",
     "@vuelidate/validators": "^2.0.4",
     "@vuepic/vue-datepicker": "^4.2.2",
     "@vuepic/vue-datepicker": "^4.2.2",
@@ -40,8 +40,9 @@
     "leaflet": "^1.9.3",
     "leaflet": "^1.9.3",
     "leaflet.markercluster": "^1.5.3",
     "leaflet.markercluster": "^1.5.3",
     "libphonenumber-js": "^1.9.38",
     "libphonenumber-js": "^1.9.38",
-    "nuxt": "^3.2.0",
+    "nuxt": "3.2.0",
     "nuxt-lodash": "^2.4.1",
     "nuxt-lodash": "^2.4.1",
+    "nuxt3-leaflet": "^1.0.12",
     "ohmyfetch": "^0.4.21",
     "ohmyfetch": "^0.4.21",
     "pinia": "^2.0.33",
     "pinia": "^2.0.33",
     "pinia-orm": "^1.5.1",
     "pinia-orm": "^1.5.1",

+ 2 - 0
pages/nous-contacter.vue

@@ -17,3 +17,5 @@
   margin-top: 48px;
   margin-top: 48px;
 }
 }
 </style>
 </style>
+<script setup lang="ts">
+</script>

文件差异内容过多而无法显示
+ 606 - 657
yarn.lock


部分文件因为文件数量过多而无法显示