From 3fa80b69bffb8f7816897c3ca92c9159ae15c477 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sat, 27 Jun 2026 20:27:44 +0700 Subject: [PATCH] fix: android dropdown menu button --- ANDROID_FIX.md | 103 ++++++++---------- frontend/dist/index.html | 102 +++++------------ .../src/components/MapProfileDropdown.tsx | 7 +- .../components/modals/FriendsManagerModal.tsx | 22 +++- .../src/components/modals/LiveChatModal.tsx | 18 ++- package-lock.json | 42 ++----- 6 files changed, 113 insertions(+), 181 deletions(-) diff --git a/ANDROID_FIX.md b/ANDROID_FIX.md index b066762..1f31019 100644 --- a/ANDROID_FIX.md +++ b/ANDROID_FIX.md @@ -1,107 +1,94 @@ -# To AI Agent: Fix Mobile Dropdown Layout Displacement and Eliminate `.filter is not a function` Runtime Crashes +# To AI Agent: Fix Mobile Dropdown Displacement and Eliminate `.filter` Array 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. +## 1. Context & Identified Bugs +During Android Simulator testing, the frontend app encounters three critical layout and runtime execution failures: +1. **Dropdown Menu Displacement:** The profile/avatar menu container gets forced down to the extreme bottom of the viewport instead of floating as an absolute element directly beneath the user's top-bar avatar anchor. +2. **`Uncaught TypeError: g.filter is not a function`:** Triggering the Friend List components crashes the interface into a blank white screen. +3. **`Error fetching connections: TypeError: (intermediate value).filter is not a function`:** Opening the Live Chat view crashes identical array loops. + - *Root Cause for 2 & 3:* The API response payload from the backend server is **not returning a clean primitive Array**. It returns an object wrapper (e.g., `{ success: true, data: [...] }`) or `undefined`/`null` due to connection timing gaps. Invoking `.filter()` on a non-array instantly kills the React rendering thread. --- -## 2. Refactoring Strategy +## 2. Refactoring Blueprint -### 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: +### Step 1: Fix Dropdown Positioning Context (`MapProfileDropdown.tsx`) +On desktop, absolute drop panels function normally. However, on mobile viewports or custom wrappers, they lose anchoring. We must force the container component to lock its coordinate space relative to the top bar element using modern CSS constraints: ```jsx -{/* ✅ RESPONSIVE FIX: Mobile-optimized absolute floating panel structure */} +{/* ✅ RESPONSIVE REFACTOR: Force absolute rendering locked beneath the profile button context */}
- {/* Menu list rows (Tạo tour, Hành trình, Thư viện ảnh...) populate cleanly here */} - + {/* Menu option items: Tạo tour, Hành trình của tôi, Thư viện ảnh... */}
-### 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: +### Step 2: Enforce Defensive Array Check on Friend List Loop (FriendsManagerModal.tsx) +Locate where the system handles friend collections. Implement an explicit array layout typecheck validation using Array.isArray() before doing any mutation logic: -// ❌ OLD CRASH-PRONE CODE: +// ❌ OLD ERROR-PRONE PATTERN: // const activeFriends = data.filter(f => f.status === 'active'); -// ✅ NEW DEFENSIVE RAY WRAPPER: +// ✅ NEW IMPERATIVE PROTECTION: 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; + const payload = 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 + // Defensively parse and normalize the input shape + if (payload && Array.isArray(payload)) { + setFriendsList(payload); + } else if (payload && Array.isArray(payload.data)) { + setFriendsList(payload.data); // Support nested response architectures safely } else { - console.error("⚠️ Expected array structure but received:", rawPayload); - setFriendsList([]); // Secure fallback to empty array initialization to protect .filter loops + console.error("⚠️ Backend returned non-array structure:", payload); + setFriendsList([]); // Default fallback to shield downstream loops } }) .catch((err) => { - console.error("Failed to compile connections list stream safely:", err); - setFriendsList([]); // Fallback safety initialization + console.error("Error reading friends list data stream:", err); + setFriendsList([]); }); }, []); -// Secure conditional processing guard rail +// Safely filter verified arrays exclusively 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: +### Step 3: Secure Live Chat Target Connections Handler (LiveChatModal.tsx) +Apply the exact same defensive architecture inside your real-time chat sync queries or global context connection monitors: const fetchUserConnections = async () => { try { const response = await api.get('/chat/connections'); - const resultData = response.data; + const responseBody = response.data; - // Direct defensive validation shield check - const verifiedConnectionsArray = Array.isArray(resultData) - ? resultData - : (resultData && Array.isArray(resultData.connections) ? resultData.connections : []); + // Check array schema context explicitly + const verifiedConnections = Array.isArray(responseBody) + ? responseBody + : (responseBody && Array.isArray(responseBody.connections) ? responseBody.connections : []); - /* ✅ CRITICAL FIX: Running .filter on verified guaranteed array structure only */ - const onlineConnections = verifiedConnectionsArray.filter((conn: any) => conn && conn.isOnline === true); + /* ✅ CRITICAL SEPARATION: Running filter only on verified array collections */ + const onlineConnections = verifiedConnections.filter((conn: any) => conn && conn.isOnline === true); - setConnections(verifiedConnectionsArray); + setConnections(verifiedConnections); } catch (error) { - console.error("[LiveChatModal] Error fetching connections cleanly:", error); - setConnections([]); // Force secure array context initialization on intercepting crash exceptions + console.error("[LiveChatModal] Runtime error fetching message feeds intercepted gracefully:", error); + setConnections([]); // Initialize to fallback empty context } }; -## 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. +## 3. Automated Verification Checklist for AI Agent +[ ] Dropdown Anchor Alignment: Confirm the avatar profile drawer panel renders right under the top-bar avatar, leaving the bottom interaction tools untouched. - [ ] 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. +[ ] White Screen Eradication: Mock an empty server exception response (500 Internal Error). Verify the client component stays functional, logs the incident, and updates the empty UI state without crashing. - [ ] 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 +[ ] Global Code Polish Check: Scan the repository to ensure no raw .filter() methods are executed directly against fetched network streams without preceding type guards. \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 74c598c..eeaccfa 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -1,28 +1,27 @@ -<<<<<<< HEAD - - - - - - - - Travel Planner - - - - - - - - - - - - - - - - + + + + + + + + Travel Planner + + + + + + + + + + + + + + + + @@ -31,54 +30,9 @@ - - - -
- -======= - - - - - - - - - Travel Planner - - - - - - - - - - - - - - - - - - - - - - - - - - - + + -
- - - ->>>>>>> 9fae6afaac44c4d9f8def031bb048d3cb856a743 +
+ \ No newline at end of file diff --git a/frontend/src/components/MapProfileDropdown.tsx b/frontend/src/components/MapProfileDropdown.tsx index e12e768..a2b7f3b 100644 --- a/frontend/src/components/MapProfileDropdown.tsx +++ b/frontend/src/components/MapProfileDropdown.tsx @@ -81,14 +81,13 @@ export const MapProfileDropdown: React.FC = ({ />
-
{isAuthenticated ? (
diff --git a/frontend/src/components/modals/FriendsManagerModal.tsx b/frontend/src/components/modals/FriendsManagerModal.tsx index c00c095..8bcf7a6 100644 --- a/frontend/src/components/modals/FriendsManagerModal.tsx +++ b/frontend/src/components/modals/FriendsManagerModal.tsx @@ -39,10 +39,16 @@ export const FriendsManagerModal: React.FC = ({ const res = await fetch('/api/v1/connections', { headers: getHeaders() }); if (res.ok) { const data = await res.json(); - setConnections(data || []); + const list = Array.isArray(data) + ? data + : (data && Array.isArray(data.connections) ? data.connections : []); + setConnections(list); + } else { + setConnections([]); } } catch (e) { console.error('[FriendsManagerModal] Error fetching connections:', e); + setConnections([]); } finally { setIsLoading(false); } @@ -170,15 +176,19 @@ export const FriendsManagerModal: React.FC = ({ if (!isOpen || !user) return null; // Filter lists - const activeFriends = connections.filter((c: any) => c.status === 'ACCEPTED'); + const activeFriends = Array.isArray(connections) + ? connections.filter((c: any) => c && c.status === 'ACCEPTED') + : []; // Received pending requests - const pendingRequests = connections.filter((c: any) => c.status === 'PENDING' && c.targetUser?.id === user?.id); + const pendingRequests = Array.isArray(connections) + ? connections.filter((c: any) => c && c.status === 'PENDING' && c.targetUser?.id === user?.id) + : []; const getStatusText = (targetUserId: string) => { - const existing = connections.find( - c => c.targetUser?.id === targetUserId || c.requester?.id === targetUserId - ); + const existing = Array.isArray(connections) + ? connections.find(c => c && (c.targetUser?.id === targetUserId || c.requester?.id === targetUserId)) + : null; if (!existing) return null; if (existing.status === 'ACCEPTED') return 'FRIEND'; if (existing.status === 'PENDING') { diff --git a/frontend/src/components/modals/LiveChatModal.tsx b/frontend/src/components/modals/LiveChatModal.tsx index 3e4047f..63d7809 100644 --- a/frontend/src/components/modals/LiveChatModal.tsx +++ b/frontend/src/components/modals/LiveChatModal.tsx @@ -46,11 +46,17 @@ export const LiveChatModal: React.FC = ({ if (res.ok) { const data = await res.json(); // Accepted connections only - const activeConns = data.filter((c: any) => c.status === 'ACCEPTED'); + const list = Array.isArray(data) + ? data + : (data && Array.isArray(data.connections) ? data.connections : []); + const activeConns = list.filter((c: any) => c && c.status === 'ACCEPTED'); setConnections(activeConns); + } else { + setConnections([]); } } catch (e) { console.error('[LiveChatModal] Error fetching connections:', e); + setConnections([]); } finally { setIsLoadingContacts(false); } @@ -236,10 +242,12 @@ export const LiveChatModal: React.FC = ({ if (!isOpen || !user) return null; // Filter connections by search query - const filteredConnections = connections.filter((conn: any) => { - const friend = conn.targetUser?.id === user?.id ? conn.requester : conn.targetUser; - return friend?.name?.toLowerCase().includes(searchQuery.toLowerCase()); - }); + const filteredConnections = Array.isArray(connections) + ? connections.filter((conn: any) => { + const friend = conn.targetUser?.id === user?.id ? conn.requester : conn.targetUser; + return friend?.name?.toLowerCase().includes(searchQuery.toLowerCase()); + }) + : []; return (
diff --git a/package-lock.json b/package-lock.json index 6d82621..ba330d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,7 +112,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -126,14 +126,14 @@ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "backend/node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0", @@ -145,7 +145,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0" @@ -174,7 +174,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1120,7 +1120,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -1138,7 +1137,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1156,7 +1154,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1174,7 +1171,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1192,7 +1188,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -1210,7 +1205,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -1228,7 +1222,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1246,7 +1239,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1264,7 +1256,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1282,7 +1273,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1300,7 +1290,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1318,7 +1307,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1336,7 +1324,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1354,7 +1341,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1372,7 +1358,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1390,7 +1375,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1408,7 +1392,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1426,7 +1409,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1444,7 +1426,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1462,7 +1443,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1480,7 +1460,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1498,7 +1477,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -1516,7 +1494,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -1534,7 +1511,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -1552,7 +1528,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -1570,7 +1545,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -4081,7 +4055,7 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/quill": { @@ -4104,7 +4078,7 @@ "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -5955,7 +5929,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/dargs": {