diff --git a/public/icons/restaurant-marker.svg b/public/icons/restaurant-marker.svg new file mode 100644 index 0000000..b150565 --- /dev/null +++ b/public/icons/restaurant-marker.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/[name]/(Dialogs)/order/track/[id]/page.tsx b/src/app/[name]/(Dialogs)/order/track/[id]/page.tsx index 708762d..86c8354 100644 --- a/src/app/[name]/(Dialogs)/order/track/[id]/page.tsx +++ b/src/app/[name]/(Dialogs)/order/track/[id]/page.tsx @@ -32,31 +32,35 @@ function OrderTrackingPage() { const router = useRouter(); const { state: cancelModal, toggle: toggleCancelModal } = useToggle(); - // 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; + const iconHtml = ` +
+ موقعیت رستوران +
+ `; + setMarkers([ { - position: initialPosition, - title: 'Restaurant Location', - icon: new L.Icon({ - iconUrl: '/icons/restaurant-marker.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], + position: mapCenter, + title: 'موقعیت رستوران', + icon: L.divIcon({ + html: iconHtml, + className: 'custom-restaurant-marker', + iconSize: [32, 32], + iconAnchor: [16, 32], + popupAnchor: [0, -32], }) } ]); }; initializeMarkers(); - }, [initialPosition]); + }, [mapCenter]); const handleZoomChange = (zoom: number) => { diff --git a/src/app/[name]/(Profile)/profile/address/hooks/useAddressData.ts b/src/app/[name]/(Profile)/profile/address/hooks/useAddressData.ts new file mode 100644 index 0000000..e5cf3dd --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/hooks/useAddressData.ts @@ -0,0 +1,19 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createAddress, getAddresses } from "../service/AddressService"; + +export const useGetAddresses = () => { + return useQuery({ + queryKey: ["addresses"], + queryFn: () => getAddresses(), + }); +}; + +export const useCreateAddress = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createAddress, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["addresses"] }); + }, + }); +}; diff --git a/src/app/[name]/(Profile)/profile/address/new/components/AddressDetailsModal.tsx b/src/app/[name]/(Profile)/profile/address/new/components/AddressDetailsModal.tsx new file mode 100644 index 0000000..a8d40ab --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/components/AddressDetailsModal.tsx @@ -0,0 +1,72 @@ +import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet'; +import Button from '@/components/button/PrimaryButton'; +import { AddressDisplay } from './AddressDisplay'; +import { AddressForm } from './AddressForm'; +import { NominatimReverseGeocodingResponse } from '../../types/Types'; + +interface AddressDetailsModalProps { + visible: boolean; + selectedAddress: NominatimReverseGeocodingResponse | null; + formData: { + title: string; + addressDetails: string; + phone: string; + postalCode: string; + isDefault: boolean; + }; + isPending: boolean; + onClose: () => void; + onFormDataChange: (data: Partial) => void; + onSubmit: (e: React.FormEvent) => void; +} + +export const AddressDetailsModal = ({ + visible, + selectedAddress, + formData, + isPending, + onClose, + onFormDataChange, + onSubmit, +}: AddressDetailsModalProps) => { + return ( + +
+
+ + +
+ + +
+
+
+
+ ); +}; + diff --git a/src/app/[name]/(Profile)/profile/address/new/components/AddressDisplay.tsx b/src/app/[name]/(Profile)/profile/address/new/components/AddressDisplay.tsx new file mode 100644 index 0000000..a232af8 --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/components/AddressDisplay.tsx @@ -0,0 +1,23 @@ +import { Skeleton } from '@/components/ui/skeleton'; +import { NominatimReverseGeocodingResponse } from '../../types/Types'; +import { formatSelectedAddress } from '../utils/formatAddress'; + +interface AddressDisplayProps { + selectedAddress: NominatimReverseGeocodingResponse | null; +} + +export const AddressDisplay = ({ selectedAddress }: AddressDisplayProps) => { + return ( +
+ + {!selectedAddress ? ( + + ) : ( + formatSelectedAddress(selectedAddress) + )} + +
+
+ ); +}; + diff --git a/src/app/[name]/(Profile)/profile/address/new/components/AddressForm.tsx b/src/app/[name]/(Profile)/profile/address/new/components/AddressForm.tsx new file mode 100644 index 0000000..2695083 --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/components/AddressForm.tsx @@ -0,0 +1,74 @@ +import InputField from '@/components/input/InputField'; +import { NominatimReverseGeocodingResponse } from '../../types/Types'; + +interface AddressFormProps { + formData: { + title: string; + addressDetails: string; + phone: string; + postalCode: string; + isDefault: boolean; + }; + selectedAddress: NominatimReverseGeocodingResponse | null; + onFormDataChange: (data: Partial) => void; +} + +export const AddressForm = ({ + formData, + selectedAddress, + onFormDataChange, +}: AddressFormProps) => { + return ( +
+ onFormDataChange({ title: e.target.value })} + /> + onFormDataChange({ addressDetails: e.target.value })} + /> + onFormDataChange({ phone: e.target.value })} + /> + onFormDataChange({ postalCode: e.target.value })} + /> +
+ + onFormDataChange({ isDefault: e.target.checked })} + type='checkbox' + checked={formData.isDefault} + /> +
+
+ ); +}; + diff --git a/src/app/[name]/(Profile)/profile/address/new/components/ConfirmPositionModal.tsx b/src/app/[name]/(Profile)/profile/address/new/components/ConfirmPositionModal.tsx new file mode 100644 index 0000000..b037ada --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/components/ConfirmPositionModal.tsx @@ -0,0 +1,40 @@ +import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet'; +import Button from '@/components/button/PrimaryButton'; + +interface ConfirmPositionModalProps { + visible: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export const ConfirmPositionModal = ({ + visible, + onConfirm, + onCancel, +}: ConfirmPositionModalProps) => { + return ( + +
+ + +
+
+ ); +}; + diff --git a/src/app/[name]/(Profile)/profile/address/new/hooks/useAddressForm.ts b/src/app/[name]/(Profile)/profile/address/new/hooks/useAddressForm.ts new file mode 100644 index 0000000..c8c39d0 --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/hooks/useAddressForm.ts @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useCreateAddress } from '../../hooks/useAddressData'; +import { toast } from '@/components/Toast'; +import { CreateAddressType, NominatimReverseGeocodingResponse } from '../../types/Types'; + +interface UseAddressFormProps { + selectedPosition: [number, number] | null; + selectedAddress: NominatimReverseGeocodingResponse | null; + initialPhone?: string; +} + +export const useAddressForm = ({ + selectedPosition, + selectedAddress, + initialPhone = '', +}: UseAddressFormProps) => { + const router = useRouter(); + const { mutate: createAddress, isPending } = useCreateAddress(); + + const [formData, setFormData] = useState({ + title: '', + addressDetails: '', + phone: initialPhone, + postalCode: '', + isDefault: false, + }); + + const handleFormDataChange = (data: Partial) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + const submitAddress = (e: React.FormEvent) => { + e.preventDefault(); + + if (!selectedPosition || !selectedAddress) { + toast('لطفا موقعیت را روی نقشه انتخاب کنید', 'error'); + return; + } + + if (!formData.title.trim()) { + toast('لطفا عنوان آدرس را وارد کنید', 'error'); + return; + } + + if (!formData.addressDetails.trim()) { + toast('لطفا جزئیات آدرس را وارد کنید', 'error'); + return; + } + + if (!formData.phone.trim()) { + toast('لطفا شماره تلفن را وارد کنید', 'error'); + return; + } + + const addressData: CreateAddressType = { + 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], + phone: formData.phone, + isDefault: formData.isDefault, + }; + + createAddress(addressData, { + onSuccess: () => { + toast('آدرس با موفقیت ثبت شد', 'success'); + router.back(); + }, + onError: (error) => { + const errorMessage = error instanceof Error ? error.message : 'خطا در ثبت آدرس'; + toast(errorMessage, 'error'); + }, + }); + }; + + return { + formData, + isPending, + handleFormDataChange, + submitAddress, + }; +}; + diff --git a/src/app/[name]/(Profile)/profile/address/new/page.tsx b/src/app/[name]/(Profile)/profile/address/new/page.tsx index d1d588b..a8cb9c7 100644 --- a/src/app/[name]/(Profile)/profile/address/new/page.tsx +++ b/src/app/[name]/(Profile)/profile/address/new/page.tsx @@ -2,93 +2,66 @@ import { useSearchParams } from 'next/navigation'; import React, { useState, useEffect } from 'react'; -import type { Icon } from 'leaflet'; import dynamic from 'next/dynamic'; -import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet'; -import Button from '@/components/button/PrimaryButton'; -import InputField from '@/components/input/InputField'; -import { Skeleton } from '@/components/ui/skeleton'; 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'; const CustomMap = dynamic( () => import('@/components/map/CustomMap'), - { ssr: false } // Leaflet requires browser APIs + { ssr: false } ); -export interface MarkerData { - position: [number, number]; - title: string; - icon?: Icon; -} - -export interface NominatimReverseGeocodingResponse { - place_id: number - licence: string - osm_type: string - osm_id: number - lat: string - lon: string - class: string - type: string - place_rank: number - importance: number - addresstype: string - name: string - display_name: string - address: NominatimReverseGeocodingResponseAddress - boundingbox: string[] -} - -export interface NominatimReverseGeocodingResponseAddress { - road: string - neighbourhood: string - suburb: string - city: string - district: string - county: string - province: string - "ISO3166-2-lvl4": string - postcode: string - country: string - state: string - country_code: string -} - function OrderTrackingPage() { const params = useSearchParams(); + const user = useAuthStore((state) => state.user); const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null); const [selectedAddress, setSelectedAddress] = useState(null); const [markers, setMarkers] = useState([]); - // 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 [mapCenter] = useState<[number, number]>([ + Number(params?.get('lat')) || 35.6892, + Number(params?.get('lon')) || 51.3890 + ]); const { state: confirmModal, toggle: toggleConfirmModal, set: setConfirmModal } = useToggle(); const { state: detailsModal, toggle: _toggleDetailsModal, set: setDetailsModal } = useToggle(); - // Example initial position (Tehran) - const initialPosition = React.useMemo<[number, number]>(() => [35.6892, 51.3890], []); + const { formData, isPending, handleFormDataChange, submitAddress } = useAddressForm({ + selectedPosition, + selectedAddress, + initialPhone: user?.number || '', + }); useEffect(() => { - // Initialize markers on the client side const initializeMarkers = async () => { - // Dynamic import of Leaflet on the client side const L = (await import('leaflet')).default; + const iconHtml = ` +
+ موقعیت رستوران +
+ `; + setMarkers([ { - position: initialPosition, - title: 'Restaurant Location', - icon: new L.Icon({ - iconUrl: '/icons/restaurant-marker.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], + position: mapCenter, + title: 'موقعیت رستوران', + icon: L.divIcon({ + html: iconHtml, + className: 'custom-restaurant-marker', + iconSize: [32, 32], + iconAnchor: [16, 32], + popupAnchor: [0, -32], }) } ]); }; initializeMarkers(); - }, [initialPosition]); + }, [mapCenter]); const handlePositionSelect = (position: [number, number]) => { setSelectedPosition((prev) => { @@ -100,8 +73,6 @@ function OrderTrackingPage() { }); setConfirmModal(position && true); setDetailsModal(false); - // You can handle the selected position here - console.log('Selected position:', position); }; const handleZoomChange = (zoom: number) => { @@ -112,7 +83,6 @@ function OrderTrackingPage() { console.log('Map clicked at:', e.latlng); }; - const toggleDetailsModal = async () => { if (!selectedPosition) { setConfirmModal(false); @@ -122,59 +92,16 @@ function OrderTrackingPage() { setConfirmModal(false); _toggleDetailsModal(); if (!detailsModal) { - const address = await fetchAddressFromCoordinates(selectedPosition[0], selectedPosition[1]); - setSelectedAddress(address); - } - } - - const fetchAddressFromCoordinates = async (lat: unknown, lon: unknown) => { - try { - const response = await fetch( - `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&addressdetails=1&accept-language=fa` + const address = await fetchAddressFromCoordinates( + selectedPosition[0], + selectedPosition[1] ); - const data = await response.json(); - return data; // Full formatted address - } catch (error) { - console.error('Error fetching address:', error); - return null; + setSelectedAddress(address); } }; - const submitAddressAction = (e: React.FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - console.log(formData); - } - - const getSelectedAddress = () => { - if (!selectedAddress) { - return "آدرس یافت نشد" - } - try { - return [ - selectedAddress.address.state ?? selectedAddress.address.province, - selectedAddress.address.city, - selectedAddress.address.neighbourhood, - !selectedAddress.address.road.startsWith('میدان') && - !selectedAddress.address.road.startsWith('بلوار') && - !selectedAddress.address.road.startsWith('خیابان') && - !selectedAddress.address.road.startsWith('کوچه') ? - selectedAddress.type === 'secondary' ? `بلوار ${selectedAddress.address.road}` : - selectedAddress.type === 'tertiary' ? `خیابان ${selectedAddress.address.road}` : - selectedAddress.type === 'residential' ? `کوچه ${selectedAddress.address.road}` : - selectedAddress.address.road : selectedAddress.address.road - ].filter(x => x).join('، ') - } catch { - if (selectedAddress.display_name) - return selectedAddress.display_name; - else - return "آدرس یافت نشد" - } - } - return (
-
- {/* {selectedPosition && ( -
-

موقعیت انتخاب شده: {selectedPosition[0].toFixed(4)}, {selectedPosition[1].toFixed(4)}

-
- )} */} -
-
- + -
- - -
-
+ onConfirm={toggleDetailsModal} + onCancel={toggleConfirmModal} + /> - -
-
-
- - {!selectedAddress ? - - : - getSelectedAddress() - } - -
- { }} - /> - { }} - /> -
-
- - -
-
-
-
+ selectedAddress={selectedAddress} + formData={formData} + isPending={isPending} + onClose={toggleDetailsModal} + onFormDataChange={handleFormDataChange} + onSubmit={submitAddress} + />
diff --git a/src/app/[name]/(Profile)/profile/address/new/utils/fetchAddress.ts b/src/app/[name]/(Profile)/profile/address/new/utils/fetchAddress.ts new file mode 100644 index 0000000..489e847 --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/utils/fetchAddress.ts @@ -0,0 +1,18 @@ +import { NominatimReverseGeocodingResponse } from '../../types/Types'; + +export const fetchAddressFromCoordinates = async ( + lat: number, + lon: number +): Promise => { + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&addressdetails=1&accept-language=fa` + ); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching address:', error); + return null; + } +}; + diff --git a/src/app/[name]/(Profile)/profile/address/new/utils/formatAddress.ts b/src/app/[name]/(Profile)/profile/address/new/utils/formatAddress.ts new file mode 100644 index 0000000..21597d2 --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/new/utils/formatAddress.ts @@ -0,0 +1,30 @@ +import { NominatimReverseGeocodingResponse } from '../../types/Types'; + +export const formatSelectedAddress = (selectedAddress: NominatimReverseGeocodingResponse | null): string => { + if (!selectedAddress) { + return "آدرس یافت نشد"; + } + + try { + return [ + selectedAddress.address.state ?? selectedAddress.address.province, + selectedAddress.address.city, + selectedAddress.address.neighbourhood, + !selectedAddress.address.road.startsWith('میدان') && + !selectedAddress.address.road.startsWith('بلوار') && + !selectedAddress.address.road.startsWith('خیابان') && + !selectedAddress.address.road.startsWith('کوچه') ? + selectedAddress.type === 'secondary' ? `بلوار ${selectedAddress.address.road}` : + selectedAddress.type === 'tertiary' ? `خیابان ${selectedAddress.address.road}` : + selectedAddress.type === 'residential' ? `کوچه ${selectedAddress.address.road}` : + selectedAddress.address.road : selectedAddress.address.road + ].filter(x => x).join('، '); + } catch { + if (selectedAddress.display_name) { + return selectedAddress.display_name; + } else { + return "آدرس یافت نشد"; + } + } +}; + diff --git a/src/app/[name]/(Profile)/profile/address/service/AddressService.ts b/src/app/[name]/(Profile)/profile/address/service/AddressService.ts new file mode 100644 index 0000000..96c20ce --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/service/AddressService.ts @@ -0,0 +1,12 @@ +import { api } from "@/lib/api/axiosInstance"; +import { CreateAddressType } from "../types/Types"; + +export const getAddresses = async () => { + const { data } = await api.get("/public/user/addresses"); + return data; +}; + +export const createAddress = async (params: CreateAddressType) => { + const { data } = await api.post("/public/user/addresses", params); + return data; +}; diff --git a/src/app/[name]/(Profile)/profile/address/types/Types.ts b/src/app/[name]/(Profile)/profile/address/types/Types.ts new file mode 100644 index 0000000..761e294 --- /dev/null +++ b/src/app/[name]/(Profile)/profile/address/types/Types.ts @@ -0,0 +1,56 @@ +import type { Icon, DivIcon } from "leaflet"; + +export type CreateAddressType = { + title: string; + address: string; + city: string; + province: string; + postalCode: string; + latitude: number; + longitude: number; + phone: string; + isDefault: boolean; +}; + +export type UpdateAddressType = CreateAddressType & { + id: string; +}; + +export interface MarkerData { + position: [number, number]; + title: string; + icon?: Icon | DivIcon; +} + +export interface NominatimReverseGeocodingResponse { + place_id: number; + licence: string; + osm_type: string; + osm_id: number; + lat: string; + lon: string; + class: string; + type: string; + place_rank: number; + importance: number; + addresstype: string; + name: string; + display_name: string; + address: NominatimReverseGeocodingResponseAddress; + boundingbox: string[]; +} + +export interface NominatimReverseGeocodingResponseAddress { + road: string; + neighbourhood: string; + suburb: string; + city: string; + district: string; + county: string; + province: string; + "ISO3166-2-lvl4": string; + postcode: string; + country: string; + state: string; + country_code: string; +} diff --git a/src/app/auth/login/service/LoginService.ts b/src/app/auth/login/service/LoginService.ts index 055f1fc..c1affbc 100644 --- a/src/app/auth/login/service/LoginService.ts +++ b/src/app/auth/login/service/LoginService.ts @@ -2,11 +2,11 @@ import { api } from "@/lib/api/axiosInstance"; import { LoginOTPRequestType, LoginVerifyOTPType } from "../types/Types"; export const loginOTP = async (params: LoginOTPRequestType) => { - const { data } = await api.post("/auth/otp/request", params); + const { data } = await api.post("/public/auth/otp/request", params); return data; }; export const loginVerifyOTP = async (params: LoginVerifyOTPType) => { - const { data } = await api.post("/auth/otp/verify", params); + const { data } = await api.post("/public/auth/otp/verify", params); return data; }; diff --git a/src/components/map/CustomMap.css b/src/components/map/CustomMap.css index c8b1095..5d34378 100644 --- a/src/components/map/CustomMap.css +++ b/src/components/map/CustomMap.css @@ -1,88 +1,123 @@ .map-google-style { - background-color: var(--background); - font-family: 'irancell', system-ui, -apple-system, sans-serif; + background-color: var(--background); + font-family: "irancell", system-ui, -apple-system, sans-serif; } /* Map tiles styling */ .map-google-style .leaflet-tile-pane { - filter: saturate(0.95) contrast(0.95) brightness(1.02); + filter: saturate(0.95) contrast(0.95) brightness(1.02); } /* Base font for all map elements */ .map-google-style, .map-google-style .leaflet-container { - font-family: 'irancell', system-ui, -apple-system, sans-serif !important; + font-family: "irancell", system-ui, -apple-system, sans-serif !important; } /* Improve tile loading appearance */ .map-google-style .leaflet-tile-loading { - opacity: 0; - transition: opacity 0.2s ease-in-out; + opacity: 0; + transition: opacity 0.2s ease-in-out; } /* Custom popup style */ .map-google-style .leaflet-popup-content-wrapper { - border-radius: 16px; - padding: 0; - box-shadow: 0 4px 16px rgba(0,0,0,0.08), - 0 2px 4px rgba(0,0,0,0.05); - border: 1px solid rgba(0,0,0,0.08); - backdrop-filter: blur(8px); - background: rgba(255, 255, 255, 0.95); + border-radius: 16px; + padding: 0; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.08); + backdrop-filter: blur(8px); + background: rgba(255, 255, 255, 0.95); } .map-google-style .leaflet-popup-content { - margin: 16px 20px; - line-height: 1.6; - font-size: 14px; - color: #1f2937; - font-weight: 500; - text-align: right; + margin: 16px 20px; + line-height: 1.6; + font-size: 14px; + color: #1f2937; + font-weight: 500; + text-align: right; } .map-google-style .leaflet-popup-tip { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(8px); - box-shadow: 0 4px 16px rgba(0,0,0,0.08); - border: 1px solid rgba(0,0,0,0.08); + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.08); } .map-google-style .leaflet-popup-close-button { - padding: 12px !important; - color: #6b7280 !important; - opacity: 0.7; - transition: opacity 0.2s ease; + padding: 12px !important; + color: #6b7280 !important; + opacity: 0.7; + transition: opacity 0.2s ease; } .map-google-style .leaflet-popup-close-button:hover { - opacity: 1; + opacity: 1; } /* Marker styles */ .map-google-style .leaflet-marker-icon, .map-google-style .leaflet-marker-shadow { - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - transform-origin: bottom center; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: bottom center; } .map-google-style .leaflet-marker-icon { - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); } .map-google-style .leaflet-marker-icon:hover { - transform: scale(1.1) translateY(-4px); - filter: drop-shadow(0 4px 6px rgba(0,0,0,0.15)); + transform: scale(1.1) translateY(-4px); + filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.15)); } .map-google-style .leaflet-marker-icon:hover + .leaflet-marker-shadow { - transform: scale(1.1) translateY(-4px); - opacity: 0.6; + opacity: 0.8; +} + +/* Custom restaurant marker styles */ +.map-google-style .custom-restaurant-marker { + background: transparent !important; + border: none !important; +} + +.map-google-style .custom-restaurant-marker img { + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +/* Restaurant popup smaller size */ +.map-google-style .restaurant-popup .leaflet-popup-content-wrapper { + padding: 8px 25px 8px 12px; + position: relative; + min-height: 32px; +} + +.map-google-style .restaurant-popup .leaflet-popup-content { + margin: 0; + font-size: 12px; + line-height: 1.4; + text-align: right; +} + +.map-google-style .restaurant-popup .leaflet-popup-close-button { + position: absolute !important; + top: 4px !important; + right: 4px !important; + padding: 4px 6px !important; + font-size: 16px !important; + line-height: 1 !important; + width: 20px !important; + height: 20px !important; + text-align: center !important; + z-index: 10; } /* Attribution styling */ .map-google-style .leaflet-control-attribution { - display: none; - /* background: rgba(255,255,255,0.9) !important; + display: none; + /* background: rgba(255,255,255,0.9) !important; backdrop-filter: blur(8px); padding: 6px 12px !important; border-radius: 12px; @@ -95,179 +130,174 @@ /* Zoom controls */ .map-zoom-controls { - position: absolute; - right: 20px; - bottom: 40px; - background: var(--background); - border-radius: 16px; - box-shadow: 0 4px 16px rgba(0,0,0,0.08), - 0 2px 4px rgba(0,0,0,0.05); - border: 1px solid rgba(0,0,0,0.08); - overflow: hidden; - backdrop-filter: blur(8px); - z-index: 1000; + position: absolute; + right: 20px; + bottom: 40px; + background: var(--background); + border-radius: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.08); + overflow: hidden; + backdrop-filter: blur(8px); + z-index: 1000; } .zoom-button { - width: 44px; - height: 44px; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: none; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - color: #4b5563; - font-size: 22px; - font-weight: 500; - -webkit-tap-highlight-color: transparent; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + color: #4b5563; + font-size: 22px; + font-weight: 500; + -webkit-tap-highlight-color: transparent; } .zoom-button:first-child { - border-bottom: 1px solid rgba(0,0,0,0.08); + border-bottom: 1px solid rgba(0, 0, 0, 0.08); } .zoom-button:hover { - background-color: rgba(59, 130, 246, 0.05); - color: #2563eb; + background-color: rgba(59, 130, 246, 0.05); + color: #2563eb; } .zoom-button:active { - background-color: rgba(59, 130, 246, 0.1); + background-color: rgba(59, 130, 246, 0.1); } /* Selected location indicator */ .selected-location-indicator { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(8px); - padding: 14px 24px; - border-radius: 16px; - box-shadow: 0 4px 16px rgba(0,0,0,0.08), - 0 2px 4px rgba(0,0,0,0.05); - border: 1px solid rgba(0,0,0,0.08); - color: #1f2937; - font-size: 14px; - font-weight: 500; - line-height: 1.6; - max-width: 90%; - width: auto; - text-align: center; - z-index: 1000; - animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + padding: 14px 24px; + border-radius: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.08); + color: #1f2937; + font-size: 14px; + font-weight: 500; + line-height: 1.6; + max-width: 90%; + width: auto; + text-align: center; + z-index: 1000; + animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1); } @keyframes slideUp { - from { - transform: translate(-50%, 100%); - opacity: 0; - } - to { - transform: translate(-50%, 0); - opacity: 1; - } + from { + transform: translate(-50%, 100%); + opacity: 0; + } + to { + transform: translate(-50%, 0); + opacity: 1; + } } /* Loading state */ .map-loading { - position: absolute; - inset: 0; - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(4px); - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; } /* Container styling */ .map-container { - position: relative; - width: 100%; - height: 100%; - background: linear-gradient(to bottom, #e5e7eb 0%, #f3f4f6 100%); + position: relative; + width: 100%; + height: 100%; + background: linear-gradient(to bottom, #e5e7eb 0%, #f3f4f6 100%); } /* Search box styling */ .map-search-container { - position: absolute; - top: 20px; - left: 50%; - transform: translateX(-50%); - width: 90%; - max-width: 480px; - z-index: 1000; + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + width: 90%; + max-width: 480px; + z-index: 1000; } .map-search-input { - width: 100%; - padding: 14px 20px; - background: rgba(255, 255, 255, 0.95); - border: 1px solid rgba(0,0,0,0.08); - border-radius: 16px; - box-shadow: 0 4px 16px rgba(0,0,0,0.08), - 0 2px 4px rgba(0,0,0,0.05); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-size: 15px; - font-weight: 500; - text-align: right; - backdrop-filter: blur(8px); - color: #1f2937; + width: 100%; + padding: 14px 20px; + background: rgba(255, 255, 255, 0.95); + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 15px; + font-weight: 500; + text-align: right; + backdrop-filter: blur(8px); + color: #1f2937; } .map-search-input:focus { - box-shadow: 0 6px 24px rgba(0,0,0,0.12), - 0 2px 8px rgba(0,0,0,0.08); - border-color: rgba(59, 130, 246, 0.5); - background: rgba(255, 255, 255, 0.98); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08); + border-color: rgba(59, 130, 246, 0.5); + background: rgba(255, 255, 255, 0.98); } .map-search-input::placeholder { - color: #6b7280; - font-weight: 400; + color: #6b7280; + font-weight: 400; } .map-search-results { - margin-top: 12px; - background: var(--container); - border-radius: 16px; - box-shadow: 0 4px 20px rgba(0,0,0,0.1), - 0 2px 8px rgba(0,0,0,0.05); - overflow: hidden; - border: 1px solid rgba(0,0,0,0.08); - backdrop-filter: blur(8px); + margin-top: 12px; + background: var(--container); + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.05); + overflow: hidden; + border: 1px solid rgba(0, 0, 0, 0.08); + backdrop-filter: blur(8px); } .map-search-result-item { - padding: 14px 20px; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - border-bottom: 1px solid rgba(0,0,0,0.05); - font-size: 13px; - font-weight: 400; - color: var(--foreground); - cursor: pointer; - display: block; - width: 100%; - text-align: right; + padding: 14px 20px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + font-size: 13px; + font-weight: 400; + color: var(--foreground); + cursor: pointer; + display: block; + width: 100%; + text-align: right; } .map-search-result-item:last-child { - border-bottom: none; + border-bottom: none; } .map-search-result-item:hover { - background-color: rgba(59, 130, 246, 0.05); - color: #2563eb; + background-color: rgba(59, 130, 246, 0.05); + color: #2563eb; } [data-theme="dark"] .map-search-result-item:hover { - color: var(--disabled-text); + color: var(--disabled-text); } .map-search-result-item:active { - background-color: rgba(59, 130, 246, 0.1); + background-color: rgba(59, 130, 246, 0.1); } diff --git a/src/components/map/CustomMap.tsx b/src/components/map/CustomMap.tsx index 53cbf76..abfc922 100644 --- a/src/components/map/CustomMap.tsx +++ b/src/components/map/CustomMap.tsx @@ -15,7 +15,7 @@ interface CustomMapProps { markers?: Array<{ position: [number, number]; title: string; - icon?: L.Icon; + icon?: L.Icon | L.DivIcon; }>; onPositionSelect?: (position: [number, number]) => void; onZoomChange?: (zoom: number) => void; @@ -79,6 +79,25 @@ function MarkerUpdater({ markers }: { markers?: CustomMapProps['markers'] }) { return null; } +// Component to auto-open popup for restaurant marker +function MarkerWithAutoOpen({ position, icon, title }: { position: [number, number]; icon?: L.Icon | L.DivIcon; title: string }) { + const markerRef = React.useRef(null); + + useEffect(() => { + if (markerRef.current && title === 'موقعیت رستوران') { + markerRef.current.openPopup(); + } + }, [title]); + + return ( + + +
{title}
+
+
+ ); +} + const CustomMap: React.FC = ({ initialPosition, zoom = 13, @@ -276,15 +295,12 @@ const CustomMap: React.FC = ({ {/* Custom markers */} {markers.map((marker, index) => ( - - -
{marker.title}
-
-
+ title={marker.title} + /> ))} {/* Selected position marker */} diff --git a/src/lib/api/axiosInstance.ts b/src/lib/api/axiosInstance.ts index 08b7be3..42319f2 100644 --- a/src/lib/api/axiosInstance.ts +++ b/src/lib/api/axiosInstance.ts @@ -27,8 +27,9 @@ const processQueue = (error: unknown, token: string | null = null) => { // Attach access token to requests api.interceptors.request.use((config) => { - const state = useAuthStore.getState(); - const token = state.user?.accessToken; + const token = localStorage.getItem( + process.env.NEXT_PUBLIC_TOKEN_NAME as string + ); if (token) { config.headers = config.headers || {};