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
|
||||
onClick={toggleBottomSheet1}
|
||||
className='bg-disabled! text-primary!'
|
||||
className='bg-disabled! text-foreground!'
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
@@ -250,7 +250,7 @@ function OrderTrackingPage() {
|
||||
<Button
|
||||
type='button'
|
||||
onClick={toggleBottomSheet2}
|
||||
className='bg-disabled! text-primary!'
|
||||
className='bg-disabled! text-foreground!'
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface CustomMapProps {
|
||||
onPositionSelect?: (position: [number, number]) => void;
|
||||
onZoomChange?: (zoom: number) => void;
|
||||
onClick?: (event: L.LeafletMouseEvent) => void;
|
||||
markerActive?: boolean
|
||||
}
|
||||
|
||||
// Component to handle map events and position updates
|
||||
@@ -27,7 +28,7 @@ function MapEvents({
|
||||
onZoomChange,
|
||||
onClick,
|
||||
center,
|
||||
setMapRef
|
||||
setMapRef,
|
||||
}: Pick<CustomMapProps, 'onZoomChange' | 'onClick'> & {
|
||||
center: [number, number];
|
||||
setMapRef: (map: L.Map) => void;
|
||||
@@ -84,6 +85,7 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
||||
onPositionSelect,
|
||||
onZoomChange,
|
||||
onClick,
|
||||
markerActive = true
|
||||
}) => {
|
||||
const [selectedPosition, setSelectedPosition] = useState<[number, number] | 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 newPosition: [number, number] = [e.latlng.lat, e.latlng.lng];
|
||||
setSelectedPosition(newPosition);
|
||||
if (markerActive) {
|
||||
setSelectedPosition(newPosition);
|
||||
}
|
||||
onPositionSelect?.(newPosition);
|
||||
onClick?.(e);
|
||||
};
|
||||
@@ -146,7 +150,7 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
||||
// 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 response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?` +
|
||||
`https://nominatim.openstreetmap.org/search?` +
|
||||
`format=json&` +
|
||||
`q=${encodeURIComponent(value)}&` +
|
||||
`countrycodes=ir&` + // Prioritize Iran
|
||||
@@ -155,11 +159,11 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
||||
`limit=10&` + // Limit results to 10
|
||||
`namedetails=1&` + // Get native names
|
||||
`accept-language=fa` // Request Persian results
|
||||
, {
|
||||
headers: {
|
||||
'Accept-Language': 'fa,en;q=0.9' // Prefer Persian, fallback to English
|
||||
}
|
||||
});
|
||||
, {
|
||||
headers: {
|
||||
'Accept-Language': 'fa,en;q=0.9' // Prefer Persian, fallback to English
|
||||
}
|
||||
});
|
||||
interface SearchResult {
|
||||
place_id: string;
|
||||
display_name: string;
|
||||
@@ -179,8 +183,8 @@ const CustomMap: React.FC<CustomMapProps> = ({
|
||||
title: /* item.namedetails?.['name:fa'] ||
|
||||
item.namedetails?.name ||
|
||||
item.namedetails?.['alt_name:fa'] ||
|
||||
item.namedetails?.alt_name || */
|
||||
item.display_name,
|
||||
item.namedetails?.alt_name || */
|
||||
item.display_name,
|
||||
position: [parseFloat(item.lat), parseFloat(item.lon)] as [number, number]
|
||||
})));
|
||||
} 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