From 175668da35751f8fcac5bedb07c8b5ddf2717601 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sat, 27 Jun 2026 20:50:00 +0700 Subject: [PATCH] feat: restore and integrate AppDownloadBanner under Top-Bar --- FIX_ANDROID.md | 69 ----------- GET_ANDROID.md | 110 ++++++++++++++++++ frontend/dist/index.html | 4 +- .../components/layout/AppDownloadBanner.tsx | 60 ++++++++++ frontend/src/pages/ExploreMap.tsx | 9 +- frontend/src/pages/LandingPage.tsx | 7 +- 6 files changed, 185 insertions(+), 74 deletions(-) delete mode 100644 FIX_ANDROID.md create mode 100644 GET_ANDROID.md create mode 100644 frontend/src/components/layout/AppDownloadBanner.tsx diff --git a/FIX_ANDROID.md b/FIX_ANDROID.md deleted file mode 100644 index 39e8997..0000000 --- a/FIX_ANDROID.md +++ /dev/null @@ -1,69 +0,0 @@ -# To AI Agent: Implement Smart Runtime Environment Detection for Backend URL (Eliminate Dynamic .env Conflicts) - -## 1. Context & Objective -We want to modify the frontend backend-URL resolution engine **exactly once** so that pulling code to the Linux Debian Server (Docker build) or a Windows PC (Android build) requires zero manual `.env` updates. - -- **The Strategy:** Instead of hardcoding `VITE_BACKEND_URL` in a static `.env` file, we will write a smart runtime detector inside the code. -- **The Logic:** - - If the app runs on a standard web browser (Docker Server), `window.location.origin` automatically resolves to `https://yotrip.labz.io.vn`. - - If the app runs inside an Android APK wrapper, the origin defaults to `http://localhost`, `capacitor://`, or `file://`. The code will detect this and automatically inject the absolute server production URL. - ---- - -## 2. Refactoring Blueprint - -### Step 1: Overhaul `socketService.ts` (or your central API configuration file) -Replace the current environment variable lookup with this smart dynamic runtime resolver: - -```typescript -import { io, Socket } from 'socket.io-client'; - -const resolveBackendEndpoint = (): string => { - // 1. Keep a fallback for explicit env overrides if deliberately set - if (import.meta.env.VITE_BACKEND_URL) { - return import.meta.env.VITE_BACKEND_URL; - } - - const origin = window.location.origin; - const userAgent = navigator.userAgent.toLowerCase(); - - /* 2. DETECT ANDROID APK WRAPPER ENVIRONMENT - Android webviews/hybrid shells run on custom local hosts or file protocols - */ - const isAndroidApp = - origin.includes('localhost') || - origin.includes('capacitor://') || - origin.startsWith('file://') || - (userAgent.includes('android') && !origin.includes('yotrip.labz.io.vn')); - - if (isAndroidApp) { - console.log("📱 Android App environment detected. Forcing absolute production domain mapping."); - return '[https://yotrip.labz.io.vn](https://yotrip.labz.io.vn)'; - } - - // 3. Default fallback for Production Web (Docker server automatically yields its own domain) - return origin; -}; - -export const SOCKET_URL = resolveBackendEndpoint(); -console.log(`🌐 Active Network Base Endpoint: ${SOCKET_URL}`); - -### Step 2: Sync API Axios Client Configuration (api.ts / axiosClient.ts) -Ensure your API client hooks directly into the newly created SOCKET_URL variable to keep them unified: - -import axios from 'axios'; -import { SOCKET_URL } from './socketService'; - -export const api = axios.create({ - baseURL: SOCKET_URL, // Dynamically synchronizes across both Docker and Android build pipelines - headers: { - 'Content-Type': 'application/json', - }, -}); - -## 3. Verification Checklist for AI Agent - [ ] Eradicate .env Dependency: Ensure that clearing out VITE_BACKEND_URL from local .env files does not cause code compilation failures. - - [ ] Web Browser Verification: When deployed via Docker on the server, verify the network tab calls requests relatively to the running host origin. - - [ ] Android Simulation Success: When compiled into an APK on Windows, confirm all API/Socket requests direct straight to https://yotrip.labz.io.vn, avoiding any localhost 404 blockages. \ No newline at end of file diff --git a/GET_ANDROID.md b/GET_ANDROID.md new file mode 100644 index 0000000..fe2e412 --- /dev/null +++ b/GET_ANDROID.md @@ -0,0 +1,110 @@ +# To AI Agent: Restore Smart Android App Download Banner Below Top-Bar Header + +## 1. Context & Feature Objective +Previously, the codebase had a promotional banner encouraging mobile users to download the native Android `.apk` file. However, during the recent overhaul of the Top-Bar header and Member Dropdown Menu configurations, this banner component was accidentally unmounted or hidden. + +**Objective:** Restore and refactor this promotional frame (`AppDownloadBanner.tsx`). It must **ONLY** appear when a user accesses the web application via a **Mobile Android Browser** (Chrome, Samsung Internet, Opera Mobile, etc.). It must be positioned dynamically as a small, clean horizontal frame pinned directly underneath the main Top-Bar header layout, without conflicting with the new absolute Profile Dropdown Menu. + +--- + +## 2. Visual & Behavioral Layout Specifications +- **Placement Context:** Directly underneath the Top-Bar layer, shifting the core page content (Map Canvas or Landing Hero) down proportionally (`relative` or `sticky` stack). It must not overlay or block map navigation tools. +- **Conditional Trigger Logic:** The banner must evaluate `navigator.userAgent`. If the user agent includes `'android'` **AND** the user is running inside a standard web browser (not inside the compiled wrapper app itself), display the banner. +- **Dismissible Interaction:** Include a small close button (`X`). Clicking it should temporarily store a flag in `sessionStorage` or `localStorage` to prevent the banner from bugging the user repeatedly during their session. + +--- + +## 3. Technical Implementation Blueprint + +### Step 1: Create the Responsive Banner Component (`AppDownloadBanner.tsx`) +Create or re-engineer the banner layout within `frontend/src/components/layout/AppDownloadBanner.tsx`: + +```typescript +import React, { useState, useEffect } from 'react'; +import { X, Download } from 'lucide-react'; + +export const AppDownloadBanner: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + const APK_DOWNLOAD_URL = `${import.meta.env.VITE_BACKEND_URL || window.location.origin}/downloads/yotrip-latest.apk`; + + useEffect(() => { + const userAgent = navigator.userAgent.toLowerCase(); + const isAndroidBrowser = userAgent.includes('android') && !window.location.origin.includes('capacitor://') && !window.location.origin.includes('localhost:80'); + const isBannerDismissed = localStorage.getItem('yotrip_apk_banner_dismissed') === 'true'; + + // ✅ Target condition match: User is on Android mobile browser and hasn't closed it yet + if (isAndroidBrowser && !isBannerDismissed) { + setIsVisible(true); + } + }, []); + + const handleDismiss = () => { + localStorage.setItem('yotrip_apk_banner_dismissed', 'true'); + setIsVisible(false); + }; + + if (!isVisible) return null; + + return ( +
+
+
+ +
+

