You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
8.8 KiB
309 lines
8.8 KiB
<script setup lang="ts">
|
|
/**
|
|
* AddressForm Component
|
|
*
|
|
* Reusable address form component with VeeValidate integration
|
|
* Can be used in checkout flow and profile settings
|
|
*
|
|
* Features:
|
|
* - Salutation dropdown (Herr, Frau, Keine Angabe)
|
|
* - Date of birth picker (German format display, YYYY-MM-DD storage)
|
|
* - Address fields (street, postCode, city, countryCode)
|
|
* - Field-level validation with error display
|
|
* - Mobile-first responsive design
|
|
*/
|
|
|
|
import { cn } from '~/lib/utils'
|
|
|
|
interface AddressData {
|
|
salutation: 'male' | 'female' | 'other'
|
|
firstName: string
|
|
lastName: string
|
|
dateOfBirth: string // YYYY-MM-DD format
|
|
street: string
|
|
postCode: string
|
|
city: string
|
|
countryCode: string
|
|
}
|
|
|
|
interface Props {
|
|
/**
|
|
* Address data (v-model)
|
|
*/
|
|
modelValue: Partial<AddressData>
|
|
/**
|
|
* Validation errors object { fieldName: errorMessage }
|
|
*/
|
|
errors?: Record<string, string>
|
|
/**
|
|
* Disabled state for all fields
|
|
*/
|
|
disabled?: boolean
|
|
/**
|
|
* Additional CSS classes
|
|
*/
|
|
class?: string
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: Partial<AddressData>]
|
|
}>()
|
|
|
|
// Local state for form fields
|
|
const localData = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value),
|
|
})
|
|
|
|
// Update individual field
|
|
const updateField = (field: keyof AddressData, value: string) => {
|
|
emit('update:modelValue', {
|
|
...props.modelValue,
|
|
[field]: value,
|
|
})
|
|
}
|
|
|
|
// Salutation options
|
|
const salutationOptions = [
|
|
{ value: 'male', label: 'Herr' },
|
|
{ value: 'female', label: 'Frau' },
|
|
{ value: 'other', label: 'Keine Angabe' },
|
|
]
|
|
|
|
// Country options (expandable in future)
|
|
const countryOptions = [
|
|
{ value: 'DE', label: 'Deutschland' },
|
|
{ value: 'AT', label: 'Österreich' },
|
|
{ value: 'CH', label: 'Schweiz' },
|
|
]
|
|
|
|
// Get error message for a field
|
|
const getError = (field: string) => props.errors?.[field]
|
|
|
|
// Format date for display (DD.MM.YYYY)
|
|
const formatDateForDisplay = (isoDate: string) => {
|
|
if (!isoDate) return ''
|
|
const [year, month, day] = isoDate.split('-')
|
|
return `${day}.${month}.${year}`
|
|
}
|
|
|
|
// Parse date from display format to ISO (YYYY-MM-DD)
|
|
const parseDateFromDisplay = (displayDate: string) => {
|
|
if (!displayDate) return ''
|
|
const parts = displayDate.split('.')
|
|
if (parts.length !== 3) return displayDate // Return as-is if not in expected format
|
|
const [day, month, year] = parts
|
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="cn('space-y-4', props.class)">
|
|
<!-- Salutation -->
|
|
<div class="space-y-2">
|
|
<Label for="salutation" class="text-white">
|
|
Anrede <span class="text-red">*</span>
|
|
</Label>
|
|
<Select
|
|
:model-value="localData.salutation"
|
|
:disabled="disabled"
|
|
@update:model-value="updateField('salutation', $event)"
|
|
>
|
|
<SelectTrigger
|
|
id="salutation"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white',
|
|
getError('salutation') && 'border-red'
|
|
)"
|
|
>
|
|
<SelectValue placeholder="Bitte wählen" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="option in salutationOptions"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p v-if="getError('salutation')" class="text-xs text-red">
|
|
{{ getError('salutation') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Name fields (side-by-side on desktop) -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<!-- First Name -->
|
|
<div class="space-y-2">
|
|
<Label for="firstName" class="text-white">
|
|
Vorname <span class="text-red">*</span>
|
|
</Label>
|
|
<Input
|
|
id="firstName"
|
|
:model-value="localData.firstName"
|
|
type="text"
|
|
placeholder="Max"
|
|
:disabled="disabled"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white placeholder:text-white/40',
|
|
getError('firstName') && 'border-red'
|
|
)"
|
|
@update:model-value="updateField('firstName', $event)"
|
|
/>
|
|
<p v-if="getError('firstName')" class="text-xs text-red">
|
|
{{ getError('firstName') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Last Name -->
|
|
<div class="space-y-2">
|
|
<Label for="lastName" class="text-white">
|
|
Nachname <span class="text-red">*</span>
|
|
</Label>
|
|
<Input
|
|
id="lastName"
|
|
:model-value="localData.lastName"
|
|
type="text"
|
|
placeholder="Mustermann"
|
|
:disabled="disabled"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white placeholder:text-white/40',
|
|
getError('lastName') && 'border-red'
|
|
)"
|
|
@update:model-value="updateField('lastName', $event)"
|
|
/>
|
|
<p v-if="getError('lastName')" class="text-xs text-red">
|
|
{{ getError('lastName') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date of Birth -->
|
|
<div class="space-y-2">
|
|
<Label for="dateOfBirth" class="text-white">
|
|
Geburtsdatum <span class="text-red">*</span>
|
|
</Label>
|
|
<Input
|
|
id="dateOfBirth"
|
|
:model-value="localData.dateOfBirth"
|
|
type="date"
|
|
:disabled="disabled"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white placeholder:text-white/40',
|
|
getError('dateOfBirth') && 'border-red'
|
|
)"
|
|
@update:model-value="updateField('dateOfBirth', $event)"
|
|
/>
|
|
<p v-if="getError('dateOfBirth')" class="text-xs text-red">
|
|
{{ getError('dateOfBirth') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Street -->
|
|
<div class="space-y-2">
|
|
<Label for="street" class="text-white">
|
|
Straße und Hausnummer <span class="text-red">*</span>
|
|
</Label>
|
|
<Input
|
|
id="street"
|
|
:model-value="localData.street"
|
|
type="text"
|
|
placeholder="Musterstraße 123"
|
|
:disabled="disabled"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white placeholder:text-white/40',
|
|
getError('street') && 'border-red'
|
|
)"
|
|
@update:model-value="updateField('street', $event)"
|
|
/>
|
|
<p v-if="getError('street')" class="text-xs text-red">
|
|
{{ getError('street') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Post Code and City (side-by-side) -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<!-- Post Code -->
|
|
<div class="space-y-2">
|
|
<Label for="postCode" class="text-white">
|
|
PLZ <span class="text-red">*</span>
|
|
</Label>
|
|
<Input
|
|
id="postCode"
|
|
:model-value="localData.postCode"
|
|
type="text"
|
|
placeholder="12345"
|
|
:disabled="disabled"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white placeholder:text-white/40',
|
|
getError('postCode') && 'border-red'
|
|
)"
|
|
@update:model-value="updateField('postCode', $event)"
|
|
/>
|
|
<p v-if="getError('postCode')" class="text-xs text-red">
|
|
{{ getError('postCode') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- City -->
|
|
<div class="space-y-2 sm:col-span-2">
|
|
<Label for="city" class="text-white">
|
|
Stadt <span class="text-red">*</span>
|
|
</Label>
|
|
<Input
|
|
id="city"
|
|
:model-value="localData.city"
|
|
type="text"
|
|
placeholder="Musterstadt"
|
|
:disabled="disabled"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white placeholder:text-white/40',
|
|
getError('city') && 'border-red'
|
|
)"
|
|
@update:model-value="updateField('city', $event)"
|
|
/>
|
|
<p v-if="getError('city')" class="text-xs text-red">
|
|
{{ getError('city') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Country -->
|
|
<div class="space-y-2">
|
|
<Label for="countryCode" class="text-white">
|
|
Land <span class="text-red">*</span>
|
|
</Label>
|
|
<Select
|
|
:model-value="localData.countryCode"
|
|
:disabled="disabled"
|
|
@update:model-value="updateField('countryCode', $event)"
|
|
>
|
|
<SelectTrigger
|
|
id="countryCode"
|
|
:class="cn(
|
|
'bg-purple-dark border-white/20 text-white',
|
|
getError('countryCode') && 'border-red'
|
|
)"
|
|
>
|
|
<SelectValue placeholder="Bitte wählen" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="option in countryOptions"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p v-if="getError('countryCode')" class="text-xs text-red">
|
|
{{ getError('countryCode') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|