diff --git a/frontend/src/pages/TourNavigationPage.tsx b/frontend/src/pages/TourNavigationPage.tsx
new file mode 100644
index 0000000..9cda0dd
--- /dev/null
+++ b/frontend/src/pages/TourNavigationPage.tsx
@@ -0,0 +1,268 @@
+import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
+import { MapContainer, TileLayer, Marker, Polyline, Popup, useMap, useMapEvents } from 'react-leaflet';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+import { ChevronLeft, Compass } from 'lucide-react';
+
+interface NavigationRouteData {
+ origin: { lat: number; lng: number };
+ destination: { lat: number; lng: number; name: string };
+ tourTitle?: string;
+}
+
+interface TourNavigationPageProps {
+ tourId: string;
+ routeData: NavigationRouteData | null;
+ onBack: () => void;
+}
+
+const FitBounds = ({ coords }: { coords: [number, number][] }) => {
+ const map = useMap();
+ useEffect(() => {
+ if (coords.length > 0) {
+ map.fitBounds(coords, { padding: [60, 60], maxZoom: 15 });
+ }
+ }, [map, coords]);
+ return null;
+};
+
+const MapRotationHandler = ({ rotation }: { rotation: number }) => {
+ const map = useMap();
+ const cumulativeRotationRef = useRef(0);
+ const prevRotationRef = useRef(0);
+
+ useEffect(() => {
+ const container = map.getContainer();
+ container.style.transformOrigin = 'center center';
+ container.style.willChange = 'transform';
+
+ if (rotation === 0) {
+ cumulativeRotationRef.current = 0;
+ prevRotationRef.current = 0;
+ container.style.transform = 'rotate(0deg) scale(1)';
+ return;
+ }
+
+ let delta = rotation - prevRotationRef.current;
+ if (delta > 180) delta -= 360;
+ else if (delta < -180) delta += 360;
+
+ cumulativeRotationRef.current += delta;
+ prevRotationRef.current = rotation;
+
+ container.style.transform = `rotate(${cumulativeRotationRef.current}deg) scale(2.2) translateZ(0)`;
+ container.style.transition = 'transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1)';
+ }, [rotation, map]);
+ return null;
+};
+
+const CompassInteractionDetector = ({
+ onUserInteraction
+}: {
+ onUserInteraction: () => void;
+}) => {
+ useMapEvents({
+ movestart: onUserInteraction,
+ zoomstart: onUserInteraction,
+ dragstart: onUserInteraction,
+ });
+ return null;
+};
+
+const MapRefSetter = ({ mapRef }: { mapRef: React.MutableRefObject
}) => {
+ const map = useMap();
+ useEffect(() => {
+ mapRef.current = map;
+ }, [map, mapRef]);
+ return null;
+};
+
+export const TourNavigationPage: React.FC = ({ tourId, routeData, onBack }) => {
+ const [isCompassActive, setIsCompassActive] = useState(false);
+ const [currentHeading, setCurrentHeading] = useState(0);
+ const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null);
+ const [error, setError] = useState(null);
+
+ const mapRef = useRef(null);
+ const watchIdRef = useRef(null);
+ const fetchLockRef = useRef(false);
+
+ const originLat = routeData?.origin?.lat;
+ const originLng = routeData?.origin?.lng;
+ const destLat = routeData?.destination?.lat;
+ const destLng = routeData?.destination?.lng;
+
+ useEffect(() => {
+ if (!routeData || !originLat || !originLng || !destLat || !destLng) {
+ setRouteGeometry(null);
+ setError(null);
+ return;
+ }
+
+ if (fetchLockRef.current) return;
+
+ const calculateOptimalRoute = async () => {
+ try {
+ fetchLockRef.current = true;
+ const url = `https://router.project-osrm.org/route/v1/driving/${originLng},${originLat};${destLng},${destLat}?overview=full&geometries=geojson`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`OSRM error: ${res.status}`);
+ const data: { code: string; routes: Array<{ geometry: { coordinates: number[][] } }> } = await res.json();
+ if (data.code !== 'Ok' || !data.routes?.length) throw new Error('Không tìm thấy lộ trình phù hợp');
+
+ const coords = data.routes[0].geometry.coordinates.map((c: number[]) => [c[1], c[0]] as [number, number]);
+ setRouteGeometry(coords);
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ fetchLockRef.current = false;
+ }
+ };
+
+ calculateOptimalRoute();
+
+ return () => {
+ fetchLockRef.current = false;
+ };
+ }, [routeData, originLat, originLng, destLat, destLng]);
+
+ const handleUserInteraction = useCallback(() => {
+ if (isCompassActive) {
+ setIsCompassActive(false);
+ }
+ }, [isCompassActive]);
+
+ useEffect(() => {
+ if (!isCompassActive) {
+ setCurrentHeading(0);
+ return;
+ }
+
+ if (navigator.geolocation) {
+ watchIdRef.current = navigator.geolocation.watchPosition(
+ (position) => {
+ if (mapRef.current) {
+ mapRef.current.setView([position.coords.latitude, position.coords.longitude], undefined, { animate: true });
+ }
+ if (position.coords.heading !== null) {
+ setCurrentHeading(position.coords.heading);
+ }
+ },
+ (err) => console.error("Compass tracking acquisition error:", err),
+ { enableHighAccuracy: true }
+ );
+ }
+
+ const handleOrientation = (event: any) => {
+ let heading: number | null = null;
+ if (event.webkitCompassHeading !== undefined && event.webkitCompassHeading !== null) {
+ heading = event.webkitCompassHeading;
+ } else if (event.alpha !== null && event.alpha !== undefined) {
+ if (event.absolute === true || event.type === 'deviceorientationabsolute') {
+ heading = (360 - event.alpha) % 360;
+ }
+ }
+ if (heading !== null) {
+ setCurrentHeading(heading);
+ }
+ };
+
+ window.addEventListener('deviceorientation', handleOrientation, true);
+ window.addEventListener('deviceorientationabsolute', handleOrientation, true);
+
+ return () => {
+ if (watchIdRef.current !== null) {
+ navigator.geolocation.clearWatch(watchIdRef.current);
+ }
+ window.removeEventListener('deviceorientation', handleOrientation, true);
+ window.removeEventListener('deviceorientationabsolute', handleOrientation, true);
+ };
+ }, [isCompassActive]);
+
+ const userIcon = useMemo(() => L.divIcon({
+ className: '!bg-transparent !border-none',
+ html: ``,
+ iconSize: [32, 32],
+ iconAnchor: [16, 16]
+ }), []);
+
+ const destIcon = useMemo(() => L.divIcon({
+ className: '!bg-transparent !border-none',
+ html: `Đ
`,
+ iconSize: [32, 32],
+ iconAnchor: [16, 16]
+ }), []);
+
+ if (!routeData) return null;
+
+ const center: [number, number] = routeData.origin && routeData.destination
+ ? [(routeData.origin.lat + routeData.destination.lat) / 2, (routeData.origin.lng + routeData.destination.lng) / 2]
+ : [0, 0];
+
+ return (
+
+
+
+
+ {routeData.tourTitle || "Bản đồ chỉ đường"}
+
+
+
+
+
+
+
+ {routeData.origin && (
+
+
+ Bạn đang ở đây
+
+
+ )}
+ {routeData.destination && (
+
+
+ {routeData.destination.name}
+
+
+ )}
+ {routeGeometry && }
+ {routeGeometry && (
+
+ )}
+
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ );
+};