diff --git a/CHAT_UI_refix.md b/CHAT_UI_refix.md deleted file mode 100644 index 35c80a1..0000000 --- a/CHAT_UI_refix.md +++ /dev/null @@ -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 -
- -
-
-
- - -
- -
-
- -
- -### 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. \ No newline at end of file diff --git a/THEME_COLORS.md b/THEME_COLORS.md new file mode 100644 index 0000000..25f45ed --- /dev/null +++ b/THEME_COLORS.md @@ -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 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. \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 540ce75..4718dba 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,21 @@ Travel Planner + + + + + + + + + + + + + + +
diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 08b10fc..c02e9ac 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -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"; diff --git a/frontend/src/components/AddLocationModal.tsx b/frontend/src/components/AddLocationModal.tsx index a69a874..00d68b4 100644 --- a/frontend/src/components/AddLocationModal.tsx +++ b/frontend/src/components/AddLocationModal.tsx @@ -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, 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
- setFormData({...formData, latitude: e.target.value as any})} /> + setFormData({...formData, latitude: e.target.value as any})} + onPaste={(e) => handleCoordinatePaste(e, 'latitude')} + placeholder="13.50731401913824" + /> +

💡 Dán "lat, lng" để tự động điền cả vĩ độ và kinh độ

- setFormData({...formData, longitude: e.target.value as any})} /> + setFormData({...formData, longitude: e.target.value as any})} + onPaste={(e) => handleCoordinatePaste(e, 'longitude')} + placeholder="109.28986362417251" + />
diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 34c09d5..dab3e71 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -102,6 +102,52 @@ export const LandingPage: React.FC = ({ 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();