feat: restore and integrate AppDownloadBanner under Top-Bar

This commit is contained in:
2026-06-27 20:50:00 +07:00
parent 7ab1342690
commit 175668da35
6 changed files with 185 additions and 74 deletions
-69
View File
@@ -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.
+110
View File
@@ -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 (
<div className="w-full bg-gradient-to-r from-blue-900 to-indigo-950 border-b border-blue-800 px-4 py-2 flex items-center justify-between text-white text-[11px] font-medium z-40 relative animate-fade-in shrink-0">
<div className="flex items-center gap-2.5 flex-1 min-w-0">
<div className="p-1 bg-blue-500/20 rounded-lg text-blue-400 shrink-0">
<Download className="w-3.5 h-3.5" />
</div>
<p className="truncate text-slate-200">
Trải nghiệm mượt mà hơn với ứng dụng <span className="text-white font-bold">YoTrip cho Android</span>!
</p>
</div>
<div className="flex items-center gap-3 ml-2 shrink-0">
{/* Direct Download Trigger Target Link */}
<a
href={APK_DOWNLOAD_URL}
download="yotrip.apk"
className="bg-blue-600 hover:bg-blue-500 px-2.5 py-1 rounded font-bold text-white transition-colors shadow-sm active:scale-95"
>
Tải APK
</a>
{/* Close Button Trigger */}
<button
onClick={handleDismiss}
className="p-1 hover:bg-slate-800 rounded text-slate-400 hover:text-slate-200 transition-colors"
title="Đóng thông báo"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
};
### 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 */}
<div className="w-screen h-screen flex flex-col">
<TopBarHeader />
<MapCanvasWorkspace />
</div>
{/* ✅ AFTER STRUCTURAL INTEGRATION: Stacked relative context */}
<div className="w-screen h-screen flex flex-col overflow-hidden">
{/* 1. Main Application Header */}
<TopBarHeader />
{/* 2. THE RESTORED ANDROID APP PROMOTIONAL BANNER */}
<AppDownloadBanner />
{/* 3. Primary Workspace Area - Shifts down cleanly when banner initializes */}
<div className="flex-1 relative min-h-0 w-full">
<MapCanvasWorkspace />
</div>
</div>
## 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.
+2 -2
View File
@@ -21,7 +21,7 @@
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
<script type="module" crossorigin src="/assets/index-BYlJbgKh.js"></script>
<script type="module" crossorigin src="/assets/index-CnmnZivz.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Cyuzqnbw.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-others-VIU5qAPG.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-DOXkNaRS.js">
@@ -30,7 +30,7 @@
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-IjT8x9OX.js">
<link rel="stylesheet" crossorigin href="/assets/vendor-maps-CcXJxNtP.css">
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
<link rel="stylesheet" crossorigin href="/assets/index-BO3HsKs5.css">
<link rel="stylesheet" crossorigin href="/assets/index-w726aohe.css">
</head>
<body>
<div id="root"></div>
@@ -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 (
<div className="w-full bg-gradient-to-r from-blue-900 to-indigo-950 border-b border-blue-800 px-4 py-2.5 flex items-center justify-between text-white text-[11px] font-medium z-40 relative shrink-0">
<div className="flex items-center gap-2.5 flex-1 min-w-0">
<div className="p-1 bg-blue-500/20 rounded-lg text-blue-400 shrink-0">
<Download className="w-3.5 h-3.5" />
</div>
<p className="truncate text-slate-200">
Trải nghiệm mượt hơn với ng dụng <span className="text-white font-bold">YoTrip cho Android</span>!
</p>
</div>
<div className="flex items-center gap-3 ml-2 shrink-0">
<a
href={APK_DOWNLOAD_URL}
download="yotrip.apk"
className="bg-blue-600 hover:bg-blue-500 px-3 py-1 rounded-xl font-bold text-white transition-all shadow-sm active:scale-95 text-[10px]"
>
Tải APK
</a>
<button
onClick={handleDismiss}
className="p-1 hover:bg-slate-800 rounded text-slate-400 hover:text-slate-200 transition-colors"
title="Đóng thông báo"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
};
+7 -2
View File
@@ -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 (
<div className="h-dvh w-full relative overflow-hidden">
{/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */}
<div className="h-dvh w-full flex flex-col overflow-hidden bg-slate-950">
<AppDownloadBanner />
<div className="flex-1 w-full relative min-h-0">
{/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */}
<div className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] left-4 right-4 z-[1002] flex items-center justify-between pointer-events-none">
{/* Nhóm bên trái: Quay lại và Thông tin vị trí */}
<div className="flex items-center gap-3 pointer-events-auto">
@@ -1125,6 +1129,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onLoginSuccess,
)}
</div>
)}
</div>
{/* Admin Modal */}
<UserManagementModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} />
+6 -1
View File
@@ -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<LandingPageProps> = ({
return (
<div className="h-dvh w-full overflow-hidden font-sans bg-[var(--background)] relative">
<div className="h-dvh w-full flex flex-col overflow-hidden font-sans bg-[var(--background)]">
<AppDownloadBanner />
<div className="flex-1 w-full relative min-h-0">
{/* Background Image with Horizontal Panning */}
<div className="absolute inset-0 z-0">
{/* Active image for panning */}
@@ -655,6 +659,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({
}
`}</style>
</div>
</div>
{/* Login Modal Component */}
<LoginModal