# 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 } /> ### 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(null); // Anchor pointer to hold your initialized Map instance const watchIdRef = useRef(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 (
{/* PART 1: TOP-BAR HEADER ZONE (Isolated from Sub-Tabs layout skins) */}

{routeData.tourTitle}

{/* PART 2: FULLSCREEN MAP EDGE-TO-EDGE CONTAINER */}
{/* Map injection Canvas - Absolutely no rounded boundaries or wrapper cushions */}
{/* PART 3: FLOATING INTERACTIVE COMPASS ACTION BUTTON */}
); }; ## 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.