Procházet zdrojové kódy

Adds phone number input component and shop validation page

Introduces a reusable phone input component with validation using libphonenumber-js.

Implements form validation and submission logic for the trial request form.
The phone number is automatically converted to international format before submission.

Adds new pages for trial request validation and confirmation.
Updates the confirmation message to reflect a validation step is required.
Olivier Massot před 5 měsíci
rodič
revize
373b3a6191

+ 56 - 0
components/Common/PhoneInput.vue

@@ -0,0 +1,56 @@
+<template>
+  <v-text-field
+    :model-value="modelValue"
+    :rules="[validateRequired, validatePhone]"
+    :label="label"
+    :required="required"
+    type="tel"
+    @update:model-value="updateValue"
+  />
+</template>
+
+<script setup lang="ts">
+import { isValidPhoneNumber } from 'libphonenumber-js'
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: '',
+  },
+  label: {
+    type: String,
+    default: 'Téléphone',
+  },
+  required: {
+    type: Boolean,
+    default: false,
+  },
+  countryCode: {
+    type: String,
+    default: 'FR',
+  },
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const updateValue = (value: string) => {
+  emit('update:modelValue', value)
+}
+
+// Validation rules
+const validateRequired = (value: string) =>
+  !props.required || !!value || 'Ce champ est obligatoire'
+
+const validatePhone = (phone: string) => {
+  if (!phone) return true
+  try {
+    // Use the provided country code or default to FR
+    return (
+      isValidPhoneNumber(phone, props.countryCode) ||
+      'Numéro de téléphone invalide'
+    )
+  } catch (error) {
+    return 'Numéro de téléphone invalide'
+  }
+}
+</script>

+ 23 - 40
pages/shop/try/artist-premium.vue

@@ -271,12 +271,11 @@
                   </v-col>
 
                   <v-col cols="12" md="6">
-                    <v-text-field
+                    <CommonPhoneInput
+                      ref="phoneInput"
                       v-model="trialRequest.representativePhone"
-                      :rules="[validateRequired, validatePhone]"
                       label="Téléphone*"
                       required
-                      type="tel"
                     />
                   </v-col>
                 </v-row>
@@ -368,13 +367,13 @@
                 </v-card-title>
                 <v-card-text class="text-center">
                   <p>
-                    Votre essai gratuit de 30 jours d'Opentalent Artist Premium
-                    a bien été activé.
+                    Votre demande d'essai gratuit de 30 jours d'Opentalent
+                    Artist Premium a bien été enregistrée, mais nécessite une
+                    validation de votre part.
                   </p>
                   <p>
-                    Vous allez recevoir un email avec vos identifiants de
-                    connexion et toutes les informations nécessaires pour
-                    commencer à utiliser la plateforme.
+                    Vous allez recevoir un email permettant de valider cette
+                    demande, à la suite de quoi votre compte sera créé.
                   </p>
                   <p>
                     Notre équipe reste à votre disposition pour vous accompagner
@@ -400,12 +399,14 @@ import { useRouter } from 'vue-router'
 import type { Ref } from 'vue'
 import { reactive } from 'vue'
 import _ from 'lodash'
-import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js'
 import { useRuntimeConfig, useAsyncData, useNuxtApp } from '#app'
 import type { TrialRequest } from '~/types/interface'
 import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
-import { slugify } from '~/services/utils/stringUtils'
+import {
+  convertPhoneNumberToInternationalFormat,
+  slugify,
+} from '~/services/utils/stringUtils'
 
 const router = useRouter()
 