+ Trải nghiệm mượt mà hơn với ứng dụng YoTrip cho Android! +

+
+ +
+ {/* Direct Download Trigger Target Link */} + + Tải APK + + + {/* Close Button Trigger */} + +
+
+ ); +}; + +### Step 2: Integrate into Layout Hierarchies (LandingPage.tsx & ExplorerMap.tsx) +Mount the verified download banner right below your primary header or top-bar row item array context: + +{/* ❌ BEFORE STRUCTURAL INTEGRATION */} +
+ + +
+ +{/* ✅ AFTER STRUCTURAL INTEGRATION: Stacked relative context */} +
+ {/* 1. Main Application Header */} + + + {/* 2. THE RESTORED ANDROID APP PROMOTIONAL BANNER */} + + + {/* 3. Primary Workspace Area - Shifts down cleanly when banner initializes */} +
+ +
+
+ +## 4. Verification & Quality Acceptance Criteria for AI Agent +[ ] Targeted UserAgent Isolation: Emulate a desktop display width (iPhone or Desktop layouts). Verify the banner remains completely hidden. Toggle the network responsive preview device model to Android (e.g., Pixel 7) and refresh. Confirm the download bar pops up seamlessly. + +[ ] Absolute Dropdown Overlay Preservation: Expand the Member Avatar menu row dropdown list while the banner is visible. Confirm that the dropdown box displays layered ON TOP of the banner context, without layout shifting or text clipping. + +[ ] State Dismissal Memory: Click the X button on the banner, then refresh the browser session. Confirm the banner remains hidden and respects the localStorage condition toggle. \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html index f3601de..aca5a64 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -21,7 +21,7 @@ - + @@ -30,7 +30,7 @@ - +
diff --git a/frontend/src/components/layout/AppDownloadBanner.tsx b/frontend/src/components/layout/AppDownloadBanner.tsx new file mode 100644 index 0000000..b689d0d --- /dev/null +++ b/frontend/src/components/layout/AppDownloadBanner.tsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from 'react'; +import { X, Download } from 'lucide-react'; +import { BACKEND_URL } from '@/utils/backendEndpoint'; + +export const AppDownloadBanner: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + // Force APK download relative to backend endpoint + const APK_DOWNLOAD_URL = `${BACKEND_URL}/downloads/yotrip-latest.apk`; + + useEffect(() => { + const userAgent = navigator.userAgent.toLowerCase(); + const isAndroidBrowser = userAgent.includes('android') && + !window.location.origin.includes('capacitor://') && + !window.location.origin.includes('localhost:80'); + const isBannerDismissed = localStorage.getItem('yotrip_apk_banner_dismissed') === 'true'; + + // Target condition match: User is on Android mobile browser and hasn't closed it yet + if (isAndroidBrowser && !isBannerDismissed) { + setIsVisible(true); + } + }, []); + + const handleDismiss = () => { + localStorage.setItem('yotrip_apk_banner_dismissed', 'true'); + setIsVisible(false); + }; + + if (!isVisible) return null; + + return ( +
+
+
+ +
+

