profile + update address with location
This commit is contained in:
Generated
+44
@@ -17,11 +17,13 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tanstack/react-query": "^5.84.1",
|
||||
"@tanstack/react-query-devtools": "^5.85.5",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.539.0",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"next": "^15.5.3",
|
||||
@@ -30,6 +32,7 @@
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-spinners": "^0.17.0",
|
||||
"swiper": "^11.2.10",
|
||||
@@ -1765,6 +1768,16 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -2141,6 +2154,11 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -2155,6 +2173,14 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
|
||||
@@ -5231,6 +5257,11 @@
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -6259,6 +6290,19 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"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",
|
||||
"@tanstack/react-query": "^5.84.1",
|
||||
"@tanstack/react-query-devtools": "^5.85.5",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.539.0",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"next": "^15.5.3",
|
||||
@@ -31,6 +33,7 @@
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-spinners": "^0.17.0",
|
||||
"swiper": "^11.2.10",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "leaflet/dist/leaflet.css";
|
||||
|
||||
/* بهبود responsive design برای موبایل */
|
||||
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 * as api from "../service/Service";
|
||||
import { useSharedStore } from "@/share/store/sharedStore";
|
||||
import { UpdateProfileRequest } from "../types/ProfileTypes";
|
||||
import { UpdateAddressType, UpdateProfileRequest } from "../types/ProfileTypes";
|
||||
|
||||
export const useGetProfile = () => {
|
||||
const { isLogin } = useSharedStore();
|
||||
@@ -21,8 +21,20 @@ export const useUpdateProfile = () => {
|
||||
onSuccess: () => {
|
||||
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 { Edit2 } from 'lucide-react'
|
||||
import Layout from '@/hoc/Layout'
|
||||
import { Heart, ShoppingCart } from 'iconsax-react'
|
||||
import { Heart, ShoppingCart, Edit } from 'iconsax-react'
|
||||
import Menu from './components/Menu'
|
||||
import AddressModal from './components/AddressModal'
|
||||
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
||||
import { UpdateAddressType } from './types/ProfileTypes'
|
||||
|
||||
// Schema validation
|
||||
const profileSchema = yup.object({
|
||||
@@ -30,6 +32,7 @@ const ProfilePage = () => {
|
||||
const { data: profile, isLoading } = useGetProfile()
|
||||
const updateProfileMutation = useUpdateProfile()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isAddressModalOpen, setIsAddressModalOpen] = useState(false)
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -76,6 +79,11 @@ const ProfilePage = () => {
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleAddressChange = async (addressData: UpdateAddressType) => {
|
||||
setValue('address', addressData.address)
|
||||
await updateProfileMutation.mutateAsync({ address: addressData.address })
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-3 sm:p-4 md:p-6">
|
||||
@@ -103,7 +111,7 @@ const ProfilePage = () => {
|
||||
variant="outline"
|
||||
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>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -147,15 +155,6 @@ const ProfilePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 sm:mt-4">
|
||||
<Input
|
||||
label="آدرس"
|
||||
{...register('address')}
|
||||
readOnly={!isEditing}
|
||||
error_text={errors.address?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="mt-4 sm:mt-6 flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
@@ -177,6 +176,25 @@ const ProfilePage = () => {
|
||||
)}
|
||||
</form>
|
||||
</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 className='flex-1 flex lg:flex-row flex-col gap-6'>
|
||||
@@ -205,6 +223,14 @@ const ProfilePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address Modal */}
|
||||
<AddressModal
|
||||
isOpen={isAddressModalOpen}
|
||||
onClose={() => setIsAddressModalOpen(false)}
|
||||
currentAddress={profile?.results?.info?.userAddress?.address || ''}
|
||||
onAddressChange={handleAddressChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UserMeResponseType } from "@/share/types/SharedTypes";
|
||||
import {
|
||||
UpdateProfileRequest,
|
||||
UpdateProfileResponse,
|
||||
AddressLocationResponse,
|
||||
UpdateAddressType,
|
||||
} from "../types/ProfileTypes";
|
||||
|
||||
export const getProfile = async () => {
|
||||
@@ -17,3 +19,15 @@ export const updateProfile = async (profileData: UpdateProfileRequest) => {
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
status: number;
|
||||
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