feat: tải ảnh lên bằng tài khoản public và cho phép bình luận
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents, Tooltip } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, 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 { X, Navigation, Image as ImageIcon, LogOut, Settings, Share2, Filter, MapPin, Loader2 } from 'lucide-react';
|
||||
import { UserManagementModal } from '@/components/UserManagementModal';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
import { CreateTourModal } from '../components/CreateTourModal';
|
||||
import { PublicPhotoModal } from '../components/PublicPhotoModal';
|
||||
|
||||
// Fix lỗi icon mặc định của Leaflet
|
||||
const DefaultIcon = L.icon({
|
||||
@@ -56,7 +57,7 @@ function MapTracker() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void, onOpenMyPhotos: () => void }) => {
|
||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos, onLoginSuccess }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void, onOpenMyPhotos: () => void, onLoginSuccess?: (user: any) => 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);
|
||||
@@ -76,6 +77,37 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos
|
||||
|
||||
const [userPos, setUserPos] = useState<[number, number]>(initialViewState?.center || [10.7769, 106.7009]);
|
||||
const [mapZoom] = useState(initialViewState?.zoom || 13);
|
||||
const [publicPhotos, setPublicPhotos] = useState<any[]>([]);
|
||||
const [selectedPhoto, setSelectedPhoto] = useState<any | null>(null);
|
||||
const [selectedPhotoGroup, setSelectedPhotoGroup] = useState<any[]>([]);
|
||||
|
||||
const groupedPhotos = React.useMemo(() => {
|
||||
const groups: { [key: string]: any[] } = {};
|
||||
publicPhotos.forEach((photo) => {
|
||||
const lat = photo.metadata?.lat;
|
||||
const lng = photo.metadata?.lng;
|
||||
if (typeof lat === 'number' && typeof lng === 'number') {
|
||||
const key = `${lat.toFixed(5)},${lng.toFixed(5)}`;
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(photo);
|
||||
}
|
||||
});
|
||||
return Object.values(groups);
|
||||
}, [publicPhotos]);
|
||||
|
||||
const fetchPublicPhotos = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/public-photos');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setPublicPhotos(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching public photos:', error);
|
||||
}
|
||||
};
|
||||
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
|
||||
@@ -194,7 +226,10 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos
|
||||
}, [publicTours, selectedFilterTag]);
|
||||
|
||||
useEffect(() => {
|
||||
// Chỉ fetch dữ liệu khi người dùng đã đăng nhập và có token
|
||||
// Luôn tải danh sách ảnh công khai để hiển thị trên bản đồ cho tất cả mọi người
|
||||
fetchPublicPhotos();
|
||||
|
||||
// Chỉ tải danh sách tour khi người dùng đã đăng nhập và có token
|
||||
if (user || localStorage.getItem('token')) {
|
||||
fetchPublicTours();
|
||||
}
|
||||
@@ -449,6 +484,46 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos
|
||||
);
|
||||
})}
|
||||
</MarkerClusterGroup>
|
||||
|
||||
{groupedPhotos.map((photoGroup) => {
|
||||
const latestPhoto = photoGroup[0];
|
||||
const lat = latestPhoto.metadata?.lat;
|
||||
const lng = latestPhoto.metadata?.lng;
|
||||
if (typeof lat !== 'number' || typeof lng !== 'number') return null;
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={latestPhoto.id}
|
||||
position={[lat, lng]}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
setSelectedPhoto(latestPhoto);
|
||||
setSelectedPhotoGroup(photoGroup);
|
||||
}
|
||||
}}
|
||||
icon={L.divIcon({
|
||||
className: 'custom-photo-bubble',
|
||||
html: `
|
||||
<div class="relative group">
|
||||
<div class="w-12 h-12 rounded-full border-4 border-emerald-500 shadow-lg overflow-hidden transition-transform group-hover:scale-110">
|
||||
<img src="${latestPhoto.imageUrl}" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="absolute -bottom-1 -right-1 bg-emerald-600 rounded-full w-5 h-5 border-2 border-white flex items-center justify-center text-[10px] font-black text-white">
|
||||
📸
|
||||
</div>
|
||||
${photoGroup.length > 1 ? `
|
||||
<div class="absolute -top-1 -left-1 bg-rose-600 rounded-full w-5 h-5 border-2 border-white flex items-center justify-center text-[9px] font-black text-white shadow-md animate-bounce">
|
||||
${photoGroup.length}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`,
|
||||
iconSize: [48, 48],
|
||||
iconAnchor: [24, 24]
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MapContainer>
|
||||
|
||||
{/* Context Menu Chia sẻ */}
|
||||
@@ -498,6 +573,20 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos
|
||||
onViewTour(tour.id);
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedPhoto && (
|
||||
<PublicPhotoModal
|
||||
isOpen={!!selectedPhoto}
|
||||
onClose={() => {
|
||||
setSelectedPhoto(null);
|
||||
setSelectedPhotoGroup([]);
|
||||
}}
|
||||
photo={selectedPhoto}
|
||||
photoGroup={selectedPhotoGroup}
|
||||
onSelectPhoto={(photo) => setSelectedPhoto(photo)}
|
||||
onLoginSuccess={onLoginSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user