From bc22bbd6d98a50ccdfbe21a107397ab9218666d6 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 17 Sep 2025 10:53:52 +0330 Subject: [PATCH] profile + update address with location --- package-lock.json | 44 ++ package.json | 3 + src/app/globals.css | 1 + src/app/profile/components/AddressModal.tsx | 458 ++++++++++++++++++++ src/app/profile/hooks/useProfileData.ts | 20 +- src/app/profile/page.tsx | 48 +- src/app/profile/service/Service.ts | 14 + src/app/profile/types/ProfileTypes.ts | 32 ++ 8 files changed, 605 insertions(+), 15 deletions(-) create mode 100644 src/app/profile/components/AddressModal.tsx diff --git a/package-lock.json b/package-lock.json index 4f52e80..7706633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c33b32f..acaf2d6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/globals.css b/src/app/globals.css index 79f1518..b918994 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import "leaflet/dist/leaflet.css"; /* بهبود responsive design برای موبایل */ html { diff --git a/src/app/profile/components/AddressModal.tsx b/src/app/profile/components/AddressModal.tsx new file mode 100644 index 0000000..5fa2a42 --- /dev/null +++ b/src/app/profile/components/AddressModal.tsx @@ -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 = ({ + 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(null) + const leafletMapRef = useRef(null) + const currentMarkerRef = useRef(null) + + // React Hook Form + const { register, handleSubmit, setValue, formState: { errors, isSubmitting } } = useForm({ + 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: '© OpenStreetMap 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(` +
+
+ آدرس شما اینجاست! +
+
+ `).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 = () => ( +
+ {/* Subtitle */} +

+ موقعیت خود را روی نقشه انتخاب کنید +

+ + {/* Selected Location Display */} + {selectedLocation && ( +
+
+
+ +
+
+
+ موقعیت انتخاب شده: + +
+

+ {isLoadingAddress ? 'در حال دریافت آدرس...' : + addressData?.results?.address || + `عرض جغرافیایی: ${selectedLocation.lat.toFixed(6)}, طول جغرافیایی: ${selectedLocation.lng.toFixed(6)}`} +

+
+
+
+ )} + + {/* Action Buttons */} +
+ + +
+
+ ) + + const renderStep2 = () => ( +
+ {/* Subtitle */} +

+ آدرس جدید خود را وارد کنید +

+ + {/* Address Form */} +
+ {/* Main Address Field */} +
+ +