erp-platform/ui/src/views/intranet/SocialWall/LocationPicker.tsx

379 lines
14 KiB
TypeScript
Raw Normal View History

2025-10-18 20:04:24 +00:00
import React, { useState, useEffect, useRef } from 'react'
import { motion } from 'framer-motion'
2025-10-20 18:38:21 +00:00
import { FaTimes, FaSearch, FaMapMarkerAlt } from 'react-icons/fa'
2025-10-18 20:04:24 +00:00
import classNames from 'classnames'
2025-10-20 18:38:21 +00:00
import { Location } from '@/types/intranet'
2025-10-18 20:04:24 +00:00
interface LocationPickerProps {
onSelect: (location: Location) => void
onClose: () => void
}
// Google Maps API key - .env dosyasından alınmalı
const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
declare global {
interface Window {
google: any
initGoogleMaps?: () => void
}
}
const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) => {
const [searchQuery, setSearchQuery] = useState('')
const [locations, setLocations] = useState<Location[]>([])
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const autocompleteServiceRef = useRef<any>(null)
const placesServiceRef = useRef<any>(null)
const debounceTimerRef = useRef<NodeJS.Timeout>()
const scriptLoadedRef = useRef(false)
// Google Maps SDK'yı yükle
useEffect(() => {
if (scriptLoadedRef.current) return
const loadGoogleMaps = () => {
if (window.google && window.google.maps && window.google.maps.places) {
setIsGoogleLoaded(true)
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
const mapDiv = document.createElement('div')
const map = new window.google.maps.Map(mapDiv)
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
return
}
if (!GOOGLE_API_KEY) {
setError('Google Maps API anahtarı bulunamadı. Lütfen .env dosyasına VITE_GOOGLE_MAPS_API_KEY ekleyin.')
return
}
// Script zaten yüklendiyse sadece bekle
const existingScript = document.querySelector('script[src*="maps.googleapis.com"]')
if (existingScript) {
const checkInterval = setInterval(() => {
if (window.google && window.google.maps && window.google.maps.places) {
clearInterval(checkInterval)
setIsGoogleLoaded(true)
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
const mapDiv = document.createElement('div')
const map = new window.google.maps.Map(mapDiv)
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
}
}, 100)
return
}
// Yeni script ekle
const script = document.createElement('script')
script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&libraries=places&language=tr`
script.async = true
script.defer = true
script.onload = () => {
if (window.google && window.google.maps && window.google.maps.places) {
setIsGoogleLoaded(true)
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
const mapDiv = document.createElement('div')
const map = new window.google.maps.Map(mapDiv)
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
}
}
script.onerror = () => {
setError('Google Maps yüklenemedi. Lütfen internet bağlantınızı kontrol edin.')
}
document.head.appendChild(script)
scriptLoadedRef.current = true
}
loadGoogleMaps()
}, [])
useEffect(() => {
searchInputRef.current?.focus()
}, [])
// Google Places Autocomplete ile konum arama
useEffect(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
if (searchQuery.trim() === '') {
setLocations([])
setError(null)
return
}
if (!isGoogleLoaded) {
return
}
debounceTimerRef.current = setTimeout(async () => {
setIsLoading(true)
setError(null)
try {
// Google Places Autocomplete Service kullan (CORS yok)
autocompleteServiceRef.current.getPlacePredictions(
{
input: searchQuery,
componentRestrictions: { country: 'tr' },
language: 'tr'
},
async (predictions: any, status: any) => {
if (status === window.google.maps.places.PlacesServiceStatus.ZERO_RESULTS) {
setLocations([])
setIsLoading(false)
return
}
if (status !== window.google.maps.places.PlacesServiceStatus.OK) {
setError('Konum arama başarısız')
setIsLoading(false)
return
}
if (!predictions || predictions.length === 0) {
setLocations([])
setIsLoading(false)
return
}
// Her bir prediction için detaylı bilgi al
const detailedLocations: Location[] = []
let completed = 0
predictions.forEach((prediction: any) => {
placesServiceRef.current.getDetails(
{
placeId: prediction.place_id,
fields: ['name', 'formatted_address', 'geometry', 'place_id']
},
(place: any, placeStatus: any) => {
completed++
if (placeStatus === window.google.maps.places.PlacesServiceStatus.OK && place) {
detailedLocations.push({
id: place.place_id,
name: place.name,
address: place.formatted_address,
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
placeId: place.place_id
})
}
// Tüm istekler tamamlandıysa state'i güncelle
if (completed === predictions.length) {
setLocations(detailedLocations)
setIsLoading(false)
}
}
)
})
}
)
} catch (err) {
console.error('Location search error:', err)
setError('Konum arama sırasında bir hata oluştu')
setIsLoading(false)
}
}, 500) // 500ms debounce
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [searchQuery, isGoogleLoaded])
const handleSelect = (location: Location) => {
setSelectedLocation(location)
}
const handleConfirm = () => {
if (selectedLocation) {
onSelect(selectedLocation)
onClose()
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Konum Ekle</h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
>
2025-10-20 18:38:21 +00:00
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
2025-10-18 20:04:24 +00:00
</button>
</div>
{/* Search */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative">
2025-10-20 18:38:21 +00:00
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
2025-10-18 20:04:24 +00:00
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Konum ara..."
disabled={!isGoogleLoaded}
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
/>
</div>
{!isGoogleLoaded && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Google Maps yükleniyor...
</p>
)}
</div>
{/* Location List */}
<div className="flex-1 overflow-y-auto p-4">
{!isGoogleLoaded ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">Google Maps yükleniyor...</p>
</div>
) : isLoading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">Konumlar aranıyor...</p>
</div>
) : error ? (
<div className="text-center py-12">
2025-10-20 18:38:21 +00:00
<FaMapMarkerAlt className="w-16 h-16 mx-auto mb-4 text-red-400" />
2025-10-18 20:04:24 +00:00
<p className="text-red-500 dark:text-red-400">{error}</p>
</div>
) : searchQuery.trim() === '' ? (
<div className="text-center py-12">
2025-10-20 18:38:21 +00:00
<FaSearch className="w-16 h-16 mx-auto mb-4 text-gray-400" />
2025-10-18 20:04:24 +00:00
<p className="text-gray-500 dark:text-gray-400">
Aramak istediğiniz konumu yazın
</p>
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
Örn: Taksim, İstanbul
</p>
</div>
) : locations.length === 0 ? (
<div className="text-center py-12">
2025-10-20 18:38:21 +00:00
<FaMapMarkerAlt className="w-16 h-16 mx-auto mb-4 text-gray-400" />
2025-10-18 20:04:24 +00:00
<p className="text-gray-500 dark:text-gray-400">
Konum bulunamadı. Farklı bir arama yapın.
</p>
</div>
) : (
<div className="space-y-2">
{locations.map((location) => (
<button
key={location.id}
onClick={() => handleSelect(location)}
className={classNames(
'w-full text-left p-3 rounded-lg transition-all hover:bg-gray-50 dark:hover:bg-gray-700',
selectedLocation?.id === location.id
? 'bg-blue-50 dark:bg-blue-900/30 border-2 border-blue-500'
: 'border-2 border-transparent'
)}
>
<div className="flex items-start gap-3">
<div className="mt-1">
2025-10-20 18:38:21 +00:00
<FaMapMarkerAlt
2025-10-18 20:04:24 +00:00
className={classNames(
'w-5 h-5',
selectedLocation?.id === location.id
? 'text-blue-600'
: 'text-gray-400'
)}
/>
</div>
<div className="flex-1 min-w-0">
<h3
className={classNames(
'font-semibold mb-1',
selectedLocation?.id === location.id
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-900 dark:text-gray-100'
)}
>
{location.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{location.address}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{location.lat.toFixed(4)}, {location.lng.toFixed(4)}
</p>
</div>
{selectedLocation?.id === location.id && (
<div className="mt-1">
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
</div>
)}
</div>
</button>
))}
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750">
<div className="text-sm text-gray-600 dark:text-gray-400">
{selectedLocation ? (
<span className="flex items-center gap-2">
2025-10-20 18:38:21 +00:00
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
2025-10-18 20:04:24 +00:00
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedLocation.name}
</span>
</span>
) : (
<span>Bir konum seçin</span>
)}
</div>
<div className="flex gap-2">
<button
onClick={onClose}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
İptal
</button>
<button
onClick={handleConfirm}
disabled={!selectedLocation}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
Ekle
</button>
</div>
</div>
</motion.div>
</div>
)
}
export default LocationPicker