add: order tracking page
This commit is contained in:
@@ -0,0 +1,228 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
|
||||||
|
import Button from '@/components/button/PrimaryButton';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { MarkerData } from '@/app/[name]/(Profile)/profile/address/new/page';
|
||||||
|
import { HandPlatter, Package } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import AnimatedBar from '@/components/utils/AnimatedBar';
|
||||||
|
import { Clock } from 'iconsax-react';
|
||||||
|
import BlurredOverlayContainer from '@/components/overlays/BlurredOverlayContainer';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
const CustomMap = dynamic(
|
||||||
|
() => import('@/components/map/CustomMap'),
|
||||||
|
{ ssr: false } // Leaflet requires browser APIs
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
function OrderTrackingPage() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { id } = useParams();
|
||||||
|
const params = useSearchParams();
|
||||||
|
// const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null);
|
||||||
|
// const [selectedAddress, setSelectedAddress] = useState<NominatimReverseGeocodingResponse | null>(null);
|
||||||
|
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [mapCenter, setMapCenter] = useState<[number, number]>([Number(params?.get('lon')) || 35.6892, Number(params?.get('lon')) || 51.3890]);
|
||||||
|
const [bottomSheet1State, setBottomSheet1State] = useState<boolean>(false);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [cancelModalVisibility, setCancelModalVisibility] = useState<boolean>(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Example initial position (Tehran)
|
||||||
|
const initialPosition = React.useMemo<[number, number]>(() => [35.6892, 51.3890], []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize markers on the client side
|
||||||
|
const initializeMarkers = async () => {
|
||||||
|
// Dynamic import of Leaflet on the client side
|
||||||
|
const L = (await import('leaflet')).default;
|
||||||
|
|
||||||
|
setMarkers([
|
||||||
|
{
|
||||||
|
position: initialPosition,
|
||||||
|
title: 'Restaurant Location',
|
||||||
|
icon: new L.Icon({
|
||||||
|
iconUrl: '/icons/restaurant-marker.png',
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeMarkers();
|
||||||
|
}, [initialPosition]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleZoomChange = (zoom: number) => {
|
||||||
|
console.log('Zoom level:', zoom);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMapClick = (e: L.LeafletMouseEvent) => {
|
||||||
|
console.log('Map clicked at:', e.latlng);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBottomSheet1 = () => {
|
||||||
|
setBottomSheet1State((prev) => !prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCancelModal = async (e?: React.MouseEvent) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
setCancelModalVisibility((state) => !state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancelOrder = (e: React.MouseEvent<HTMLButtonElement> | null) => {
|
||||||
|
if (!e) return;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-gray-50">
|
||||||
|
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<CustomMap
|
||||||
|
initialPosition={mapCenter}
|
||||||
|
zoom={14}
|
||||||
|
markers={markers}
|
||||||
|
onZoomChange={handleZoomChange}
|
||||||
|
onClick={handleMapClick}
|
||||||
|
markerActive={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-[1000]">
|
||||||
|
<div className='relative w-full h-full' >
|
||||||
|
<AnimatedBottomSheet
|
||||||
|
bgOpacity={0}
|
||||||
|
inDuration={0}
|
||||||
|
blurOpacity={100}
|
||||||
|
noBlur
|
||||||
|
visible={bottomSheet1State}
|
||||||
|
showCloseButton={false}
|
||||||
|
showTitle={false}
|
||||||
|
overrideClassName='!pt-6'
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-2 gap-4 px-4'>
|
||||||
|
<Button
|
||||||
|
onClick={toggleCancelModal}
|
||||||
|
>
|
||||||
|
انتخاب
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={toggleBottomSheet1}
|
||||||
|
className='bg-disabled! text-primary!'
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AnimatedBottomSheet>
|
||||||
|
|
||||||
|
<AnimatedBottomSheet
|
||||||
|
bgOpacity={0}
|
||||||
|
inDuration={0}
|
||||||
|
blurOpacity={null}
|
||||||
|
noBlur
|
||||||
|
visible={true}
|
||||||
|
showCloseButton={false}
|
||||||
|
showTitle={false}
|
||||||
|
title='آدرس'
|
||||||
|
onClick={toggleCancelModal}
|
||||||
|
overrideClassName='pt-1!'
|
||||||
|
>
|
||||||
|
<div className='px-4'>
|
||||||
|
<div className='px-4 mt-2 bg-container flex flex-col gap-6'>
|
||||||
|
<div className='w-[35px] place-self-center border-2 border-border rounded'></div>
|
||||||
|
<h1 className='text-base font-medium text-center'>ارسال توسط پیک رستوران</h1>
|
||||||
|
<hr className='border border-border ' />
|
||||||
|
<div className='text-sm'>
|
||||||
|
<Skeleton
|
||||||
|
className='h-6 w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='text-sm2'>
|
||||||
|
<Skeleton
|
||||||
|
className='h-6 w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr className='border border-border' />
|
||||||
|
<div className='text-sm'>
|
||||||
|
<Skeleton
|
||||||
|
className='h-6 w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='text-sm2 flex gap-2 items-center'>
|
||||||
|
<Clock className='stroke-primary size-5' />
|
||||||
|
<Skeleton
|
||||||
|
className='h-6 w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<HandPlatter className='text-disabled-text ' size={24} />
|
||||||
|
<AnimatedBar percentage={50} />
|
||||||
|
<Package className='text-disabled-text' size={24} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-2 gap-4 mt-11'>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
type='button'>
|
||||||
|
بازگشت به منو
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
onClick={toggleCancelModal}
|
||||||
|
className='bg-disabled! text-foreground!'
|
||||||
|
>
|
||||||
|
لغو سفارش
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimatedBottomSheet>
|
||||||
|
|
||||||
|
{/* Logout Modal */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx(
|
||||||
|
'fixed inset-0 z-1001',
|
||||||
|
!cancelModalVisibility && 'pointer-events-none'
|
||||||
|
)}>
|
||||||
|
<BlurredOverlayContainer visible={cancelModalVisibility} onClick={toggleCancelModal} inDuration={100} outDuration={100} outDelay={75}>
|
||||||
|
<div className="absolute top-1/2 left-6 right-6 px-6">
|
||||||
|
<motion.div
|
||||||
|
animate={{ y: cancelModalVisibility ? [10, 0] : [0, 10] }}
|
||||||
|
transition={{ duration: 0.1, ease: 'easeInOut' }}
|
||||||
|
className="top-1/2 absolute left-1/2 min-w-xs w-full max-w-sm text-center -translate-1/2 px-6 pt-8 pb-9 drop-shadow-container bg-white/63 rounded-4xl"
|
||||||
|
>
|
||||||
|
<h2 className="text-base font-medium">
|
||||||
|
لفو سفارش
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs font-medium mt-6">
|
||||||
|
آیا از درخواست خود اطمینان دارید؟
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-5.5 mt-8">
|
||||||
|
<Button onClick={onCancelOrder} className="text-sm font-normal">
|
||||||
|
بله
|
||||||
|
</Button>
|
||||||
|
<Button onClick={toggleCancelModal} className="bg-disabled! text-disabled2! text-sm font-normal">
|
||||||
|
منصرف شدم
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</BlurredOverlayContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderTrackingPage;
|
||||||
@@ -186,7 +186,7 @@ function OrderTrackingPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={toggleBottomSheet1}
|
onClick={toggleBottomSheet1}
|
||||||
className='bg-disabled! text-primary!'
|
className='bg-disabled! text-foreground!'
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
@@ -250,7 +250,7 @@ function OrderTrackingPage() {
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={toggleBottomSheet2}
|
onClick={toggleBottomSheet2}
|
||||||
className='bg-disabled! text-primary!'
|
className='bg-disabled! text-foreground!'
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface CustomMapProps {
|
|||||||
onPositionSelect?: (position: [number, number]) => void;
|
onPositionSelect?: (position: [number, number]) => void;
|
||||||
onZoomChange?: (zoom: number) => void;
|
onZoomChange?: (zoom: number) => void;
|
||||||
onClick?: (event: L.LeafletMouseEvent) => void;
|
onClick?: (event: L.LeafletMouseEvent) => void;
|
||||||
|
markerActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component to handle map events and position updates
|
// Component to handle map events and position updates
|
||||||
@@ -27,7 +28,7 @@ function MapEvents({
|
|||||||
onZoomChange,
|
onZoomChange,
|
||||||
onClick,
|
onClick,
|
||||||
center,
|
center,
|
||||||
setMapRef
|
setMapRef,
|
||||||
}: Pick<CustomMapProps, 'onZoomChange' | 'onClick'> & {
|
}: Pick<CustomMapProps, 'onZoomChange' | 'onClick'> & {
|
||||||
center: [number, number];
|
center: [number, number];
|
||||||
setMapRef: (map: L.Map) => void;
|
setMapRef: (map: L.Map) => void;
|
||||||
@@ -84,6 +85,7 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
|||||||
onPositionSelect,
|
onPositionSelect,
|
||||||
onZoomChange,
|
onZoomChange,
|
||||||
onClick,
|
onClick,
|
||||||
|
markerActive = true
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null);
|
const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null);
|
||||||
const [mapInstance, setMapInstance] = useState<L.Map | null>(null);
|
const [mapInstance, setMapInstance] = useState<L.Map | null>(null);
|
||||||
@@ -120,7 +122,9 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
|||||||
|
|
||||||
const handleMapClick = (e: L.LeafletMouseEvent) => {
|
const handleMapClick = (e: L.LeafletMouseEvent) => {
|
||||||
const newPosition: [number, number] = [e.latlng.lat, e.latlng.lng];
|
const newPosition: [number, number] = [e.latlng.lat, e.latlng.lng];
|
||||||
setSelectedPosition(newPosition);
|
if (markerActive) {
|
||||||
|
setSelectedPosition(newPosition);
|
||||||
|
}
|
||||||
onPositionSelect?.(newPosition);
|
onPositionSelect?.(newPosition);
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
};
|
};
|
||||||
@@ -146,7 +150,7 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
|||||||
// Iran's bounding box coordinates (roughly)
|
// Iran's bounding box coordinates (roughly)
|
||||||
const viewbox = '44.0,25.0,63.0,40.0'; // [min lon, min lat, max lon, max lat]
|
const viewbox = '44.0,25.0,63.0,40.0'; // [min lon, min lat, max lon, max lat]
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://nominatim.openstreetmap.org/search?` +
|
`https://nominatim.openstreetmap.org/search?` +
|
||||||
`format=json&` +
|
`format=json&` +
|
||||||
`q=${encodeURIComponent(value)}&` +
|
`q=${encodeURIComponent(value)}&` +
|
||||||
`countrycodes=ir&` + // Prioritize Iran
|
`countrycodes=ir&` + // Prioritize Iran
|
||||||
@@ -155,11 +159,11 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
|||||||
`limit=10&` + // Limit results to 10
|
`limit=10&` + // Limit results to 10
|
||||||
`namedetails=1&` + // Get native names
|
`namedetails=1&` + // Get native names
|
||||||
`accept-language=fa` // Request Persian results
|
`accept-language=fa` // Request Persian results
|
||||||
, {
|
, {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept-Language': 'fa,en;q=0.9' // Prefer Persian, fallback to English
|
'Accept-Language': 'fa,en;q=0.9' // Prefer Persian, fallback to English
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
place_id: string;
|
place_id: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
@@ -179,8 +183,8 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
|||||||
title: /* item.namedetails?.['name:fa'] ||
|
title: /* item.namedetails?.['name:fa'] ||
|
||||||
item.namedetails?.name ||
|
item.namedetails?.name ||
|
||||||
item.namedetails?.['alt_name:fa'] ||
|
item.namedetails?.['alt_name:fa'] ||
|
||||||
item.namedetails?.alt_name || */
|
item.namedetails?.alt_name || */
|
||||||
item.display_name,
|
item.display_name,
|
||||||
position: [parseFloat(item.lat), parseFloat(item.lon)] as [number, number]
|
position: [parseFloat(item.lat), parseFloat(item.lon)] as [number, number]
|
||||||
})));
|
})));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
percentage: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnimatedBar({ percentage }: Props) {
|
||||||
|
return (
|
||||||
|
<span className='relative w-full h-1 place-self-center bg-border rounded-full'>
|
||||||
|
<motion.span
|
||||||
|
animate={{ width: [0, `${percentage}%`] }}
|
||||||
|
className='absolute top-0 left-0 w-[10%] h-1 bg-primary rounded-full'
|
||||||
|
>
|
||||||
|
</motion.span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimatedBar
|
||||||
Reference in New Issue
Block a user