fix: paste trực tiếp tọa độ vào kinh độ vĩ độ

This commit is contained in:
2026-06-23 17:50:05 +07:00
parent 28ea9abccd
commit 7182391241
6 changed files with 238 additions and 107 deletions
-102
View File
@@ -1,102 +0,0 @@
# To AI Agent: Complete Component Relocation - Move Navigation Tabs to Top Header and Fix Button Text Wrapping
## 1. Context & Structural Defects Analysis
We are fixing a persistent UI layout failure on the mobile view (`yotrip.labz.io.vn`) as highlighted in the annotated screenshot `image_c61225.png`:
- **Component Misplacement (Lower Red Box):** The sub-navigation tabs (`Lộ trình`, `Chi phí`, `Ảnh`, `Trò chuyện`...) are currently detached and floating erratically in the middle of the chat history viewport, overlapping message bubbles.
- **Empty Empty Top Gap (Upper Red Box):** There is an empty dark/blank bar container sitting directly below the Main Tour Title Header ("Hoa vàng cỏ xanh 20...").
- **Text Wrapping Bug:** The labels on the floating navigation items are wrapping onto multiple lines (e.g., `Lộ\ntrình`, `Chi\nphí`), ruining the horizontal tab presentation.
---
## 2. Refactoring Objectives
1. **DOM Relocation:** Completely extract the Sub-Navigation Tabs out of the chat message block. Relocate and mount it directly into the upper red box zone so it sits flush immediately underneath the Main Tour Header.
2. **Text & Width Optimization:** Enforce strict styles on the tab container so that all navigation buttons adjust dynamically to fit within the viewport width. The button text names **MUST NOT** wrap down to a new line (`white-space: nowrap`).
---
## 3. Targeted Fix Instructions
### Step 1: Correct the DOM / React Component Tree Hierarchy
Ensure the navigation component sits inside the sticky top layout zone, completely separated from the scrollable chat list container:
```html
<div class="chat-viewport-wrapper">
<div class="fixed-top-header-block">
<header class="main-tour-header">
</header>
<nav class="sub-navigation-tabs">
<button class="nav-tab-item">Lộ trình</button>
<button class="nav-tab-item">Chi phí</button>
<button class="nav-tab-item">Ảnh</button>
<button class="nav-tab-item active">Trò chuyện</button>
<button class="nav-tab-item">Thành viên</button>
<button class="nav-tab-item">Cài đặt</button>
</nav>
</div>
<main class="chat-messages-scroll-area">
</main>
</div>
### Step 2: Apply CSS/Tailwind Rules for Button Shrinkage & Single-Line Restraints
Apply these structural adjustments to make sure the tabs fill the screen width evenly without line breaks:
/* Container for header components stacked on top */
.fixed-top-header-block {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
position: sticky !important;
top: 0 !important;
z-index: 50 !important;
background-color: #ffffff;
}
/* FIX: Ensure tabs fit inside the viewport layout cleanly */
.sub-navigation-tabs {
display: flex !important;
width: 100% !important;
margin: 0 !important;
padding: 4px 8px !important; /* Tight padding to utilize screen real estate */
border-radius: 0 !important;
box-shadow: none !important;
border-bottom: 1px solid #e5e7eb;
background-color: #ffffff;
/* Distribute items evenly across the width */
justify-content: space-between !important;
align-items: center !important;
}
/* FIX: Absolute enforcement of no word-wrapping on buttons */
.nav-tab-item {
display: flex !important;
flex-direction: column !important; /* Stack icon and text if applicable */
align-items: center !important;
justify-content: center !important;
flex: 1 1 0% !important; /* Allow dynamic even shrinkage */
min-width: 0 !important; /* Overcomes default flex minimums */
/* Text layout fixes */
white-space: nowrap !important; /* PREVENT TEXT FROM WRAPPING TO NEW LINE */
font-size: 11px !important; /* Scaled down to prevent out-of-bounds clipping */
padding: 4px 2px !important;
text-align: center !important;
}
/* Handle icon sizes inside tabs if applicable */
.nav-tab-item svg, .nav-tab-item i {
font-size: 16px !important;
margin-bottom: 2px !important;
}
## 4. Verification Check for AI Agent
[ ] Verify that the navigation row is no longer hovering over the chat text bubble flow.
[ ] Confirm there is zero dead space or black margin layers between the tour header and the navigation buttons.
[ ] Inspect text elements for words like "Lộ trình" and "Chi phí"—they must read completely on a single line horizontally.
+116
View File
@@ -0,0 +1,116 @@
# To AI Agent: Implement Unified Dual-Theme System (Light & Dark) for Yotrip App
## 1. Context & Design System Overview
We are building a robust, high-contrast, yet eye-friendly dual-theme system (Light and Dark modes) for our mobile application (`yotrip.labz.io.vn`).
- **Dark Theme Constraint:** Must use the pre-configured smoky grape base (`Vintage Grape` & `Dusty Grape`) with vivid lime/aquatic accents. No pitch-black.
- **Light Theme Constraint:** Must use the soft parchment base (`Parchment`) to eliminate screen glare, using deep academic blue (`Yale Blue`) for high-contrast readability. No pure blazing white backgrounds.
---
## 2. Comprehensive Color Token Mapping
### A. LIGHT THEME PALETTE ("Classic Heritage & Soft Sand")
- **`Parchment` (`#f6f0ed`):** Map to **Global Page Backgrounds**. A warm, ancient-manuscript off-white that eliminates mobile screen glare.
- **`Yale Blue` (`#28536b`):** Map to **Primary Text, Main Titles, and Primary Action Buttons**. Provides dependable trust and sharp contrast.
- **`Steel Blue` (`#7ea8be`):** Map to **Active Navigation Tabs, Active Icons, and Secondary Actions**.
- **`Khaki Beige` (`#bbb193`):** Map to **Borders, Dividers, and Deactivated/Muted States**.
- **`Rosy Taupe` (`#c2948a`):** Map to **Special Highlight Badges, Notification Banners, or Warm Accent Elements**.
### B. DARK THEME PALETTE ("Vintage Velvet & Aquatic Zest")
- **`Vintage Grape` (`#513b56`):** Map to **Global Page Backgrounds**.
- **`Dusty Grape` (`#525174`):** Map to **Component Surfaces (Cards, Chat Bubbles, Accordion Rows)**.
- **`Lime Cream` (`#bce784`):** Map to **Brand Text Highlights, Active Icons**.
- **`Bondi Blue` (`#348aa7`):** Map to **Primary Action Buttons, Links**.
- **`Emerald` (`#5dd39e`):** Map to **Success Utilities & "Tối ưu" badges**.
---
## 3. Technical Global Configuration
### Option A: Clean CSS Variables (`global.css`)
Replace or update the root variable tokens inside your main stylesheet:
```css
/* --- THEME SÁNG (Mềm mại, Tương phản cao, Không chói) --- */
:root {
--background: #f6f0ed; /* Parchment */
--surface: #ffffff; /* Pure White for crisp card layers */
--surface-muted: #bbb193; /* Khaki Beige */
--border: #bbb193; /* Khaki Beige */
--text-primary: #28536b; /* Yale Blue (High contrast text) */
--text-secondary: #7ea8be; /* Steel Blue */
--text-accent: #c2948a; /* Rosy Taupe */
--primary: #28536b; /* Yale Blue for main buttons */
--primary-hover: #1f4154;
--secondary-active: #7ea8be; /* Steel Blue */
}
/* --- THEME TỐI (Dịu mắt, Sang trọng, Không đen kịt) --- */
:root.dark {
--background: #513b56; /* Vintage Grape */
--surface: #525174; /* Dusty Grape */
--surface-muted: #626186;
--border: #626186;
--text-primary: #f8fafc; /* Soft White */
--text-secondary: #94a3b8; /* Muted Slate */
--text-brand: #bce784; /* Lime Cream */
--primary: #348aa7; /* Bondi Blue */
--primary-hover: #296f86;
--success-accent: #5dd39e; /* Emerald */
}
### Option B: Tailwind Extension Configuration (tailwind.config.js)
Expose these custom design tokens cleanly into the utility library framework:
theme: {
extend: {
colors: {
yotripLight: {
bg: '#f6f0ed', // Parchment
text: '#28536b', // Yale Blue
steel: '#7ea8be', // Steel Blue
khaki: '#bbb193', // Khaki Beige
rosy: '#c2948a', // Rosy Taupe
},
yotripDark: {
bg: '#513b56', // Vintage Grape
surface: '#525174', // Dusty Grape
lime: '#bce784', // Lime Cream
bondi: '#348aa7', // Bondi Blue
emerald: '#5dd39e', // Emerald
}
}
}
}
## 4. UI Component Application Reference
Apply these unified color classes to ensure the interface flips seamlessly:
Chat Wrapper & Itinerary Timelines: - Light Mode: Background is yotripLight-bg, General Text is yotripLight-text.
Dark Mode: Background is yotripDark-bg, General Text is #f8fafc.
Navigation Tabs Menu:
Light Mode: Inactive text is yotripLight-steel. Active tab gets a yotripLight-text highlight indicator.
Dark Mode: Inactive text is yotripDark-surface. Active tab gets a yotripDark-lime highlight indicator.
Main Action Call-To-Actions (e.g., "Thêm địa điểm"):
Light Mode: Background is yotripLight-text (Yale Blue) with clean white text.
Dark Mode: Background is yotripDark-bondi (Bondi Blue) with clean white text.
## 5. Acceptance Criteria for Verification
[ ] Ensure that toggling the .dark class on the <html> root triggers a global transition without style flashes (transition-colors duration-200).
[ ] Text legibility inside Light Mode satisfies standard WCAG accessibility contrast limits on mobile displays out in the sun.
[ ] The background of the expanded folder nodes/chat bubbles maps cleanly to their respective surface tokens across both system states.
+15
View File
@@ -6,6 +6,21 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script src="https://accounts.google.com/gsi/client" async defer></script>
<title>Travel Planner</title>
<!-- Open Graph Meta Tags for Social Media Sharing -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://yotrip.labz.io.vn" />
<meta property="og:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
<meta property="og: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 property="og:image" content="https://yotrip.labz.io.vn/background.avif" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<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" />
</head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -22,7 +22,7 @@ server {
}
# Images and fonts - long caching (NOT including /uploads/)
location ~* ^(?!/uploads/).*\.(?:jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
location ~* ^(?!/uploads/).*\.(?:jpg|jpeg|png|gif|ico|svg|avif|woff|woff2|ttf|eot)$ {
root /usr/share/nginx/html;
expires 1y;
add_header Cache-Control "public, max-age=31536000";
+60 -4
View File
@@ -88,6 +88,45 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
const { legs, addLocation, updateLocation, updateTourStartPoint, updateTourEndPoint, mapCenter, currentTour, userRole } = useTourStore();
const notify = useNotification();
// Helper function to parse coordinates from pasted text (format: "lat, lng")
const parseCoordinates = (text: string): { latitude: number; longitude: number } | null => {
const trimmed = text.trim();
// Match format: number, number (supports negative numbers and decimals)
const coordMatch = trimmed.match(/^(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)$/);
if (coordMatch) {
const lat = parseFloat(coordMatch[1]);
const lng = parseFloat(coordMatch[2]);
// Validate latitude range: -90 to 90
// Validate longitude range: -180 to 180
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
return { latitude: lat, longitude: lng };
}
}
return null;
};
// Handle paste event for coordinate inputs
const handleCoordinatePaste = (e: React.ClipboardEvent<HTMLInputElement>, field: 'latitude' | 'longitude') => {
const pastedText = e.clipboardData.getData('text');
const parsed = parseCoordinates(pastedText);
if (parsed) {
e.preventDefault();
setFormData(prev => ({
...prev,
latitude: parsed.latitude,
longitude: parsed.longitude
}));
notify({
title: 'Thành công',
message: `Tọa độ được phân tích: ${parsed.latitude}, ${parsed.longitude}`,
type: 'success'
});
}
};
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
useEffect(() => {
if (isOpen) {
@@ -524,13 +563,30 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1"> đ</label>
<input type="number" step="any" required className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
value={formData.latitude} onChange={e => setFormData({...formData, latitude: e.target.value as any})} />
<input
type="number"
step="any"
required
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
value={formData.latitude}
onChange={e => setFormData({...formData, latitude: e.target.value as any})}
onPaste={(e) => handleCoordinatePaste(e, 'latitude')}
placeholder="13.50731401913824"
/>
<p className="text-xs text-gray-400 mt-1">💡 Dán "lat, lng" đ tự đng điền cả đ kinh đ</p>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Kinh đ</label>
<input type="number" step="any" required className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
value={formData.longitude} onChange={e => setFormData({...formData, longitude: e.target.value as any})} />
<input
type="number"
step="any"
required
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
value={formData.longitude}
onChange={e => setFormData({...formData, longitude: e.target.value as any})}
onPaste={(e) => handleCoordinatePaste(e, 'longitude')}
placeholder="109.28986362417251"
/>
</div>
</div>
<div>
+46
View File
@@ -102,6 +102,52 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
}
};
// Update Open Graph meta tags when public photos are fetched
useEffect(() => {
if (publicPhotos.length > 0) {
const mainPhoto = publicPhotos[0];
const photoUrl = mainPhoto.imageUrl || mainPhoto.originalUrl || '/background.avif';
const photoTitle = mainPhoto.metadata?.title || 'Travel Planner - Khám phá chuyến đi tuyệt vời';
const photoDescription = mainPhoto.metadata?.description || `Được chia sẻ bởi ${mainPhoto.uploader?.name || 'một thành viên'}. Khám phá những hành trình tuyệt vời trên Travel Planner.`;
// Update og:image
let ogImage = document.querySelector('meta[property="og:image"]');
if (!ogImage) {
ogImage = document.createElement('meta');
ogImage.setAttribute('property', 'og:image');
document.head.appendChild(ogImage);
}
ogImage.setAttribute('content', `https://yotrip.labz.io.vn${photoUrl}`);
// Update og:title
let ogTitle = document.querySelector('meta[property="og:title"]');
if (!ogTitle) {
ogTitle = document.createElement('meta');
ogTitle.setAttribute('property', 'og:title');
document.head.appendChild(ogTitle);
}
ogTitle.setAttribute('content', photoTitle);
// Update og:description
let ogDescription = document.querySelector('meta[property="og:description"]');
if (!ogDescription) {
ogDescription = document.createElement('meta');
ogDescription.setAttribute('property', 'og:description');
document.head.appendChild(ogDescription);
}
ogDescription.setAttribute('content', photoDescription);
// Update twitter:image
let twitterImage = document.querySelector('meta[name="twitter:image"]');
if (!twitterImage) {
twitterImage = document.createElement('meta');
twitterImage.setAttribute('name', 'twitter:image');
document.head.appendChild(twitterImage);
}
twitterImage.setAttribute('content', `https://yotrip.labz.io.vn${photoUrl}`);
}
}, [publicPhotos]);
useEffect(() => {
fetchPublicPhotos();
fetchTrustedUsers();