erp-platform/ui/src/views/intranet/SocialWall/LocationPicker.tsx
2025-10-20 21:38:21 +03:00

378 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react'
import { motion } from 'framer-motion'
import { FaTimes, FaSearch, FaMapMarkerAlt } from 'react-icons/fa'
import classNames from 'classnames'
import { Location } from '@/types/intranet'
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"
>
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Search */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<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">
<FaMapMarkerAlt className="w-16 h-16 mx-auto mb-4 text-red-400" />
<p className="text-red-500 dark:text-red-400">{error}</p>
</div>
) : searchQuery.trim() === '' ? (
<div className="text-center py-12">
<FaSearch className="w-16 h-16 mx-auto mb-4 text-gray-400" />
<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">
<FaMapMarkerAlt className="w-16 h-16 mx-auto mb-4 text-gray-400" />
<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">
<FaMapMarkerAlt
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">
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
<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