From 2f5ef424c8404c8a52a994578d44589cf45de142 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 28 Jun 2026 14:55:25 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20s=E1=BB=ADa=20UI=20photoview=20hi?= =?UTF-8?q?=E1=BB=83n=20th=E1=BB=8B=20=E1=BB=9F=20public?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GET_ANDROID.md | 110 ---- UI_ANDROID.md | 151 +++++ frontend/dist/index.html | 4 +- frontend/src/components/PublicPhotoModal.tsx | 557 +++++++++---------- 4 files changed, 405 insertions(+), 417 deletions(-) delete mode 100644 GET_ANDROID.md create mode 100644 UI_ANDROID.md diff --git a/GET_ANDROID.md b/GET_ANDROID.md deleted file mode 100644 index fe2e412..0000000 --- a/GET_ANDROID.md +++ /dev/null @@ -1,110 +0,0 @@ -# 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/UI_ANDROID.md b/UI_ANDROID.md new file mode 100644 index 0000000..cca5fec --- /dev/null +++ b/UI_ANDROID.md @@ -0,0 +1,151 @@ +# To AI Agent: Restructure Mobile Photo Viewer Layout with Fixed Viewport, Overlay Metadata, and Scrollable Comments + +## 1. Context & Architectural UI Refactor +We are redesigning the full-screen mobile photo inspector interface based on the visual layout annotated in `image.png`. The current structure is fragmented, causing layout instability when switching between images. + +### Key Refactor Requirements: +1. **Fixed Image Viewport (Blue Zone):** Secure the photo viewport inside a strict, unyielding aspect-locked square container. Regardless of whether the active image is Landscape or Portrait, the container dimension must NOT snap, resize, or cause screen layout jumps. +2. **Geospatial Overlay (Red Arrow Destination):** Completely remove the static location metadata box from the bottom white panel. Relocate and render this location string as a clean, translucent text overlay pinned directly to the **bottom-left corner inside the image viewport**. +3. **Title Transformation (Green Arrow Destination):** Remove the text header segment "Lịch sử ảnh tại vị trí này (...)". In its place, dynamically render the active **Title of the Photo** (Tiêu đề của ảnh), serving as the primary text separator. +4. **Isolated Scrollable Comment Feed:** The entire lower comment zone must be configured to scroll dynamically. When users swipe up to read multiple comments, the layout thread must slide seamlessly underneath the fixed sticky image frame container (`z-20`). + +--- + +## 2. Structural Layer Elevation (`z-index`) Matrix + +┌────────────────────────────────────────────────────────┐ +│ Tier 4: System Action Row & Closes (z-50) │ ➔ Native buttons (X, Close) +├────────────────────────────────────────────────────────┤ +│ Tier 3: Fixed Photo Frame Viewport (z-20 / sticky) │ ➔ Aspect-Square Image Box +│ └─► Sub-Layer: Location & Time Overlay (z-30) │ ➔ Pinned Bottom-Left on Image +├────────────────────────────────────────────────────────┤ +│ Tier 2: Scrollable Comments Panel (z-10) │ ➔ Slides BEHIND Tier 3 when swiped +└────────────────────────────────────────────────────────┘ + +--- + +## 3. Code Refactoring Blueprint + +### Step 1: Overhaul Layout Architecture (`MobilePhotoViewerModal.tsx`) +Update or create the mobile component layout to enforce the absolute layer boundaries and fixed dimensions: + +```typescript +import React, { useState } from 'react'; +import { X, MapPin, Calendar, Heart, Send } from 'lucide-react'; + +interface PhotoDetail { + id: string; + url: string; + title: string; + description?: string; + latitude: number; + longitude: number; + locationName: string; + capturedAt: string; + likesCount: number; +} + +export const MobilePhotoViewerModal: React.FC<{ photo: PhotoDetail; onClose: () => void }> = ({ photo, onClose }) => { + const [commentInput, setCommentInput] = useState(''); + + return ( +
+ + {/* TIER 4: FIXED SYSTEM ACTIONS CONTROL ROW (z-50) */} +
+ +
+ + {/* TIER 3: FIXED BLUE ZONE - ASPECT LOCKED PHOTO FRAME (z-20 / sticky) */} +
+ {photo.title} + + {/* TIER 3.1: RELOCATED DYNAMIC LOCATION & TIMESTAMP OVERLAY (z-30) */} +
+
+ + {photo.locationName || "Vị trí không xác định"} +
+
+ + Ngày chụp: {new Date(photo.capturedAt).toLocaleDateString('vi-VN')} +
+
+ + {/* Floating Like Badge Counter */} +
+ + {photo.likesCount} +
+
+ + {/* TIER 2: SCROLLABLE CORE CONTENTS & COMMENTS PANEL (z-10) */} + {/* This entire workspace container slides underneath the photo layout on swipe-up */} +
+ + {/* REPLACED HEADER ZONE: Title replaces the old History text strip */} +
+

+ {photo.title || "Chưa có tiêu đề"} +

