fix: paste trực tiếp tọa độ vào kinh độ vĩ độ
This commit is contained in:
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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">Vĩ độ</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ả vĩ độ và 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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user