Vitron-Front/app/components/InteractiveMap.tsx
2026-04-29 01:44:16 +03:30

1138 lines
39 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from "react";
import {
MapContainer,
TileLayer,
Marker,
Popup,
useMapEvents,
useMap,
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import { Search, X, ChevronLeft } from "lucide-react";
import AppInput from "./AppInput";
import AppTextArea from "./AppTextArea";
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from "../components/ui/drawer";
import {
useServiceCreateAddress,
useServiceUpdateAddress,
} from "../utils/RequestHandler";
import { useToast } from "../hooks/use-toast";
import { useNavigate, useSearchParams } from "@remix-run/react";
import { Address } from "../../src/api/types";
import {
iranianCities,
iranianProvinces,
getCitiesByProvince,
type IranianCity,
} from "../data/iranian-cities";
// Form data interface
interface FormData {
title: string;
receiver_name: string;
phone_number: string;
main_address: string;
postal_code: string;
entry_reference: string;
details: string;
latitude: number;
longitude: number;
is_default: boolean;
}
// Props interface
interface InteractiveMapProps {
onLocationSelected?: (location: {
position: [number, number];
address: string;
postalCode?: string;
}) => void;
initialAddress?: Address;
isEditing?: boolean;
addressId?: string;
formOnly?: boolean; // Show only the form without map
}
// Convert Persian/Arabic digits to English
function convertPersianToEnglish(str: string): string {
const persianDigits = "۰۱۲۳۴۵۶۷۸۹";
const arabicDigits = "٠١٢٣٤٥٦٧٨٩";
let result = str;
for (let i = 0; i < 10; i++) {
result = result.replace(new RegExp(persianDigits[i], "g"), i.toString());
result = result.replace(new RegExp(arabicDigits[i], "g"), i.toString());
}
return result;
}
// Component to change map view when city is selected
function CityChangeView({ center }: { center: [number, number] }) {
const map = useMap();
useEffect(() => {
map.setView(center, 13);
}, [center, map]);
return null;
}
// Component to handle map clicks
function MapClickHandler({
onMapClick,
}: {
onMapClick: (latlng: [number, number]) => void;
}) {
useMapEvents({
click: (e) => {
const { lat, lng } = e.latlng;
onMapClick([lat, lng]);
},
});
return null;
}
export default function InteractiveMap({
onLocationSelected,
initialAddress,
isEditing = false,
addressId,
formOnly = false,
}: InteractiveMapProps) {
const [selectedPosition, setSelectedPosition] = useState<
[number, number] | null
>(null);
const [selectedAddress, setSelectedAddress] = useState<string>("");
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
const [isCityDrawerOpen, setIsCityDrawerOpen] = useState<boolean>(false);
const [selectedCity, setSelectedCity] = useState<string>(
isEditing ? "" : "تهران"
);
const [currentCenter, setCurrentCenter] = useState<[number, number]>(
iranianCities[0].position
);
const [mounted, setMounted] = useState<boolean>(false);
const [formData, setFormData] = useState<FormData>({
title: "",
receiver_name: "",
phone_number: "",
main_address: "",
postal_code: "",
entry_reference: "",
details: "",
latitude: 0,
longitude: 0,
is_default: false,
});
// State for city search
const [citySearchTerm, setCitySearchTerm] = useState<string>("");
const [filteredCities, setFilteredCities] =
useState<IranianCity[]>(iranianCities);
const [selectedProvince, setSelectedProvince] = useState<string | null>(null);
// Use a ref for the unique map ID to prevent it changing on re-renders
const mapIdRef = useRef<string>(
`map-${Math.random().toString(36).substring(2, 9)}-${Date.now()}-${isEditing ? "edit" : "add"}-${addressId || "new"}`
);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// Set up the toast notification system
const { toast } = useToast();
// Get return parameters from URL
const returnTo = searchParams.get("returnTo");
const cartId = searchParams.get("cartId");
// Set up the mutation for creating addresses
const createAddressMutation = useServiceCreateAddress();
// For address updates, we'll use the hook with current form data
const updateAddressMutation = useServiceUpdateAddress(addressId || "", {
title: formData.title,
receiverName: formData.receiver_name,
phoneNumber: formData.phone_number,
mainAddress: formData.main_address,
postalCode: formData.postal_code,
entryReference: formData.entry_reference,
details: formData.details,
latitude: formData.latitude,
longitude: formData.longitude,
isDefault: formData.is_default,
});
// Fix Leaflet's default icon path issues when the component mounts
useEffect(() => {
// Fix Leaflet icon issues
delete (L.Icon.Default.prototype as { _getIconUrl?: string })._getIconUrl;
L.Icon.Default.mergeOptions({
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
iconRetinaUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
shadowUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
});
// Cleanup function to remove map instance when component unmounts
return () => {
// This code ensures previous maps are cleaned up
// eslint-disable-next-line react-hooks/exhaustive-deps
const mapContainer = document.getElementById(mapIdRef.current);
if (mapContainer) {
// Clean up previous map on this element
// In Leaflet version 1+, using map._leaflet no longer works
// and instead removeAttribute should be used
mapContainer.innerHTML = "";
mapContainer.removeAttribute("data-leaflet-internal-id");
mapContainer.removeAttribute("_leaflet_id");
console.log("Map container cleaned up successfully");
}
};
}, []);
// Initialize with the existing address data when in editing mode
useEffect(() => {
if (isEditing && initialAddress) {
// Set form data with initial values from address
setFormData({
title: initialAddress.title || "",
receiver_name: initialAddress.receiverName || "",
phone_number: initialAddress.phoneNumber || "",
main_address: initialAddress.mainAddress || "",
postal_code: initialAddress.postalCode || "",
entry_reference: initialAddress.entryReference || "",
details: initialAddress.details || "",
latitude: initialAddress.latitude || 0,
longitude: initialAddress.longitude || 0,
is_default: initialAddress.isDefault || false,
});
if (initialAddress.latitude && initialAddress.longitude) {
console.log(
"Setting position from initial address:",
initialAddress.latitude,
initialAddress.longitude
);
// Set the marker position to the address location
setSelectedPosition([
initialAddress.latitude,
initialAddress.longitude,
]);
setSelectedAddress(initialAddress.mainAddress || "");
}
}
}, [isEditing, initialAddress]);
// Handle city search
useEffect(() => {
let cities = selectedProvince
? getCitiesByProvince(selectedProvince)
: iranianCities;
if (citySearchTerm) {
cities = cities.filter(
(city) =>
city.name.includes(citySearchTerm) ||
city.province.includes(citySearchTerm)
);
}
setFilteredCities(cities);
}, [citySearchTerm, selectedProvince]);
// Function to get address from coordinates
const getAddressFromCoordinates = async (latlng: [number, number]) => {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`,
{
headers: {
"Accept-Language": "fa",
},
}
);
const data = await response.json();
return data.display_name || "آدرس نامشخص";
} catch (error) {
console.error("Error fetching address:", error);
return "";
}
};
// Extract postal code from address data
const getPostalCodeFromAddress = async (latlng: [number, number]) => {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`,
{
headers: {
"Accept-Language": "fa",
},
}
);
const data = await response.json();
if (data && data.address && data.address.postcode) {
return data.address.postcode;
}
return "";
} catch (error) {
console.error("Error fetching postal code:", error);
return "";
}
};
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleMapClick = async (latlng: [number, number]) => {
const address = await getAddressFromCoordinates(latlng);
const postalCode = await getPostalCodeFromAddress(latlng);
setSelectedPosition(latlng);
setSelectedAddress(address);
// Don't update postal_code in edit mode, preserve the original value
setFormData({
...formData,
main_address: address,
// Don't set postal_code here in either mode, preserve the existing value in edit mode
latitude: latlng[0],
longitude: latlng[1],
});
// If using as a location picker in another component, call the callback
if (onLocationSelected) {
onLocationSelected({
position: latlng,
address,
postalCode: isEditing ? formData.postal_code : postalCode,
});
}
};
const handleCitySelect = (cityName: string) => {
setSelectedCity(cityName);
const city = iranianCities.find((c) => c.name === cityName);
if (city) {
setCurrentCenter(city.position);
}
setSelectedProvince(null);
setCitySearchTerm("");
setIsCityDrawerOpen(false);
};
const handleOpenDrawer = () => {
setIsDrawerOpen(true);
};
const handleCloseDrawer = () => {
setIsDrawerOpen(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setErrors({});
// Validate required fields
const newErrors: Record<string, string> = {};
// عنوان آدرس
if (!formData.title || !formData.title.trim()) {
newErrors.title = "عنوان آدرس الزامی است";
}
// نام گیرنده
if (!formData.receiver_name || !formData.receiver_name.trim()) {
newErrors.receiver_name = "نام گیرنده الزامی است";
} else if (formData.receiver_name.trim().length < 3) {
newErrors.receiver_name = "نام گیرنده باید حداقل 3 کاراکتر باشد";
}
// شماره تماس
if (!formData.phone_number || !formData.phone_number.trim()) {
newErrors.phone_number = "شماره تماس الزامی است";
} else {
const phoneRegex = /^(\+98|0)?9\d{9}$/;
const cleanPhone = convertPersianToEnglish(
formData.phone_number.replace(/[\s-]/g, "")
);
if (!phoneRegex.test(cleanPhone)) {
newErrors.phone_number = "شماره تماس معتبر نیست (مثال: 09123456789)";
}
}
// آدرس کامل
if (!formData.main_address || !formData.main_address.trim()) {
newErrors.main_address = "آدرس الزامی است";
} else if (formData.main_address.trim().length < 10) {
newErrors.main_address = "آدرس باید حداقل 10 کاراکتر باشد";
}
// کد پستی
if (!formData.postal_code || !formData.postal_code.trim()) {
newErrors.postal_code = "کد پستی الزامی است";
} else {
const postalCodeRegex = /^\d{10}$/;
const cleanPostalCode = convertPersianToEnglish(
formData.postal_code.replace(/[\s-]/g, "")
);
if (!postalCodeRegex.test(cleanPostalCode)) {
newErrors.postal_code = "کد پستی باید 10 رقم باشد";
}
}
// پلاک
if (!formData.entry_reference || !formData.entry_reference.trim()) {
newErrors.entry_reference = "پلاک الزامی است";
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
setIsSubmitting(false);
return;
}
try {
if (isEditing && addressId) {
// Update existing address - use mutation instead of refetch
console.log("Updating address with ID:", addressId);
console.log("Form data for update:", formData);
// In edit mode, use mutation for update
await updateAddressMutation.mutateAsync();
toast({
title: "آدرس با موفقیت ویرایش شد",
description: "آدرس شما با موفقیت ویرایش شد.",
});
} else {
// Create new address
await createAddressMutation.mutateAsync(formData);
toast({
title: "آدرس جدید با موفقیت اضافه شد",
description: "آدرس جدید شما با موفقیت اضافه شد.",
});
}
// Redirect based on where the user came from
if (returnTo === "cart" && cartId) {
// Return to cart with step=1 to show shipping info
navigate(`/cart/${cartId}?step=1`);
} else {
// Default behavior - go to address list
navigate("/profile/location");
}
} catch (error) {
console.error("Error submitting address:", error);
toast({
variant: "destructive",
title: "خطا در ثبت آدرس",
description: "متاسفانه خطایی در ثبت آدرس رخ داد. لطفا مجددا تلاش کنید.",
});
} finally {
setIsSubmitting(false);
}
};
// Clear validation errors when form data changes
useEffect(() => {
setErrors({});
}, [formData]);
// ایجاد useEffect جدید برای ثبت ID منحصر به فرد نقشه در کنسول برای دیباگ
useEffect(() => {
console.log("Map ID created:", mapIdRef.current);
}, []);
// تنظیم حالت mounted در اولین رندر
useEffect(() => {
setMounted(true);
return () => {
// پاکسازی هنگام خروج از صفحه
setMounted(false);
console.log("Map component unmounting, cleaning up state");
};
}, []);
// Initial center position setup - ensure it's synced with location in edit mode
useEffect(() => {
if (
isEditing &&
initialAddress &&
initialAddress.latitude &&
initialAddress.longitude
) {
// Make sure the map center is always set to the location being edited initially
// This is separate from the form data initialization to ensure map always stays centered
// on the current location regardless of other data changes
setCurrentCenter([initialAddress.latitude, initialAddress.longitude]);
console.log("Map center synchronized with edited location coordinates");
}
}, [isEditing, initialAddress]);
// Add keyframes for the pulse animation
useEffect(() => {
const styleTag = document.createElement("style");
styleTag.innerHTML = `
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.5);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0;
}
}
`;
document.head.appendChild(styleTag);
// Listen for the custom location event
const handleLocationFound = (event: Event) => {
const customEvent = event as CustomEvent<{ lat: number; lng: number }>;
const { lat, lng } = customEvent.detail;
handleMapClick([lat, lng]);
};
window.addEventListener(
"user-location-found",
handleLocationFound as EventListener
);
return () => {
document.head.removeChild(styleTag);
window.removeEventListener(
"user-location-found",
handleLocationFound as EventListener
);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// If formOnly mode, render only the form
if (formOnly) {
return (
<div className="flex z-10 flex-col w-full relative">
<div className="p-4 bg-WHITE">
<form onSubmit={handleSubmit} dir="rtl">
<div className="mb-4">
<AppInput
inputTitle="عنوان آدرس *"
inputValue={formData.title}
inputPlaceholder="مثال: خانه، محل کار، و..."
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
title: e.target.value,
})
}
inputDir="rtl"
/>
{errors.title && (
<p className="text-xs text-red-500 mt-1">{errors.title}</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="نام گیرنده *"
inputValue={formData.receiver_name}
inputPlaceholder="نام و نام خانوادگی گیرنده"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
receiver_name: e.target.value,
})
}
inputDir="rtl"
/>
{errors.receiver_name && (
<p className="text-xs text-red-500 mt-1">
{errors.receiver_name}
</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="شماره تماس *"
inputValue={formData.phone_number}
inputPlaceholder="شماره تماس گیرنده"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
phone_number: e.target.value,
})
}
inputDir={formData.phone_number ? "ltr" : "rtl"}
/>
{errors.phone_number && (
<p className="text-xs text-red-500 mt-1">
{errors.phone_number}
</p>
)}
</div>
<div className="mb-4">
<AppTextArea
inputTitle="آدرس کامل *"
inputValue={formData.main_address}
inputPlaceholder="آدرس دقیق"
inputOnChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setFormData({
...formData,
main_address: e.target.value,
})
}
inputDir="rtl"
/>
{errors.main_address && (
<p className="text-xs text-red-500 mt-1">
{errors.main_address}
</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="کد پستی *"
inputValue={formData.postal_code}
inputPlaceholder="کد پستی ۱۰ رقمی"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
postal_code: e.target.value,
})
}
inputDir="ltr"
/>
{errors.postal_code && (
<p className="text-xs text-red-500 mt-1">
{errors.postal_code}
</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="پلاک *"
inputValue={formData.entry_reference}
inputPlaceholder="پلاک"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
entry_reference: e.target.value,
})
}
inputDir="rtl"
/>
{errors.entry_reference && (
<p className="text-xs text-red-500 mt-1">
{errors.entry_reference}
</p>
)}
</div>
<div className="mb-4">
<AppTextArea
inputTitle="توضیحات اضافی"
inputValue={formData.details}
inputPlaceholder="توضیحات اضافی"
inputOnChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setFormData({
...formData,
details: e.target.value,
})
}
inputDir="rtl"
/>
</div>
<div className="flex items-center justify-between p-3 bg-GRAY3 rounded-lg mb-4">
<span className="text-sm font-medium text-BLACK2">
تنظیم به عنوان آدرس پیشفرض
</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={formData.is_default}
onChange={(e) =>
setFormData({
...formData,
is_default: e.target.checked,
})
}
className="sr-only peer"
aria-label="تنظیم به عنوان آدرس پیش‌فرض"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-BLACK text-WHITE h-[55px] py-3 rounded-md text-center font-medium"
>
{isSubmitting ? (
<>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-WHITE inline"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
درحال ذخیره...
</>
) : (
"ثبت تغییرات"
)}
</button>
</form>
</div>
</div>
);
}
return (
<div className="flex z-10 flex-col w-full relative">
{/* City selector in a nice blue box */}
<div
role="button"
tabIndex={0}
className="bg-WHITE"
onClick={() => setIsCityDrawerOpen(true)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setIsCityDrawerOpen(true);
}
}}
>
<div className="p-2 flex items-center gap-2 justify-center">
<span className="font-bold text-B16">{selectedCity}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-B16"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</div>
{/* City Selector Drawer */}
<Drawer open={isCityDrawerOpen} onOpenChange={(open) => {
setIsCityDrawerOpen(open);
if (!open) {
setSelectedProvince(null);
setCitySearchTerm("");
}
}}>
<DrawerContent className="h-[90vh]">
<DrawerHeader className="flex flex-col py-3">
<div className="flex relative pt-2 text-B12 font-bold items-center justify-center w-full">
{selectedProvince ? (
<button
className="absolute right-4 flex items-center gap-1 text-primary"
onClick={() => setSelectedProvince(null)}
>
<ChevronLeft className="w-5 h-5" />
<span className="text-sm">استانها</span>
</button>
) : (
<X
className="absolute right-4 cursor-pointer"
onClick={() => setIsCityDrawerOpen(false)}
/>
)}
<DrawerTitle>
{selectedProvince ? `شهرهای ${selectedProvince}` : "انتخاب استان و شهر"}
</DrawerTitle>
</div>
</DrawerHeader>
<div className="px-4">
<AppInput
inputTitle={""}
inputDir="rtl"
inputIcon={Search}
inputValue={citySearchTerm}
inputPlaceholder={selectedProvince ? "جستجوی شهر..." : "جستجوی استان یا شهر..."}
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCitySearchTerm(e.target.value)
}
/>
</div>
{/* Province/City List */}
<div className="flex-1 overflow-auto">
<div className="grid grid-cols-1">
{!selectedProvince && !citySearchTerm ? (
// Show provinces
iranianProvinces.map((province) => (
<button
key={province}
onClick={() => setSelectedProvince(province)}
className="p-3 border-input focus-visible:outline-none flex items-center justify-between text-BLACK2 px-4 border-b w-full hover:bg-gray-50"
>
<span>{province}</span>
<ChevronLeft className="w-5 h-5 text-GRAY" />
</button>
))
) : filteredCities.length > 0 ? (
// Show cities
filteredCities.map((city) => (
<button
key={`${city.province}-${city.name}`}
onClick={() => handleCitySelect(city.name)}
className="p-3 border-input focus-visible:outline-none flex flex-col items-start text-BLACK2 px-4 border-b w-full hover:bg-gray-50"
>
<span className="font-medium">{city.name}</span>
{!selectedProvince && (
<span className="text-xs text-GRAY">{city.province}</span>
)}
</button>
))
) : (
<div className="text-center py-8 text-GRAY">
<p>نتیجهای یافت نشد</p>
</div>
)}
</div>
</div>
</DrawerContent>
</Drawer>
{/* Map Container */}
<div className="dd z-0 h-[500px] w-full relative">
<div id={mapIdRef.current} className="h-full w-full">
{mounted && (
<MapContainer
key={mapIdRef.current}
center={currentCenter}
zoom={13}
scrollWheelZoom={true}
style={{ height: "100%", width: "100%" }}
>
<CityChangeView center={currentCenter} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapClickHandler onMapClick={handleMapClick} />
{/* <LocationButton /> */}
{selectedPosition && (
<Marker position={selectedPosition}>
<Popup>
<div dir="rtl" className="text-right">
<p className="font-bold mb-1">آدرس:</p>
<p className="mb-2">{selectedAddress}</p>
<p className="mb-1">
<span className="font-bold">مختصات: </span>
{selectedPosition[0].toFixed(6)},{" "}
{selectedPosition[1].toFixed(6)}
</p>
</div>
</Popup>
</Marker>
)}
</MapContainer>
)}
</div>
</div>
{/* Select Location Button */}
{!onLocationSelected && selectedPosition && !isDrawerOpen && (
<button
onClick={handleOpenDrawer}
className="max-w-md mx-auto fixed bottom-20 left-4 right-4 bg-BLACK text-WHITE py-3 rounded-md text-center font-medium z-30"
>
{isEditing ? "ثبت موقعیت جدید و ادامه ویرایش" : "انتخاب این مکان"}
</button>
)}
{/* Address Form Drawer */}
{!onLocationSelected && (
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerContent className="h-[90vh]">
<DrawerHeader className="flex flex-col py-3">
<div className="flex relative pt-2 text-B12 font-bold items-center justify-center w-full">
<X
className="absolute right-4"
onClick={() => setIsDrawerOpen(false)}
/>
<DrawerTitle>
{isEditing ? "ویرایش آدرس" : "ثبت آدرس"}
</DrawerTitle>
</div>
</DrawerHeader>
<div className="p-4 overflow-y-auto flex-1 max-h-[calc(90vh-5rem)]">
<form onSubmit={handleSubmit} dir="rtl">
<div className="mb-4">
<AppInput
inputTitle="عنوان آدرس *"
inputValue={formData.title}
inputPlaceholder="مثال: خانه، محل کار، و..."
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
title: e.target.value,
})
}
inputDir="rtl"
/>
{errors.title && (
<p className="text-xs text-red-500 mt-1">{errors.title}</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="نام گیرنده *"
inputValue={formData.receiver_name}
inputPlaceholder="نام و نام خانوادگی گیرنده"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
receiver_name: e.target.value,
})
}
inputDir="rtl"
/>
{errors.receiver_name && (
<p className="text-xs text-red-500 mt-1">
{errors.receiver_name}
</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="شماره تماس *"
inputValue={formData.phone_number}
inputPlaceholder="شماره تماس گیرنده"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
phone_number: e.target.value,
})
}
inputDir={formData.phone_number ? "ltr" : "rtl"}
/>
{errors.phone_number && (
<p className="text-xs text-red-500 mt-1">
{errors.phone_number}
</p>
)}
</div>
<div className="mb-4">
<AppTextArea
inputTitle="آدرس کامل *"
inputValue={formData.main_address}
inputPlaceholder="آدرس دقیق"
inputOnChange={(
e: React.ChangeEvent<HTMLTextAreaElement>
) =>
setFormData({
...formData,
main_address: e.target.value,
})
}
inputDir="rtl"
/>
{errors.main_address && (
<p className="text-xs text-red-500 mt-1">
{errors.main_address}
</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="کد پستی *"
inputValue={formData.postal_code}
inputPlaceholder="کد پستی ۱۰ رقمی"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
postal_code: e.target.value,
})
}
inputDir="ltr"
/>
{errors.postal_code && (
<p className="text-xs text-red-500 mt-1">
{errors.postal_code}
</p>
)}
</div>
<div className="mb-4">
<AppInput
inputTitle="پلاک *"
inputValue={formData.entry_reference}
inputPlaceholder="پلاک"
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFormData({
...formData,
entry_reference: e.target.value,
})
}
inputDir="rtl"
/>
{errors.entry_reference && (
<p className="text-xs text-red-500 mt-1">
{errors.entry_reference}
</p>
)}
</div>
<div className="mb-4">
<AppTextArea
inputTitle="توضیحات اضافی"
inputValue={formData.details}
inputPlaceholder="توضیحات اضافی"
inputOnChange={(
e: React.ChangeEvent<HTMLTextAreaElement>
) =>
setFormData({
...formData,
details: e.target.value,
})
}
inputDir="rtl"
/>
</div>
<div className="flex items-center justify-between p-3 bg-GRAY3 rounded-lg">
<span className="text-sm font-medium text-BLACK2">
تنظیم به عنوان آدرس پیشفرض
</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={formData.is_default}
onChange={(e) =>
setFormData({
...formData,
is_default: e.target.checked,
})
}
className="sr-only peer"
aria-label="تنظیم به عنوان آدرس پیش‌فرض"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-BLACK text-WHITE h-[55px] py-3 rounded-md text-center font-medium mt-3"
>
{isSubmitting ? (
<>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-WHITE inline"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
درحال ذخیره...
</>
) : isEditing ? (
"ثبت تغییرات"
) : (
"اعمال تغییرات"
)}
</button>
</form>
</div>
</DrawerContent>
</Drawer>
)}
{/* Overlay background when drawer is open */}
{!onLocationSelected && isDrawerOpen && (
<div
role="button"
tabIndex={0}
aria-label="Close drawer"
className="fixed inset-0 bg-BLACK bg-opacity-30 z-40"
onClick={handleCloseDrawer}
onKeyDown={(e) => {
if (e.key === "Escape") {
handleCloseDrawer();
}
}}
></div>
)}
{/* Selection button for location picker mode */}
{onLocationSelected && selectedPosition && (
<button
onClick={() => {
if (onLocationSelected && selectedPosition) {
onLocationSelected({
position: selectedPosition,
address: selectedAddress,
postalCode: formData.postal_code,
});
}
}}
className="fixed bottom-4 left-4 right-4 bg-blue-600 text-WHITE py-3 rounded-md text-center font-medium z-30"
>
تایید و انتخاب این موقعیت
</button>
)}
</div>
);
}