+ {photo.description && ( +

+ {photo.description} +

+ )} +
+ + {/* DYNAMIC COMMENTS FEED BLOCK */} +
+ {/* Mock iteration loop representing user chat bubbles */} + {[...Array(5)].map((_, index) => ( +
+
U
+
+
+ Thành viên YoTrip + Vừa xong +
+

Góc chụp đẹp quá, bối cảnh nhìn rất thoáng và đầy đủ ánh sáng tự nhiên!

+
+
+ ))} +
+ + {/* STICKY BOTTOM INPUT SEND TRAY BAR */} +
+ setCommentInput(e.target.value)} + placeholder="Viết bình luận công khai..." + className="flex-1 bg-slate-900 border border-slate-800 text-white rounded-xl p-3 text-xs outline-none focus:border-blue-500 transition-colors placeholder-slate-500" + /> + +
+ +
+ +
+ ); +}; + +## 4. Quality Verification & Acceptance Criteria for AI Agent +[ ] Dimension Stability Test: Switch back and forth between an ultra-wide panoramic photo and a 4:3 vertical shot. The parent blue container (aspect-square) must remain exactly static on the viewport layout, blocking size shifts. + +[ ] Overlay Check Validation: Verify the location marker coordinates box is completely wiped from the lower white profile zone and draws successfully on top of the image canvas at the bottom-left edge. + +[ ] Text Swap Alignment: Ensure the string text "Lịch sử ảnh tại vị trí này" is replaced completely by the active image title asset hook. + +[ ] Scroll Pass Inspection: Swipe up to read the text inside the comments panel. Verify the message rows pass behind the bottom border line of the image canvas container (z-20), while the close button at the top remains fully accessible. \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 8f51ba6..f9d498f 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/PublicPhotoModal.tsx b/frontend/src/components/PublicPhotoModal.tsx index 0d23195..778e60d 100644 --- a/frontend/src/components/PublicPhotoModal.tsx +++ b/frontend/src/components/PublicPhotoModal.tsx @@ -427,358 +427,308 @@ export const PublicPhotoModal: React.FC = ({ - {/* Left Side: Photo Detail */} -
- - {/* Photo wrapper for mobile view (handles top overlay name and bottom-right like) */} -
- {/* Mobile Only: Uploader details overlay */} -
-
- -
- - {photo.uploader?.name || 'Ẩn danh'} - + {/* ============================================================ + TIER 3: FIXED PHOTO FRAME VIEWPORT (z-20 / sticky on mobile) + Aspect-square container. Does NOT resize on image swap. + Desktop: left 3/5 column, absolute-fill with overlay metadata. + ============================================================ */} +
+ + {/* Image fill */} + { + e.preventDefault(); + setIsFullscreen(true); + }} + > + Public Map Upload { if (!isLoggedIn) e.preventDefault(); }} + onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} + className="w-full h-full object-contain select-none pointer-events-none" + draggable={false} + /> + {(!isAuthorized || !isLoggedIn) && ( +
+ )} + + + {/* TIER 3.1: LOCATION & TIMESTAMP OVERLAY — bottom-left inside image (z-30) */} +
+
+ + {resolvedAddress} +
+
+ + {new Date(photo.capturedAt).toLocaleDateString('vi-VN', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit' + })}
- - {/* Like Button Overlay */} - - - { - e.preventDefault(); - setIsFullscreen(true); - }} - > - Public Map Upload { if (!isLoggedIn) e.preventDefault(); }} - onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} - className={`w-full h-auto max-h-[85vh] object-contain md:h-full md:max-h-none select-none ${ - !isLoggedIn ? 'pointer-events-none' : '' - }`} - draggable={false} - /> - {/* Shield overlay to prevent Save Image As on right-click / long press for non-owners */} - {(!isAuthorized || !isLoggedIn) && ( - - {/* Info & Timeline overlay inside photo panel */} -
- - {/* Timeline scroll */} + {/* Like button — bottom-right on mobile, top-left on desktop */} + + + {/* Uploader pill — top-left on mobile */} +
+
+ +
+ + {photo.uploader?.name || 'Ẩn danh'} + +
+ + {/* Desktop: overlay metadata gradient at bottom of photo column */} +
+

+ {photo.metadata?.title || ''} +

+ {photo.metadata?.description && ( +

{photo.metadata.description}

+ )} +
+ + {resolvedAddress} +
+
+ + {new Date(photo.capturedAt).toLocaleDateString('vi-VN', { + day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' + })} +
+
+ + {/* Desktop thumbnail timeline carousel at the bottom */} + {photoGroup && photoGroup.length > 1 && ( +
+ {photoGroup.map((p) => { + const isActive = p.id === photo.id; + return ( + + ); + })} +
+ )} +
+ + {/* ============================================================ + TIER 2: SCROLLABLE COMMENTS PANEL (z-10) + Slides BEHIND the sticky image frame when swiping up on mobile. + Desktop: right 2/5 column with its own overflow-y-auto. + ============================================================ */} +
+ + {/* ---- TITLE HEADER: replaces old "Lịch sử ảnh tại vị trí này" strip ---- */} +
+ + {/* Row 1: Title + Edit button */} +
+
+

+ {photo.metadata?.title || 'Chưa có tiêu đề'} +

+ {photo.metadata?.description && ( +

+ {photo.metadata.description} +

+ )} +
+ {isAuthorized && ( + + )} +
+ + {/* Row 2: Thumbnail timeline strip — below title, mobile only */} {photoGroup && photoGroup.length > 1 && ( -
- - - Lịch sử ảnh tại vị trí này ({photoGroup.length}) - -
- {photoGroup.map((p) => { - const isActive = p.id === photo.id; - return ( - - ); - })} -
+
+ {photoGroup.map((p) => { + const isActive = p.id === photo.id; + const dateObj = new Date(p.capturedAt); + const day = String(dateObj.getDate()).padStart(2, '0'); + const month = String(dateObj.getMonth() + 1).padStart(2, '0'); + return ( + + ); + })}
)} +
- {isEditing ? ( + {/* Edit form (when editing) */} + {isEditing && ( +

Chỉnh sửa thông tin ảnh

-
- setEditTitle(e.target.value)} + setEditTitle(e.target.value)} placeholder="Nhập tiêu đề cho ảnh..." - className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500" - /> + className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500" />
-
-