Add shared form fields for contact details

This commit is contained in:
MatthieuTD
2025-09-25 14:44:42 +02:00
parent a4840c454f
commit 041478e9d4
11 changed files with 523 additions and 96 deletions

View File

@@ -63,14 +63,19 @@
<label class="label"><span class="label-text">Nom</span></label>
<input v-model="createForm.name" type="text" class="input input-bordered" required />
</div>
<div class="form-control mb-3">
<label class="label"><span class="label-text">Email</span></label>
<input v-model="createForm.email" type="email" class="input input-bordered" placeholder="ex: contact@constructeur.com" />
</div>
<div class="form-control mb-3">
<label class="label"><span class="label-text">Téléphone</span></label>
<input v-model="createForm.phone" type="text" class="input input-bordered" placeholder="ex: 01 23 45 67 89" />
</div>
<FieldEmail
v-model="createForm.email"
class="mb-3"
label="Email"
placeholder="ex: contact@constructeur.com"
autocomplete="email"
/>
<FieldPhone
v-model="createForm.phone"
class="mb-3"
label="Téléphone"
placeholder="ex: 01 23 45 67 89"
/>
<div class="modal-action">
<button type="button" class="btn" @click="closeCreateModal">Annuler</button>
@@ -87,6 +92,8 @@
<script setup>
import { ref, watch, computed, onMounted, onBeforeUnmount } from 'vue'
import FieldEmail from '~/components/form/FieldEmail.vue'
import FieldPhone from '~/components/form/FieldPhone.vue'
import { useConstructeurs } from '~/composables/useConstructeurs'
import IconLucideChevronsUpDown from '~icons/lucide/chevrons-up-down'

View File

