add: order tracking page

This commit is contained in:
Mahyar Khanbolooki
2025-08-08 20:44:29 +03:30
parent 95a21b84ab
commit 0e67d02b5a
4 changed files with 264 additions and 12 deletions
@@ -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>
+14 -10
View File
@@ -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) {
+20
View File
@@ -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