import React, { useEffect, useState } from 'react'; import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents, Tooltip } from 'react-leaflet'; import _MarkerClusterGroup from 'react-leaflet-cluster'; const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { useTourStore } from '@/store/useTourStore'; import { X, Navigation, Image as ImageIcon, LogOut, Settings, Edit2, Trash2, Share2, Filter, Tag as TagIcon, MapPin, Loader2 } from 'lucide-react'; import { UserManagementModal } from '@/components/UserManagementModal'; import { useNotification } from '@/hooks/useNotification'; import { CreateTourModal } from '../components/CreateTourModal'; // Fix lỗi icon mặc định của Leaflet const DefaultIcon = L.icon({ iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], }); L.Marker.prototype.options.icon = DefaultIcon; // Component Helper để cập nhật tâm bản đồ khi vị trí người dùng thay đổi function RecenterMap({ position }: { position: [number, number] }) { const map = useMap(); useEffect(() => { map.setView(position, map.getZoom()); }, [position, map]); return null; } // Component Helper để đóng menu khi tương tác với bản đồ function MapEvents({ onMapAction }: { onMapAction: () => void }) { useMapEvents({ click: () => onMapAction(), movestart: onMapAction, dragstart: onMapAction, }); return null; } // Component Helper để theo dõi sự di chuyển của người dùng trên bản đồ function MapTracker() { const setMapCenter = useTourStore(state => state.setMapCenter); useMapEvents({ moveend: (e) => { const map = e.target; const center = map.getCenter(); const zoom = map.getZoom(); const coords: [number, number] = [center.lat, center.lng]; setMapCenter(coords); // Lưu vị trí và mức zoom vào localStorage để sử dụng cho lần sau localStorage.setItem('map_view_state', JSON.stringify({ center: coords, zoom })); }, }); return null; } export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void }) => { // Tối ưu hóa việc lấy dữ liệu từ store bằng selectors để tránh re-render thừa const publicTours = useTourStore(state => state.publicTours); const fetchPublicTours = useTourStore(state => state.fetchPublicTours); const fetchTour = useTourStore(state => state.fetchTour); const setMapCenter = useTourStore(state => state.setMapCenter); const notify = useNotification(); // Khởi tạo vị trí từ localStorage nếu có, nếu không dùng mặc định (TP.HCM) const [initialViewState] = useState(() => { const saved = localStorage.getItem('map_view_state'); if (saved) { try { return JSON.parse(saved); } catch (e) { return null; } } return null; }); const [userPos, setUserPos] = useState<[number, number]>(initialViewState?.center || [10.7769, 106.7009]); const [mapZoom] = useState(initialViewState?.zoom || 13); const [isAdminModalOpen, setIsAdminModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); // State để điều khiển hiển thị dropdown lọc const [searchQuery, setSearchQuery] = useState(''); // State cho giá trị tìm kiếm const [suggestions, setSuggestions] = useState<{ type: 'tour' | 'location', id: string, name: string, lat?: number, lon?: number }[]>([]); const [isSearchingSuggestions, setIsSearchingSuggestions] = useState(false); // Logic xử lý gợi ý tự động khi người dùng gõ useEffect(() => { const timer = setTimeout(async () => { if (searchQuery.trim().length < 2) { setSuggestions([]); return; } setIsSearchingSuggestions(true); try { // 1. Lọc các Tour hiện có khớp với từ khóa const tourMatches = publicTours .filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase())) .map(t => ({ type: 'tour' as const, id: t.id, name: t.title })); // 2. Tìm kiếm địa điểm thực tế trên bản đồ qua OpenStreetMap const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1&accept-language=vi`); const data = await res.json(); const locationMatches = data.map((item: any) => ({ type: 'location' as const, id: item.place_id, name: item.display_name, lat: parseFloat(item.lat), lon: parseFloat(item.lon) })); // Hợp nhất kết quả: Tour ưu tiên lên đầu setSuggestions([...tourMatches, ...locationMatches]); } catch (err) { console.error("Lỗi tìm kiếm gợi ý:", err); } finally { setIsSearchingSuggestions(false); } }, 500); // Debounce 500ms để tránh gọi API quá nhiều return () => clearTimeout(timer); }, [searchQuery, publicTours]); const [selectedFilterTag, setSelectedFilterTag] = useState(null); const availableTags = ['Văn hóa', 'Mạo hiểm', 'Nghỉ dưỡng', 'Thư giãn', 'Ẩm thực', 'Khám phá', 'Gia đình']; // Tổng hợp nhãn từ danh sách Tour đang có để hiển thị bộ lọc đầy đủ (bao gồm cả nhãn tùy chỉnh) const allFilterTags = React.useMemo(() => { const tagsSet = new Set(availableTags); publicTours.forEach(tour => { tour.tags?.forEach((tag: string) => tagsSet.add(tag)); }); return Array.from(tagsSet); }, [publicTours]); // State cho menu chuột phải chia sẻ const [shareMenu, setShareMenu] = useState<{ x: number, y: number, id: string, title: string, canShare: boolean } | null>(null); const handleShare = (id: string, title: string) => { const shareUrl = `${window.location.origin}?viewTour=${id}`; if (navigator.share) { navigator.share({ title: title, text: `Khám phá hành trình du lịch: ${title}`, url: shareUrl, }).catch(() => {}); } else if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(shareUrl).then(() => { notify({ title: 'Thành công', message: 'Đã sao chép liên kết chuyến đi vào bộ nhớ tạm. Bạn có thể gửi cho bạn bè ngay!', type: 'success' }); }); } else { // Giải pháp dự phòng cho môi trường không có HTTPS const textArea = document.createElement("textarea"); textArea.value = shareUrl; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); notify({ title: 'Thành công', message: 'Đã sao chép liên kết chuyến đi vào bộ nhớ tạm. Bạn có thể gửi cho bạn bè ngay!', type: 'success' }); } catch (err) {} document.body.removeChild(textArea); } setShareMenu(null); }; const handleSelectSuggestion = (s: any) => { if (s.type === 'tour') { onViewTour(s.id); } else if (s.lat && s.lon) { const pos: [number, number] = [s.lat, s.lon]; setUserPos(pos); setMapCenter(pos); notify({ title: 'Tìm thấy địa điểm', message: `Đã di chuyển bản đồ tới: ${s.name.split(',')[0]}`, type: 'success' }); } setSuggestions([]); setSearchQuery(''); }; const filteredTours = React.useMemo(() => { if (!selectedFilterTag) return publicTours; return publicTours.filter(tour => tour.tags?.includes(selectedFilterTag)); }, [publicTours, selectedFilterTag]); useEffect(() => { // Chỉ fetch dữ liệu khi người dùng đã đăng nhập và có token if (user || localStorage.getItem('token')) { fetchPublicTours(); } // Nếu không có vị trí lưu từ trước, mới yêu cầu lấy vị trí hiện tại của thiết bị if (!initialViewState) { navigator.geolocation.getCurrentPosition( (pos) => { const posArray: [number, number] = [pos.coords.latitude, pos.coords.longitude]; setUserPos(posArray); setMapCenter(posArray); }, () => console.log("Không thể lấy vị trí người dùng") ); } else { // Cập nhật store để đồng bộ với vị trí khởi tạo từ cache setMapCenter(initialViewState.center); } }, []); return (
{/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */}
{/* Nhóm bên trái: Quay lại và Thông tin vị trí */}
{/* Nút lọc Tag và Dropdown */}
{/* Filter Dropdown Content */} {isFilterDropdownOpen && (
Lọc theo loại
{/* Hiển thị tag theo chiều dọc */} {allFilterTags.map(tag => ( ))}
)}
{/* Search Box - Thay thế div "Khám phá khu vực" */}
setSearchQuery(e.target.value)} /> {isSearchingSuggestions && } {searchQuery && ( )} {/* Dropdown danh sách gợi ý */} {suggestions.length > 0 && (
{suggestions.map((s, idx) => ( ))}
)}
{/* Nhóm bên phải: Các thao tác người dùng */}
{/* Nút tạo Tour mới */} {user && ( )} {/* Nút quản lý người dùng cho Admin */} {user?.isAdmin && ( )} {/* Nút đăng xuất */} {onLogout && ( )}
{/* Theo dõi di chuyển bản đồ */} {/* Đóng menu và dropdown khi tương tác bản đồ */} { setShareMenu(null); setSuggestions([]); setIsFilterDropdownOpen(false); }} /> {/* Tự động di chuyển bản đồ đến vị trí người dùng khi tìm thấy tọa độ */} {filteredTours.map((tour) => { const startLoc = tour.legs?.[0]?.locations?.[0]; const tourImage = tour.photos?.[0]?.imageUrl || `https://picsum.photos/seed/${tour.id}/200/200`; const markerPos = startLoc ? [startLoc.latitude, startLoc.longitude] as [number, number] : userPos; return ( onViewTour(tour.id), contextmenu: (e) => { // Kiểm tra quyền chia sẻ (OWNER, MANAGER, MEMBER) const role = tour.participants?.[0]?.role; const canShare = ['OWNER', 'MANAGER', 'MEMBER'].includes(role); // Hiển thị menu tại vị trí chuột setShareMenu({ x: e.containerPoint.x, y: e.containerPoint.y, id: tour.id, title: tour.title, canShare }); } }} icon={L.divIcon({ className: 'custom-bubble', html: `
S
`, iconSize: [48, 48], iconAnchor: [24, 24] })} >
{tour.title}
{tour.tags && tour.tags.length > 0 && (
{tour.tags.map((tag: string) => ( {tag} ))}
)} {tour.description && (
{tour.description}
)}
); })}
{/* Context Menu Chia sẻ */} {shareMenu && (
e.stopPropagation()} > {shareMenu.canShare ? ( ) : (
Bạn không có quyền chia sẻ tour này
)}
)} {/* Admin Modal */} setIsAdminModalOpen(false)} /> {/* Create Tour Modal */} setIsCreateModalOpen(false)} onSuccess={(tour) => { fetchTour(tour.id); onViewTour(tour.id); }} />
); };