feat: thêm nút tìm kiếm địa điểm cho phần tạo tour, phần thêm địa điểm

This commit is contained in:
2026-06-16 15:45:26 +07:00
parent 1772ced959
commit 989d60643b
2 changed files with 135 additions and 52 deletions
+72
View File
@@ -24,6 +24,8 @@ import {
List,
Map as MapIconLucide,
MapPin,
Search,
Loader2,
Flag,
Clock,
Check,
@@ -198,6 +200,29 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
const [commentLocationName, setCommentLocationName] = useState('');
const [joinRequestActionId, setJoinRequestActionId] = useState<string | null>(null);
// State tìm kiếm cho chế độ Bản đồ trong Tab Lộ trình
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const [isSearching, setIsSearching] = useState(false);
const handleSearchLocation = async (query: string) => {
setSearchQuery(query);
if (query.trim().length < 2) {
setSearchResults([]);
return;
}
setIsSearching(true);
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1&namedetails=1&accept-language=vi`);
const data = await res.json();
setSearchResults(data);
} catch (e) {
console.error("Lỗi tìm kiếm:", e);
} finally {
setIsSearching(false);
}
};
const fetchTour = useTourStore(state => state.fetchTour);
const fetchPublicTourDetails = useTourStore(state => state.fetchPublicTourDetails); // New action
@@ -803,6 +828,53 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
}} isPublicView={isPublicView} />
) : (
<div className="h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative">
{/* Search Bar Overlay - Thanh tìm kiếm địa điểm trên bản đồ lộ trình */}
{!isPublicView && (
<div className="absolute top-4 right-4 z-[1001] w-64 md:w-80">
<div className="relative group">
<input
type="text"
placeholder="Tìm địa điểm để ghim..."
className="w-full pl-10 pr-10 py-3 bg-white/95 backdrop-blur-md border border-white/20 rounded-2xl shadow-xl outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm font-bold"
value={searchQuery}
onChange={e => handleSearchLocation(e.target.value)}
/>
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500" />
{isSearching ? (
<Loader2 className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 animate-spin text-blue-500" />
) : searchQuery && (
<button onClick={() => { setSearchQuery(''); setSearchResults([]); }} className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-red-500 transition-colors">
<X className="w-4 h-4" />
</button>
)}
{/* Kết quả tìm kiếm */}
{searchResults.length > 0 && (
<div className="absolute left-0 right-0 mt-2 bg-white/95 backdrop-blur-md border border-gray-100 rounded-2xl shadow-2xl overflow-hidden max-h-48 overflow-y-auto animate-in slide-in-from-top-2 duration-200">
{searchResults.map((result, idx) => (
<button
key={idx}
onClick={() => {
setMapCenter([parseFloat(result.lat), parseFloat(result.lon)]);
setSearchResults([]);
setSearchQuery('');
notify({
title: 'Tìm thấy địa điểm',
message: `Đã di chuyển bản đồ tới: ${result.namedetails?.name || result.display_name.split(',')[0]}`,
type: 'success'
});
}}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-50 last:border-0 transition-colors"
>
<div className="font-bold text-xs text-gray-900 truncate">{result.namedetails?.name || result.display_name.split(',')[0]}</div>
<div className="text-[10px] text-gray-500 truncate leading-tight">{result.display_name}</div>
</button>
))}
</div>
)}
</div>
</div>
)}
<MapContainer
center={initialViewState?.center || mapCenter}
zoom={mapZoom}