fix: lỗi hiển thị trong itineraryTimeline

This commit is contained in:
2026-06-23 11:40:41 +07:00
parent a65be48d36
commit 6b15e7ff02
17 changed files with 625 additions and 135 deletions
+102
View File
@@ -0,0 +1,102 @@
# 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.
+142
View File
@@ -0,0 +1,142 @@
# To AI Agent: Refactor Itinerary Timeline into an Exclusive File Tree Directory System (Single Expansion Mode)
## 1. Context & Architectural Analogy
We are refactoring the `ItineraryTimeline` component on the mobile view (`yotrip.labz.io.vn`) using a **File Tree Directory System** analogy:
- **Parent Folders ("Thư mục mẹ"):** Represented by the Stages ("Chặng 1", "Chặng 2"...).
- **Child Nodes ("Thư mục con"):** Represented by the Locations/Destinations inside that stage (`Trần Cao Vân`, `Đồng Khởi`...).
### CRITICAL LOGIC CONSTRAINT (Exclusive Accordion):
- When a Parent Folder (Stage) is **collapsed**, all of its Child Nodes (Locations) must immediately hide cleanly inside it.
- **Single Expansion Rule:** The system must enforce an **exclusive single-expansion mode**. On the entire timeline screen, **MAXIMUM ONE** Parent Folder can be expanded at any given time.
- Opening/Expanding a new Stage folder must **automatically collapse** whichever Stage folder was previously open.
---
## 2. State Management Specification (For Script/Logic Implementation)
To enforce the "Maximum 1 Expanded Folder" rule, do NOT use isolated individual boolean flags for each stage. Instead, implement a centralized single-active-state variable:
```javascript
// Example React State Hook Blueprint:
// Track the ID or index of the single active expanded stage folder.
// If null, all stages are collapsed.
const [expandedStageId, setExpandedStageId] = useState(initialStageId);
const handleStageToggle = (stageId) => {
// If clicking the already open folder, close it. Otherwise, open the new one and shut the rest.
setExpandedStageId(prevId => prevId === stageId ? null : stageId);
};
## 3. Component DOM Tree & CSS Blueprint
### A. Component Layout Structure
<div class="file-tree-itinerary-container">
<div class="fixed-top-header-block">
<header class="main-tour-header">...</header>
<nav class="sub-navigation-tabs">...</nav>
</div>
<main class="directory-scroll-viewport">
<section class="folder-node-wrapper collapsed">
<div class="folder-header-row" onclick="handleStageToggle('stage_1')">
<span class="folder-badge-index">1</span>
<h3 class="folder-title">Chặng khởi đầu</h3>
</div>
<div class="folder-child-content-box">
</div>
</section>
<section class="folder-node-wrapper expanded">
<div class="folder-header-row" onclick="handleStageToggle('stage_2')">
<span class="folder-badge-index">2</span>
<h3 class="folder-title">Chặng 2</h3>
</div>
<div class="folder-child-content-box">
<div class="child-nodes-list">
<div class="location-child-card">Trần Cao Vân</div>
<div class="location-child-card">Đồng Khởi</div>
</div>
</div>
</section>
</main>
</div>
### B. Core CSS Layout Styles
.file-tree-itinerary-container {
display: flex !important;
flex-direction: column !important;
height: 100dvh !important;
width: 100vw !important;
overflow: hidden !important;
}
.directory-scroll-viewport {
flex: 1 1 0% !important;
overflow-y: auto !important;
padding: 0 !important;
margin: 0 !important;
background-color: #f8fafc;
}
/* Base style for folder blocks stacked flush against each other */
.folder-node-wrapper {
width: 100% !important;
background-color: #ffffff;
margin-bottom: 1px !important; /* Micro hair-line divider between folders */
display: flex;
flex-direction: column;
}
.folder-header-row {
width: 100%;
padding: 12px 16px !important;
display: flex;
align-items: center;
background-color: #ffffff;
cursor: pointer;
/* Sticky behavior remains active when scrolling within an open directory */
position: sticky !important;
top: 0px;
z-index: 30;
}
/* CSS Grid Transition Engine for smooth directory expansions */
.folder-child-content-box {
display: grid !important;
grid-template-rows: 0fr;
transition: grid-template-rows 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
width: 100%;
}
.child-nodes-list {
overflow: hidden;
min-height: 0px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* --- MUTUALLY EXCLUSIVE STATE STYLES --- */
/* Collapsed Folder: Sub-files shrink immediately to 0 height */
.folder-node-wrapper.collapsed .folder-child-content-box {
grid-template-rows: 0fr !important;
}
/* Expanded Folder: Opens dynamically to accommodate dynamic content height */
.folder-node-wrapper.expanded .folder-child-content-box {
grid-template-rows: 1fr !important;
}
## 4. Acceptance Criteria for AI Agent Verification
[ ] Mutually Exclusive Test: Clicking an inactive Stage header while another stage is open must trigger a simultaneous transition: the old stage collapses back into a tight header row, and the clicked stage expands its location cards.
[ ] Zero-Gap Validation: All collapsed folder components must sit completely flush against the top tab block and one another, eliminating all gray margin bleeding.
[ ] Under-Clip Containment: When scrolling a long open folder, the list items must slide behind their sticky active parent folder row and disappear without visual overlapping artifacts.
-79
View File
@@ -1,79 +0,0 @@
# To AI Agent: Implement Mobile Horizontal Image Panning Component
## 1. Context & Objective
We are developing a travel application. We need to implement a mobile-first image viewer component.
**CRITICAL REQUIREMENT:** When a user swipes/drags horizontally on mobile, the UI must NOT switch to the next image. Instead, it must smoothly scroll/pan horizontally to reveal the hidden, unexposed parts of the *same* wide/panoramic image.
---
## 2. Technical Stack & Scope
- **Target Platform:** Mobile Web / Responsive (Touch-friendly).
- **Preferred Method:** CSS-First approach utilizing Viewport Overflow (for optimal GPU performance and native inertia scrolling).
- **Avoid:** Do NOT use global slider libraries (like standard Swiper/Slick) if they force image switching behavior.
---
## 3. UI/UX Specifications
### A. DOM Structure
- A wrapper/container acting as the "window view" (`.image-pan-container`).
- The target wide/panoramic image (`.image-pan-element`).
### B. CSS Rules & Constraints
1. **Container (`.image-pan-container`):**
- Must have a fixed width (e.g., `100vw` or `100%` of parent).
- Must set `overflow-x: auto` and `overflow-y: hidden` to enable horizontal touch scrolling only.
- Must enable smooth scrolling (`scroll-behavior: smooth`) and native touch momentum (`-webkit-overflow-scrolling: touch`).
- **Crucial:** Hide the native scrollbar across all major browsers (Webkit, Firefox, IE/Edge) to make it look like a native mobile app feature.
2. **Image Element (`.image-pan-element`):**
- Must fit the container's height perfectly (`height: 100%`).
- Width must be calculated automatically based on aspect ratio (`width: auto`).
- Must override any global framework styles: enforce `max-width: none !important`.
- Do NOT use `object-fit: cover` as it will crop the scrolling data.
---
## 4. Reference Code Blueprint
Use the following snippet as a baseline for your implementation:
```html
<div class="image-pan-container">
<img src="YOUR_PANORAMIC_IMAGE_URL" class="image-pan-element" alt="Panoramic View" />
</div>
/* Styling Architecture */
.image-pan-container {
width: 100%;
height: 400px; /* Adjust height based on project guidelines */
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
/* Hide scrollbars entirely */
.image-pan-container::-webkit-scrollbar {
display: none;
}
.image-pan-container {
-ms-overflow-style: none;
scrollbar-width: none;
}
.image-pan-element {
height: 100%;
width: auto;
max-width: none !important;
display: block;
}
5. Acceptance Criteria
[ ] The wide photo fills the component height and overflows horizontally without distortion.
[ ] Users can smoothly swipe left/right with their fingers to view all details of the photo.
[ ] No desktop/mobile scrollbars are visible during the interaction.
[ ] Ensure max-width override is active so Tailwind or other CSS frameworks don't crush the image width to 100%.
+3
View File
@@ -1582,6 +1582,7 @@ let TourController = class TourController {
async joinByToken(body, req) {
const { token } = body;
console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
console.log('[joinByToken] Token received (full):', token);
if (!token) {
console.error('[joinByToken] No token provided');
throw new common_1.BadRequestException('Vui lòng cung cấp token lời mời.');
@@ -1595,6 +1596,8 @@ let TourController = class TourController {
console.error('[joinByToken] Invitation not found for token:', token.substring(0, 20) + '...');
const totalInvitations = await this.prisma.tourInvitation.count();
console.log('[joinByToken] Total invitations in database:', totalInvitations);
const allInvitations = await this.prisma.tourInvitation.findMany({ select: { token: true, email: true, tourId: true } });
console.log('[joinByToken] All invitation tokens:', allInvitations);
throw new common_1.NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.');
}
const userEmail = req.user.email;
+1 -1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<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>
<script type="module" crossorigin src="/assets/index-Chfn55jq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D_50XTpY.css">
<script type="module" crossorigin src="/assets/index-FtyTkPzF.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DQ8dDKht.css">
</head>
<body>
<div id="root"></div>
+35 -15
View File
@@ -76,6 +76,14 @@ export const ItineraryTimeline = ({
const [commentLocationId, setCommentLocationId] = useState('');
const [commentLocationName, setCommentLocationName] = useState('');
// State to track single expanded stage (exclusive single-expansion mode)
const [expandedStageId, setExpandedStageId] = useState<string | null>(legs.length > 0 ? legs[0]?.id : null);
const toggleStageExpanded = (legId: string) => {
// Exclusive mode: if clicking the same stage, close it. Otherwise, open only the clicked one.
setExpandedStageId(prevId => prevId === legId ? null : legId);
};
// Tối ưu hóa: Cập nhật UI ngay lập tức bằng cách can thiệp vào State của Store
const handleCommentIncrement = (locationId: string) => {
const currentLegs = useTourStore.getState().legs;
@@ -197,11 +205,14 @@ export const ItineraryTimeline = ({
useEffect(() => {
console.log("ItineraryTimeline: Legs updated", legs);
// Initialize first leg as expanded when legs change
if (legs.length > 0 && !expandedStageId) {
setExpandedStageId(legs[0].id);
}
}, [legs]);
return (
<div id="itinerary-timeline-print-zone" className="max-w-2xl mx-auto p-0 bg-gray-50 min-h-screen">
<div className="px-2 pt-4">
<div id="itinerary-timeline-print-zone" className="timeline-scroll-container itinerary-timeline-container">
{legs.length === 0 ? (
<div className="text-center py-20 bg-white rounded-3xl border-2 border-dashed border-gray-200">
<List className="w-12 h-12 text-gray-300 mx-auto mb-4" />
@@ -220,9 +231,12 @@ export const ItineraryTimeline = ({
const prevLegLastLoc = legIdx > 0 ? legs[legIdx - 1]?.locations.slice(-1)[0] : null;
return (
<div key={leg.id} className="relative mb-12 animate-in fade-in slide-in-from-bottom-4 duration-300">
{/* Leg Header */}
<div className="sticky top-[136px] z-20 bg-gray-50/90 backdrop-blur-sm flex items-center mb-6 py-2 px-2">
<section key={leg.id} className={`folder-node-wrapper ${expandedStageId === leg.id ? 'expanded' : 'collapsed'}`}>
{/* Folder header row - clickable to toggle exclusive expansion */}
<div
onClick={() => toggleStageExpanded(leg.id)}
className="folder-header-row animate-in fade-in slide-in-from-bottom-4 duration-300 hover:bg-gray-50/50 transition-colors"
>
<div className="font-black text-blue-600 text-xl truncate flex-1 flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="bg-blue-600 text-white w-8 h-8 rounded-lg flex items-center justify-center text-sm shrink-0">
@@ -245,24 +259,24 @@ export const ItineraryTimeline = ({
</div>
)}
</div>
<div className="flex items-center gap-2 ml-4">
<div className="flex items-center gap-2 ml-4" onClick={(e) => e.stopPropagation()}>
{canEdit && (
<>
<button
onClick={() => onAddLocation?.(leg.id, legIdx === 0 && leg.locations.length === 0)}
onClick={(e) => { e.stopPropagation(); onAddLocation?.(leg.id, legIdx === 0 && leg.locations.length === 0); }}
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
title="Thêm địa điểm vào chặng này"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => handleEditLeg(leg)}
onClick={(e) => { e.stopPropagation(); handleEditLeg(leg); }}
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteLeg(leg.id)}
onClick={(e) => { e.stopPropagation(); handleDeleteLeg(leg.id); }}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all"
>
<Trash2 className="w-4 h-4" />
@@ -298,9 +312,13 @@ export const ItineraryTimeline = ({
)} {/* Only show optimize button if canEdit */}
</div>
{/* Vertical Line for the whole leg */}
{/* Mở rộng đường kẻ xuống dưới (bottom-[-3rem]) để nối liền với chặng tiếp theo */}
<div className={`absolute left-6 top-16 ${legIdx === legs.length - 1 ? 'bottom-0' : 'bottom-[-3.5rem]'} w-0.5 bg-blue-100 -z-0`} />
{/* Scrollable content body with proper z-index layering */}
<div className="child-nodes-list-wrapper">
{/* Folder child content box - Grid accordion for exclusive expansion */}
<div className={`folder-child-content-box ${expandedStageId === leg.id ? 'expanded' : ''}`}>
<div className="child-nodes-list relative">
{/* Vertical Line for the whole leg - Dynamic height */}
<div className="absolute left-6 top-16 w-0.5 bg-blue-100 -z-0 h-full" />
<div className="ml-2">
{/* Nút thêm nhanh "Điểm xuất phát" cho Chặng 1 nếu chưa có */}
@@ -522,8 +540,11 @@ export const ItineraryTimeline = ({
</div>
);
})}
</div>
</div>
</div> {/* End of ml-2 wrapper */}
</div> {/* End of child-nodes-list */}
</div> {/* End of folder-child-content-box */}
</div> {/* End of child-nodes-list-wrapper */}
</section>
);
})
)}
@@ -546,7 +567,6 @@ export const ItineraryTimeline = ({
</button>
</div>
)}
</div>
{/* Modal Khai báo số chặng (Popover) */}
{isLegCountModalOpen && (
+18 -14
View File
@@ -5,9 +5,10 @@ import { useNotification } from '@/hooks/useNotification';
interface TourChatProps {
tourId: string;
embedded?: boolean;
}
export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false }) => {
const notify = useNotification();
const [messages, setMessages] = useState<any[]>([]);
const [newMessage, setNewMessage] = useState('');
@@ -416,21 +417,24 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
}
};
// currentUserId is defined at the top
return (
<div className="bg-white border border-gray-150 rounded-2xl shadow-lg overflow-hidden flex flex-col h-[500px]">
{/* Chat Header */}
<div className="p-4 border-b border-gray-150 flex items-center gap-2 bg-gray-50/50">
<div className={`chat-viewport-wrapper flex flex-col !overflow-hidden !w-full ${embedded ? 'h-full flex-1' : 'h-[100dvh]'} bg-white`}>
{/* Fixed Top Layout Block - Header and Tabs Container */}
<div className="fixed-top-layout-block flex-shrink-0 !w-full z-50 bg-white border-b border-gray-200">
{/* Chat Header - Only show when not embedded */}
{!embedded && (
<div className="p-4 border-b border-gray-200 bg-white flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-500" />
<div>
<h3 className="text-sm font-bold text-gray-800">Trò chuyện nhóm hành trình</h3>
<p className="text-[10px] text-gray-400 font-medium">Nơi trao đi thông tin, hình nh đnh vị giữa các thành viên</p>
</div>
</div>
)}
</div>
{/* Messages list */}
<div className="flex-1 p-4 overflow-y-auto flex flex-col gap-3 min-h-0 bg-slate-50/20">
{/* Chat History Scroll Viewport - ONLY scrollable area */}
<div className="chat-history-scroll-viewport flex-1 min-h-0 !overflow-y-auto !overflow-x-hidden p-4 flex flex-col gap-3 bg-slate-50/20">
{loading ? (
<div className="flex-1 flex items-center justify-center text-gray-400 text-xs gap-1.5">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" /> Đang tải tin nhắn...
@@ -468,7 +472,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
<div className={`p-3 rounded-2xl text-xs font-medium leading-relaxed flex flex-col gap-1.5 relative group ${
isMe
? 'bg-blue-600 text-white rounded-tr-none'
: 'bg-white text-gray-700 rounded-tl-none border border-gray-150 shadow-sm'
: 'bg-white text-gray-700 rounded-tl-none border border-gray-200 shadow-sm'
}`}>
{/* Attachment Image */}
{msg.attachmentUrl && (
@@ -498,7 +502,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-[11px] font-bold transition-all border ${
isMe
? 'bg-blue-700 border-blue-600 text-blue-100 hover:bg-blue-800'
: 'bg-gray-100 border-gray-200 text-gray-750 hover:bg-gray-200'
: 'bg-gray-100 border-gray-200 text-gray-700 hover:bg-gray-200'
}`}
>
<MapPin className="w-3.5 h-3.5 text-rose-500 shrink-0 animate-bounce" />
@@ -525,7 +529,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
{/* Previews (Image & GPS Location) */}
{(imagePreview || attachedLocation) && (
<div className="px-4 py-2 border-t border-gray-150 bg-gray-50/80 flex flex-wrap gap-2">
<div className="px-4 py-2 border-t border-gray-200 bg-gray-50/80 flex flex-wrap gap-2 !flex-shrink-0">
{imagePreview && (
<div className="relative w-16 h-16 rounded-lg overflow-hidden border border-gray-200 shadow-sm">
<img src={imagePreview} alt="Preview" className="w-full h-full object-cover" />
@@ -557,8 +561,8 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
</div>
)}
{/* Chat Input wrapper */}
<div className="relative">
{/* Locked Chat Input Footer */}
<div className="locked-chat-input-footer !flex-shrink-0 !w-full !border-t border-gray-200 bg-white !z-40" style={{ paddingBottom: 'env(safe-area-inset-bottom, 12px)' }}>
{/* Mention list dropdown */}
{showMentionList && filteredParticipants.length > 0 && (
<div
@@ -589,7 +593,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId }) => {
{/* Chat Input form */}
<form
onSubmit={handleSendMessage}
className="p-3 border-t border-gray-150 bg-gray-50 flex gap-2 items-center"
className="p-3 flex gap-2 items-center !w-full relative"
>
<input
type="file"
+289
View File
@@ -1,10 +1,299 @@
@import "tailwindcss";
@layer base {
:root {
--main-header-height: 56px;
--sub-nav-height: 48px;
--combined-top-height: 104px;
}
html, body, #root, .app-container {
height: 100%;
margin: 0;
padding: 0;
max-width: 100vw !important;
overflow-x: hidden !important;
}
/* Fix viewport overflow on all container levels */
body, html {
width: 100%;
max-width: 100vw;
overflow-x: hidden;
}
.itinerary-timeline-page {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
.main-top-bar {
position: sticky;
top: 0;
height: var(--main-header-height);
z-index: 50;
background-color: #ffffff;
}
.sub-nav-menu {
position: sticky;
top: var(--main-header-height);
height: var(--sub-nav-height);
z-index: 40;
width: 100% !important;
margin: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
border-bottom: 1px solid #e5e7eb;
background-color: #ffffff;
}
/* Master viewport wrapper - contains entire itinerary */
.itinerary-viewport {
display: flex !important;
flex-direction: column !important;
height: 100dvh !important;
width: 100vw !important;
overflow: hidden !important;
}
.itinerary-viewport-wrapper {
display: flex !important;
flex-direction: column !important;
height: 100dvh !important;
width: 100vw !important;
overflow: hidden !important;
}
/* Global sticky header zone - locks main header + sub-nav at top */
.global-sticky-header-zone {
position: sticky !important;
top: 0;
z-index: 100 !important;
background-color: #ffffff;
flex-shrink: 0 !important;
}
/* Fixed header block (handled at parent TourDetailPage level) */
.fixed-top-header-block {
flex-shrink: 0 !important;
width: 100% !important;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
/* Timeline scroll container - main scrollable region */
.directory-scroll-viewport {
flex: 1 !important;
overflow-y: auto !important;
position: relative !important;
z-index: 10 !important;
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
background-color: #f8fafc;
}
.timeline-scroll-container {
flex: 1 !important;
overflow-y: auto !important;
position: relative !important;
z-index: 10 !important;
width: 100% !important;
padding: 0 !important;
}
/* Scrollable timeline area - removes all padding gaps */
.timeline-scroll-area {
flex: 1 1 0% !important;
overflow-y: auto !important;
width: 100% !important;
padding: 0 !important;
margin-top: 0px !important;
background-color: #f8fafc;
}
.timeline-scroll-viewport {
flex: 1;
overflow-y: auto;
width: 100%;
}
/* Main timeline container with dynamic height support */
.itinerary-timeline-container {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100vw !important;
height: auto !important;
overflow-x: hidden !important;
position: relative !important;
}
/* Individual folder node wrapper - base stage container */
.folder-node-wrapper {
width: 100% !important;
background-color: #ffffff;
margin-bottom: 1px !important;
display: flex;
flex-direction: column;
}
/* Individual stage card block - backwards compatibility */
.stage-card-block {
width: 100% !important;
background-color: #ffffff;
margin-bottom: 1px !important;
display: flex;
flex-direction: column;
}
/* Stage section - semantic wrapper */
.stage-section {
position: relative;
width: 100%;
display: block !important;
margin: 0 !important;
padding: 0 !important;
}
/* Folder header row - sticky positioning for pinned effect */
.folder-header-row {
position: sticky !important;
top: 0px !important;
z-index: 30 !important;
width: 100% !important;
padding: 12px 16px !important;
display: flex;
align-items: center;
background-color: #ffffff !important;
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
transition: background-color 0.2s;
}
.folder-header-row:hover {
background-color: #f9fafb !important;
}
/* Stage header row - sticky positioning for pinned effect */
.stage-header-row {
position: sticky !important;
top: 0px !important;
z-index: 40 !important;
width: 100% !important;
padding: 12px 16px !important;
display: flex;
align-items: center;
background-color: #ffffff !important;
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
transition: background-color 0.2s;
}
.stage-header-row:hover {
background-color: #f9fafb !important;
}
/* Alternate sticky header styling (kept for backwards compatibility) */
.stage-sticky-header {
position: sticky;
top: 0;
z-index: 30;
background-color: #ffffff;
width: 100%;
padding: 8px 16px;
border-bottom: 1px solid #f3f4f6;
display: flex;
align-items: center;
cursor: pointer;
}
/* Folder child content box - Grid accordion for exclusive expansion */
.folder-child-content-box {
display: grid !important;
grid-template-rows: 0fr;
transition: grid-template-rows 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
width: 100%;
}
/* Expanded folder state - children visible */
.folder-child-content-box.expanded {
grid-template-rows: 1fr !important;
}
/* Wrapper for child nodes list - proper z-index layering */
.child-nodes-list-wrapper {
position: relative;
z-index: 20 !important;
width: 100%;
}
/* Child nodes list - content container within expanded folder */
.child-nodes-list {
overflow: hidden;
min-height: 0px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* CSS Grid Accordion Engine - Smooth expand/collapse without layout shattering */
.stage-accordion-content {
display: grid !important;
grid-template-rows: 0fr;
transition: grid-template-rows 0.25s ease-out !important;
width: 100%;
}
/* Expanded state - opens to full content height */
.stage-accordion-content.expanded {
grid-template-rows: 1fr !important;
}
/* Scrollable content body - lower z-index so it scrolls behind header */
.stage-scrollable-content-body {
position: relative;
z-index: 20 !important;
width: 100%;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Inner content wrapper - structural compliance */
.stage-points-list {
overflow: hidden;
min-height: 0px;
padding: 0 16px 16px 16px;
}
/* Legacy accordion container classes (for backwards compatibility) */
.stage-accordion-transition-container {
display: grid !important;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
width: 100% !important;
}
.stage-accordion-transition-container.expanded {
grid-template-rows: 1fr !important;
}
.stage-accordion-inner-content {
overflow: hidden !important;
min-height: 0px !important;
width: 100% !important;
display: flex;
flex-direction: column;
}
.stage-content-body {
width: 100%;
padding: 16px;
}
}
+1 -1
View File
@@ -1683,7 +1683,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
{/* TAB 4: REALTIME CHAT */}
{activeTab === 'chats' && (
<div className="flex-1 w-full bg-slate-900/40 border-0 md:border border-slate-800/80 md:rounded-3xl overflow-hidden flex animate-in fade-in slide-in-from-bottom-2 duration-300">
<div className="flex flex-col h-[100dvh] md:h-auto w-full bg-slate-900/40 border-0 md:border border-slate-800/80 md:rounded-3xl overflow-hidden flex animate-in fade-in slide-in-from-bottom-2 duration-300">
{/* Chats List sidebar: show if not mobile OR if mobile and no active chat user */}
{(!isMobile || !activeChatUser) && (
+14 -5
View File
@@ -2003,9 +2003,9 @@ export const TourDetailPage = ({
</div>
{/* Main Content Area - Điều chỉnh max-width khi ở chế độ bản đồ */}
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full' : 'max-w-2xl mx-auto'} mt-6 px-4 pb-24`}>
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full' : 'max-w-2xl mx-auto'} px-4 pb-24`}>
{/* Tab Switcher */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-20 z-20">
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-[60px] z-40">
{tabs.map((tab) => (
<button
key={tab.id}
@@ -2013,7 +2013,7 @@ export const TourDetailPage = ({
setActiveTab(tab.id as any);
if (tab.id === 'chat') setUnreadChatCount(0);
}}
className={`flex-1 flex items-center justify-center py-3 rounded-xl text-sm font-semibold transition-all relative ${tab.id === 'settings' && isPublicView ? 'hidden' : ''} ${ // Hide settings tab in public view
className={`flex-1 flex items-center justify-center py-2 rounded-xl text-xs font-semibold transition-all relative ${tab.id === 'settings' && isPublicView ? 'hidden' : ''} ${ // Hide settings tab in public view
activeTab === tab.id
? 'bg-blue-50 text-blue-600 shadow-sm'
: 'text-gray-400 hover:text-gray-500 hover:bg-gray-50'
@@ -3004,8 +3004,17 @@ export const TourDetailPage = ({
/>
)}
{activeTab === 'chat' && currentTour && (
<div className="animate-in fade-in slide-in-from-bottom-2">
<TourChat tourId={currentTour.id} />
<div className="fixed left-0 right-0 bottom-0 flex flex-col bg-white z-[15]" style={{top: 'calc(60px + 5px)'}}>
{/* Chat Group Header Banner - Fixed below nav + tabs, sticky to title */}
<div className="flex-shrink-0 p-3 border-b border-gray-200 bg-white flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-500" />
<div>
<h3 className="text-sm font-bold text-gray-800">Trò chuyện trong nhóm</h3>
<p className="text-[10px] text-gray-400 font-medium">Nơi trao đi thông tin, hình nh đnh vị giữa các thành viên</p>
</div>
</div>
{/* TourChat with proper scrollable area */}
<TourChat tourId={currentTour.id} embedded={true} />
</div>
)}
</div>