Compare commits
10 Commits
0ff035b6b4
...
8328f3a68d
| Author | SHA1 | Date | |
|---|---|---|---|
| 8328f3a68d | |||
| 31c11e3540 | |||
| fbbfba8865 | |||
| 15ddeb94c7 | |||
| 6448b251e4 | |||
| 65d99c7f7a | |||
| efd4ec0cfa | |||
| 4dc98c48d9 | |||
| 6bd706aa04 | |||
| 9a1d796b90 |
@@ -8,14 +8,14 @@ import {
|
|||||||
|
|
||||||
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
|
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
|
||||||
const { data } = await api.get<ShipmentMethodsResponse>(
|
const { data } = await api.get<ShipmentMethodsResponse>(
|
||||||
"/public/delivery-methods/restaurant"
|
"/public/delivery-methods/restaurant",
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPaymentMethod = async (): Promise<PaymentMethodsResponse> => {
|
export const getPaymentMethod = async (): Promise<PaymentMethodsResponse> => {
|
||||||
const { data } = await api.get<PaymentMethodsResponse>(
|
const { data } = await api.get<PaymentMethodsResponse>(
|
||||||
"/public/payments/methods/restaurant"
|
"/public/payments/methods/restaurant",
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,7 @@ export const createOrder = async (orderData?: {
|
|||||||
}): Promise<CreateOrderResponse> => {
|
}): Promise<CreateOrderResponse> => {
|
||||||
const { data } = await api.post<CreateOrderResponse>(
|
const { data } = await api.post<CreateOrderResponse>(
|
||||||
"/public/checkout",
|
"/public/checkout",
|
||||||
orderData
|
orderData,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -70,14 +70,14 @@ export const setTableNumber = async (number: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getOrderDetail = async (
|
export const getOrderDetail = async (
|
||||||
id: string
|
id: string,
|
||||||
): Promise<OrderDetailResponse> => {
|
): Promise<OrderDetailResponse> => {
|
||||||
const { data } = await api.get<OrderDetailResponse>(`/public/orders/${id}`);
|
const { data } = await api.get<OrderDetailResponse>(`/public/orders/${id}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cancelOrder = async (id: string) => {
|
export const cancelOrder = async (id: string) => {
|
||||||
const { data } = await api.patch(`/public/orders/${id}/cancel`);
|
const { data } = await api.patch(`/public/orders/${id}/canceled`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ function OrderTrackingPage() {
|
|||||||
const { mutate: cancelOrder } = useCancelOrder();
|
const { mutate: cancelOrder } = useCancelOrder();
|
||||||
|
|
||||||
const onCancelOrder = (e: React.MouseEvent | null) => {
|
const onCancelOrder = (e: React.MouseEvent | null) => {
|
||||||
|
|
||||||
if (!e || !id) return;
|
if (!e || !id) return;
|
||||||
|
|
||||||
cancelOrder(id as string, {
|
cancelOrder(id as string, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||||
@@ -7,6 +8,74 @@ import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
|||||||
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
|
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
|
||||||
import { Category } from "@/app/[name]/(Main)/types/Types";
|
import { Category } from "@/app/[name]/(Main)/types/Types";
|
||||||
|
|
||||||
|
const SVG_MASK_STYLE = {
|
||||||
|
maskSize: "contain",
|
||||||
|
maskRepeat: "no-repeat",
|
||||||
|
maskPosition: "center",
|
||||||
|
WebkitMaskSize: "contain",
|
||||||
|
WebkitMaskRepeat: "no-repeat",
|
||||||
|
WebkitMaskPosition: "center",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function CategoryImage({
|
||||||
|
src,
|
||||||
|
size,
|
||||||
|
alt,
|
||||||
|
proxyBase,
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
size: number;
|
||||||
|
alt: string;
|
||||||
|
proxyBase: string | null;
|
||||||
|
}) {
|
||||||
|
const isSvg = src.endsWith(".svg");
|
||||||
|
|
||||||
|
if (isSvg) {
|
||||||
|
const isSameOrigin =
|
||||||
|
src.startsWith("/") ||
|
||||||
|
(typeof window !== "undefined" &&
|
||||||
|
new URL(src, window.location.href).origin === window.location.origin);
|
||||||
|
|
||||||
|
const maskUrl = isSameOrigin
|
||||||
|
? src
|
||||||
|
: proxyBase
|
||||||
|
? `${proxyBase}/api/proxy-svg?url=${encodeURIComponent(src)}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (maskUrl) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="shrink-0 bg-primary"
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
maskImage: `url(${maskUrl})`,
|
||||||
|
WebkitMaskImage: `url(${maskUrl})`,
|
||||||
|
...SVG_MASK_STYLE,
|
||||||
|
}}
|
||||||
|
role="img"
|
||||||
|
aria-label={alt}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
alt={alt}
|
||||||
|
className="shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image priority src={src} width={size} height={size} alt={alt} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Variant = "large" | "small";
|
type Variant = "large" | "small";
|
||||||
|
|
||||||
const variantConfig: Record<
|
const variantConfig: Record<
|
||||||
@@ -41,6 +110,8 @@ const CategoryScroll = ({
|
|||||||
variant = "large",
|
variant = "large",
|
||||||
className,
|
className,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const segment = usePathname()?.split("/").filter(Boolean)[0];
|
||||||
|
const proxyBase = segment != null ? `/${segment}` : null;
|
||||||
const { renderer: Renderer, imageSize } = variantConfig[variant];
|
const { renderer: Renderer, imageSize } = variantConfig[variant];
|
||||||
|
|
||||||
const handleSelect = (categoryId: string) => () => onSelect(categoryId);
|
const handleSelect = (categoryId: string) => () => onSelect(categoryId);
|
||||||
@@ -76,12 +147,11 @@ const CategoryScroll = ({
|
|||||||
className={clsx(isSelected && "bg-container!")}
|
className={clsx(isSelected && "bg-container!")}
|
||||||
onClick={handleSelect(item.id)}
|
onClick={handleSelect(item.id)}
|
||||||
>
|
>
|
||||||
<Image
|
<CategoryImage
|
||||||
priority
|
|
||||||
src={item.avatarUrl || "/assets/images/food-image.png"}
|
src={item.avatarUrl || "/assets/images/food-image.png"}
|
||||||
width={imageSize}
|
size={imageSize}
|
||||||
height={imageSize}
|
|
||||||
alt="category image"
|
alt="category image"
|
||||||
|
proxyBase={proxyBase}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-foreground text-center">{item.title}</span>
|
<span className="text-xs text-foreground text-center">{item.title}</span>
|
||||||
</Renderer>
|
</Renderer>
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ const MenuIndex = () => {
|
|||||||
}
|
}
|
||||||
}, [isInitialMount, setSearch, setSelectedIngredients, setSelectedDeliveryId, setSorting]);
|
}, [isInitialMount, setSearch, setSelectedIngredients, setSelectedDeliveryId, setSorting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('selectedCategory', selectedCategory);
|
||||||
|
|
||||||
|
if (categoriesData?.data && selectedCategory === '0') {
|
||||||
|
setSelectedCategory(categoriesData?.data?.[0]?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedCategory, categoriesData])
|
||||||
|
|
||||||
|
|
||||||
const onScroll = useCallback(() => {
|
const onScroll = useCallback(() => {
|
||||||
if (!wrapperRef?.current?.parentElement?.parentElement?.scrollTop || !smallCategoriesRef.current?.offsetTop) return;
|
if (!wrapperRef?.current?.parentElement?.parentElement?.scrollTop || !smallCategoriesRef.current?.offsetTop) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Skeleton } from '@/components/ui/skeleton';
|
|
||||||
import { NominatimReverseGeocodingResponse } from '../../types/Types';
|
import { NominatimReverseGeocodingResponse } from '../../types/Types';
|
||||||
import { formatSelectedAddress } from '../utils/formatAddress';
|
import { formatSelectedAddress } from '../utils/formatAddress';
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ export const AddressDisplay = ({ selectedAddress }: AddressDisplayProps) => {
|
|||||||
<div className='px-4 mt-2 bg-container'>
|
<div className='px-4 mt-2 bg-container'>
|
||||||
<span className='text-sm'>
|
<span className='text-sm'>
|
||||||
{!selectedAddress ? (
|
{!selectedAddress ? (
|
||||||
<Skeleton className='h-6 w-full' />
|
'آدرس را به صورت دستی وارد کنید'
|
||||||
) : (
|
) : (
|
||||||
formatSelectedAddress(selectedAddress)
|
formatSelectedAddress(selectedAddress)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CreateAddressType, UpdateAddressType, NominatimReverseGeocodingResponse
|
|||||||
interface UseAddressFormProps {
|
interface UseAddressFormProps {
|
||||||
selectedPosition: [number, number] | null;
|
selectedPosition: [number, number] | null;
|
||||||
selectedAddress: NominatimReverseGeocodingResponse | null;
|
selectedAddress: NominatimReverseGeocodingResponse | null;
|
||||||
|
fallbackPosition?: [number, number] | null;
|
||||||
initialPhone?: string;
|
initialPhone?: string;
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
addressData?: Address | null;
|
addressData?: Address | null;
|
||||||
@@ -15,6 +16,7 @@ interface UseAddressFormProps {
|
|||||||
export const useAddressForm = ({
|
export const useAddressForm = ({
|
||||||
selectedPosition,
|
selectedPosition,
|
||||||
selectedAddress,
|
selectedAddress,
|
||||||
|
fallbackPosition = [0, 0],
|
||||||
initialPhone = '',
|
initialPhone = '',
|
||||||
editMode = false,
|
editMode = false,
|
||||||
addressData = null,
|
addressData = null,
|
||||||
@@ -53,10 +55,7 @@ export const useAddressForm = ({
|
|||||||
const submitAddress = (e: React.FormEvent<HTMLFormElement>) => {
|
const submitAddress = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!selectedPosition || !selectedAddress) {
|
const positionToUse: [number, number] = selectedPosition || fallbackPosition || [0, 0];
|
||||||
toast('لطفا موقعیت را روی نقشه انتخاب کنید', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.title.trim()) {
|
if (!formData.title.trim()) {
|
||||||
toast('لطفا عنوان آدرس را وارد کنید', 'error');
|
toast('لطفا عنوان آدرس را وارد کنید', 'error');
|
||||||
@@ -76,11 +75,11 @@ export const useAddressForm = ({
|
|||||||
const baseAddressData = {
|
const baseAddressData = {
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
address: formData.addressDetails,
|
address: formData.addressDetails,
|
||||||
city: selectedAddress.address.city || selectedAddress.address.district || '',
|
city: selectedAddress?.address?.city || selectedAddress?.address?.district || addressData?.city || '',
|
||||||
province: selectedAddress.address.province || selectedAddress.address.state || '',
|
province: selectedAddress?.address?.province || selectedAddress?.address?.state || addressData?.province || '',
|
||||||
postalCode: formData.postalCode || selectedAddress.address.postcode || '',
|
postalCode: formData.postalCode || selectedAddress?.address?.postcode || addressData?.postalCode || '',
|
||||||
latitude: selectedPosition[0],
|
latitude: positionToUse[0],
|
||||||
longitude: selectedPosition[1],
|
longitude: positionToUse[1],
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
isDefault: formData.isDefault,
|
isDefault: formData.isDefault,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { ArrowLeft } from 'iconsax-react';
|
import { ArrowLeft } from 'iconsax-react';
|
||||||
import useToggle from '@/hooks/helpers/useToggle';
|
|
||||||
import { useAuthStore } from '@/zustand/authStore';
|
import { useAuthStore } from '@/zustand/authStore';
|
||||||
import { MarkerData, NominatimReverseGeocodingResponse } from '../types/Types';
|
|
||||||
import { ConfirmPositionModal } from './components/ConfirmPositionModal';
|
|
||||||
import { AddressDetailsModal } from './components/AddressDetailsModal';
|
import { AddressDetailsModal } from './components/AddressDetailsModal';
|
||||||
import { useAddressForm } from './hooks/useAddressForm';
|
import { useAddressForm } from './hooks/useAddressForm';
|
||||||
import { fetchAddressFromCoordinates } from './utils/fetchAddress';
|
|
||||||
import { useGetAddressById } from '../hooks/useAddressData';
|
import { useGetAddressById } from '../hooks/useAddressData';
|
||||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
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() {
|
function OrderTrackingPage() {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
const id = params.get('id');
|
const id = params.get('id');
|
||||||
@@ -31,204 +19,42 @@ function OrderTrackingPage() {
|
|||||||
const { data: aboutData } = useGetAbout();
|
const { data: aboutData } = useGetAbout();
|
||||||
const address = addressResponse?.data;
|
const address = addressResponse?.data;
|
||||||
const user = useAuthStore((state) => state.user);
|
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 restaurant = aboutData?.data;
|
||||||
|
|
||||||
const [mapCenter, setMapCenter] = useState<[number, number]>(() => [
|
const fallbackPosition = useMemo<[number, number] | null>(() => {
|
||||||
Number(params?.get('lat')) || 35.6892,
|
if (editMode && address?.latitude && address?.longitude) {
|
||||||
Number(params?.get('lon')) || 51.3890
|
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({
|
const { formData, isPending, handleFormDataChange, submitAddress } = useAddressForm({
|
||||||
selectedPosition,
|
selectedPosition: null,
|
||||||
selectedAddress,
|
selectedAddress: null,
|
||||||
|
fallbackPosition,
|
||||||
initialPhone: user?.number || '',
|
initialPhone: user?.number || '',
|
||||||
editMode,
|
editMode,
|
||||||
addressData: address || null,
|
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) {
|
if (editMode && isLoadingAddress) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
|
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
|
||||||
@@ -241,101 +67,29 @@ function OrderTrackingPage() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasServiceArea = restaurant?.serviceArea &&
|
|
||||||
restaurant.serviceArea.coordinates &&
|
|
||||||
restaurant.serviceArea.coordinates.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-50">
|
<div className="fixed inset-0 bg-gray-50 flex flex-col">
|
||||||
<div className="absolute inset-0">
|
<div className="px-4 pt-4">
|
||||||
<CustomMap
|
<button
|
||||||
initialPosition={mapCenter}
|
onClick={() => router.back()}
|
||||||
zoom={14}
|
className="p-2 rounded-full bg-container shadow-sm"
|
||||||
markers={markers}
|
>
|
||||||
serviceArea={restaurant?.serviceArea || null}
|
<ArrowLeft size={24} className="stroke-foreground" />
|
||||||
onPositionSelect={handlePositionSelect}
|
</button>
|
||||||
onZoomChange={handleZoomChange}
|
</div>
|
||||||
onClick={handleMapClick}
|
|
||||||
searchEnabled={false}
|
<div className="flex-1 relative">
|
||||||
onMapReady={setMapInstance}
|
<AddressDetailsModal
|
||||||
|
visible
|
||||||
|
selectedAddress={null}
|
||||||
|
formData={formData}
|
||||||
|
isPending={isPending}
|
||||||
|
editMode={editMode}
|
||||||
|
onClose={() => router.back()}
|
||||||
|
onFormDataChange={handleFormDataChange}
|
||||||
|
onSubmit={submitAddress}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
|
const CACHE_MAX_AGE = "public, max-age=3600";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const url = request.nextUrl.searchParams.get("url");
|
||||||
|
if (!url) {
|
||||||
|
return NextResponse.json({ error: "url is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||||
|
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch SVG" },
|
||||||
|
{ status: res.status }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
if (!text.trim().toLowerCase().includes("<svg")) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Response is not SVG" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(text, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "image/svg+xml",
|
||||||
|
"Cache-Control": CACHE_MAX_AGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch SVG" },
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-5
@@ -1,11 +1,50 @@
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// Map host → tenant path
|
||||||
export function middleware(request: NextRequest) {
|
const HOST_MAP: Record<string, string> = {
|
||||||
|
"theboote.tahavol-mr.ir": "/boote",
|
||||||
|
"thesun.tahavol-mr.ir": "/suncafe",
|
||||||
|
"passataplus.ir": "/passata",
|
||||||
|
// دامنههای جدید اینجا اضافه میشوند
|
||||||
|
};
|
||||||
|
|
||||||
|
export function middleware(req: NextRequest) {
|
||||||
|
const host = req.headers.get("host") || "";
|
||||||
|
const tenantPath = HOST_MAP[host.toLowerCase()] || "";
|
||||||
|
|
||||||
|
if (!tenantPath) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathname = req.nextUrl.pathname;
|
||||||
|
|
||||||
|
// مسیرهای api و استاتیک را rewrite نکن
|
||||||
|
if (
|
||||||
|
pathname.startsWith("/api") ||
|
||||||
|
pathname.startsWith("/_next") ||
|
||||||
|
pathname.includes(".")
|
||||||
|
) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر کاربر ریشه دامنه را باز کرده، به مسیر tenant ریدایرکت کن (آدرسبار /passata و غیره نشون بده)
|
||||||
|
if (pathname === "/") {
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.pathname = tenantPath;
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر مسیر از قبل با tenant یکی است (مثلاً /passata یا /passata/menu)، rewrite نکن
|
||||||
|
if (pathname === tenantPath || pathname.startsWith(tenantPath + "/")) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.pathname = `${tenantPath}${pathname}`;
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// only applies this middleware to files in the app directory
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: '/((?!api|static|favicon.ico|.*\\..*|_next).*)'
|
matcher: "/:path*",
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user