Chỉnh sửa giao diện Tour Dashboard
This commit is contained in:
+24
-4
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { useTourStore } from './useTourStore.js';
|
||||
@@ -25,8 +25,21 @@ function RecenterMap({ position }: { position: [number, number] }) {
|
||||
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();
|
||||
setMapCenter([center.lat, center.lng]);
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void }) => {
|
||||
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour } = useTourStore();
|
||||
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour, mapCenter, setMapCenter } = useTourStore();
|
||||
const [userPos, setUserPos] = useState<[number, number]>([10.7769, 106.7009]); // Mặc định TP.HCM
|
||||
const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
@@ -34,7 +47,11 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
useEffect(() => {
|
||||
fetchPublicTours();
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => setUserPos([pos.coords.latitude, pos.coords.longitude]),
|
||||
(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")
|
||||
);
|
||||
}, []);
|
||||
@@ -111,12 +128,15 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapContainer center={userPos} zoom={13} className="h-full w-full">
|
||||
<MapContainer center={mapCenter} zoom={13} className="h-full w-full">
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution='© OpenStreetMap contributors'
|
||||
/>
|
||||
|
||||
{/* Theo dõi di chuyển bản đồ */}
|
||||
<MapTracker />
|
||||
|
||||
{/* Tự động di chuyển bản đồ đến vị trí người dùng khi tìm thấy tọa độ */}
|
||||
<RecenterMap position={userPos} />
|
||||
|
||||
|
||||
+125
-21
@@ -15,21 +15,37 @@ import {
|
||||
Quote,
|
||||
Plus,
|
||||
List,
|
||||
Map as MapIconLucide
|
||||
Map as MapIconLucide,
|
||||
MapPin,
|
||||
Flag
|
||||
} from 'lucide-react';
|
||||
import L from 'leaflet';
|
||||
|
||||
// Định nghĩa kiểu dữ liệu cho Địa điểm để khớp với Schema Prisma
|
||||
type LocationType = 'MOVE' | 'VISIT' | 'REST' | 'EAT';
|
||||
|
||||
// Menu ngữ cảnh cho bản đồ
|
||||
const MapContextMenu = ({ onAction }: { onAction: (action: string, latlng: L.LatLng) => void }) => {
|
||||
const [menuPos, setMenuPos] = useState<{ x: number, y: number, latlng: L.LatLng } | null>(null);
|
||||
const { legs } = useTourStore();
|
||||
const { legs, setMapCenter } = useTourStore();
|
||||
|
||||
useMapEvents({
|
||||
contextmenu: (e) => {
|
||||
// Ngăn menu mặc định của trình duyệt hiện lên.
|
||||
// Điều này là cần thiết để menu tùy chỉnh của Leaflet có thể tương tác được.
|
||||
// Nếu không có, menu của trình duyệt sẽ đè lên và chặn các sự kiện click.
|
||||
if (e.originalEvent) {
|
||||
L.DomEvent.preventDefault(e.originalEvent);
|
||||
}
|
||||
setMenuPos({ x: e.containerPoint.x, y: e.containerPoint.y, latlng: e.latlng });
|
||||
},
|
||||
click: () => setMenuPos(null),
|
||||
dragstart: () => setMenuPos(null)
|
||||
dragstart: () => setMenuPos(null),
|
||||
moveend: (e) => {
|
||||
const map = e.target;
|
||||
const center = map.getCenter();
|
||||
setMapCenter([center.lat, center.lng]);
|
||||
}
|
||||
});
|
||||
|
||||
if (!menuPos) return null;
|
||||
@@ -40,10 +56,10 @@ const MapContextMenu = ({ onAction }: { onAction: (action: string, latlng: L.Lat
|
||||
style={{ top: menuPos.y, left: menuPos.x }}
|
||||
>
|
||||
<button onClick={() => { onAction('START', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-gray-700 flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" /> Bắt đầu từ đây
|
||||
<div className="w-2 h-2 rounded-full bg-blue-600" /> Bắt đầu từ đây
|
||||
</button>
|
||||
<button onClick={() => { onAction('END', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-gray-700 flex items-center gap-2 border-b border-gray-50">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" /> Kết thúc ở đây
|
||||
<button onClick={() => { onAction('END', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-green-50 text-sm font-bold text-gray-700 flex items-center gap-2 border-b border-gray-50">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" /> Kết thúc ở đây
|
||||
</button>
|
||||
<div className="px-4 py-2 text-[10px] font-black text-gray-400 uppercase tracking-widest">Thêm vào chặng</div>
|
||||
{legs.map(leg => (
|
||||
@@ -63,7 +79,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan');
|
||||
const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline');
|
||||
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
|
||||
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole, legs, addLocation } = useTourStore();
|
||||
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole, legs, addLocation, mapCenter } = useTourStore();
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -82,33 +98,61 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
}
|
||||
}, [publicTours, currentTour, fetchTour]);
|
||||
|
||||
// Hàm xử lý các hành động từ Context Menu của bản đồ
|
||||
const handleMapAction = async (action: string, latlng: L.LatLng) => {
|
||||
if (!currentTour || legs.length === 0) return;
|
||||
// Đảm bảo có tour và ít nhất một chặng để ghim
|
||||
const currentLegs = useTourStore.getState().legs;
|
||||
if (!currentTour || currentLegs.length === 0) {
|
||||
alert("Tour chưa có chặng nào. Vui lòng tạo chặng trước khi thêm địa điểm.");
|
||||
return;
|
||||
}
|
||||
|
||||
let targetLegId = legs[0].id; // Mặc định là chặng đầu
|
||||
let targetLegId = currentLegs[0].id; // Mặc định là chặng đầu
|
||||
let defaultName = "Địa điểm mới";
|
||||
let locationType: LocationType = 'VISIT'; // Mặc định là tham quan
|
||||
|
||||
if (action === 'START') defaultName = "Điểm bắt đầu";
|
||||
if (action === 'START') {
|
||||
defaultName = "Điểm bắt đầu";
|
||||
locationType = 'MOVE'; // Điểm bắt đầu thường liên quan đến di chuyển
|
||||
}
|
||||
if (action === 'END') {
|
||||
targetLegId = legs[legs.length - 1].id;
|
||||
targetLegId = currentLegs[currentLegs.length - 1].id;
|
||||
defaultName = "Điểm kết thúc";
|
||||
locationType = 'MOVE'; // Điểm kết thúc cũng liên quan đến di chuyển
|
||||
}
|
||||
if (action.startsWith('ADD_TO_LEG_')) {
|
||||
targetLegId = action.replace('ADD_TO_LEG_', '');
|
||||
// Loại địa điểm mặc định vẫn là VISIT nếu thêm vào chặng cụ thể
|
||||
}
|
||||
|
||||
const name = window.prompt("Nhập tên địa điểm:", defaultName);
|
||||
// Tự động lấy tên địa điểm từ tọa độ (Reverse Geocoding)
|
||||
let detectedName = "";
|
||||
try {
|
||||
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`);
|
||||
const data = await res.json();
|
||||
detectedName = data.display_name?.split(',')[0] || "";
|
||||
} catch (e) {}
|
||||
|
||||
const name = window.prompt("Xác nhận tên địa điểm:", detectedName || defaultName);
|
||||
if (name) {
|
||||
await addLocation(currentTour.id, {
|
||||
name,
|
||||
address: '', // Gửi chuỗi rỗng cho địa chỉ nếu không có
|
||||
latitude: latlng.lat,
|
||||
longitude: latlng.lng,
|
||||
legId: targetLegId,
|
||||
type: 'VISIT'
|
||||
type: locationType as any,
|
||||
plannedStart: null, // Gửi null cho thời gian nếu không có
|
||||
plannedEnd: null, // Gửi null cho thời gian nếu không có
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Xác định Điểm xuất phát và Điểm kết thúc hiển thị dưới widget tài chính
|
||||
const startPoint = legs[0]?.locations[0];
|
||||
const lastLeg = legs[legs.length - 1];
|
||||
const endPoint = lastLeg?.locations[lastLeg.locations.length - 1];
|
||||
|
||||
// Định nghĩa các tab dựa trên quyền hạn (RBAC) từ store
|
||||
const tabs = [
|
||||
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: true },
|
||||
@@ -150,17 +194,27 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
</div>
|
||||
|
||||
{/* Tour Header Info */}
|
||||
<div className="relative h-64 w-full bg-blue-900">
|
||||
<div className="relative min-h-[480px] w-full bg-blue-900 flex flex-col justify-end">
|
||||
<img
|
||||
src={tourInfo.coverImage}
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-60"
|
||||
alt="Tour Cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
<div className="absolute bottom-16 left-0 right-0 p-6 text-white">
|
||||
<div className="max-w-2xl mx-auto space-y-2">
|
||||
<div className="relative z-10 p-6 text-white pt-28 pb-20">
|
||||
<div className="max-w-2xl mx-auto space-y-4">
|
||||
<h2 className="text-3xl font-black tracking-tight drop-shadow-md">{tourInfo.title}</h2>
|
||||
{/* Dòng tóm tắt Lộ trình */}
|
||||
<div className="mt-3 text-sm font-bold text-blue-100 bg-blue-800/30 backdrop-blur-sm px-4 py-2 rounded-xl border border-white/5 inline-block max-w-full truncate">
|
||||
<span className="text-white/60 mr-1">Lộ trình:</span>
|
||||
<span>Điểm xuất phát: </span>
|
||||
<span className="text-white">{startPoint?.name || '...'}</span>
|
||||
<span className="mx-2 text-white/40">-</span>
|
||||
<span>Điểm kết thúc: </span>
|
||||
<span className="text-white">{endPoint?.name || '...'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm font-medium opacity-90">
|
||||
<div className="flex items-center bg-black/20 backdrop-blur-sm px-3 py-1 rounded-full border border-white/10">
|
||||
<Calendar className="w-4 h-4 mr-1.5" />
|
||||
@@ -172,6 +226,8 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Member Avatars Stack */}
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<div className="flex -space-x-3">
|
||||
@@ -216,6 +272,32 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Khối hiển thị Điểm đầu & Điểm cuối (Dưới Financial Quick-View) */}
|
||||
<div className="max-w-2xl mx-auto mt-4 px-4 grid grid-cols-1 sm:grid-cols-2 gap-3 animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
{startPoint && (
|
||||
<div className="bg-white p-4 rounded-2xl border border-blue-50 flex items-center gap-3 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center text-blue-600 shadow-inner">
|
||||
<MapPin className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-[10px] font-black text-blue-400 uppercase tracking-widest mb-0.5">Điểm xuất phát</p>
|
||||
<p className="text-sm font-bold text-gray-800 truncate">{startPoint.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{endPoint && (
|
||||
<div className="bg-white p-4 rounded-2xl border border-green-50 flex items-center gap-3 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="w-10 h-10 rounded-xl bg-green-100 flex items-center justify-center text-green-600 shadow-inner">
|
||||
<Flag className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-[10px] font-black text-green-400 uppercase tracking-widest mb-0.5">Điểm kết thúc</p>
|
||||
<p className="text-sm font-bold text-gray-800 truncate">{endPoint.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main Content Area - Điều chỉnh max-width khi ở chế độ bản đồ */}
|
||||
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full' : 'max-w-2xl mx-auto'} mt-6 px-4 pb-24`}>
|
||||
{/* Tab Switcher */}
|
||||
@@ -262,7 +344,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
<ItineraryTimeline />
|
||||
) : (
|
||||
<div className="h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative">
|
||||
<MapContainer center={[10.7769, 106.7009]} zoom={13} className="h-full w-full">
|
||||
<MapContainer center={mapCenter} zoom={13} className="h-full w-full">
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<MapContextMenu onAction={handleMapAction} />
|
||||
|
||||
@@ -272,14 +354,36 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
return <Polyline key={leg.id} positions={positions as any} color="#3b82f6" weight={3} dashArray="5, 10" />;
|
||||
})}
|
||||
|
||||
{legs.flatMap(l => l.locations).map((loc: any) => (
|
||||
<Marker key={loc.id} position={[loc.latitude, loc.longitude]}>
|
||||
{legs.flatMap(l => l.locations).map((loc: any) => {
|
||||
const isStart = startPoint?.id === loc.id;
|
||||
const isEnd = endPoint?.id === loc.id;
|
||||
|
||||
let customIcon = undefined;
|
||||
if (isStart) {
|
||||
customIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="w-5 h-5 bg-blue-600 rounded-full border-2 border-white shadow-lg"></div>`,
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
} else if (isEnd) {
|
||||
customIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="w-5 h-5 bg-green-500 rounded-full border-2 border-white shadow-lg"></div>`,
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Marker key={loc.id} position={[loc.latitude, loc.longitude]} icon={customIcon}>
|
||||
<Popup>
|
||||
<div className="font-bold">{loc.name}</div>
|
||||
<div className="text-xs text-gray-500">{loc.type}</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</MapContainer>
|
||||
<div className="absolute top-4 left-4 z-[1000] bg-white/90 backdrop-blur-md p-3 rounded-2xl text-[10px] font-bold text-gray-500 shadow-lg border border-white">
|
||||
Mẹo: Nhấn giữ (Mobile) hoặc Chuột phải để ghim địa điểm
|
||||
|
||||
Vendored
+19
-4
@@ -1,6 +1,6 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { useTourStore } from './useTourStore.js';
|
||||
@@ -21,14 +21,29 @@ function RecenterMap({ position }) {
|
||||
}, [position, map]);
|
||||
return null;
|
||||
}
|
||||
function MapTracker() {
|
||||
const setMapCenter = useTourStore(state => state.setMapCenter);
|
||||
useMapEvents({
|
||||
moveend: (e) => {
|
||||
const map = e.target;
|
||||
const center = map.getCenter();
|
||||
setMapCenter([center.lat, center.lng]);
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
|
||||
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour } = useTourStore();
|
||||
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour, mapCenter, setMapCenter } = useTourStore();
|
||||
const [userPos, setUserPos] = useState([10.7769, 106.7009]);
|
||||
const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
fetchPublicTours();
|
||||
navigator.geolocation.getCurrentPosition((pos) => setUserPos([pos.coords.latitude, pos.coords.longitude]), () => console.log("Không thể lấy vị trí người dùng"));
|
||||
navigator.geolocation.getCurrentPosition((pos) => {
|
||||
const posArray = [pos.coords.latitude, pos.coords.longitude];
|
||||
setUserPos(posArray);
|
||||
setMapCenter(posArray);
|
||||
}, () => console.log("Không thể lấy vị trí người dùng"));
|
||||
}, []);
|
||||
const handleEditTour = async (tour) => {
|
||||
const newTitle = window.prompt("Nhập tên mới cho Tour:", tour.title);
|
||||
@@ -51,7 +66,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
return (_jsxs("div", { className: "h-screen w-full relative", children: [_jsx("button", { onClick: onBack, className: "absolute top-6 left-6 z-[1000] bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all", children: _jsx(X, { className: "w-6 h-6 text-gray-800" }) }), onLogout && (_jsxs("button", { onClick: onLogout, className: "absolute top-6 right-6 z-[1000] bg-white px-4 py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center gap-2 font-bold text-gray-700", children: [_jsx(LogOut, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "\u0110\u0103ng xu\u1EA5t" })] })), user?.isAdmin && (_jsxs("button", { onClick: () => setIsAdminModalOpen(true), className: "absolute top-6 right-40 z-[1000] bg-blue-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-blue-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Settings, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "Qu\u1EA3n l\u00FD h\u1EC7 th\u1ED1ng" })] })), user && (_jsxs("button", { onClick: () => setIsCreateModalOpen(true), className: "absolute top-6 right-80 z-[1000] bg-green-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-green-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Navigation, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "T\u1EA1o Tour m\u1EDBi" })] })), _jsx("div", { className: "absolute top-6 left-20 z-[1000] bg-white/90 backdrop-blur-md px-6 py-3 rounded-2xl shadow-xl border border-white/20 hidden sm:block", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-blue-600" }), _jsx("span", { className: "font-bold text-gray-800", children: "\u0110ang kh\u00E1m ph\u00E1 khu v\u1EF1c c\u1EE7a b\u1EA1n" })] }) }), _jsxs(MapContainer, { center: userPos, zoom: 13, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '\u00A9 OpenStreetMap contributors' }), _jsx(RecenterMap, { position: userPos }), publicTours.map((tour) => {
|
||||
return (_jsxs("div", { className: "h-screen w-full relative", children: [_jsx("button", { onClick: onBack, className: "absolute top-6 left-6 z-[1000] bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all", children: _jsx(X, { className: "w-6 h-6 text-gray-800" }) }), onLogout && (_jsxs("button", { onClick: onLogout, className: "absolute top-6 right-6 z-[1000] bg-white px-4 py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center gap-2 font-bold text-gray-700", children: [_jsx(LogOut, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "\u0110\u0103ng xu\u1EA5t" })] })), user?.isAdmin && (_jsxs("button", { onClick: () => setIsAdminModalOpen(true), className: "absolute top-6 right-40 z-[1000] bg-blue-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-blue-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Settings, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "Qu\u1EA3n l\u00FD h\u1EC7 th\u1ED1ng" })] })), user && (_jsxs("button", { onClick: () => setIsCreateModalOpen(true), className: "absolute top-6 right-80 z-[1000] bg-green-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-green-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Navigation, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "T\u1EA1o Tour m\u1EDBi" })] })), _jsx("div", { className: "absolute top-6 left-20 z-[1000] bg-white/90 backdrop-blur-md px-6 py-3 rounded-2xl shadow-xl border border-white/20 hidden sm:block", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-blue-600" }), _jsx("span", { className: "font-bold text-gray-800", children: "\u0110ang kh\u00E1m ph\u00E1 khu v\u1EF1c c\u1EE7a b\u1EA1n" })] }) }), _jsxs(MapContainer, { center: mapCenter, zoom: 13, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '\u00A9 OpenStreetMap contributors' }), _jsx(MapTracker, {}), _jsx(RecenterMap, { position: userPos }), publicTours.map((tour) => {
|
||||
const location = tour.legs?.[0]?.locations?.[0];
|
||||
if (!location)
|
||||
return null;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+64
-14
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -4,6 +4,7 @@ interface TourState {
|
||||
publicTours: any[];
|
||||
userRole: string | null;
|
||||
activeLegId: string | null;
|
||||
mapCenter: [number, number];
|
||||
setTour: (tour: any) => void;
|
||||
updateLegs: (legs: any[]) => void;
|
||||
createTour: (tourData: any) => Promise<any>;
|
||||
@@ -15,6 +16,7 @@ interface TourState {
|
||||
addLocation: (tourId: string, locationData: any) => Promise<void>;
|
||||
optimizeRouting: (legId: string) => Promise<void>;
|
||||
setActiveLegId: (id: string | null) => void;
|
||||
setMapCenter: (pos: [number, number]) => void;
|
||||
fetchTour: (id: string) => Promise<void>;
|
||||
fetchPublicTours: () => Promise<void>;
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -5,9 +5,11 @@ export const useTourStore = create((set, get) => ({
|
||||
publicTours: [],
|
||||
userRole: null,
|
||||
activeLegId: null,
|
||||
mapCenter: [10.7769, 106.7009],
|
||||
setTour: (tour) => set({ currentTour: tour }),
|
||||
updateLegs: (legs) => set({ legs }),
|
||||
setActiveLegId: (id) => set({ activeLegId: id }),
|
||||
setMapCenter: (pos) => set({ mapCenter: pos }),
|
||||
fetchTour: async (id) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ interface TourState {
|
||||
publicTours: any[];
|
||||
userRole: string | null;
|
||||
activeLegId: string | null;
|
||||
mapCenter: [number, number];
|
||||
setTour: (tour: any) => void;
|
||||
updateLegs: (legs: any[]) => void;
|
||||
createTour: (tourData: any) => Promise<any>;
|
||||
@@ -17,6 +18,7 @@ interface TourState {
|
||||
addLocation: (tourId: string, locationData: any) => Promise<void>;
|
||||
optimizeRouting: (legId: string) => Promise<void>;
|
||||
setActiveLegId: (id: string | null) => void;
|
||||
setMapCenter: (pos: [number, number]) => void;
|
||||
fetchTour: (id: string) => Promise<void>;
|
||||
fetchPublicTours: () => Promise<void>;
|
||||
}
|
||||
@@ -27,9 +29,11 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
publicTours: [],
|
||||
userRole: null,
|
||||
activeLegId: null,
|
||||
mapCenter: [10.7769, 106.7009],
|
||||
setTour: (tour) => set({ currentTour: tour }),
|
||||
updateLegs: (legs) => set({ legs }),
|
||||
setActiveLegId: (id) => set({ activeLegId: id }),
|
||||
setMapCenter: (pos) => set({ mapCenter: pos }),
|
||||
fetchTour: async (id: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`);
|
||||
|
||||
Reference in New Issue
Block a user