254 lines
8.8 KiB
TypeScript
254 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { MessageCircle, X } from 'lucide-react';
|
|
import type { FulfillmentMode } from '@/domain/cart/inquiry';
|
|
import type { DeliveryInquiryDetails, PickupInquiryDetails } from '@/domain/cart/inquiry';
|
|
|
|
export interface CartInquiryModalLabels {
|
|
titlePickup: string;
|
|
titleDelivery: string;
|
|
nameLabel: string;
|
|
namePlaceholder: string;
|
|
phoneLabel: string;
|
|
phonePlaceholder: string;
|
|
addressLabel: string;
|
|
addressPlaceholder: string;
|
|
preferredTimeLabel: string;
|
|
preferredTimePlaceholder: string;
|
|
submitPickup: string;
|
|
submitDelivery: string;
|
|
cancel: string;
|
|
close: string;
|
|
nameRequired: string;
|
|
phoneRequired: string;
|
|
addressRequired: string;
|
|
timeRequired: string;
|
|
}
|
|
|
|
interface CartInquiryModalProps {
|
|
mode: FulfillmentMode;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmitPickup: (details: PickupInquiryDetails) => void;
|
|
onSubmitDelivery: (details: DeliveryInquiryDetails) => void;
|
|
labels: CartInquiryModalLabels;
|
|
}
|
|
|
|
const emptyPickup = (): PickupInquiryDetails => ({ name: '', phone: '' });
|
|
const emptyDelivery = (): DeliveryInquiryDetails => ({
|
|
name: '',
|
|
phone: '',
|
|
address: '',
|
|
preferredTime: '',
|
|
});
|
|
|
|
export default function CartInquiryModal({
|
|
mode,
|
|
isOpen,
|
|
onClose,
|
|
onSubmitPickup,
|
|
onSubmitDelivery,
|
|
labels,
|
|
}: CartInquiryModalProps) {
|
|
const [pickup, setPickup] = useState(emptyPickup);
|
|
const [delivery, setDelivery] = useState(emptyDelivery);
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
},
|
|
[onClose],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
setErrors({});
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, handleKeyDown, mode]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setPickup(emptyPickup());
|
|
setDelivery(emptyDelivery());
|
|
setErrors({});
|
|
}
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const title = mode === 'pickup' ? labels.titlePickup : labels.titleDelivery;
|
|
const submitLabel = mode === 'pickup' ? labels.submitPickup : labels.submitDelivery;
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const nextErrors: Record<string, string> = {};
|
|
|
|
if (mode === 'pickup') {
|
|
if (!pickup.name.trim()) nextErrors.name = labels.nameRequired;
|
|
if (!pickup.phone.trim()) nextErrors.phone = labels.phoneRequired;
|
|
if (Object.keys(nextErrors).length) {
|
|
setErrors(nextErrors);
|
|
return;
|
|
}
|
|
onSubmitPickup(pickup);
|
|
return;
|
|
}
|
|
|
|
if (!delivery.name.trim()) nextErrors.name = labels.nameRequired;
|
|
if (!delivery.phone.trim()) nextErrors.phone = labels.phoneRequired;
|
|
if (!delivery.address.trim()) nextErrors.address = labels.addressRequired;
|
|
if (!delivery.preferredTime.trim()) nextErrors.preferredTime = labels.timeRequired;
|
|
if (Object.keys(nextErrors).length) {
|
|
setErrors(nextErrors);
|
|
return;
|
|
}
|
|
onSubmitDelivery(delivery);
|
|
};
|
|
|
|
const inputClass =
|
|
'mt-1.5 w-full rounded-xl border border-[#EDE6D9] bg-[#FFFCF7] px-3 py-2.5 text-sm text-[#2C2A26] placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20';
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="fixed inset-0 z-[1000] bg-black/50 backdrop-blur-[2px]"
|
|
onClick={onClose}
|
|
aria-hidden
|
|
/>
|
|
|
|
<div
|
|
className="fixed inset-0 z-[1010] flex items-end sm:items-center justify-center p-0 sm:p-6 pointer-events-none"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="cart-inquiry-title"
|
|
>
|
|
<div
|
|
className="pointer-events-auto w-full sm:max-w-md max-h-[90dvh] overflow-y-auto rounded-t-3xl sm:rounded-2xl bg-[#FFFCF7] border border-[#EDE6D9] shadow-2xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between border-b border-[#EDE6D9] px-6 py-4">
|
|
<h2 id="cart-inquiry-title" className="font-serif text-xl tracking-[-0.3px] text-[#101724]">
|
|
{title}
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label={labels.close}
|
|
className="flex h-9 w-9 items-center justify-center rounded-full border border-[#EDE6D9] bg-white text-[#6B665F] hover:text-[#101724]"
|
|
>
|
|
<X className="h-4 w-4" aria-hidden />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4 p-6">
|
|
<div>
|
|
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
|
|
{labels.nameLabel}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={mode === 'pickup' ? pickup.name : delivery.name}
|
|
onChange={(e) => {
|
|
const v = e.target.value;
|
|
if (mode === 'pickup') setPickup((p) => ({ ...p, name: v }));
|
|
else setDelivery((d) => ({ ...d, name: v }));
|
|
if (errors.name) setErrors((err) => ({ ...err, name: '' }));
|
|
}}
|
|
placeholder={labels.namePlaceholder}
|
|
className={inputClass}
|
|
autoComplete="name"
|
|
/>
|
|
{errors.name && (
|
|
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.name}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
|
|
{labels.phoneLabel}
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={mode === 'pickup' ? pickup.phone : delivery.phone}
|
|
onChange={(e) => {
|
|
const v = e.target.value;
|
|
if (mode === 'pickup') setPickup((p) => ({ ...p, phone: v }));
|
|
else setDelivery((d) => ({ ...d, phone: v }));
|
|
if (errors.phone) setErrors((err) => ({ ...err, phone: '' }));
|
|
}}
|
|
placeholder={labels.phonePlaceholder}
|
|
className={inputClass}
|
|
autoComplete="tel"
|
|
/>
|
|
{errors.phone && (
|
|
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.phone}</p>
|
|
)}
|
|
</div>
|
|
|
|
{mode === 'delivery' && (
|
|
<>
|
|
<div>
|
|
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
|
|
{labels.addressLabel}
|
|
</label>
|
|
<textarea
|
|
value={delivery.address}
|
|
onChange={(e) => {
|
|
setDelivery((d) => ({ ...d, address: e.target.value }));
|
|
if (errors.address) setErrors((err) => ({ ...err, address: '' }));
|
|
}}
|
|
placeholder={labels.addressPlaceholder}
|
|
rows={3}
|
|
className={`${inputClass} resize-none`}
|
|
/>
|
|
{errors.address && (
|
|
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.address}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
|
|
{labels.preferredTimeLabel}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={delivery.preferredTime}
|
|
onChange={(e) => {
|
|
setDelivery((d) => ({ ...d, preferredTime: e.target.value }));
|
|
if (errors.preferredTime) setErrors((err) => ({ ...err, preferredTime: '' }));
|
|
}}
|
|
placeholder={labels.preferredTimePlaceholder}
|
|
className={inputClass}
|
|
/>
|
|
{errors.preferredTime && (
|
|
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.preferredTime}</p>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 rounded-full border border-[#EDE6D9] py-3 text-sm font-medium text-[#6B665F] hover:bg-[#F8F5F0]"
|
|
>
|
|
{labels.cancel}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn-primary flex flex-1 items-center justify-center gap-2 rounded-full py-3 text-sm font-medium tracking-wide"
|
|
>
|
|
<MessageCircle className="h-4 w-4" aria-hidden />
|
|
{submitLabel}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
} |