This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { NominatimReverseGeocodingResponse } from '../../types/Types';
|
||||
import { formatSelectedAddress } from '../utils/formatAddress';
|
||||
|
||||
@@ -11,7 +10,7 @@ export const AddressDisplay = ({ selectedAddress }: AddressDisplayProps) => {
|
||||
<div className='px-4 mt-2 bg-container'>
|
||||
<span className='text-sm'>
|
||||
{!selectedAddress ? (
|
||||
<Skeleton className='h-6 w-full' />
|
||||
'آدرس را به صورت دستی وارد کنید'
|
||||
) : (
|
||||
formatSelectedAddress(selectedAddress)
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CreateAddressType, UpdateAddressType, NominatimReverseGeocodingResponse
|
||||
interface UseAddressFormProps {
|
||||
selectedPosition: [number, number] | null;
|
||||
selectedAddress: NominatimReverseGeocodingResponse | null;
|
||||
fallbackPosition?: [number, number] | null;
|
||||
initialPhone?: string;
|
||||
editMode?: boolean;
|
||||
addressData?: Address | null;
|
||||
@@ -15,6 +16,7 @@ interface UseAddressFormProps {
|
||||
export const useAddressForm = ({
|
||||
selectedPosition,
|
||||
selectedAddress,
|
||||
fallbackPosition = [0, 0],
|
||||
initialPhone = '',
|
||||
editMode = false,
|
||||
addressData = null,
|
||||
@@ -53,10 +55,7 @@ export const useAddressForm = ({
|
||||
const submitAddress = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!selectedPosition || !selectedAddress) {
|
||||
toast('لطفا موقعیت را روی نقشه انتخاب کنید', 'error');
|
||||
return;
|
||||
}
|
||||
const positionToUse: [number, number] = selectedPosition || fallbackPosition || [0, 0];
|
||||
|
||||
if (!formData.title.trim()) {
|
||||
toast('لطفا عنوان آدرس را وارد کنید', 'error');
|
||||
@@ -76,11 +75,11 @@ export const useAddressForm = ({
|
||||
const baseAddressData = {
|
||||
title: formData.title,
|
||||
address: formData.addressDetails,
|
||||
city: selectedAddress.address.city || selectedAddress.address.district || '',
|
||||
province: selectedAddress.address.province || selectedAddress.address.state || '',
|
||||
postalCode: formData.postalCode || selectedAddress.address.postcode || '',
|
||||
latitude: selectedPosition[0],
|
||||
longitude: selectedPosition[1],
|
||||
city: selectedAddress?.address?.city || selectedAddress?.address?.district || addressData?.city || '',
|
||||
province: selectedAddress?.address?.province || selectedAddress?.address?.state || addressData?.province || '',
|
||||
postalCode: formData.postalCode || selectedAddress?.address?.postcode || addressData?.postalCode || '',
|
||||
latitude: positionToUse[0],
|
||||
longitude: positionToUse[1],
|
||||
phone: formData.phone,
|
||||
isDefault: formData.isDefault,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ArrowLeft } from 'iconsax-react';
|
||||
import useToggle from '@/hooks/helpers/useToggle';
|
||||
import { useAuthStore } from '@/zustand/authStore';
|
||||
import { MarkerData, NominatimReverseGeocodingResponse } from '../types/Types';
|
||||
import { ConfirmPositionModal } from './components/ConfirmPositionModal';
|
||||
import { AddressDetailsModal } from './components/AddressDetailsModal';
|
||||
import { useAddressForm } from './hooks/useAddressForm';
|
||||
import { fetchAddressFromCoordinates } from './utils/fetchAddress';
|
||||
import { useGetAddressById } from '../hooks/useAddressData';
|
||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||
import { ActiveSearchbox } from '@/components/input/ActiveSearchbox';
|
||||
|
||||
const CustomMap = dynamic(
|
||||
() => import('@/components/map/CustomMap'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
function OrderTrackingPage() {
|
||||
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
const id = params.get('id');
|
||||
@@ -31,204 +19,42 @@ function OrderTrackingPage() {
|
||||
const { data: aboutData } = useGetAbout();
|
||||
const address = addressResponse?.data;
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null);
|
||||
const [selectedAddress, setSelectedAddress] = useState<NominatimReverseGeocodingResponse | null>(null);
|
||||
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
||||
const [initialPositionSet, setInitialPositionSet] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<Array<{ id: string; title: string; position: [number, number] }>>([]);
|
||||
const [mapInstance, setMapInstance] = useState<L.Map | null>(null);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
|
||||
const restaurant = aboutData?.data;
|
||||
|
||||
const [mapCenter, setMapCenter] = useState<[number, number]>(() => [
|
||||
Number(params?.get('lat')) || 35.6892,
|
||||
Number(params?.get('lon')) || 51.3890
|
||||
const fallbackPosition = useMemo<[number, number] | null>(() => {
|
||||
if (editMode && address?.latitude && address?.longitude) {
|
||||
return [address.latitude, address.longitude];
|
||||
}
|
||||
|
||||
const latFromQuery = Number(params?.get('lat'));
|
||||
const lonFromQuery = Number(params?.get('lon'));
|
||||
if (!Number.isNaN(latFromQuery) && !Number.isNaN(lonFromQuery) && latFromQuery !== 0 && lonFromQuery !== 0) {
|
||||
return [latFromQuery, lonFromQuery];
|
||||
}
|
||||
|
||||
if (restaurant?.latitude && restaurant?.longitude) {
|
||||
return [restaurant.latitude, restaurant.longitude];
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [
|
||||
editMode,
|
||||
address?.latitude,
|
||||
address?.longitude,
|
||||
params,
|
||||
restaurant?.latitude,
|
||||
restaurant?.longitude
|
||||
]);
|
||||
const { state: confirmModal, toggle: toggleConfirmModal, set: setConfirmModal } = useToggle();
|
||||
const { state: detailsModal, toggle: _toggleDetailsModal, set: setDetailsModal } = useToggle();
|
||||
|
||||
const { formData, isPending, handleFormDataChange, submitAddress } = useAddressForm({
|
||||
selectedPosition,
|
||||
selectedAddress,
|
||||
selectedPosition: null,
|
||||
selectedAddress: null,
|
||||
fallbackPosition,
|
||||
initialPhone: user?.number || '',
|
||||
editMode,
|
||||
addressData: address || null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode && address) {
|
||||
setMapCenter([address.latitude, address.longitude]);
|
||||
} else if (restaurant?.latitude && restaurant?.longitude) {
|
||||
setMapCenter([restaurant.latitude, restaurant.longitude]);
|
||||
}
|
||||
}, [editMode, address, restaurant]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode && address && !initialPositionSet) {
|
||||
const position: [number, number] = [address.latitude, address.longitude];
|
||||
setSelectedPosition(position);
|
||||
setInitialPositionSet(true);
|
||||
|
||||
fetchAddressFromCoordinates(address.latitude, address.longitude).then((addr) => {
|
||||
if (addr) {
|
||||
setSelectedAddress(addr);
|
||||
setDetailsModal(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [editMode, address, initialPositionSet, setDetailsModal]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const initializeMarkers = async () => {
|
||||
const L = (await import('leaflet')).default;
|
||||
|
||||
const iconHtml = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; text-align: center;">
|
||||
<img src="/icons/restaurant-marker.svg" alt="موقعیت رستوران" style="width: 32px; height: 40px; display: block;" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
const restaurantPosition: [number, number] | null =
|
||||
restaurant?.latitude && restaurant?.longitude
|
||||
? [restaurant.latitude, restaurant.longitude]
|
||||
: null;
|
||||
|
||||
const markersToSet: MarkerData[] = [];
|
||||
|
||||
if (restaurantPosition) {
|
||||
markersToSet.push({
|
||||
position: restaurantPosition,
|
||||
title: 'موقعیت رستوران',
|
||||
icon: L.divIcon({
|
||||
html: iconHtml,
|
||||
className: 'custom-restaurant-marker',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -40],
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setMarkers(markersToSet);
|
||||
};
|
||||
|
||||
initializeMarkers();
|
||||
}, [restaurant]);
|
||||
|
||||
const handlePositionSelect = (position: [number, number]) => {
|
||||
setSelectedPosition((prev) => {
|
||||
if (prev && prev[0] === position[0] && prev[1] === position[1]) {
|
||||
return prev;
|
||||
}
|
||||
setSelectedAddress(null);
|
||||
return position;
|
||||
});
|
||||
if (editMode && detailsModal) {
|
||||
setDetailsModal(false);
|
||||
}
|
||||
setConfirmModal(position && true);
|
||||
};
|
||||
|
||||
const handleZoomChange = (zoom: number) => {
|
||||
console.log('Zoom level:', zoom);
|
||||
};
|
||||
|
||||
const handleMapClick = (e: L.LeafletMouseEvent) => {
|
||||
console.log('Map clicked at:', e.latlng);
|
||||
};
|
||||
|
||||
const toggleDetailsModal = async () => {
|
||||
if (!selectedPosition) {
|
||||
setConfirmModal(false);
|
||||
setDetailsModal(false);
|
||||
return;
|
||||
}
|
||||
setConfirmModal(false);
|
||||
_toggleDetailsModal();
|
||||
if (!detailsModal) {
|
||||
const address = await fetchAddressFromCoordinates(
|
||||
selectedPosition[0],
|
||||
selectedPosition[1]
|
||||
);
|
||||
setSelectedAddress(address);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePosition = () => {
|
||||
setDetailsModal(false);
|
||||
setSelectedPosition(null);
|
||||
setSelectedAddress(null);
|
||||
};
|
||||
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout>();
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
setIsSearchOpen(true);
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
const viewbox = '44.0,25.0,63.0,40.0';
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?` +
|
||||
`format=json&` +
|
||||
`q=${encodeURIComponent(value)}&` +
|
||||
`countrycodes=ir&` +
|
||||
`viewbox=${viewbox}&` +
|
||||
`bounded=0&` +
|
||||
`limit=10&` +
|
||||
`namedetails=1&` +
|
||||
`accept-language=fa`
|
||||
, {
|
||||
headers: {
|
||||
'Accept-Language': 'fa,en;q=0.9'
|
||||
}
|
||||
});
|
||||
interface SearchResult {
|
||||
place_id: string;
|
||||
display_name: string;
|
||||
lat: string;
|
||||
lon: string;
|
||||
}
|
||||
const data: SearchResult[] = await response.json();
|
||||
setSearchResults(data.map((item) => ({
|
||||
id: item.place_id,
|
||||
title: item.display_name,
|
||||
position: [parseFloat(item.lat), parseFloat(item.lon)] as [number, number]
|
||||
})));
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
setSearchResults([]);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
setSearchTimeout(timeout);
|
||||
};
|
||||
|
||||
const handleSearchResultSelect = (result: { id: string; title: string; position: [number, number] }) => {
|
||||
setSelectedPosition(result.position);
|
||||
setMapCenter(result.position);
|
||||
setIsSearchOpen(false);
|
||||
if (mapInstance) {
|
||||
mapInstance.setView(result.position, mapInstance.getZoom());
|
||||
}
|
||||
};
|
||||
|
||||
if (editMode && isLoadingAddress) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
|
||||
@@ -241,101 +67,29 @@ function OrderTrackingPage() {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasServiceArea = restaurant?.serviceArea &&
|
||||
restaurant.serviceArea.coordinates &&
|
||||
restaurant.serviceArea.coordinates.length > 0;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-50">
|
||||
<div className="absolute inset-0">
|
||||
<CustomMap
|
||||
initialPosition={mapCenter}
|
||||
zoom={14}
|
||||
markers={markers}
|
||||
serviceArea={restaurant?.serviceArea || null}
|
||||
onPositionSelect={handlePositionSelect}
|
||||
onZoomChange={handleZoomChange}
|
||||
onClick={handleMapClick}
|
||||
searchEnabled={false}
|
||||
onMapReady={setMapInstance}
|
||||
<div className="fixed inset-0 bg-gray-50 flex flex-col">
|
||||
<div className="px-4 pt-4">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="p-2 rounded-full bg-container shadow-sm"
|
||||
>
|
||||
<ArrowLeft size={24} className="stroke-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<AddressDetailsModal
|
||||
visible
|
||||
selectedAddress={null}
|
||||
formData={formData}
|
||||
isPending={isPending}
|
||||
editMode={editMode}
|
||||
onClose={() => router.back()}
|
||||
onFormDataChange={handleFormDataChange}
|
||||
onSubmit={submitAddress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-4 left-4 right-4 z-1001">
|
||||
<div className="flex items-start gap-2" dir="ltr">
|
||||
{!isSearchOpen && (
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="p-2 rounded-full bg-container/90 backdrop-blur-sm shadow-sm shrink-0"
|
||||
>
|
||||
<ArrowLeft size={24} className="stroke-foreground" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex-1 min-w-0 relative">
|
||||
<div className="[&_.map-search-container]:static! [&_.map-search-container]:transform-none! [&_.map-search-container]:w-full! [&_.map-search-container]:max-w-none! [&_.map-search-container]:top-0!" dir="rtl">
|
||||
<ActiveSearchbox
|
||||
placeholder="جستجو"
|
||||
onSearch={handleSearch}
|
||||
isLoading={isSearching}
|
||||
results={searchResults}
|
||||
onResultSelect={(v) => {
|
||||
const result = searchResults.find(r => r.id === v.id && r.title === v.title);
|
||||
if (result) {
|
||||
handleSearchResultSelect(result);
|
||||
}
|
||||
}}
|
||||
onFocus={() => setIsSearchOpen(true)}
|
||||
className="[&_div]:h-9! [&_input]:text-xs! [&_div]:px-3! [&_input]:px-2! [&_div]:dir-rtl! [&_input]:text-right! [&_input]:dir-rtl!"
|
||||
/>
|
||||
</div>
|
||||
{isSearchOpen && (isSearching || searchResults.length > 0) && (
|
||||
<div className="mt-2 flex items-start gap-2" dir="ltr">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsSearchOpen(false);
|
||||
setSearchResults([]);
|
||||
}}
|
||||
className="p-2 rounded-full bg-container/90 backdrop-blur-sm shadow-sm shrink-0 mt-1"
|
||||
>
|
||||
<ArrowLeft size={24} className="stroke-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasServiceArea && (
|
||||
<div className="absolute bottom-4 left-4 z-1000 max-w-sm">
|
||||
<div className="bg-blue-50/90 border border-blue-200 rounded-lg p-3 shadow-sm">
|
||||
<p className="text-xs text-blue-800">
|
||||
محدوده سرویسدهی روی نقشه با رنگ آبی مشخص شده است
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 z-1000">
|
||||
<div className='relative w-full h-full'>
|
||||
<ConfirmPositionModal
|
||||
visible={confirmModal}
|
||||
onConfirm={toggleDetailsModal}
|
||||
onCancel={toggleConfirmModal}
|
||||
/>
|
||||
|
||||
<AddressDetailsModal
|
||||
visible={detailsModal}
|
||||
selectedAddress={selectedAddress}
|
||||
formData={formData}
|
||||
isPending={isPending}
|
||||
editMode={editMode}
|
||||
onClose={toggleDetailsModal}
|
||||
onChangePosition={handleChangePosition}
|
||||
onFormDataChange={handleFormDataChange}
|
||||
onSubmit={submitAddress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user