Vue 3 + TypeScript + Vite + Tailwind CSS v4 multi-step lead capture form with config-driven white-labeling, externalized content (content.json), and "starting at" pricing estimates. Mobile-first with camera photo upload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
5.1 KiB
Vue
118 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useLeadForm } from '../composables/useLeadForm'
|
|
import { useShopConfig } from '../composables/useShopConfig'
|
|
import { useContent } from '../composables/useContent'
|
|
|
|
const { formData } = useLeadForm()
|
|
const { config } = useShopConfig()
|
|
const { content } = useContent()
|
|
const t = content.review
|
|
const tp = content.pricing
|
|
|
|
const selectedService = computed(() =>
|
|
config.services.find((s) => s.name === formData.serviceType)
|
|
)
|
|
|
|
function formatDate(dateStr: string) {
|
|
if (!dateStr) return ''
|
|
const d = new Date(dateStr + 'T00:00:00')
|
|
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-gray-900 mb-1">{{ t.title }}</h2>
|
|
<p class="text-sm text-gray-500 mb-6">{{ t.subtitle }}</p>
|
|
|
|
<div class="space-y-5">
|
|
<!-- Equipment -->
|
|
<div class="rounded-lg border border-gray-200 p-4">
|
|
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">{{ t.equipmentSection }}</h3>
|
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
|
<dt class="text-gray-500">{{ t.typeLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.equipmentType || t.emptyValue }}</dd>
|
|
<dt class="text-gray-500">{{ t.makeLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.make || t.emptyValue }}</dd>
|
|
<dt class="text-gray-500">{{ t.modelLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.model || t.emptyValue }}</dd>
|
|
<dt class="text-gray-500">{{ t.serviceLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.serviceType || t.emptyValue }}</dd>
|
|
</dl>
|
|
<p v-if="formData.problemDescription" class="text-sm text-gray-600 mt-2 border-t border-gray-100 pt-2">
|
|
{{ formData.problemDescription }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Pricing -->
|
|
<div v-if="selectedService" class="rounded-lg bg-primary-light/20 border border-primary/20 px-4 py-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium text-gray-900">{{ t.estimatedPriceLabel }}</span>
|
|
<span v-if="selectedService.startingAt != null" class="text-sm font-semibold text-primary">
|
|
{{ tp.startingAt }} ${{ selectedService.startingAt }}
|
|
</span>
|
|
<span v-else class="text-sm text-gray-500 italic">{{ tp.quoteUponInspection }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photos -->
|
|
<div v-if="formData.photos.length > 0" class="rounded-lg border border-gray-200 p-4">
|
|
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
{{ t.photosSection }} ({{ formData.photos.length }})
|
|
</h3>
|
|
<div class="flex gap-2 overflow-x-auto">
|
|
<img
|
|
v-for="(photo, i) in formData.photos"
|
|
:key="i"
|
|
:src="photo.preview"
|
|
:alt="`${content.photos.photoAlt} ${i + 1}`"
|
|
class="w-16 h-16 rounded-md object-cover shrink-0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Schedule -->
|
|
<div class="rounded-lg border border-gray-200 p-4">
|
|
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">{{ t.scheduleSection }}</h3>
|
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
|
<dt class="text-gray-500">{{ t.preferenceLabel }}</dt>
|
|
<dd class="text-gray-900">{{ t.pickupLabels[formData.pickupOrDropoff] }}</dd>
|
|
<dt class="text-gray-500">{{ t.dateRangeLabel }}</dt>
|
|
<dd class="text-gray-900">
|
|
<template v-if="formData.preferredDateStart">
|
|
{{ formatDate(formData.preferredDateStart) }}
|
|
<template v-if="formData.preferredDateEnd"> — {{ formatDate(formData.preferredDateEnd) }}</template>
|
|
</template>
|
|
<template v-else>{{ t.emptyValue }}</template>
|
|
</dd>
|
|
</dl>
|
|
<p v-if="formData.scheduleNotes" class="text-sm text-gray-600 mt-2 border-t border-gray-100 pt-2">
|
|
{{ formData.scheduleNotes }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Contact -->
|
|
<div class="rounded-lg border border-gray-200 p-4">
|
|
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">{{ t.contactSection }}</h3>
|
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
|
<dt class="text-gray-500">{{ t.nameLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.firstName }} {{ formData.lastName }}</dd>
|
|
<dt class="text-gray-500">{{ t.phoneLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.phone || t.emptyValue }}</dd>
|
|
<dt class="text-gray-500">{{ t.emailLabel }}</dt>
|
|
<dd class="text-gray-900">{{ formData.email || t.emptyValue }}</dd>
|
|
<dt class="text-gray-500">{{ t.addressLabel }}</dt>
|
|
<dd class="text-gray-900">
|
|
<template v-if="formData.address">
|
|
{{ formData.address }}<br />
|
|
{{ formData.city }}, {{ formData.state }} {{ formData.zip }}
|
|
</template>
|
|
<template v-else>{{ t.emptyValue }}</template>
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|