- Fix seed-products.ts TS error (implicit any) and BOM handling - Re-run full seed to restore correct Persian encoding in DB - Fix productService query→search param mismatch - Fix IngredientWiki hardcoded port 4000→use settingsStore - Restore seed-products-data.json from git after accidental corruption
292 lines
14 KiB
TypeScript
292 lines
14 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect } from "react";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import { X, MapPin, User, Phone, Hash, Save, Check } from "lucide-react";
|
||
import { toPersian, cn } from "../lib/utils";
|
||
import { Address } from "../lib/store/userStore";
|
||
import { toast } from "sonner";
|
||
|
||
interface AddressModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onSave: (address: Address) => void;
|
||
editingAddress?: Address | null;
|
||
}
|
||
|
||
export default function AddressModal({ isOpen, onClose, onSave, editingAddress }: AddressModalProps) {
|
||
const [formData, setFormData] = useState<Omit<Address, "id">>({
|
||
title: "",
|
||
receptorName: "",
|
||
phone: "",
|
||
province: "",
|
||
city: "",
|
||
detail: "",
|
||
zipCode: "",
|
||
isDefault: false
|
||
});
|
||
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (editingAddress) {
|
||
setFormData({
|
||
title: editingAddress.title,
|
||
receptorName: editingAddress.receptorName,
|
||
phone: editingAddress.phone,
|
||
province: editingAddress.province,
|
||
city: editingAddress.city,
|
||
detail: editingAddress.detail,
|
||
zipCode: editingAddress.zipCode,
|
||
isDefault: editingAddress.isDefault
|
||
});
|
||
} else {
|
||
setFormData({
|
||
title: "",
|
||
receptorName: "",
|
||
phone: "",
|
||
province: "",
|
||
city: "",
|
||
detail: "",
|
||
zipCode: "",
|
||
isDefault: false
|
||
});
|
||
}
|
||
setErrors({});
|
||
setIsSubmitting(false);
|
||
}, [editingAddress, isOpen]);
|
||
|
||
const normalizeDigits = (str: string) => {
|
||
const persianDigits = [/۰/g, /۱/g, /۲/g, /۳/g, /۴/g, /۵/g, /۶/g, /۷/g, /۸/g, /۹/g];
|
||
const arabicDigits = [/٠/g, /١/g, /٢/g, /٣/g, /٤/g, /٥/g, /٦/g, /٧/g, /٨/g, /٩/g];
|
||
for (let i = 0; i < 10; i++) {
|
||
str = str.replace(persianDigits[i], i.toString()).replace(arabicDigits[i], i.toString());
|
||
}
|
||
return str;
|
||
};
|
||
|
||
const validate = () => {
|
||
const newErrors: Record<string, string> = {};
|
||
if (!formData.title) newErrors.title = "لطفاً عنوان آدرس را وارد کنید";
|
||
if (!formData.receptorName) newErrors.receptorName = "نام گیرنده را وارد کنید";
|
||
|
||
const normalizedPhone = normalizeDigits(formData.phone);
|
||
if (!normalizedPhone) {
|
||
newErrors.phone = "شماره تماس الزامی است";
|
||
} else if (!/^09\d{9}$/.test(normalizedPhone)) {
|
||
newErrors.phone = "شماره موبایل وارد شده معتبر نیست";
|
||
}
|
||
|
||
if (!formData.province) newErrors.province = "استان را مشخص کنید";
|
||
if (!formData.city) newErrors.city = "شهر را مشخص کنید";
|
||
if (!formData.detail) newErrors.detail = "آدرس دقیق پستی را وارد کنید";
|
||
|
||
const normalizedZip = normalizeDigits(formData.zipCode);
|
||
if (!normalizedZip) {
|
||
newErrors.zipCode = "کد پستی الزامی است";
|
||
} else if (!/^\d{10}$/.test(normalizedZip)) {
|
||
newErrors.zipCode = "کد پستی باید ۱۰ رقم باشد";
|
||
}
|
||
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
};
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (validate()) {
|
||
setIsSubmitting(true);
|
||
// Simulate small delay for UX
|
||
setTimeout(() => {
|
||
onSave({
|
||
...formData,
|
||
phone: normalizeDigits(formData.phone),
|
||
zipCode: normalizeDigits(formData.zipCode),
|
||
id: editingAddress?.id || Math.random().toString(36).substr(2, 9),
|
||
isDefault: formData.isDefault
|
||
});
|
||
toast.success(editingAddress ? "تغییرات آدرس با موفقیت ذخیره شد" : "آدرس جدید با موفقیت اضافه شد");
|
||
setIsSubmitting(false);
|
||
onClose();
|
||
}, 600);
|
||
} else {
|
||
toast.error("لطفاً موارد خطای فرم را اصلاح کنید");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
onClick={onClose}
|
||
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||
/>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
className="bg-white w-full max-w-xl rounded-[3rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right pointer-events-auto"
|
||
dir="rtl"
|
||
>
|
||
<div className="bg-medical-gray-50 px-8 py-6 border-b border-medical-gray-100 flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white">
|
||
<MapPin className="w-5 h-5" />
|
||
</div>
|
||
<h3 className="text-xl font-black text-medical-gray-900 italic">
|
||
{editingAddress ? "ویرایش آدرس ارسال" : "افزودن آدرس جدید"}
|
||
</h3>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
||
<div className="space-y-4">
|
||
{/* Row 1: Title */}
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">عنوان آدرس (مثلاً: خانه، محل کار)</label>
|
||
<input
|
||
autoFocus
|
||
type="text"
|
||
value={formData.title}
|
||
onChange={e => setFormData({...formData, title: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.title ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="مثال: منزل اصلی"
|
||
/>
|
||
{errors.title && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.title}</p>}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{/* Row 2: Name & Phone */}
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">نام گیرنده</label>
|
||
<input
|
||
type="text"
|
||
value={formData.receptorName}
|
||
onChange={e => setFormData({...formData, receptorName: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.receptorName ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="نام و نامخانوادگی..."
|
||
/>
|
||
{errors.receptorName && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.receptorName}</p>}
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">شماره تماس</label>
|
||
<input
|
||
type="text"
|
||
value={formData.phone}
|
||
onChange={e => setFormData({...formData, phone: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 text-left", errors.phone ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="09120000000"
|
||
dir="ltr"
|
||
/>
|
||
{errors.phone && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.phone}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{/* Row 3: Province & City */}
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">استان</label>
|
||
<input
|
||
type="text"
|
||
value={formData.province}
|
||
onChange={e => setFormData({...formData, province: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.province ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="نام استان..."
|
||
/>
|
||
{errors.province && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.province}</p>}
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">شهر</label>
|
||
<input
|
||
type="text"
|
||
value={formData.city}
|
||
onChange={e => setFormData({...formData, city: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.city ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="نام شهر..."
|
||
/>
|
||
{errors.city && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.city}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Row 4: Detail Address */}
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">آدرس دقیق</label>
|
||
<textarea
|
||
rows={2}
|
||
value={formData.detail}
|
||
onChange={e => setFormData({...formData, detail: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 resize-none", errors.detail ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="خیابان، کوچه، پلاک، واحد..."
|
||
/>
|
||
{errors.detail && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.detail}</p>}
|
||
</div>
|
||
|
||
{/* Row 5: Zip Code */}
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">کد پستی (۱۰ رقم)</label>
|
||
<input
|
||
type="text"
|
||
value={formData.zipCode}
|
||
onChange={e => setFormData({...formData, zipCode: e.target.value})}
|
||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 text-left", errors.zipCode ? "border-red-300" : "border-medical-gray-100")}
|
||
placeholder="1234567890"
|
||
dir="ltr"
|
||
/>
|
||
{errors.zipCode && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.zipCode}</p>}
|
||
</div>
|
||
|
||
{/* Row 6: Default Toggle */}
|
||
<div className="flex items-center gap-3 pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData({...formData, isDefault: !formData.isDefault})}
|
||
className={cn(
|
||
"w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all",
|
||
formData.isDefault ? "bg-canina-blue border-canina-blue text-white" : "border-medical-gray-200 text-transparent"
|
||
)}
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
</button>
|
||
<label className="text-sm font-bold text-medical-gray-600">انتخاب به عنوان آدرس پیشفرض ارسال</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-4 pt-4">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="flex-1 py-4 bg-medical-gray-50 text-medical-gray-400 rounded-2xl font-black text-sm hover:bg-medical-gray-100 transition-all"
|
||
>
|
||
انصراف
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="flex-[2] py-4 bg-canina-blue text-white rounded-2xl font-black text-sm flex items-center justify-center gap-3 hover:bg-medical-gray-900 transition-all shadow-lg shadow-canina-blue/20 disabled:opacity-50 disabled:cursor-wait"
|
||
>
|
||
{isSubmitting ? (
|
||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||
) : (
|
||
<Save className="w-5 h-5" />
|
||
)}
|
||
{isSubmitting ? "در حال ثبت..." : (editingAddress ? "ثبت تغییرات" : "ثبت آدرس نهایی")}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|