Tạo UI cho người dùng tạo Tour

This commit is contained in:
2026-06-13 20:46:31 +07:00
parent c51ddc34c7
commit 25fcd5d926
28 changed files with 767 additions and 76 deletions
+114 -39
View File
@@ -1,18 +1,23 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { ItineraryTimeline } from './ItineraryTimeline.js';
import { ExpenseManager } from './ExpenseManager.js';
import { useTourStore } from './useTourStore.js';
import { AddLocationModal } from './AddLocationModal.js';
import {
Map as MapIcon,
Wallet,
Image as ImageIcon,
Calendar,
Users,
ChevronLeft
ChevronLeft,
Settings,
Quote,
Plus
} from 'lucide-react';
export const TourDetailPage = () => {
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo'>('plan');
export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan');
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole } = useTourStore();
useEffect(() => {
@@ -34,31 +39,36 @@ export const TourDetailPage = () => {
// Định nghĩa các tab dựa trên quyền hạn (RBAC) từ store
const tabs = [
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: ['OWNER', 'EDITOR', 'MEMBER_PLAN_ONLY'].includes(userRole || '') },
{ id: 'expense', label: 'Chi phí', icon: Wallet, visible: ['OWNER', 'EDITOR'].includes(userRole || '') },
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: true },
{ id: 'expense', label: 'Chi phí', icon: Wallet, visible: ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '') },
{ id: 'photo', label: 'Ảnh', icon: ImageIcon, visible: true },
{ id: 'settings', label: 'Cài đặt', icon: Settings, visible: ['OWNER', 'MANAGER'].includes(userRole || '') },
].filter(t => t.visible);
// Tự động chuyển tab nếu người dùng không có quyền xem 'plan'
useEffect(() => {
if (userRole === 'MEMBER_PHOTO_ONLY') {
setActiveTab('photo');
}
}, [userRole]);
const hasFinanceAccess = ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
const travelQuotes = [
"Đừng nghe họ nói, hãy tự mình đi xem.",
"Thế giới là một cuốn sách, và ai không đi du lịch thì chỉ mới đọc được một trang.",
"Hành trình ngàn dặm bắt đầu từ một bước chân.",
"Đi là để trở về, nhưng với một tâm hồn mới."
];
const randomQuote = useMemo(() => travelQuotes[Math.floor(Math.random() * travelQuotes.length)], []);
// Mock dữ liệu header (Trong thực tế sẽ lấy từ currentTour)
const tourInfo = {
title: currentTour?.title || "Hành trình khám phá TP.HCM",
date: "20 - 21 Tháng 11, 2023",
members: 5,
budget: "2.500.000 VND"
date: currentTour?.startDate ? `${new Date(currentTour.startDate).toLocaleDateString('vi-VN')} - ${new Date(currentTour.endDate).toLocaleDateString('vi-VN')}` : "Chưa xác định ngày",
membersCount: currentTour?.participants?.length || 0,
budget: currentTour?.totalCost ? `${Number(currentTour.totalCost).toLocaleString()} VND` : "0 VND",
coverImage: currentTour?.photos?.[0]?.imageUrl || "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?q=80&w=2070&auto=format&fit=crop"
};
return (
<div className="min-h-screen bg-white">
<div className="min-h-screen bg-gray-50 pb-20">
{/* Top Navigation Bar */}
<div className="sticky top-0 z-30 bg-white border-b border-gray-100 px-4 py-3 flex items-center justify-between">
<button className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<div className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-gray-100 px-4 py-3 flex items-center justify-between">
<button onClick={onBack} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<ChevronLeft className="w-6 h-6 text-gray-600" />
</button>
<h1 className="text-lg font-bold text-gray-800 truncate px-4">
@@ -68,29 +78,76 @@ export const TourDetailPage = () => {
</div>
{/* Tour Header Info */}
<div className="bg-blue-600 text-white p-6 pb-20">
<div className="max-w-2xl mx-auto space-y-4">
<div className="flex flex-wrap gap-4 text-sm opacity-90">
<div className="flex items-center">
<Calendar className="w-4 h-4 mr-1.5" />
{tourInfo.date}
<div className="relative h-64 w-full bg-blue-900">
<img
src={tourInfo.coverImage}
className="w-full h-full object-cover opacity-60"
alt="Tour Cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
<div className="absolute bottom-16 left-0 right-0 p-6 text-white">
<div className="max-w-2xl mx-auto space-y-2">
<h2 className="text-3xl font-black tracking-tight drop-shadow-md">{tourInfo.title}</h2>
<div className="flex flex-wrap gap-4 text-sm font-medium opacity-90">
<div className="flex items-center bg-black/20 backdrop-blur-sm px-3 py-1 rounded-full border border-white/10">
<Calendar className="w-4 h-4 mr-1.5" />
{tourInfo.date}
</div>
<div className="flex items-center bg-black/20 backdrop-blur-sm px-3 py-1 rounded-full border border-white/10">
<Users className="w-4 h-4 mr-1.5" />
{tourInfo.membersCount} thành viên
</div>
</div>
<div className="flex items-center">
<Users className="w-4 h-4 mr-1.5" />
{tourInfo.members} thành viên
{/* Member Avatars Stack */}
<div className="flex items-center gap-2 mt-4">
<div className="flex -space-x-3">
{currentTour?.participants?.slice(0, 5).map((p: any, i: number) => (
<div key={i} className="w-10 h-10 rounded-full border-2 border-white bg-blue-100 flex items-center justify-center text-blue-600 font-bold overflow-hidden shadow-lg">
<img src={`https://i.pravatar.cc/100?u=${p.userId}`} alt="Avatar" />
</div>
))}
{tourInfo.membersCount > 5 && (
<div className="w-10 h-10 rounded-full border-2 border-white bg-gray-800 flex items-center justify-center text-[10px] font-bold text-white shadow-lg">
+{tourInfo.membersCount - 5}
</div>
)}
</div>
<button className="p-2 bg-white/10 hover:bg-white/20 rounded-full border border-white/20 backdrop-blur-sm transition-all ml-2">
<Plus className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">{tourInfo.budget}</span>
<span className="text-blue-100 text-sm">dự kiến</span>
</div>
</div>
{/* Financial Quick-View Widget or Quote */}
<div className="max-w-2xl mx-auto -mt-10 px-4 relative z-10">
<div className={`rounded-3xl p-6 shadow-2xl transition-all duration-500 ${hasFinanceAccess ? 'bg-indigo-600 text-white shadow-indigo-200' : 'bg-white text-gray-600 border border-gray-100'}`}>
<div className="flex justify-between items-center">
{hasFinanceAccess ? (
<>
<div>
<p className="text-indigo-100 text-xs font-black uppercase tracking-widest mb-1">Tổng chi tiêu hiện tại</p>
<h3 className="text-3xl font-black">{tourInfo.budget}</h3>
</div>
<div className="p-4 bg-white/10 rounded-2xl backdrop-blur-md"><Wallet className="w-8 h-8" /></div>
</>
) : (
<div className="flex items-start gap-4 py-2">
<Quote className="w-8 h-8 text-blue-500 opacity-30 flex-shrink-0" />
<p className="italic text-lg font-medium leading-relaxed">"{randomQuote}"</p>
</div>
)}
</div>
</div>
</div>
{/* Main Content Area */}
<div className="max-w-2xl mx-auto -mt-12 px-4 pb-24">
<div className="max-w-2xl mx-auto mt-6 px-4 pb-24">
{/* Tab Switcher */}
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-1 flex mb-6">
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-20 z-20">
{tabs.map((tab) => (
<button
key={tab.id}
@@ -98,10 +155,10 @@ export const TourDetailPage = () => {
className={`flex-1 flex items-center justify-center py-3 rounded-xl text-sm font-semibold transition-all ${
activeTab === tab.id
? 'bg-blue-50 text-blue-600 shadow-sm'
: 'text-gray-400 hover:text-gray-600'
: 'text-gray-400 hover:text-gray-500 hover:bg-gray-50'
}`}
>
<tab.icon className={`w-4 h-4 mr-2 ${activeTab === tab.id ? 'animate-pulse' : ''}`} />
<tab.icon className={`w-4 h-4 mr-2 ${activeTab === tab.id ? 'scale-110' : ''}`} />
{tab.label}
</button>
))}
@@ -122,28 +179,46 @@ export const TourDetailPage = () => {
)}
{activeTab === 'photo' && (
<div className="grid grid-cols-3 gap-2 animate-in fade-in">
<div className="grid grid-cols-3 gap-1.5 animate-in fade-in">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="aspect-square bg-gray-100 rounded-lg overflow-hidden relative group">
<div key={i} className="aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white">
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
<img
src={`https://picsum.photos/seed/${i + 10}/400/400`}
alt="Tour photo"
className="w-full h-full object-cover"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
</div>
))}
</div>
)}
{activeTab === 'settings' && (
<div className="p-8 text-center bg-white rounded-3xl border border-dashed border-gray-200 animate-in zoom-in-95">
<Settings className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500 font-medium">Tính năng quản thành viên đang đưc cập nhật...</p>
</div>
)}
</div>
</div>
{/* Floating Action Button (Mobile) */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
<button className="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold">
<button
onClick={() => activeTab === 'plan' ? setIsAddLocationOpen(true) : null}
className="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold">
{activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh'}
</button>
</div>
{/* Add Location Modal */}
{currentTour && (
<AddLocationModal
isOpen={isAddLocationOpen}
onClose={() => setIsAddLocationOpen(false)}
tourId={currentTour.id}
/>
)}
</div>
);
};