fix: tags không có trong csdl gây nên lỗi crash backend
This commit is contained in:
@@ -5,7 +5,7 @@ const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerCluste
|
||||
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 } from 'lucide-react';
|
||||
import { X, Navigation, Image as ImageIcon, LogOut, Settings, Edit2, Trash2, Share2, Filter, Tag as TagIcon } from 'lucide-react';
|
||||
import { UserManagementModal } from '@/components/UserManagementModal';
|
||||
import { NotificationModal, useNotificationModal } from '@/components/NotificationModal';
|
||||
import { CreateTourModal } from '../components/CreateTourModal';
|
||||
@@ -79,6 +79,18 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
const [selectedFilterTag, setSelectedFilterTag] = useState<string | null>(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);
|
||||
|
||||
@@ -109,6 +121,11 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
setShareMenu(null);
|
||||
};
|
||||
|
||||
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')) {
|
||||
@@ -182,6 +199,33 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bộ lọc theo Tag */}
|
||||
<div className="absolute top-24 left-6 z-[1000] flex flex-col gap-2 pointer-events-none">
|
||||
<div className="bg-white/90 backdrop-blur-md p-2 rounded-2xl shadow-xl border border-white/20 pointer-events-auto flex flex-col gap-2 max-w-[200px]">
|
||||
<div className="flex items-center gap-2 px-2 py-1 border-b border-gray-100 mb-1">
|
||||
<Filter className="w-3.5 h-3.5 text-blue-600" />
|
||||
<span className="text-[11px] font-black uppercase text-gray-500 tracking-wider">Lọc theo loại</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
onClick={() => setSelectedFilterTag(null)}
|
||||
className={`px-2.5 py-1 rounded-lg text-[10px] font-bold transition-all ${!selectedFilterTag ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
|
||||
>
|
||||
Tất cả
|
||||
</button>
|
||||
{allFilterTags.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => setSelectedFilterTag(tag === selectedFilterTag ? null : tag)}
|
||||
className={`px-2.5 py-1 rounded-lg text-[10px] font-bold transition-all ${selectedFilterTag === tag ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapContainer
|
||||
center={userPos}
|
||||
zoom={mapZoom}
|
||||
@@ -203,7 +247,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
<RecenterMap position={userPos} />
|
||||
|
||||
<MarkerClusterGroup chunkedLoading>
|
||||
{publicTours.map((tour) => {
|
||||
{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
|
||||
@@ -250,6 +294,13 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
<Tooltip direction="top" offset={[0, -20]} opacity={1}>
|
||||
<div className="p-1 max-w-[180px]">
|
||||
<div className="font-black text-blue-600 text-[11px] mb-0.5 uppercase tracking-tight truncate">{tour.title}</div>
|
||||
{tour.tags && tour.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{tour.tags.map((tag: string) => (
|
||||
<span key={tag} className="px-1.5 py-0.5 bg-blue-50 text-blue-500 rounded text-[8px] font-bold border border-blue-100">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tour.description && (
|
||||
<div className="text-[10px] text-gray-500 line-clamp-2 leading-tight italic">
|
||||
{tour.description}
|
||||
|
||||
Reference in New Issue
Block a user