Rocky_Mountain_Vending/components/forms/request-machine-form.tsx

467 lines
16 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { CheckCircle, AlertCircle } from "lucide-react"
import Link from "next/link"
import { FormInput } from "./form-input"
import { FormButton } from "./form-button"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { WebhookClient } from "@/lib/webhook-client"
interface RequestMachineFormData {
firstName: string
lastName: string
email: string
phone: string
company: string
employeeCount: string
machineType: string
machineCount: string
message?: string
marketingConsent: boolean
termsAgreement: boolean
source?: string
page?: string
confirmEmail?: string
}
interface SubmittedRequestMachineFormData extends RequestMachineFormData {
timestamp: string
url: string
}
interface RequestMachineFormProps {
onSubmit?: (data: SubmittedRequestMachineFormData) => void
className?: string
}
export function RequestMachineForm({ onSubmit, className }: RequestMachineFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitted, setIsSubmitted] = useState(false)
const [submitError, setSubmitError] = useState<string | null>(null)
const {
register,
handleSubmit,
formState: { errors },
reset,
setValue,
watch,
} = useForm<RequestMachineFormData>({
defaultValues: {
source: "website",
page: "",
marketingConsent: false,
termsAgreement: false,
},
})
const watchedValues = watch()
useEffect(() => {
if (typeof window !== 'undefined') {
setValue("page", window.location.pathname)
}
}, [setValue])
const onFormSubmit = async (data: RequestMachineFormData) => {
setIsSubmitting(true)
setSubmitError(null)
try {
// Add URL and timestamp
const formData: SubmittedRequestMachineFormData = {
...data,
timestamp: new Date().toISOString(),
url: window.location.href,
}
// Call the webhook client directly
const result = await WebhookClient.submitRequestMachineForm(formData)
if (!result.success) {
throw new Error(result.error || "Failed to submit form")
}
setIsSubmitted(true)
reset()
// Call custom onSubmit handler if provided
if (onSubmit) {
onSubmit(formData)
}
} catch (error) {
console.error("Form submission error:", error)
setSubmitError("Failed to submit form. Please try again.")
} finally {
setIsSubmitting(false)
}
}
const onFormReset = () => {
setIsSubmitted(false)
setSubmitError(null)
reset()
}
if (isSubmitted) {
return (
<div className={`space-y-4 text-center ${className}`}>
<div className="flex justify-center">
<CheckCircle className="h-12 w-12 text-green-500" />
</div>
<h3 className="text-xl font-semibold">Thank You!</h3>
<p className="text-muted-foreground">
We've received your request and will call you within 24 hours to discuss your vending machine needs.
</p>
<button
type="button"
onClick={onFormReset}
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
>
Request Another Machine
</button>
</div>
)
}
return (
<form onSubmit={handleSubmit(onFormSubmit)} className={`space-y-4 ${className}`}>
{/* Honeypot field for spam prevention */}
<div className="sr-only">
<label htmlFor="confirm-email">
Confirm Email (leave empty)
</label>
<input
id="confirm-email"
type="email"
{...register("confirmEmail")}
className="sr-only"
tabIndex={-1}
/>
</div>
{/* Name Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormInput
id="firstName"
label="First Name"
placeholder="John"
error={errors.firstName?.message}
{...register("firstName", {
required: "First name is required",
})}
/>
<FormInput
id="lastName"
label="Last Name"
placeholder="Doe"
error={errors.lastName?.message}
{...register("lastName", {
required: "Last name is required",
})}
/>
</div>
{/* Contact Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormInput
id="email"
type="email"
label="Email"
placeholder="john@example.com"
error={errors.email?.message}
{...register("email", {
required: "Email is required",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Please enter a valid email address",
},
})}
/>
<FormInput
id="phone"
type="tel"
inputMode="tel"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}|[0-9]{10}"
label="Phone"
placeholder="(435) 233-9668"
error={errors.phone?.message}
{...register("phone", {
required: "Phone number is required",
validate: (value) => {
// Remove all non-digit characters
const phoneDigits = value.replace(/\D/g, '')
return phoneDigits.length >= 10 && phoneDigits.length <= 15 || "Please enter a valid phone number (10-15 digits)"
},
})}
/>
</div>
{/* Company Information */}
<FormInput
id="company"
label="Company/Business Name"
placeholder="Your Company Name"
error={errors.company?.message}
{...register("company", {
required: "Company name is required",
})}
/>
{/* Employee Count */}
<FormInput
id="employeeCount"
type="number"
min="1"
label="Number of People/Employees"
placeholder="50"
helperText="Approximate number of people at your location"
error={errors.employeeCount?.message}
{...register("employeeCount", {
required: "Number of people is required",
validate: (value) => {
const num = parseInt(value)
return num >= 1 && num <= 10000 || "Please enter a valid number between 1 and 10,000"
},
})}
/>
{/* Marketing Consent */}
<div className="flex items-start space-x-1.5 pt-1">
<Checkbox
id="marketingConsent"
checked={watchedValues.marketingConsent || false}
onCheckedChange={(checked) => setValue("marketingConsent", !!checked)}
className="mt-0.5"
/>
<Label htmlFor="marketingConsent" className="text-xs text-muted-foreground leading-relaxed">
I consent to receive marketing communications from Rocky Mountain Vending via email and phone. I can unsubscribe at any time.
</Label>
</div>
{errors.marketingConsent && (
<p className="text-xs text-destructive pt-1">{errors.marketingConsent.message}</p>
)}
{/* Terms Agreement */}
<div className="flex items-start space-x-1.5 pt-1">
<Checkbox
id="termsAgreement"
checked={watchedValues.termsAgreement || false}
onCheckedChange={(checked) => setValue("termsAgreement", !!checked)}
className="mt-0.5"
/>
<Label htmlFor="termsAgreement" className="text-xs text-muted-foreground leading-relaxed">
I agree to the <Link href="/terms-and-conditions" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Terms and Conditions</Link>.
</Label>
</div>
{errors.termsAgreement && (
<p className="text-xs text-destructive pt-1">{errors.termsAgreement.message}</p>
)}
{/* Hidden form fields for validation */}
<input
type="hidden"
{...register("marketingConsent", {
required: "You must consent to marketing communications",
})}
/>
<input
type="hidden"
{...register("termsAgreement", {
required: "You must agree to the Terms and Conditions",
})}
/>
{/* Machine Type */}
<div className="space-y-3">
<Label className="text-sm font-medium">
Type of Vending Machine Needed (select all that apply)
</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-snack"
checked={watchedValues.machineType?.includes('snack') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'snack']
: currentTypes.filter(type => type !== 'snack')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-snack" className="text-sm">Snack Vending Machine</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-beverage"
checked={watchedValues.machineType?.includes('beverage') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'beverage']
: currentTypes.filter(type => type !== 'beverage')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-beverage" className="text-sm">Beverage Vending Machine</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-combo"
checked={watchedValues.machineType?.includes('combo') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'combo']
: currentTypes.filter(type => type !== 'combo')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-combo" className="text-sm">Combo/Snack & Beverage</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-cold-food"
checked={watchedValues.machineType?.includes('cold-food') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'cold-food']
: currentTypes.filter(type => type !== 'cold-food')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-cold-food" className="text-sm">Cold Food Vending Machine</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-hot-food"
checked={watchedValues.machineType?.includes('hot-food') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'hot-food']
: currentTypes.filter(type => type !== 'hot-food')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-hot-food" className="text-sm">Hot Food Vending Machine</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-coffee"
checked={watchedValues.machineType?.includes('coffee') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'coffee']
: currentTypes.filter(type => type !== 'coffee')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-coffee" className="text-sm">Coffee Vending Machine</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-micro-market"
checked={watchedValues.machineType?.includes('micro-market') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'micro-market']
: currentTypes.filter(type => type !== 'micro-market')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-micro-market" className="text-sm">Micro Market</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="machineType-other"
checked={watchedValues.machineType?.includes('other') || false}
onCheckedChange={(checked) => {
const currentTypes = watchedValues.machineType ? watchedValues.machineType.split(',').filter(Boolean) : []
const newTypes = checked
? [...currentTypes, 'other']
: currentTypes.filter(type => type !== 'other')
setValue("machineType", newTypes.join(','))
}}
/>
<Label htmlFor="machineType-other" className="text-sm">Other (specify below)</Label>
</div>
</div>
{errors.machineType && (
<p className="text-sm text-destructive">{errors.machineType.message}</p>
)}
</div>
{/* Machine Count */}
<FormInput
id="machineCount"
type="number"
min="1"
max="100"
label="Number of Machines"
placeholder="1"
error={errors.machineCount?.message}
{...register("machineCount", {
required: "Number of machines is required",
validate: (value) => {
const num = parseInt(value)
return num >= 1 && num <= 100 || "Please enter a number between 1 and 100"
},
})}
/>
{/* Optional Message */}
<FormInput
id="message"
label="Additional Information (Optional)"
placeholder="Any specific requirements or questions?"
error={errors.message?.message}
{...register("message")}
/>
{/* Submit Error */}
{submitError && (
<div className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{submitError}</span>
</div>
)}
{/* Submit Button */}
<FormButton
type="submit"
className="w-full"
loading={isSubmitting}
size="lg"
>
{isSubmitting ? "Submitting..." : "Get Your FREE Consultation"}
</FormButton>
<input
type="hidden"
{...register("source")}
value={watchedValues.source || "website"}
/>
<input
type="hidden"
{...register("page")}
value={watchedValues.page || ""}
/>
</form>
)
}