From 3bc7c8a160024a0fb8571e58ee1da6026e1f765d Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sat, 27 Jun 2026 19:38:05 +0700 Subject: [PATCH] fix: menu on android --- ANDROID_FIX.md | 107 +++++++++++++++ DATE_FIX_TOUR.md | 126 ------------------ frontend/dist/index.html | 2 +- .../src/components/MapProfileDropdown.tsx | 5 + frontend/src/pages/MemberDashboard.tsx | 25 ++-- 5 files changed, 128 insertions(+), 137 deletions(-) create mode 100644 ANDROID_FIX.md delete mode 100644 DATE_FIX_TOUR.md diff --git a/ANDROID_FIX.md b/ANDROID_FIX.md new file mode 100644 index 0000000..b066762 --- /dev/null +++ b/ANDROID_FIX.md @@ -0,0 +1,107 @@ +# To AI Agent: Fix Mobile Dropdown Layout Displacement and Eliminate `.filter is not a function` Runtime Crashes + +## 1. Bug Analysis & Context +We are resolving three critical layout and runtime bugs observed during Android Simulator testing (`image.png`): + +1. **Dropdown Menu Displacement:** The menu drawer/dropdown container is losing its absolute coordinate anchor pool. Instead of floating gracefully on top of the map layer as a neat dropdown card, it wraps below the viewport grid, conflicting with the lower navigation action buttons ("Khám phá", "Chụp ảnh") and community photo streams. +2. **`g.filter is not a function` (Friends List Crash):** Triggering the Friend List components crashes the UI thread into a White Screen of Death (WSoD). +3. **`Error fetching connections: TypeError: ...filter is not a function` (LiveChat Crash):** Opening the Live Chat view displays the exact same array mutation failure. + - *Root Cause for 2 & 3:* The asynchronous API network response payload or socket stream data returned from the server backend is **not a clean Array structure**. It is likely either wrapped inside an object wrapper (e.g., `{ success: true, friends: [] }`) or returns `null`/`undefined` due to network delays. Invoking `.filter()` directly on a non-array object instantly freezes the React rendering lifecycle. + +--- + +## 2. Refactoring Strategy + +### 2.1. Fix Dropdown Layout Context for Mobile/Android Viewports +On mobile viewports, traditional hover/click absolute dropdown boxes overflow or clip out. We must force the dropdown menu container inside `MapProfileDropdown.tsx` to act as a structured **Sticky Floating Overlay** or a dedicated **Mobile Bottom-Sheet Box** with a precise `z-index`. + +### 2.2. Implement Defensive Array Architecture (Fix Crashes 2 & 3) +We must implement a protective array fallback guard across all data mapping blocks (`friends.filter`, `connections.filter`) using **`Array.isArray()`** validation, coupled with unified error boundaries. + +--- + +## 3. Code Refactoring Blueprint + +### Step 1: Overhaul Dropdown Styling for Mobile Boundaries (`MapProfileDropdown.tsx`) +Ensure the menu card isolates itself perfectly above the underlying view map layout and handles sizing dimensions cleanly: + +```jsx +{/* ✅ RESPONSIVE FIX: Mobile-optimized absolute floating panel structure */} +
+ {/* Menu list rows (Tạo tour, Hành trình, Thư viện ảnh...) populate cleanly here */} + +
+ +### Step 2: Fix Friend List Array Mutation Crash (FriendsManagerModal.tsx) +Locate where the backend data hook is consumed. Inject an explicit defensive array verification check: + +// ❌ OLD CRASH-PRONE CODE: +// const activeFriends = data.filter(f => f.status === 'active'); + +// ✅ NEW DEFENSIVE RAY WRAPPER: +const [friendsList, setFriendsList] = useState([]); + +useEffect(() => { + api.get('/friends/connections') + .then((res) => { + // Deconstruct and verify input type carefully before committing to state + const rawPayload = res.data; + + if (rawPayload && Array.isArray(rawPayload)) { + setFriendsList(rawPayload); + } else if (rawPayload && Array.isArray(rawPayload.data)) { + setFriendsList(rawPayload.data); // Fallback unpacker matching nested API responses + } else { + console.error("⚠️ Expected array structure but received:", rawPayload); + setFriendsList([]); // Secure fallback to empty array initialization to protect .filter loops + } + }) + .catch((err) => { + console.error("Failed to compile connections list stream safely:", err); + setFriendsList([]); // Fallback safety initialization + }); +}, []); + +// Secure conditional processing guard rail +const activeFriends = Array.isArray(friendsList) + ? friendsList.filter((f: any) => f && f.status === 'active') + : []; + +### Step 3: Fix Live Chat Connections Filtering Failure (LiveChatModal.tsx) +Locate the data fetcher inside your real-time socket listener or chat state parser module and re-engineer it with identical fallback shields: + +const fetchUserConnections = async () => { + try { + const response = await api.get('/chat/connections'); + const resultData = response.data; + + // Direct defensive validation shield check + const verifiedConnectionsArray = Array.isArray(resultData) + ? resultData + : (resultData && Array.isArray(resultData.connections) ? resultData.connections : []); + + /* ✅ CRITICAL FIX: Running .filter on verified guaranteed array structure only */ + const onlineConnections = verifiedConnectionsArray.filter((conn: any) => conn && conn.isOnline === true); + + setConnections(verifiedConnectionsArray); + + } catch (error) { + console.error("[LiveChatModal] Error fetching connections cleanly:", error); + setConnections([]); // Force secure array context initialization on intercepting crash exceptions + } +}; + +## 4. Verification & Quality Acceptance Criteria for AI Agent + [ ] Dropdown Layout Verification: Open the avatar menu on the Android Simulator. The profile dropdown modal panel must lock floating positions right below the top header profile cluster, without displacing the community images ribbon or bleeding into the screen bottom. + + [ ] WSoD Prevention Validation: Simulate an empty or broken server endpoint response (500 or blank text). Confirm that the component prints the error safely to the console tracker log while the screen continues to render placeholder empty-states beautifully without turning into a white void. + + [ ] Array Guard Robustness: Run a global textual scan over modified view sheets. Confirm all custom list operations on fetched database schemas verify array validity using Array.isArray() wrappers prior to calling processing filters. \ No newline at end of file diff --git a/DATE_FIX_TOUR.md b/DATE_FIX_TOUR.md deleted file mode 100644 index 68e39c1..0000000 --- a/DATE_FIX_TOUR.md +++ /dev/null @@ -1,126 +0,0 @@ -# To AI Agent: Implement Smart Date-Based Accordion Auto-Expansion and Smooth Viewport Auto-Scroll - -## 1. Context & UI/UX Requirements -We are enhancing the routing UX between `MemberDashboard.tsx` and `ItineraryTimeline.tsx` based on `image_cc31a4.png`. - -### Functional Specifications: -1. **Date-Matching Evaluation:** When a user clicks "Chi tiết hành trình" on a tour card, calculate if the current user system local date falls inside any Stage/Leg timeline block. -2. **Auto-Expansion State:** On timeline page load, the matched Leg accordion must be expanded by default (`expandedStageId === leg.id`). -3. **Smart Auto-Scroll Layout:** The viewport must smoothly auto-scroll so that the expanded Leg's title bar lands **exactly below the sticky navigation tab bar** ("Lộ trình", "Chi phí", etc.), making it fully visible at the top of the viewport without layout clipping. - ---- - -## 2. Technical Architecture & Layout Constraints - -- **The Sticky Header Challenge:** Since the top navigation tab bar uses sticky/fixed positioning, a naive `scrollIntoView({ block: 'start' })` will cause the Leg's title to crawl *underneath* the tabs, hiding it. -- **The Solution:** We will inject a dynamic CSS scroll-margin-top parameter (`scroll-mt-[70px]` or matching header height) on each Leg container, and trigger a minor delayed layout effect pool using a React `setTimeout` to wait for the DOM accordion expansion reflow before pulling the scroll trigger. - ---- - -## 3. Code Refactoring Blueprint - -### Step 1: Update Navigation Payload Handler in Dashboard Component -Ensure the `MemberDashboard.tsx` accurately packages the matched target identifier into the client route state bucket: - -```typescript -import { useNavigate } from 'react-router-dom'; - -const navigate = useNavigate(); - -const handleNavigateToItinerary = (tour: any) => { - let targetLegId = null; - - if (tour?.legs && tour.legs.length > 0) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - - for (const leg of tour.legs) { - const rawStart = leg.startDate || leg.plannedStart || leg.date; - const rawEnd = leg.endDate || leg.plannedEnd || leg.date; - - if (rawStart) { - const startBound = new Date(rawStart); - startBound.setHours(0, 0, 0, 0); - - const endBound = rawEnd ? new Date(rawEnd) : new Date(rawStart); - endBound.setHours(23, 59, 59, 999); - - if (today >= startBound && today <= endBound) { - targetLegId = leg.id; - break; - } - } - } - - if (!targetLegId) { - targetLegId = tour.legs[0].id; // Fallback to Chặng 1 if out of tour bounds - } - } - - navigate(`/tour/${tour.id}`, { - state: { defaultExpandedLegId: targetLegId, shouldScrollToTarget: true } - }); -}; - -### Step 2: Inject Safe Identifiers and Scroll Margin inside ItineraryTimeline -Locate the top-level outer container div of each Leg item inside the timeline loop (currentTour.legs.map). Assign a unique id and a scroll margin class utility: - -{currentTour.legs.map((leg: any, legIdx: number) => ( -
- {/* Accordion Header Title ("Chặng 2: Dạo chơi ở xứ hoa vàng...") */} -
- ... -
-
-))} - -### Step 3: Implement Lifecycle Interaction Trigger Engine -Inside ItineraryTimeline.tsx, listen to the passed history route parameters. Set the state, then handle the async smooth scrolling action: - -import { useLocation } from 'react-router-dom'; - -const location = useLocation(); -const [expandedStageId, setExpandedStageId] = useState(null); - -useEffect(() => { - if (location.state?.defaultExpandedLegId) { - const targetId = location.state.defaultExpandedLegId; - - // 1. Instantly trigger the accordion state layout expansion - setExpandedStageId(targetId); - - // 2. Schedule a deferred macro-task callback to allow DOM re-renders to finish - if (location.state?.shouldScrollToTarget) { - const scrollTimer = setTimeout(() => { - const targetElement = document.getElementById(`leg-anchor-node-${targetId}`); - if (targetElement) { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - } - - // Clear history router state token flags to prevent repeating scroll on subsequent reload shifts - window.history.replaceState({}, document.title); - }, 150); // 150ms ensures smooth accordion height deployment transition finishes - - return () => clearTimeout(scrollTimer); - } - } else if (currentTour?.legs && currentTour.legs.length > 0 && !expandedStageId) { - setExpandedStageId(currentTour.legs[0].id); - } -}, [location.state, currentTour]); - -## 4. Quality Control & Acceptance Verification -[ ] Date Matching Accuracy: Set today's date context matching a target Leg configuration profile. Tap the transition interface button from dashboard. The page must route directly and expand the target container block node. - -[ ] Flush Viewport Ceiling Test: The animated target header segment node must slide upwards smoothly. It must lock positions cleanly right below the lowest baseline layer shadow boundary of the Tab Controller without passing behind it. - -[ ] State Cleanliness Check: Refreshing or navigating back and forth within the active Timeline panel after the initial landing should not lock or force layout views to keep jumping scroll heights automatically. \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html index e3ae735..eef6913 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -21,7 +21,7 @@ - + diff --git a/frontend/src/components/MapProfileDropdown.tsx b/frontend/src/components/MapProfileDropdown.tsx index 5360c6d..e12e768 100644 --- a/frontend/src/components/MapProfileDropdown.tsx +++ b/frontend/src/components/MapProfileDropdown.tsx @@ -82,6 +82,11 @@ export const MapProfileDropdown: React.FC = ({
diff --git a/frontend/src/pages/MemberDashboard.tsx b/frontend/src/pages/MemberDashboard.tsx index 0a6a0f7..9fc732d 100644 --- a/frontend/src/pages/MemberDashboard.tsx +++ b/frontend/src/pages/MemberDashboard.tsx @@ -1,10 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; -<<<<<<< Updated upstream -======= import { io, Socket } from 'socket.io-client'; import { Capacitor } from '@capacitor/core'; import { BACKEND_URL } from '@/utils/backendEndpoint'; ->>>>>>> Stashed changes import { Compass, Users, @@ -607,12 +604,17 @@ export const MemberDashboard: React.FC = ({ const res = await fetch('/api/v1/connections', { headers: getHeaders() }); if (res.ok) { const data = await res.json(); - setConnections(data.connections || []); - setReceivedRequests(data.receivedRequests || []); - setSentRequests(data.sentRequests || []); + // Defensive Array.isArray guards — API may return objects or null on error + setConnections(Array.isArray(data.connections) ? data.connections : []); + setReceivedRequests(Array.isArray(data.receivedRequests) ? data.receivedRequests : []); + setSentRequests(Array.isArray(data.sentRequests) ? data.sentRequests : []); } } catch (err) { console.error('Lỗi khi tải danh sách kết nối:', err); + // Ensure state is always a clean array even on network failures + setConnections([]); + setReceivedRequests([]); + setSentRequests([]); } }; @@ -621,10 +623,12 @@ export const MemberDashboard: React.FC = ({ const res = await fetch('/api/v1/users/me/photos', { headers: getHeaders() }); if (res.ok) { const data = await res.json(); - setPhotos(data || []); + // Defensive guard — API may return { photos: [] } or null + setPhotos(Array.isArray(data) ? data : (Array.isArray(data?.photos) ? data.photos : [])); } } catch (err) { console.error('Lỗi tải ảnh cá nhân:', err); + setPhotos([]); } }; @@ -647,7 +651,8 @@ export const MemberDashboard: React.FC = ({ const res = await fetch(`/api/v1/users?q=${encodeURIComponent(searchQuery)}`, { headers: getHeaders() }); if (res.ok) { const data = await res.json(); - setSearchResults(data || []); + // Defensive guard — search endpoint may return { users: [] } or plain array + setSearchResults(Array.isArray(data) ? data : (Array.isArray(data?.users) ? data.users : [])); } } catch (err) { console.error('Lỗi tìm kiếm thành viên:', err); @@ -964,7 +969,7 @@ export const MemberDashboard: React.FC = ({ @@ -1224,7 +1229,7 @@ export const MemberDashboard: React.FC = ({