@@ -457,7 +458,10 @@ const trialRequest = reactive<TrialRequest>({
 
 // Function to fill the form with dummy data
 const fillWithDummyData = () => {
-  trialRequest.structureName = 'Compagnie Artistique Test'
+  // Generate a short timestamp (unix timestamp in seconds)
+  const shortTimestamp = Math.floor(Date.now() / 1000).toString()
+
+  trialRequest.structureName = `Compagnie Artistique Test ${shortTimestamp}`
   trialRequest.address = '123 Rue des Arts'
   trialRequest.addressComplement = 'Bâtiment B'
   trialRequest.postalCode = '75001'
@@ -465,8 +469,7 @@ const fillWithDummyData = () => {
   trialRequest.structureEmail = 'contact@compagnie-test.fr'
   trialRequest.structureType = 'ARTISTIC_PRACTICE_ONLY'
   trialRequest.legalStatus = 'ASSOCIATION_LAW_1901'
-  trialRequest.structureIdentifier =
-    'compagnie-test-' + Math.floor(Math.random() * 1000)
+  trialRequest.structureIdentifier = `compagnie-test-${shortTimestamp}`
   trialRequest.siren = '123456789'
   trialRequest.representativeFirstName = 'Jean'
   trialRequest.representativeLastName = 'Dupont'
@@ -534,15 +537,6 @@ const validateEmail = (email: string) =>
 const validatePostalCode = (postalCode: string) =>
   /^\d{5}$/.test(postalCode) || 'Code postal invalide (5 chiffres)'
 
-const validatePhone = (phone: string) => {
-  try {
-    // Assume French phone number if no country code is provided
-    return isValidPhoneNumber(phone, 'FR') || 'Numéro de téléphone invalide'
-  } catch (error) {
-    return 'Numéro de téléphone invalide'
-  }
-}
-
 const validateSiren = (siren: string) =>
   !siren || /^\d{9}$/.test(siren) || 'SIREN invalide (9 chiffres)'
 
@@ -571,21 +565,8 @@ const validateSubdomainAvailability = (value: string) => {
 // Form state
 const trialRequestSent: Ref<boolean> = ref(false)
 const errorMsg: Ref<string | null> = ref(null)
-
-// Function to convert phone number to international format
-const convertToInternationalFormat = (phone: string): string => {
-  try {
-    // Assume French phone number if no country code is provided
-    const phoneNumber = parsePhoneNumber(phone, 'FR')
-    if (phoneNumber && phoneNumber.isValid()) {
-      return phoneNumber.format('E.164') // E.164 format: +33123456789
-    }
-    return phone
-  } catch (error) {
-    console.error('Error converting phone number:', error)
-    return phone
-  }
-}
+// Reference to the phone input component
+const phoneInput = ref(null)
 
 // Submit function
 const submit = async (): Promise<void> => {
@@ -596,9 +577,11 @@ const submit = async (): Promise<void> => {
   }
 
   // Convert phone number to international format before submission
-  trialRequest.representativePhone = convertToInternationalFormat(
-    trialRequest.representativePhone
-  )
+  if (phoneInput.value) {
+    trialRequest.representativePhone = convertPhoneNumberToInternationalFormat(
+      trialRequest.representativePhone
+    )
+  }
 
   try {
     const { data, error } = await useAsyncData('submit-trial-request', () =>

+ 146 - 0
pages/shop/try/validation.vue

@@ -0,0 +1,146 @@
+<template>
+  <v-card>
+    <div v-if="loading" class="text-center pa-8">
+      <v-progress-circular indeterminate color="primary" />
+      <p class="mt-4">Validation en cours...</p>
+    </div>
+
+    <div v-else-if="validationSuccess">
+      <v-card-title class="text-center">
+        <v-icon icon="fas fa-check mr-2" color="success" max-height="48" />
+        Félicitations !
+      </v-card-title>
+      <v-card-text class="text-center">
+        <p>Merci d'avoir validé votre demande.</p>
+        <p>Votre compte est en cours de création.</p>
+        <p>
+          Vous recevrez un email dès que votre compte sera prêt à être utilisé.
+        </p>
+      </v-card-text>
+      <v-card-actions class="justify-center">
+        <v-btn color="primary" to="/opentalent-artist">
+          Retour à la page Opentalent Artist
+        </v-btn>
+      </v-card-actions>
+    </div>
+
+    <div v-else class="text-center pa-8">
+      <v-card-title class="text-center">
+        <v-icon
+          icon="fas fa-exclamation-triangle mr-2"
+          color="warning"
+          max-height="48"
+        />
+        Erreur de validation
+      </v-card-title>
+      <v-card-text class="text-center">
+        <p class="error-message">
+          {{ errorMsg }}
+        </p>
+        <p class="mt-4">
+          Si le problème persiste, n'hésitez pas à nous contacter.
+        </p>
+      </v-card-text>
+      <v-card-actions class="justify-center">
+        <v-btn color="primary" to="/nous-contacter">Nous contacter</v-btn>
+      </v-card-actions>
+    </div>
+  </v-card>
+</template>
+
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+import { useNuxtApp } from '#app'
+import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+
+const route = useRoute()
+const { ap2iRequestService } = useAp2iRequestService()
+
+const loading = ref(true)
+const validationSuccess = ref(false)
+const errorMsg = ref(
+  "Une erreur s'est produite lors de la validation de votre demande."
+)
+
+// UUID validation regex
+const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
+
+// Get token from query parameters
+const token = computed(() => {
+  const queryToken = route.query.token
+  return typeof queryToken === 'string' ? queryToken : ''
+})
+
+// Validate token and make API request
+onMounted(async () => {
+  try {
+    // Check if token is present
+    if (!token.value) {
+      errorMsg.value = "Aucun jeton de validation n'a été fourni."
+      loading.value = false
+      return
+    }
+
+    // Validate token format (UUID)
+    if (!uuidRegex.test(token.value)) {
+      errorMsg.value = 'Le format du jeton de validation est invalide.'
+      loading.value = false
+      return
+    }
+
+    // Make API request to validate token
+    await ap2iRequestService.get('/api/public/shop/validate/' + token.value)
+
+    // If we get here, the validation was successful
+    validationSuccess.value = true
+    loading.value = false
+  } catch (error) {
+    console.error('Error validating token:', error)
+
+    // Try to extract the specific error message from the API response
+    if (
+      error &&
+      typeof error === 'object' &&
+      'data' in error &&
+      error.data &&
+      typeof error.data === 'object'
+    ) {
+      const errorData = error.data as { detail?: string }
+      if (errorData.detail) {
+        const { $i18n } = useNuxtApp()
+        // Use translation if available, otherwise use the original message
+        errorMsg.value = $i18n.t(errorData.detail) || errorData.detail
+      }
+    }
+
+    loading.value = false
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.v-card {
+  margin: 4rem 20%;
+  padding: 2rem;
+}
+
+.v-card-title {
+  font-size: 1.8rem;
+  font-weight: 700;
+  color: var(--primary-color);
+}
+
+.v-card-text {
+  font-size: 1.1rem;
+  line-height: 1.6;
+
+  p {
+    margin-bottom: 1rem;
+  }
+}
+
+.error-message {
+  color: var(--warning-color);
+  font-weight: 500;
+}
+</style>

+ 19 - 0
services/utils/stringUtils.ts

@@ -1,6 +1,7 @@
 /**
  * String utility functions
  */
+import { type CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js'
 
 /**
  * Convert a string to a slug format
@@ -23,3 +24,21 @@ export function slugify(text: string): string {
     .replace(/-+$/, '') // Trim - from end of text
     .substring(0, 30) // Limit to 30 characters
 }
+
+// Function to convert phone number to international format
+export function convertPhoneNumberToInternationalFormat(
+  phone: string,
+  countryCode: CountryCode = 'FR'
+): string {
+  try {
+    // Use the provided country code or default to FR
+    const phoneNumber = parsePhoneNumberWithError(phone, countryCode)
+    if (phoneNumber && phoneNumber.isValid()) {
+      return phoneNumber.format('E.164') // format: +33123456789
+    }
+    return phone
+  } catch (error) {
+    console.error('Error converting phone number:', error)
+    return phone
+  }
+}