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 (
+
+ );
+};
+
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={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 || {};