fix: bản đồ không zoom hay thu nhỏ được, không hiển thị nút vị trí của tôi trên bản đồ

This commit is contained in:
2026-06-25 15:57:25 +07:00
parent 33a5996bee
commit 4e93722ba5
4 changed files with 268 additions and 252 deletions
-196
View File
@@ -1,196 +0,0 @@
# To AI Agent: Create Standalone Navigation Route and Page for Mobile Map Viewport
## 1. Context & Architectural Goal
We are replacing the modal-based map system. On mobile viewports, when a user clicks the circular timeline node next to a location card, the application must transition entirely to a **new, dedicated standalone page** rather than opening a popup.
This architectural shift prevents the map engine from being trapped inside parent stacking contexts (accordions/scroll wrappers) or restricted app layout frames, allowing the map to take up 100% of the mobile viewport safely.
---
## 2. Structural Requirements for the New Page Layout
The new page (`TourNavigationPage.tsx`) must strictly render only two visual zones:
1. **Top-bar (Zone 1):** A sticky/fixed header containing a back button (`<`) to return to the previous tour timeline, and the text title of the active Tour.
2. **Fullscreen Map (Zone 2):** Extending from the absolute bottom edge of the Top-bar all the way to the bottom edge of the browser viewport (`100vw` by `100vh minus header height`).
3. **Compass Floating Action Button:** A separate, high-priority circular button placed explicitly at the **bottom-right corner** (`bottom-6 right-6`) floating directly on top of the map grid tiles.
---
## 3. Step-by-Step Implementation Refactoring Blueprint
### Step 1: Register the Standalone Route
Locate your central application routing file (e.g., `frontend/src/App.tsx`, `backend/src/main.ts`, or `routes.tsx`) and register the isolated navigation path:
```typescript
// Insert this route path inside your React Router / routing array configuration
<Route path="/tour/:id/navigation" element={<TourNavigationPage />} />
### Step 2: Update Trigger Behavior in ItineraryTimeline.tsx
Locate the white circular node button component. Convert the click handler from opening a modal state to a standard React Router redirection hook, passing all geospatial metadata safely within the history state container:
import { useNavigate, useParams } from 'react-router-dom';
const navigate = useNavigate();
const { id: tourId } = useParams();
const handleTriggerNavigation = (location: any, tourTitle: string) => {
if (!location.latitude || !location.longitude) {
alert("Địa điểm này chưa được cấu hình tọa độ GPS chính xác.");
return;
}
if (!navigator.geolocation) {
alert("Thiết bị không hỗ trợ định vị GPS toàn cầu.");
return;
}
// Request device coordinates before firing routing transitions
navigator.geolocation.getCurrentPosition(
(position) => {
// Transition out of the timeline and push spatial coordinates via state pack
navigate(`/tour/${tourId}/navigation`, {
state: {
origin: {
lat: position.coords.latitude,
lng: position.coords.longitude
},
destination: {
lat: parseFloat(location.latitude),
lng: parseFloat(location.longitude),
name: location.name || "Điểm đến"
},
tourTitle: tourTitle || "Chi tiết hành trình"
}
});
},
(error) => {
alert("Vui lòng bật quyền truy cập vị trí (GPS) trên trình duyệt để tìm đường.");
},
{ enableHighAccuracy: true, timeout: 7000 }
);
};
### Step 3: Create the New Page File (TourNavigationPage.tsx)
Create a brand new separate file at pages/TourNavigationPage.tsx. Enforce a flat layout architecture free from layout decorators or panel decorators:
import React, { useEffect, useState, useRef } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { ChevronLeft, Compass } from 'lucide-react';
export const TourNavigationPage: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { id: tourId } = useParams();
const [isCompassActive, setIsCompassActive] = useState(false);
const mapRef = useRef<any>(null); // Anchor pointer to hold your initialized Map instance
const watchIdRef = useRef<number | null>(null);
const routeData = location.state?.origin && location.state?.destination ? location.state : null;
// Security Rail Guard: Bounce user back to timeline overview if accessed via raw URL parameters
useEffect(() => {
if (!routeData) {
console.warn("Direct route access missing state payload variables. Backtracking.");
navigate(`/tour/${tourId}`);
}
}, [routeData, navigate, tourId]);
// Compass Toggle Control Loops (Auto-rotation and Gesture cancellation)
useEffect(() => {
if (!mapRef.current || !routeData) return;
const map = mapRef.current;
const disableCompassOnGesture = () => {
if (isCompassActive) {
console.log("User manual gesture detected. Detaching compass auto-centering.");
setIsCompassActive(false);
}
};
// Listen to manual map manipulations to automatically drop tracking state
map.on('movestart', disableCompassOnGesture);
map.on('zoomstart', disableCompassOnGesture);
map.on('dragstart', disableCompassOnGesture);
return () => {
map.off('movestart', disableCompassOnGesture);
map.off('zoomstart', disableCompassOnGesture);
map.off('dragstart', disableCompassOnGesture);
};
}, [isCompassActive, routeData]);
useEffect(() => {
if (isCompassActive && navigator.geolocation) {
watchIdRef.current = navigator.geolocation.watchPosition(
(pos) => {
if (mapRef.current) {
// Dynamically center and turn map bearing heading to follow direction of travel
mapRef.current.easeTo({
center: [pos.coords.longitude, pos.coords.latitude],
bearing: pos.coords.heading || 0,
duration: 800
});
}
},
(err) => console.error(err),
{ enableHighAccuracy: true }
);
} else {
if (watchIdRef.current !== null) {
navigator.geolocation.clearWatch(watchIdRef.current);
watchIdRef.current = null;
}
if (mapRef.current) mapRef.current.easeTo({ bearing: 0, duration: 400 });
}
return () => { if (watchIdRef.current !== null) navigator.geolocation.clearWatch(watchIdRef.current); };
}, [isCompassActive]);
if (!routeData) return null;
return (
<div className="w-screen h-screen min-h-screen bg-slate-950 flex flex-col overflow-hidden select-none text-white antialiased">
{/* PART 1: TOP-BAR HEADER ZONE (Isolated from Sub-Tabs layout skins) */}
<div className="w-full bg-[#1e293b]/95 backdrop-blur-md border-b border-slate-800 px-4 py-3.5 flex items-center gap-3 z-50">
<button
onClick={() => navigate(`/tour/${tourId}`)}
className="text-slate-300 hover:text-white p-1 rounded-lg hover:bg-slate-800 transition-colors"
title="Quay lại danh sách lộ trình"
>
<ChevronLeft className="w-6 h-6" />
</button>
<h2 className="text-sm font-bold truncate tracking-wide text-slate-100">
{routeData.tourTitle}
</h2>
</div>
{/* PART 2: FULLSCREEN MAP EDGE-TO-EDGE CONTAINER */}
<div className="w-full h-full relative flex-1">
{/* Map injection Canvas - Absolutely no rounded boundaries or wrapper cushions */}
<div id="dedicated-page-map-canvas" className="w-full h-full absolute inset-0 rounded-none border-none" />
{/* PART 3: FLOATING INTERACTIVE COMPASS ACTION BUTTON */}
<button
onClick={() => setIsCompassActive(!isCompassActive)}
className={`absolute bottom-8 right-6 z-40 w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
isCompassActive
? 'bg-blue-600 border-blue-400 text-white animate-pulse'
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-blue-400'
}`}
title="Chuyển đổi chế độ xoay bản đồ tự động theo hướng di chuyển"
>
<Compass className="w-7 h-7" />
</button>
</div>
</div>
);
};
## 4. Verification & Quality Acceptance Criteria for AI Agent
[ ] Eradication of Inset Borders: The map canvas must draw edge-to-edge on mobile browser views without exposing structural margins or inner framing borders.
[ ] Tab Bar Exclusion: The secondary menu header rows ("Lộ trình, Chi phí, Ảnh...") must be 100% hidden on this path layout.
[ ] Lifecycle Integrity Test: Verify that performing a quick finger-drag pan gesture on the map surface immediately turns off the pulse mode state of the bottom-right Compass button.
+134
View File
@@ -0,0 +1,134 @@
# To AI Agent: Audit Location Lock Bug and Refactor GPS Tracking into a Toggle Stateful Button
## 1. Context & Problem Statement
Currently, in our travel planner application map engine (on pages like `TourNavigationPage.tsx`, `LocationNavigationModal.tsx`, or map utilities), the viewport is continuously forced to lock onto the user's live GPS position. This architecture severely damages mobile UX because:
1. It prevents users from manually dragging, panning, or scouting other areas of the map terrain.
2. There is no control interface to temporarily mute or disable live GPS tracking.
**Objective:** - Run a global audit across the entire codebase to locate functions driving this forced-center trap (e.g., custom hooks, `requestGpsPosition`, native geolocation callbacks, or reactive map state updates).
- Refactor the logic so that live tracking is bound strictly to an independent toggle state button. The map must **ONLY** lock/re-center on the user's coordinates when this tracking toggle button is actively switched **ON**.
---
## 2. Phase 1: Codebase Audit Plan (Where to Search)
Scan the entire project repository (specifically `/frontend/src`) using code search patterns to intercept the lock mechanism. Target the following files and keywords:
### Key Target Files to Inspect:
- `frontend/src/pages/TourNavigationPage.tsx`
- `frontend/src/components/LocationNavigationModal.tsx`
- Any custom hooks or contexts handling geography, such as `useGeolocation.ts`, `useMap.ts`, or generic map setup wrappers.
### Regex & Keyword Global Search Queries:
- Search for native background watchers: `navigator.geolocation.watchPosition` or `navigator.geolocation.getCurrentPosition`
- Search for custom map-centering loops: `requestGpsPosition`, `followUser`, `centerToUser`
- Search for viewport mutation commands specific to our active map engine stack:
- **Leaflet:** `.setView(`, `.panTo(`, `center={`
- **Mapbox GL JS:** `.flyTo(`, `.easeTo(`, `.jumpTo(`
---
## 3. Phase 2: Technical Refactoring Blueprint
Once the tracking logic code blocks are isolated from Phase 1, implement the structural state safety rails below:
### Step 1: Initialize the Tracking State Guard
Introduce a state hook controller (`isTrackingLocation`) to manage whether the view should actively mirror device coordinates:
```typescript
// Add inside the map controller/page container component
const [isTrackingLocation, setIsTrackingLocation] = useState(false);
const watchIdRef = useRef<number | null>(null);
### Step 2: Encapsulate the Geolocation Watcher Handler
Wrap your positioning tracking engine loop inside a conditional check governed directly by the state guard. Ensure that if the tracking state is disabled, the background watcher cleanly unmounts:
useEffect(() => {
if (isTrackingLocation) {
if (navigator.geolocation) {
console.log("GPS Location Tracking engaged. Syncing viewport to center...");
watchIdRef.current = navigator.geolocation.watchPosition(
(position) => {
const { latitude, longitude, heading } = position.coords;
if (mapRef.current) {
// ✅ CORRECTION: Viewport ONLY repositions center when tracking button is active
mapRef.current.easeTo({
center: [longitude, latitude],
zoom: 16, // Lock to comfortable navigation zoom level
duration: 600
});
}
},
(error) => console.error("GPS stream tracking lost:", error),
{ enableHighAccuracy: true }
);
}
} else {
// Clean up tracking process instantly when toggled OFF
if (watchIdRef.current !== null) {
navigator.geolocation.clearWatch(watchIdRef.current);
watchIdRef.current = null;
console.log("GPS Location Tracking disabled. Map control released to user.");
}
}
return () => {
if (watchIdRef.current !== null) navigator.geolocation.clearWatch(watchIdRef.current);
};
}, [isTrackingLocation]);
### Step 3: Implement Gesture Detection (UX Safety Rail)
If the user manually drags the screen while tracking is active, the tracking state must automatically toggle OFF so the viewport doesn't fight against the user's finger movements:
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
const breakTrackingOnGesture = () => {
if (isTrackingLocation) {
console.log("User touch map interaction detected. Disengaging auto-center lock.");
setIsTrackingLocation(false); // Automatically drop tracking flag on map pan/zoom
}
};
map.on('dragstart', breakTrackingOnGesture);
map.on('zoomstart', breakTrackingOnGesture);
map.on('movestart', breakTrackingOnGesture);
return () => {
map.off('dragstart', breakTrackingOnGesture);
map.off('zoomstart', breakTrackingOnGesture);
map.off('movestart', breakTrackingOnGesture);
};
}, [isTrackingLocation]);
### Step 4: Render the UI Toggle Button UI Component
Deploy a new independent floating button on top of the map canvas workspace (placed at bottom-24 right-6, just right above your custom Compass button layout):
{/* FLOATING LIVE GPS TRACKING CENTER LOCK BUTTON */}
<button
onClick={() => setIsTrackingLocation(!isTrackingLocation)}
className={`absolute bottom-24 right-6 z-40 w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
isTrackingLocation
? 'bg-green-600 border-green-400 text-white animate-pulse'
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-green-500'
}`}
title={isTrackingLocation ? "Tắt tự động định tâm vị trí" : "Bật tự động định tâm theo vị trí của bạn"}
>
{/* Replace Crosshair icon element with your active layout icon package asset */}
<svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
## 4. Verification & Quality Acceptance Criteria
[ ] Code Erasure Verification: Confirm that old continuous loops or uncontrolled recursive .setView/.easeTo methods triggered instantly on map load are fully removed or properly contained inside the state block.
[ ] Default State Freedom: Upon opening the map page path, tracking must default to OFF. Users must be able to drag the map anywhere in the world without the screen snapped or yanked back to their physical house position.
[ ] Toggle Activation Centering: Pressing the new GPS tracking button must instantly engage the animation, center the map view directly on top of the user blue dot icon, and follow them smoothly if they move.
[ ] Manual Override Interception: Turn tracking ON. Drag the map manually with a finger gesture. Verify that the tracking button instantly changes style states back to deactivated and tracking shuts down cleanly.
+24 -1
View File
@@ -177,8 +177,11 @@ const MapTourBounds = ({ locations }: { locations: any[] }) => {
// Component Helper để di chuyển tâm bản đồ về vị trí người dùng khi nhấn nút
const RecenterUser = ({ position, trigger }: { position: [number, number] | null, trigger: number }) => {
const map = useMap();
const lastTriggerRef = useRef(0);
useEffect(() => {
if (position && trigger > 0) {
if (position && trigger > lastTriggerRef.current) {
lastTriggerRef.current = trigger;
map.setView(position, 16, { animate: true });
}
}, [trigger, position, map]);
@@ -2481,6 +2484,16 @@ export const TourDetailPage = ({
})}
</MarkerClusterGroup>
</MapContainer>
{/* Floating Locate User Button */}
<button
onClick={() => setLocateTrigger(prev => prev + 1)}
disabled={!userLocation}
className={`absolute bottom-4 right-4 z-[1001] w-10 h-10 bg-white/90 backdrop-blur-md rounded-xl border border-white shadow-xl text-blue-600 hover:bg-blue-50 transition-all active:scale-95 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed`}
title="Vị trí của tôi"
>
<LocateFixed className="w-5 h-5" />
</button>
{/* Menu ngữ cảnh khi click chuột phải vào con đường */}
{routeMenu && (
@@ -3268,6 +3281,16 @@ export const TourDetailPage = ({
})}
</MapContainer>
{/* Floating Locate User Button (Fullscreen) */}
<button
onClick={() => setLocateTrigger(prev => prev + 1)}
disabled={!userLocation}
className={`absolute bottom-8 right-6 z-[1001] w-12 h-12 bg-white/90 backdrop-blur-md rounded-2xl border border-white shadow-xl text-blue-600 hover:bg-blue-50 transition-all active:scale-95 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed`}
title="Vị trí của tôi"
>
<LocateFixed className="w-6 h-6" />
</button>
{/* Overlay điều khiển trên bản đồ toàn màn hình */}
<div className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] right-4 z-[1001] flex flex-col gap-2">
<button
+110 -55
View File
@@ -137,62 +137,24 @@ const MapSizeHandler = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | nul
export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId, routeData, onBack }) => {
const [isCompassActive, setIsCompassActive] = useState(false);
const [isLocatingUser, setIsLocatingUser] = useState(false);
const [isTrackingLocation, setIsTrackingLocation] = useState(false);
const [currentHeading, setCurrentHeading] = useState(0);
const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null);
const [error, setError] = useState<string | null>(null);
const mapRef = useRef<L.Map | null>(null);
const watchIdRef = useRef<number | null>(null);
const trackingWatchIdRef = useRef<number | null>(null);
const compassWatchIdRef = useRef<number | null>(null);
const fetchLockRef = useRef(false);
const hasInteractedRef = useRef(false);
const isProgrammaticMoveRef = useRef(false);
const originLat = routeData?.origin?.lat;
const originLng = routeData?.origin?.lng;
const destLat = routeData?.destination?.lat;
const destLng = routeData?.destination?.lng;
const stopLocating = useCallback(() => {
setIsLocatingUser(false);
}, []);
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const onMoveStart = () => {
if (isLocatingUser) {
stopLocating();
}
};
map.on('movestart', onMoveStart);
return () => {
map.off('movestart', onMoveStart);
};
}, [isLocatingUser, stopLocating]);
useEffect(() => {
if (!isLocatingUser || !mapRef.current) return;
if (navigator.geolocation) {
watchIdRef.current = navigator.geolocation.watchPosition(
(position) => {
if (mapRef.current) {
const currentZoom = mapRef.current.getZoom();
mapRef.current.setView([position.coords.latitude, position.coords.longitude], currentZoom, { animate: true });
}
},
(err) => console.error("User location tracking error:", err),
{ enableHighAccuracy: true }
);
}
return () => {
if (watchIdRef.current !== null) {
navigator.geolocation.clearWatch(watchIdRef.current);
watchIdRef.current = null;
}
};
}, [isLocatingUser]);
useEffect(() => {
if (!routeData || !originLat || !originLng || !destLat || !destLng) {
@@ -228,6 +190,74 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
};
}, [routeData, originLat, originLng, destLat, destLng]);
// Effect 1: Live tracking using watchPosition
useEffect(() => {
if (isTrackingLocation) {
if (navigator.geolocation) {
console.log("GPS Location Tracking engaged. Syncing viewport to center...");
trackingWatchIdRef.current = navigator.geolocation.watchPosition(
(position) => {
const { latitude, longitude } = position.coords;
if (mapRef.current) {
const currentZoom = mapRef.current.getZoom();
isProgrammaticMoveRef.current = true;
mapRef.current.setView([latitude, longitude], Math.max(currentZoom, 16), { animate: true });
setTimeout(() => {
isProgrammaticMoveRef.current = false;
}, 100);
}
},
(error) => console.error("GPS stream tracking lost:", error),
{ enableHighAccuracy: true }
);
}
} else {
if (trackingWatchIdRef.current !== null) {
navigator.geolocation.clearWatch(trackingWatchIdRef.current);
trackingWatchIdRef.current = null;
console.log("GPS Location Tracking disabled. Map control released to user.");
}
}
return () => {
if (trackingWatchIdRef.current !== null) {
navigator.geolocation.clearWatch(trackingWatchIdRef.current);
trackingWatchIdRef.current = null;
}
};
}, [isTrackingLocation]);
// Effect 2: Gesture detection to auto-toggle tracking OFF when user interacts
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
const breakTrackingOnGesture = () => {
if (isProgrammaticMoveRef.current) {
return;
}
if (isTrackingLocation) {
console.log("User touch map interaction detected. Disengaging auto-center lock.");
setIsTrackingLocation(false);
}
if (isCompassActive) {
console.log("User touch map interaction detected. Disengaging compass lock.");
setIsCompassActive(false);
}
};
map.on('dragstart', breakTrackingOnGesture);
map.on('zoomstart', breakTrackingOnGesture);
map.on('movestart', breakTrackingOnGesture);
return () => {
map.off('dragstart', breakTrackingOnGesture);
map.off('zoomstart', breakTrackingOnGesture);
map.off('movestart', breakTrackingOnGesture);
};
}, [isTrackingLocation, isCompassActive]);
const handleUserInteraction = useCallback(() => {
hasInteractedRef.current = true;
if (isCompassActive) {
@@ -242,10 +272,14 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
}
if (navigator.geolocation) {
watchIdRef.current = navigator.geolocation.watchPosition(
compassWatchIdRef.current = navigator.geolocation.watchPosition(
(position) => {
if (mapRef.current) {
isProgrammaticMoveRef.current = true;
mapRef.current.setView([position.coords.latitude, position.coords.longitude], undefined, { animate: true });
setTimeout(() => {
isProgrammaticMoveRef.current = false;
}, 100);
}
if (position.coords.heading !== null) {
setCurrentHeading(position.coords.heading);
@@ -274,8 +308,9 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
window.addEventListener('deviceorientationabsolute', handleOrientation, true);
return () => {
if (watchIdRef.current !== null) {
navigator.geolocation.clearWatch(watchIdRef.current);
if (compassWatchIdRef.current !== null) {
navigator.geolocation.clearWatch(compassWatchIdRef.current);
compassWatchIdRef.current = null;
}
window.removeEventListener('deviceorientation', handleOrientation, true);
window.removeEventListener('deviceorientationabsolute', handleOrientation, true);
@@ -304,15 +339,24 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
const centerOnUser = () => {
if (navigator.geolocation) {
setIsLocatingUser(true);
navigator.geolocation.getCurrentPosition(
(position) => {
setIsLocatingUser(false);
if (mapRef.current) {
const currentZoom = mapRef.current.getZoom();
mapRef.current.setView([position.coords.latitude, position.coords.longitude], currentZoom, { animate: true });
canFitRef.current = false;
isProgrammaticMoveRef.current = true;
mapRef.current.setView([position.coords.latitude, position.coords.longitude], Math.max(currentZoom, 16), { animate: true });
setTimeout(() => {
isProgrammaticMoveRef.current = false;
}, 100);
hasInteractedRef.current = true;
}
},
(err) => console.error("Cannot get user location:", err),
(err) => {
setIsLocatingUser(false);
console.error("Cannot get user location:", err);
},
{ enableHighAccuracy: true }
);
}
@@ -362,18 +406,13 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
{/* Locate User Button */}
<button
onClick={() => {
setIsLocatingUser(!isLocatingUser);
if (!isLocatingUser) {
centerOnUser();
}
}}
onClick={centerOnUser}
className={`absolute bottom-8 left-6 z-[999] w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
isLocatingUser
? 'bg-blue-600 border-blue-400 text-white animate-pulse'
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-blue-400'
}`}
title="Vị trí của tôi"
title="Định vị vị trí hiện tại của bạn"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3" />
@@ -381,6 +420,22 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
</svg>
</button>
{/* FLOATING LIVE GPS TRACKING CENTER LOCK BUTTON */}
<button
onClick={() => setIsTrackingLocation(!isTrackingLocation)}
className={`absolute bottom-24 right-6 z-[999] w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
isTrackingLocation
? 'bg-green-600 border-green-400 text-white animate-pulse'
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-green-500'
}`}
title={isTrackingLocation ? "Tắt tự động định tâm vị trí" : "Bật tự động định tâm theo vị trí của bạn"}
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3" />
<path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
</svg>
</button>
<button
onClick={() => setIsCompassActive(!isCompassActive)}
className={`absolute bottom-8 right-6 z-[999] w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${