profile + update address with location
This commit is contained in:
Generated
+44
@@ -17,11 +17,13 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tanstack/react-query": "^5.84.1",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
"@tanstack/react-query-devtools": "^5.85.5",
|
"@tanstack/react-query-devtools": "^5.85.5",
|
||||||
|
"@types/leaflet": "^1.9.20",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"next": "^15.5.3",
|
"next": "^15.5.3",
|
||||||
@@ -30,6 +32,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
@@ -1765,6 +1768,16 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-leaflet/core": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -2141,6 +2154,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -2155,6 +2173,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/leaflet": {
|
||||||
|
"version": "1.9.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz",
|
||||||
|
"integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/moment-jalaali": {
|
"node_modules/@types/moment-jalaali": {
|
||||||
"version": "0.7.9",
|
"version": "0.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
|
||||||
@@ -5231,6 +5257,11 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -6259,6 +6290,19 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-leaflet": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-otp-input": {
|
"node_modules/react-otp-input": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tanstack/react-query": "^5.84.1",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
"@tanstack/react-query-devtools": "^5.85.5",
|
"@tanstack/react-query-devtools": "^5.85.5",
|
||||||
|
"@types/leaflet": "^1.9.20",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"next": "^15.5.3",
|
"next": "^15.5.3",
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
/* بهبود responsive design برای موبایل */
|
/* بهبود responsive design برای موبایل */
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import { Location, TickCircle, CloseCircle, ArrowRight2, ArrowLeft2 } from 'iconsax-react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import { useGetAddressByLocation, useUpdateAddress } from '../hooks/useProfileData'
|
||||||
|
import { UpdateAddressType } from '../types/ProfileTypes'
|
||||||
|
import type { Map as LeafletMap, LeafletMouseEvent, Marker } from 'leaflet'
|
||||||
|
|
||||||
|
interface AddressModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
currentAddress?: string
|
||||||
|
onAddressChange: (addressData: UpdateAddressType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddressModal: React.FC<AddressModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onAddressChange
|
||||||
|
}) => {
|
||||||
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number } | null>(null)
|
||||||
|
const [isMapLoaded, setIsMapLoaded] = useState(false)
|
||||||
|
const mapRef = useRef<HTMLDivElement>(null)
|
||||||
|
const leafletMapRef = useRef<LeafletMap | null>(null)
|
||||||
|
const currentMarkerRef = useRef<Marker | null>(null)
|
||||||
|
|
||||||
|
// React Hook Form
|
||||||
|
const { register, handleSubmit, setValue, formState: { errors, isSubmitting } } = useForm<UpdateAddressType>({
|
||||||
|
defaultValues: {
|
||||||
|
lat: '',
|
||||||
|
lon: '',
|
||||||
|
provinceId: 0,
|
||||||
|
cityId: 0,
|
||||||
|
postalCode: '',
|
||||||
|
address: '',
|
||||||
|
plaque: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get address by location
|
||||||
|
const { data: addressData, isLoading: isLoadingAddress } = useGetAddressByLocation(
|
||||||
|
selectedLocation?.lat,
|
||||||
|
selectedLocation?.lng
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update address mutation
|
||||||
|
const updateAddressMutation = useUpdateAddress()
|
||||||
|
|
||||||
|
const loadMap = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const L = await import('leaflet')
|
||||||
|
|
||||||
|
// Fix default icon issue
|
||||||
|
delete (L.Icon.Default.prototype as unknown as { _getIconUrl: unknown })._getIconUrl
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (mapRef.current && !leafletMapRef.current) {
|
||||||
|
// Get initial coordinates (user's location or default to Tehran)
|
||||||
|
const initialLat = selectedLocation?.lat || 35.6892
|
||||||
|
const initialLng = selectedLocation?.lng || 51.3890
|
||||||
|
|
||||||
|
// Create map
|
||||||
|
leafletMapRef.current = L.map(mapRef.current).setView([initialLat, initialLng], 13)
|
||||||
|
|
||||||
|
// Add tile layer
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(leafletMapRef.current)
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
leafletMapRef.current.on('click', (e: LeafletMouseEvent) => {
|
||||||
|
const { lat, lng } = e.latlng
|
||||||
|
handleMapClick(lat, lng)
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsMapLoaded(true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading map:', error)
|
||||||
|
}
|
||||||
|
}, [selectedLocation?.lat, selectedLocation?.lng])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && typeof window !== 'undefined' && !isMapLoaded) {
|
||||||
|
loadMap()
|
||||||
|
}
|
||||||
|
}, [isOpen, isMapLoaded, loadMap])
|
||||||
|
|
||||||
|
// Get user's current location on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
const { latitude, longitude } = position.coords
|
||||||
|
setSelectedLocation({ lat: latitude, lng: longitude })
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error getting current location:', error)
|
||||||
|
// Fallback to Tehran coordinates
|
||||||
|
setSelectedLocation({ lat: 35.6892, lng: 51.3890 })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
// Update map view when selectedLocation changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (leafletMapRef.current && selectedLocation) {
|
||||||
|
leafletMapRef.current.setView([selectedLocation.lat, selectedLocation.lng], 13)
|
||||||
|
}
|
||||||
|
}, [selectedLocation])
|
||||||
|
|
||||||
|
const handleMapClick = async (lat: number, lng: number) => {
|
||||||
|
// Add marker if map exists
|
||||||
|
if (leafletMapRef.current) {
|
||||||
|
const L = await import('leaflet')
|
||||||
|
|
||||||
|
// Remove existing marker
|
||||||
|
if (currentMarkerRef.current) {
|
||||||
|
leafletMapRef.current.removeLayer(currentMarkerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new marker
|
||||||
|
currentMarkerRef.current = L.marker([lat, lng]).addTo(leafletMapRef.current)
|
||||||
|
currentMarkerRef.current.bindPopup(`
|
||||||
|
<div style="text-align: center; padding: 8px;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 8px;">
|
||||||
|
<span style="color: #16a34a; font-weight: 500;">آدرس شما اینجاست!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).openPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set selected location
|
||||||
|
setSelectedLocation({ lat, lng })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update form data when address data is received
|
||||||
|
useEffect(() => {
|
||||||
|
if (addressData?.results) {
|
||||||
|
setValue('address', addressData.results.address)
|
||||||
|
setValue('provinceId', addressData.results.provinceId)
|
||||||
|
setValue('cityId', addressData.results.cityId)
|
||||||
|
}
|
||||||
|
}, [addressData, setValue])
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
if (selectedLocation) {
|
||||||
|
setCurrentStep(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrevStep = () => {
|
||||||
|
setCurrentStep(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdateAddressType) => {
|
||||||
|
if (selectedLocation) {
|
||||||
|
try {
|
||||||
|
// Prepare data for API
|
||||||
|
const updateData: UpdateAddressType = {
|
||||||
|
...data,
|
||||||
|
lat: selectedLocation.lat.toString(),
|
||||||
|
lon: selectedLocation.lng.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call update address API
|
||||||
|
await updateAddressMutation.mutateAsync(updateData)
|
||||||
|
|
||||||
|
// Call parent callback with form data
|
||||||
|
onAddressChange(updateData)
|
||||||
|
handleClose()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating address:', error)
|
||||||
|
// You can add toast notification here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// Reset state when closing
|
||||||
|
setCurrentStep(1)
|
||||||
|
setSelectedLocation(null)
|
||||||
|
// Reset form
|
||||||
|
setValue('lat', '')
|
||||||
|
setValue('lon', '')
|
||||||
|
setValue('provinceId', 0)
|
||||||
|
setValue('cityId', 0)
|
||||||
|
setValue('postalCode', '')
|
||||||
|
setValue('address', '')
|
||||||
|
setValue('plaque', '')
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentLocation = () => {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
const { latitude, longitude } = position.coords
|
||||||
|
handleMapClick(latitude, longitude)
|
||||||
|
|
||||||
|
// Pan map to current location
|
||||||
|
if (leafletMapRef.current) {
|
||||||
|
leafletMapRef.current.setView([latitude, longitude], 16)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error getting location:', error)
|
||||||
|
alert('خطا در دریافت موقعیت فعلی')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
alert('مرورگر شما از موقعیتیابی پشتیبانی نمیکند')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
setSelectedLocation(null)
|
||||||
|
|
||||||
|
// Remove marker from map
|
||||||
|
if (leafletMapRef.current && currentMarkerRef.current) {
|
||||||
|
leafletMapRef.current.removeLayer(currentMarkerRef.current)
|
||||||
|
currentMarkerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (leafletMapRef.current) {
|
||||||
|
leafletMapRef.current.remove()
|
||||||
|
leafletMapRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const renderStep1 = () => (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Subtitle */}
|
||||||
|
<p className="text-sm text-gray-600 mb-4 text-center">
|
||||||
|
موقعیت خود را روی نقشه انتخاب کنید
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Selected Location Display */}
|
||||||
|
{selectedLocation && (
|
||||||
|
<div className="mt-4 p-4 bg-gradient-to-r from-green-50 to-blue-50 rounded-lg border border-green-200">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
<TickCircle size={20} className="text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium text-green-800">موقعیت انتخاب شده:</span>
|
||||||
|
<button
|
||||||
|
onClick={handleClearSelection}
|
||||||
|
className="text-red-400 hover:text-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 leading-relaxed">
|
||||||
|
{isLoadingAddress ? 'در حال دریافت آدرس...' :
|
||||||
|
addressData?.results?.address ||
|
||||||
|
`عرض جغرافیایی: ${selectedLocation.lat.toFixed(6)}, طول جغرافیایی: ${selectedLocation.lng.toFixed(6)}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 justify-end mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full sm:w-auto order-2 sm:order-1"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleNextStep}
|
||||||
|
className="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 order-1 sm:order-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={!selectedLocation}
|
||||||
|
>
|
||||||
|
مرحله بعد
|
||||||
|
<ArrowLeft2 size={20} color='white' className="mr-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderStep2 = () => (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col h-full">
|
||||||
|
{/* Subtitle */}
|
||||||
|
<p className="text-sm text-gray-600 mb-4 text-center">
|
||||||
|
آدرس جدید خود را وارد کنید
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Address Form */}
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
{/* Main Address Field */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
آدرس کامل
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
{...register('address', { required: 'آدرس کامل الزامی است' })}
|
||||||
|
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||||
|
rows={4}
|
||||||
|
placeholder="آدرس کامل خود را وارد کنید"
|
||||||
|
/>
|
||||||
|
{errors.address && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{errors.address.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Province and City Row - Readonly */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
شهر
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={addressData?.results?.city || ''}
|
||||||
|
readOnly
|
||||||
|
className="w-full bg-gray-50 cursor-not-allowed"
|
||||||
|
placeholder="شهر"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
استان
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={addressData?.results?.province || ''}
|
||||||
|
readOnly
|
||||||
|
className="w-full bg-gray-50 cursor-not-allowed"
|
||||||
|
placeholder="استان"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Postal Code and Plaque Row */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
کد پستی
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...register('postalCode', { required: 'کد پستی الزامی است' })}
|
||||||
|
placeholder="کد پستی"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
{errors.postalCode && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{errors.postalCode.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
پلاک
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...register('plaque', { required: 'پلاک الزامی است' })}
|
||||||
|
placeholder="پلاک"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
{errors.plaque && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{errors.plaque.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 justify-end mt-6">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handlePrevStep}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full sm:w-auto order-2 sm:order-1"
|
||||||
|
>
|
||||||
|
<ArrowRight2 color='black' size={16} className="ml-1" />
|
||||||
|
مرحله قبل
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full sm:w-auto order-3 sm:order-2"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 order-1 sm:order-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isSubmitting || updateAddressMutation.isPending}
|
||||||
|
>
|
||||||
|
<TickCircle color='white' size={16} className="ml-1" />
|
||||||
|
{isSubmitting || updateAddressMutation.isPending ? 'در حال ذخیره...' : 'تایید'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={isOpen}
|
||||||
|
close={handleClose}
|
||||||
|
isHeader={true}
|
||||||
|
title_header="تغییر آدرس"
|
||||||
|
width={500}
|
||||||
|
>
|
||||||
|
{/* Map Container - Only show in step 1 */}
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div className="flex-1 relative rounded-lg overflow-hidden border border-gray-200 bg-gray-100 min-h-[300px]">
|
||||||
|
<div
|
||||||
|
ref={mapRef}
|
||||||
|
className="h-full w-full rounded-lg"
|
||||||
|
style={{ minHeight: '300px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isMapLoaded && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
||||||
|
<div className="text-center text-gray-500">
|
||||||
|
<Location size={48} color="#015699" className="mx-auto mb-4" />
|
||||||
|
<p className="text-lg">در حال بارگذاری نقشه...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current Location Button Overlay */}
|
||||||
|
<div className="absolute bottom-4 left-4 z-[1000]">
|
||||||
|
<Button
|
||||||
|
onClick={handleCurrentLocation}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Location color={'black'} size={20} className="ml-0.5" />
|
||||||
|
موقعیت فعلی
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 1 ? renderStep1() : renderStep2()}
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddressModal
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/Service";
|
import * as api from "../service/Service";
|
||||||
import { useSharedStore } from "@/share/store/sharedStore";
|
import { useSharedStore } from "@/share/store/sharedStore";
|
||||||
import { UpdateProfileRequest } from "../types/ProfileTypes";
|
import { UpdateAddressType, UpdateProfileRequest } from "../types/ProfileTypes";
|
||||||
|
|
||||||
export const useGetProfile = () => {
|
export const useGetProfile = () => {
|
||||||
const { isLogin } = useSharedStore();
|
const { isLogin } = useSharedStore();
|
||||||
@@ -21,8 +21,20 @@ export const useUpdateProfile = () => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
});
|
||||||
console.error("خطا در بهروزرسانی پروفایل:", error);
|
};
|
||||||
},
|
|
||||||
|
export const useGetAddressByLocation = (lat?: number, lon?: number) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["address-by-location", lat, lon],
|
||||||
|
queryFn: () => api.getAddressByLocation(lat!, lon!),
|
||||||
|
enabled: lat !== undefined && lon !== undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateAddress = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (addressData: UpdateAddressType) =>
|
||||||
|
api.updateAddress(addressData),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+37
-11
@@ -7,9 +7,11 @@ import { Button } from '@/components/ui/button'
|
|||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import { Edit2 } from 'lucide-react'
|
import { Edit2 } from 'lucide-react'
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
import { Heart, ShoppingCart } from 'iconsax-react'
|
import { Heart, ShoppingCart, Edit } from 'iconsax-react'
|
||||||
import Menu from './components/Menu'
|
import Menu from './components/Menu'
|
||||||
|
import AddressModal from './components/AddressModal'
|
||||||
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
||||||
|
import { UpdateAddressType } from './types/ProfileTypes'
|
||||||
|
|
||||||
// Schema validation
|
// Schema validation
|
||||||
const profileSchema = yup.object({
|
const profileSchema = yup.object({
|
||||||
@@ -30,6 +32,7 @@ const ProfilePage = () => {
|
|||||||
const { data: profile, isLoading } = useGetProfile()
|
const { data: profile, isLoading } = useGetProfile()
|
||||||
const updateProfileMutation = useUpdateProfile()
|
const updateProfileMutation = useUpdateProfile()
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [isAddressModalOpen, setIsAddressModalOpen] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -76,6 +79,11 @@ const ProfilePage = () => {
|
|||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddressChange = async (addressData: UpdateAddressType) => {
|
||||||
|
setValue('address', addressData.address)
|
||||||
|
await updateProfileMutation.mutateAsync({ address: addressData.address })
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-3 sm:p-4 md:p-6">
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
@@ -103,7 +111,7 @@ const ProfilePage = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2 text-sm sm:text-base"
|
className="flex items-center gap-2 text-sm sm:text-base"
|
||||||
>
|
>
|
||||||
<Edit2 size={16} />
|
<Edit2 color='black' size={16} />
|
||||||
<span className="hidden sm:inline">ویرایش</span>
|
<span className="hidden sm:inline">ویرایش</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,15 +155,6 @@ const ProfilePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 sm:mt-4">
|
|
||||||
<Input
|
|
||||||
label="آدرس"
|
|
||||||
{...register('address')}
|
|
||||||
readOnly={!isEditing}
|
|
||||||
error_text={errors.address?.message}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className="mt-4 sm:mt-6 flex flex-col sm:flex-row gap-3">
|
<div className="mt-4 sm:mt-6 flex flex-col sm:flex-row gap-3">
|
||||||
<Button
|
<Button
|
||||||
@@ -177,6 +176,25 @@ const ProfilePage = () => {
|
|||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Address Section */}
|
||||||
|
<div className="bg-[#FAFAFA] rounded-lg p-3 sm:p-4 md:p-6 mt-4 sm:mt-6">
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-4 sm:mb-6 gap-3 sm:gap-0">
|
||||||
|
<h2 className="text-base sm:text-lg font-bold text-gray-900">آدرس</h2>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsAddressModalOpen(true)}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
<Edit2 color='black' size={16} />
|
||||||
|
<span className="hidden sm:inline">ویرایش</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 rounded-lg p-3 text-sm text-gray-600 min-h-[60px] flex items-center">
|
||||||
|
{profile?.results?.info?.userAddress?.address || 'آدرسی وارد نشده است'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-1 flex lg:flex-row flex-col gap-6'>
|
<div className='flex-1 flex lg:flex-row flex-col gap-6'>
|
||||||
@@ -205,6 +223,14 @@ const ProfilePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Address Modal */}
|
||||||
|
<AddressModal
|
||||||
|
isOpen={isAddressModalOpen}
|
||||||
|
onClose={() => setIsAddressModalOpen(false)}
|
||||||
|
currentAddress={profile?.results?.info?.userAddress?.address || ''}
|
||||||
|
onAddressChange={handleAddressChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { UserMeResponseType } from "@/share/types/SharedTypes";
|
|||||||
import {
|
import {
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UpdateProfileResponse,
|
UpdateProfileResponse,
|
||||||
|
AddressLocationResponse,
|
||||||
|
UpdateAddressType,
|
||||||
} from "../types/ProfileTypes";
|
} from "../types/ProfileTypes";
|
||||||
|
|
||||||
export const getProfile = async () => {
|
export const getProfile = async () => {
|
||||||
@@ -17,3 +19,15 @@ export const updateProfile = async (profileData: UpdateProfileRequest) => {
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAddressByLocation = async (lat: number, lon: number) => {
|
||||||
|
const { data } = await axios.get<AddressLocationResponse>(
|
||||||
|
`/address/location/reverse?lat=${lat}&lon=${lon}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateAddress = async (addressData: UpdateAddressType) => {
|
||||||
|
const { data } = await axios.post("/address/user/save", addressData);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,28 @@ export interface UpdateProfileRequest {
|
|||||||
address?: string;
|
address?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddressFormData {
|
||||||
|
address: string;
|
||||||
|
mobileNumber: string;
|
||||||
|
province: string;
|
||||||
|
city: string;
|
||||||
|
postalCode: string;
|
||||||
|
plaque: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressLocationResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
province: string;
|
||||||
|
provinceId: number;
|
||||||
|
city: string;
|
||||||
|
cityId: number;
|
||||||
|
address: string;
|
||||||
|
region: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateProfileResponse {
|
export interface UpdateProfileResponse {
|
||||||
status: number;
|
status: number;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -58,3 +80,13 @@ export interface UpdateProfileResponse {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateAddressType = {
|
||||||
|
lat: string;
|
||||||
|
lon: string;
|
||||||
|
provinceId: number;
|
||||||
|
cityId: number;
|
||||||
|
postalCode: string;
|
||||||
|
address: string;
|
||||||
|
plaque: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user