+ Trải nghiệm mượt mà hơn với ứng dụng YoTrip cho Android! +

+
+ +
+ + Tải APK + + + +
+
+ ); +}; diff --git a/frontend/src/pages/ExploreMap.tsx b/frontend/src/pages/ExploreMap.tsx index d4de887..c4faf9e 100644 --- a/frontend/src/pages/ExploreMap.tsx +++ b/frontend/src/pages/ExploreMap.tsx @@ -20,6 +20,7 @@ import { MyToursModal } from '../components/modals/MyToursModal'; import { PhotoGalleryModal as MyPhotosModal } from '../components/modals/PhotoGalleryModal'; import { LiveChatModal } from '../components/modals/LiveChatModal'; import { FriendsManagerModal } from '../components/modals/FriendsManagerModal'; +import { AppDownloadBanner } from '../components/layout/AppDownloadBanner'; // Fix lỗi icon mặc định của Leaflet const DefaultIcon = L.icon({ @@ -654,8 +655,11 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onLoginSuccess, }, [publicPhotos]); return ( -
- {/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */} +
+ + +
+ {/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */}
{/* Nhóm bên trái: Quay lại và Thông tin vị trí */}
@@ -1125,6 +1129,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onLoginSuccess, )}
)} +
{/* Admin Modal */} setIsAdminModalOpen(false)} /> diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 99ec120..e26bd75 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -9,6 +9,7 @@ import { MyToursModal } from '../components/modals/MyToursModal'; import { PhotoGalleryModal as MyPhotosModal } from '../components/modals/PhotoGalleryModal'; import { LiveChatModal } from '../components/modals/LiveChatModal'; import { FriendsManagerModal } from '../components/modals/FriendsManagerModal'; +import { AppDownloadBanner } from '../components/layout/AppDownloadBanner'; import { useNotification } from '@/hooks/useNotification'; import { processImageModeration } from '../hooks/useImageModeration'; import { useTranslation } from '../hooks/useTranslation'; @@ -343,7 +344,10 @@ export const LandingPage: React.FC = ({ return ( -
+
+ + +
{/* Background Image with Horizontal Panning */}
{/* Active image for panning */} @@ -655,6 +659,7 @@ export const LandingPage: React.FC = ({ } `}
+
{/* Login Modal Component */}