@@ -0,0 +1,112 @@
<template>
<div class="form-control">
<label v-if="label" class="label" :for="inputId">
<span class="label-text">{{ label }}</span>
<span v-if="required" class="label-text-alt text-error">*</span>
</label>
<input
:id="inputId"
:value="modelValue"
type="email"
class="input input-bordered"
:class="{ 'input-error': Boolean(errorMessage) }"
:placeholder="placeholder"
:required="required"
:disabled="disabled"
:name="name"
:autocomplete="autocomplete"
:pattern="EMAIL_INPUT_PATTERN"
:aria-invalid="Boolean(errorMessage)"
:aria-describedby="describedBy"
@input="onInput"
@blur="onBlur"
@focus="(event) => emit('focus', event)"
/>
<p v-if="help" :id="helpId" class="mt-2 text-xs text-gray-500">
{{ help }}
</p>
<p v-if="errorMessage" :id="errorId" class="mt-2 text-xs text-error">
{{ errorMessage }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, useId } from 'vue'
import { normalizeEmail } from '~/utils/formatters/email'
import { EMAIL_INPUT_PATTERN, EMAIL_VALIDATION_ERROR, emailSchema } from '~/shared/validation/email'
type Emits = {
(event: 'update:modelValue', value: string): void
(event: 'blur', value: FocusEvent): void
(event: 'focus', value: FocusEvent): void
}
const emit = defineEmits<Emits>()
const props = withDefaults(
defineProps<{
modelValue: string
label?: string
required?: boolean
error?: string | null
help?: string | null
placeholder?: string
disabled?: boolean
name?: string
id?: string
autocomplete?: string
normalizeOnBlur?: boolean
validateOnBlur?: boolean
}>(),
{
modelValue: '',
label: 'Email',
required: false,
error: null,
help: null,
placeholder: 'ex: contact@example.com',
disabled: false,
name: undefined,
id: undefined,
autocomplete: 'email',
normalizeOnBlur: false,
validateOnBlur: false,
}
)
const fallbackId = useId()
const inputId = computed(() => props.id || fallbackId)
const helpId = computed(() => (props.help ? `${inputId.value}-help` : undefined))
const errorMessage = computed(() => props.error || null)
const errorId = computed(() => (errorMessage.value ? `${inputId.value}-error` : undefined))
const describedBy = computed(() => [helpId.value, errorId.value].filter(Boolean).join(' ') || undefined)
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const onBlur = (event: FocusEvent) => {
const target = event.target as HTMLInputElement
if (props.normalizeOnBlur) {
const normalized = normalizeEmail(target.value)
if (normalized !== target.value) {
target.value = normalized
emit('update:modelValue', normalized)
}
}
if (props.validateOnBlur && !errorMessage.value) {
const validation = emailSchema.validate(target.value)
if (!validation.valid) {
target.setCustomValidity(EMAIL_VALIDATION_ERROR)
} else {
target.setCustomValidity('')
}
}
emit('blur', event)
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<div class="form-control">
<label v-if="label" class="label" :for="inputId">
<span class="label-text">{{ label }}</span>
<span v-if="required" class="label-text-alt text-error">*</span>
</label>
<input
:id="inputId"
:value="modelValue"
type="tel"
class="input input-bordered"
:class="{ 'input-error': Boolean(errorMessage) }"
:placeholder="placeholder"
:required="required"
:disabled="disabled"
:name="name"
:autocomplete="autocomplete"
:pattern="PHONE_INPUT_PATTERN"
inputmode="tel"
:aria-invalid="Boolean(errorMessage)"
:aria-describedby="describedBy"
@input="onInput"
@blur="onBlur"
@focus="(event) => emit('focus', event)"
/>
<p v-if="help" :id="helpId" class="mt-2 text-xs text-gray-500">
{{ help }}
</p>
<p v-if="errorMessage" :id="errorId" class="mt-2 text-xs text-error">
{{ errorMessage }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, useId } from 'vue'
import { formatPhone } from '~/utils/formatters/phone'
import { PHONE_INPUT_PATTERN, PHONE_VALIDATION_ERROR, phoneSchema } from '~/shared/validation/phone'
type Emits = {
(event: 'update:modelValue', value: string): void
(event: 'blur', value: FocusEvent): void
(event: 'focus', value: FocusEvent): void
}
const emit = defineEmits<Emits>()
const props = withDefaults(
defineProps<{
modelValue: string
label?: string
required?: boolean
error?: string | null
help?: string | null
placeholder?: string
disabled?: boolean
name?: string
id?: string
autocomplete?: string
normalizeOnBlur?: boolean
validateOnBlur?: boolean
}>(),
{
modelValue: '',
label: 'Téléphone',
required: false,
error: null,
help: null,
placeholder: 'Ex: 06 00 00 00 00',
disabled: false,
name: undefined,
id: undefined,
autocomplete: 'tel',
normalizeOnBlur: false,
validateOnBlur: false,
}
)
const fallbackId = useId()
const inputId = computed(() => props.id || fallbackId)
const helpId = computed(() => (props.help ? `${inputId.value}-help` : undefined))
const errorMessage = computed(() => props.error || null)
const errorId = computed(() => (errorMessage.value ? `${inputId.value}-error` : undefined))
const describedBy = computed(() => [helpId.value, errorId.value].filter(Boolean).join(' ') || undefined)
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const onBlur = (event: FocusEvent) => {
const target = event.target as HTMLInputElement
if (props.normalizeOnBlur) {
const formatted = formatPhone(target.value)
if (formatted !== target.value) {
target.value = formatted
emit('update:modelValue', formatted)
}
}
if (props.validateOnBlur && !errorMessage.value) {
const validation = phoneSchema.validate(target.value)
if (!validation.valid) {
target.setCustomValidity(PHONE_VALIDATION_ERROR)
} else {
target.setCustomValidity('')
}
}
emit('blur', event)
}
</script>

View File

@@ -13,18 +13,7 @@
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Téléphone</span>
</label>
<input
v-model="form.contactPhone"
type="tel"
placeholder="Ex: 06 00 00 00 00"
class="input input-bordered"
required
/>
</div>
<FieldPhone v-model="form.contactPhone" required />
<div class="form-control">
<label class="label">
@@ -73,6 +62,8 @@
import { toRefs } from 'vue'
import type { PropType } from 'vue'
import FieldPhone from '~/components/form/FieldPhone.vue'
type SiteForm = {
contactName: string
contactPhone: string