feat: apply theme colors to all pages and modals - implement light/dark theme CSS variables across UI components

This commit is contained in:
2026-06-23 18:28:05 +07:00
parent 7182391241
commit a64af5f9cf
18 changed files with 458 additions and 311 deletions
+42 -42
View File
@@ -45,13 +45,13 @@ const MapPicker = ({ onPick, center }: { onPick: (latlng: L.LatLng) => void, cen
{menuPos && (
<div
ref={menuRef}
className="absolute z-[3000] bg-white rounded-xl shadow-xl border border-gray-100 py-1 w-44 animate-in zoom-in-95 duration-200"
className="absolute z-[3000] bg-[var(--surface)] rounded-xl shadow-xl border border-[var(--border)] py-1 w-44 animate-in zoom-in-95 duration-200"
style={{ top: menuPos.y, left: menuPos.x }}
>
<button
type="button"
onClick={() => { onPick(menuPos.latlng); setMenuPos(null); }}
className="w-full text-left px-4 py-2 hover:bg-blue-50 text-xs font-bold text-blue-600 flex items-center gap-2"
className="w-full text-left px-4 py-2 hover:bg-[var(--background)] text-xs font-bold text-blue-600 flex items-center gap-2"
>
<MapPin className="w-3 h-3" /> Thêm vào chặng hiện tại
</button>
@@ -387,18 +387,18 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
return (
<div className="fixed inset-0 z-[2000] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden p-8 max-h-[90vh] overflow-y-auto">
<div className="relative w-full max-w-lg bg-[var(--surface)] rounded-3xl shadow-2xl overflow-hidden p-8 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<h2 className="text-2xl font-bold text-[var(--text-primary)] flex items-center gap-2">
<MapIcon className="w-6 h-6 text-blue-600" /> {titleText}
</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-6 h-6 text-gray-400" />
<button onClick={onClose} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
<X className="w-6 h-6 text-[var(--text-muted)]" />
</button>
</div>
{/* Mini Map Picker */}
<div className="h-64 w-full rounded-3xl overflow-hidden mb-6 border border-gray-100 relative shadow-xl group">
<div className="h-64 w-full rounded-3xl overflow-hidden mb-6 border border-[var(--border)] relative shadow-xl group">
{/* Map Search Bar Overlay - Tích hợp tìm kiếm trực tiếp trên bản đồ */}
<div className="absolute top-3 left-3 right-3 z-[1001] pointer-events-none">
<div className="relative max-w-sm pointer-events-auto">
@@ -406,7 +406,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
<input
type="text"
placeholder="Tìm địa điểm trên bản đồ..."
className="w-full pl-11 pr-10 py-3 bg-white/95 backdrop-blur-md border border-white/20 rounded-2xl shadow-lg outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm font-bold text-gray-800"
className="w-full pl-11 pr-10 py-3 bg-white/95 backdrop-blur-md border border-white/20 rounded-2xl shadow-lg outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm font-bold text-[var(--text-primary)]"
value={formData.name}
onChange={e => handleSearchLocation(e.target.value)}
/>
@@ -417,7 +417,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
<button
type="button"
onClick={() => { setFormData({...formData, name: ''}); setSearchResults([]); setHasNoResults(false); }}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 hover:bg-gray-100 rounded-full text-gray-400 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 hover:bg-[var(--background)] rounded-full text-[var(--text-muted)] transition-colors"
>
<X className="w-4 h-4" />
</button>
@@ -426,24 +426,24 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
{/* Dropdown kết quả tìm kiếm ngay trong khung bản đồ */}
{(searchResults.length > 0 || hasNoResults) && (
<div className="absolute left-0 right-0 mt-2 bg-white/95 backdrop-blur-md border border-gray-100 rounded-2xl shadow-2xl overflow-hidden max-h-40 overflow-y-auto animate-in slide-in-from-top-2 duration-200">
<div className="absolute left-0 right-0 mt-2 bg-white/95 backdrop-blur-md border border-[var(--border)] rounded-2xl shadow-2xl overflow-hidden max-h-40 overflow-y-auto animate-in slide-in-from-top-2 duration-200">
{hasNoResults ? (
<div className="px-4 py-4 text-center text-gray-400 text-xs italic">Không tìm thấy đa điểm phù hợp...</div>
<div className="px-4 py-4 text-center text-[var(--text-muted)] text-xs italic">Không tìm thấy đa điểm phù hợp...</div>
) : (
searchResults.map((result, idx) => (
<button
key={idx}
type="button"
onClick={() => selectSearchResult(result)}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-50 last:border-0 transition-colors flex flex-col gap-0.5"
className="w-full text-left px-4 py-3 hover:bg-[var(--background)] border-b border-[var(--border)] last:border-0 transition-colors flex flex-col gap-0.5"
>
<div className="flex items-center justify-between gap-2">
<div className="font-bold text-xs text-gray-900 truncate">{result.namedetails?.name || result.display_name.split(',')[0]}</div>
<div className="font-bold text-xs text-[var(--text-primary)] truncate">{result.namedetails?.name || result.display_name.split(',')[0]}</div>
{result.type && (
<span className="text-[8px] font-black uppercase text-blue-400 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100 shrink-0">{result.type}</span>
)}
</div>
<div className="text-[10px] text-gray-500 truncate leading-tight">{result.display_name}</div>
<div className="text-[10px] text-[var(--text-muted)] truncate leading-tight">{result.display_name}</div>
</button>
))
)}
@@ -457,7 +457,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
<Marker position={currentCoords} />
<MapPicker center={currentCoords} onPick={handlePickLocation} />
</MapContainer>
<div className="absolute bottom-2 left-2 z-[1000] bg-white/90 backdrop-blur-sm px-2 py-1 rounded-lg text-[10px] font-black text-gray-500 shadow-sm border border-gray-100">
<div className="absolute bottom-2 left-2 z-[1000] bg-white/90 backdrop-blur-sm px-2 py-1 rounded-lg text-[10px] font-black text-[var(--text-muted)] shadow-sm border border-[var(--border)]">
CHUỘT PHẢI Đ CHỌN VỊ TRÍ
</div>
</div>
@@ -475,23 +475,23 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Tên đa điểm</label>
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Tên đa điểm</label>
<input
required
placeholder="Tên địa điểm..."
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 transition-all font-bold"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none focus:ring-2 focus:ring-blue-500 transition-all font-bold"
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Đa chỉ</label>
<input className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Address</label>
<input className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none"
value={formData.address} onChange={e => setFormData({...formData, address: e.target.value})} />
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Ghi chú đa điểm / Dịch vụ sử dụng</label>
<textarea className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none resize-none" rows={2}
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Ghi chú đa điểm / Dịch vụ sử dụng</label>
<textarea className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none resize-none" rows={2}
placeholder="Ví dụ: Ăn trưa tại quán X, thuê hướng dẫn viên..."
value={formData.note} onChange={e => setFormData({...formData, note: e.target.value})} />
</div>
@@ -499,8 +499,8 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
<p className="text-xs font-black text-blue-500 uppercase tracking-widest">Chi phí nhanh tại điểm này</p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Số tiền (VNĐ)</label>
<input type="text" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-xs font-bold text-[var(--text-secondary)] mb-1">Số tiền (VNĐ)</label>
<input type="text" className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
placeholder="0"
value={formData.expenseAmount} onChange={e => {
const rawValue = e.target.value.replace(/\D/g, ""); // Chỉ lấy số
@@ -509,8 +509,8 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
}} />
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Loại dịch vụ</label>
<select className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-xs font-bold text-[var(--text-secondary)] mb-1">Loại dịch vụ</label>
<select className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
value={formData.expenseCategory} onChange={e => setFormData({...formData, expenseCategory: e.target.value})}>
<option value="FOOD">Ăn uống</option>
<option value="TRANSPORT">Di chuyển</option>
@@ -521,20 +521,20 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</div>
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Dịch vụ / tả</label>
<input className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-xs font-bold text-[var(--text-secondary)] mb-1">Dịch vụ / tả</label>
<input className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
placeholder="Ví dụ: Ăn trưa, taxi, vé..."
value={formData.expenseDescription} onChange={e => setFormData({...formData, expenseDescription: e.target.value})} />
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Ghi chú chi phí</label>
<textarea className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none resize-none text-sm" rows={2}
<label className="block text-xs font-bold text-[var(--text-secondary)] mb-1">Ghi chú chi phí</label>
<textarea className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none resize-none text-sm" rows={2}
placeholder="Ghi chú thêm..."
value={formData.expenseNote} onChange={e => setFormData({...formData, expenseNote: e.target.value})} />
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Thành viên đã thanh toán</label>
<select className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-xs font-bold text-[var(--text-secondary)] mb-1">Thành viên đã thanh toán</label>
<select className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
value={formData.paidById} onChange={e => setFormData({...formData, paidById: e.target.value})}>
<option value="">-- Chọn người thanh toán --</option>
{currentTour?.participants?.filter((p: any) => p.user || p.displayName)?.map((p: any) => {
@@ -549,8 +549,8 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Gán vào chặng</label>
<select required className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Gán vào chặng</label>
<select required className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none"
value={currentLegId} onChange={e => setFormData({...formData, legId: e.target.value})}>
{legs.map(leg => (
<option key={leg.id} value={leg.id}>
@@ -562,26 +562,26 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1"> đ</label>
<label className="block text-sm font-bold text-[var(--text-secondary)] 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"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] 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>
<p className="text-xs text-[var(--text-muted)] 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>
<label className="block text-sm font-bold text-[var(--text-secondary)] 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"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none"
value={formData.longitude}
onChange={e => setFormData({...formData, longitude: e.target.value as any})}
onPaste={(e) => handleCoordinatePaste(e, 'longitude')}
@@ -590,8 +590,8 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Loại</label>
<select className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Loại</label>
<select className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none"
value={formData.type} onChange={e => setFormData({...formData, type: e.target.value as any})}>
<option value="VISIT">Tham quan</option>
<option value="EAT">Ăn uống</option>
@@ -601,8 +601,8 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Bắt đu</label>
<input type="datetime-local" className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Bắt đu</label>
<input type="datetime-local" className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none"
value={formData.plannedStart} onChange={e => setFormData({...formData, plannedStart: e.target.value})} />
</div>
</div>
+8 -8
View File
@@ -174,12 +174,12 @@ export const AddPhotoModal: React.FC<AddPhotoModalProps> = ({ isOpen, onClose, t
return (
<div className="fixed inset-0 z-[2500] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden p-8 max-h-[90vh] flex flex-col animate-in zoom-in-95 duration-200">
<div className="relative w-full max-w-lg bg-[var(--surface)] rounded-3xl shadow-2xl overflow-hidden p-8 max-h-[90vh] flex flex-col animate-in zoom-in-95 duration-200">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<h2 className="text-2xl font-bold text-[var(--text-primary)] flex items-center gap-2">
<ImageIcon className="w-6 h-6 text-blue-600" /> Tải nh lên
</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors text-gray-400">
<button onClick={onClose} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors text-[var(--text-muted)]">
<X className="w-6 h-6" />
</button>
</div>
@@ -187,22 +187,22 @@ export const AddPhotoModal: React.FC<AddPhotoModalProps> = ({ isOpen, onClose, t
<form onSubmit={handleSubmit} className="flex-1 flex flex-col min-h-0">
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-gray-200 rounded-[32px] p-10 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-50/50 hover:border-blue-200 transition-all mb-6 group"
className="border-2 border-dashed border-[var(--border)] rounded-[32px] p-10 flex flex-col items-center justify-center cursor-pointer hover:bg-[var(--background)]/50 hover:border-blue-200 transition-all mb-6 group"
>
<input type="file" ref={fileInputRef} className="hidden" multiple accept="image/*" onChange={handleFileChange} />
<div className="w-16 h-16 rounded-2xl bg-blue-50 flex items-center justify-center text-blue-600 mb-4 group-hover:scale-110 transition-transform shadow-inner">
<Upload className="w-8 h-8" />
</div>
<p className="text-sm font-black text-gray-700">Nhấn đ chọn nh</p>
<p className="text-xs text-gray-400 mt-1 font-medium">Hỗ trợ JPG, PNG, WEBP</p>
<p className="text-sm font-black text-[var(--text-secondary)]">Nhấn đ chọn nh</p>
<p className="text-xs text-[var(--text-muted)] mt-1 font-medium">Hỗ trợ JPG, PNG, WEBP</p>
</div>
{previews.length > 0 && (
<div className="flex-1 overflow-y-auto mb-6 pr-2">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3">Đã chọn {previews.length} tệp</p>
<p className="text-[10px] font-black text-[var(--text-muted)] uppercase tracking-widest mb-3">Đã chọn {previews.length} tệp</p>
<div className="grid grid-cols-3 gap-3">
{previews.map((src, idx) => (
<div key={idx} className="relative aspect-square rounded-2xl overflow-hidden border border-gray-100 shadow-sm group">
<div key={idx} className="relative aspect-square rounded-2xl overflow-hidden border border-[var(--border)] shadow-sm group">
<img src={src} className="w-full h-full object-cover" alt="preview" />
<button
type="button"
+15 -15
View File
@@ -134,27 +134,27 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
return (
<div className="fixed inset-0 z-[5000] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-200" onClick={onClose} />
<div className="relative w-full max-w-md bg-white rounded-[32px] shadow-2xl overflow-hidden flex flex-col max-h-[80vh] animate-in zoom-in-95 duration-200">
<div className="relative w-full max-w-md bg-[var(--surface)] rounded-[32px] shadow-2xl overflow-hidden flex flex-col max-h-[80vh] animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="p-6 border-b border-gray-100 flex justify-between items-center bg-white sticky top-0 z-10">
<div className="p-6 border-b border-[var(--border)] flex justify-between items-center bg-[var(--surface)] sticky top-0 z-10">
<div>
<h3 className="text-xl font-black text-gray-900 flex items-center gap-2">
<h3 className="text-xl font-black text-[var(--text-primary)] flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-600" />
Bình luận
</h3>
<p className="text-xs text-gray-500 font-bold uppercase tracking-widest mt-1 truncate max-w-[250px]">{locationName}</p>
<p className="text-xs text-[var(--text-muted)] font-bold uppercase tracking-widest mt-1 truncate max-w-[250px]">{locationName}</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-5 h-5 text-gray-400" />
<button onClick={onClose} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
<X className="w-5 h-5 text-[var(--text-muted)]" />
</button>
</div>
{/* Comment List */}
<div className="flex-1 overflow-y-auto p-6 space-y-4 bg-gray-50/50">
<div className="flex-1 overflow-y-auto p-6 space-y-4 bg-[var(--background)]/50">
{isLoading ? (
<div className="flex justify-center py-10"><Loader2 className="w-6 h-6 animate-spin text-blue-600" /></div>
) : comments.length === 0 ? (
<div className="text-center py-10 text-gray-400 italic text-sm">Chưa bình luận nào.</div>
<div className="text-center py-10 text-[var(--text-muted)] italic text-sm">Chưa bình luận nào.</div>
) : (
comments.map((c) => (
<div key={c.id} className="flex gap-3 animate-in slide-in-from-left-2 duration-300">
@@ -162,21 +162,21 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
<User className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1">
<div className="bg-white p-3 rounded-2xl rounded-tl-none border border-gray-100 shadow-sm">
<div className="bg-[var(--surface)] p-3 rounded-2xl rounded-tl-none border border-[var(--border)] shadow-sm">
<div className="flex justify-between items-start mb-1">
<p className="text-xs font-black text-gray-900">{c.userName}</p>
<p className="text-xs font-black text-[var(--text-primary)]">{c.userName}</p>
{(userRole === 'OWNER' || userRole === 'MANAGER' || c.userId === currentUserId) && (
<button
onClick={() => setConfirmState({ open: true, commentId: c.id })}
className="text-gray-400 hover:text-red-500 transition-colors"
className="text-[var(--text-muted)] hover:text-red-500 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
<p className="text-sm text-gray-600 leading-relaxed">{c.content}</p>
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">{c.content}</p>
</div>
<p className="text-[10px] text-gray-400 mt-1 ml-1 font-medium">
<p className="text-[10px] text-[var(--text-muted)] mt-1 ml-1 font-medium">
{new Date(c.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
</div>
@@ -186,7 +186,7 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
</div>
{/* Input Area */}
<div className="p-4 bg-white border-t border-gray-100">
<div className="p-4 bg-[var(--surface)] border-t border-[var(--border)]">
<div className="relative flex items-center gap-2">
<input
type="text"
@@ -194,7 +194,7 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
onChange={(e) => setNewComment(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder={isPublicView ? 'Đăng nhập để bình luận...' : 'Viết bình luận...'}
className="flex-1 bg-gray-50 border border-gray-200 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
className="flex-1 bg-[var(--background)] border border-[var(--border)] rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] transition-all"
/>
<button onClick={handleSend} disabled={!newComment.trim() || isPublicView} className="p-3 bg-blue-600 text-white rounded-2xl hover:bg-blue-700 disabled:opacity-50 disabled:bg-gray-300 transition-all active:scale-95 shadow-lg shadow-blue-100">
<Send className="w-4 h-4" />
+6 -6
View File
@@ -18,25 +18,25 @@ export const ConfirmModal: React.FC<ConfirmModalProps> = ({ isOpen, title, messa
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-200" onClick={onCancel} />
{/* Modal Content */}
<div className="relative w-full max-w-sm bg-white rounded-[32px] shadow-2xl p-8 animate-in zoom-in-95 duration-200">
<div className="relative w-full max-w-sm bg-[var(--surface)] rounded-[32px] shadow-2xl p-8 animate-in zoom-in-95 duration-200">
<div className="flex justify-between items-center mb-4">
<div className="w-12 h-12 rounded-2xl bg-red-50 flex items-center justify-center text-red-500 shadow-inner">
<AlertTriangle className="w-6 h-6" />
</div>
<button onClick={onCancel} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-5 h-5 text-gray-400" />
<button onClick={onCancel} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
<X className="w-5 h-5 text-[var(--text-muted)]" />
</button>
</div>
<h3 className="text-xl font-black text-gray-900 mb-2">{title || 'Xác nhận'}</h3>
<p className="text-sm text-gray-500 mb-8 leading-relaxed">
<h3 className="text-xl font-black text-[var(--text-primary)] mb-2">{title || 'Xác nhận'}</h3>
<p className="text-sm text-[var(--text-muted)] mb-8 leading-relaxed">
{message || 'Bạn có chắc chắn muốn thực hiện hành động này không?'}
</p>
<div className="grid grid-cols-2 gap-3">
<button
onClick={onCancel}
className="py-4 bg-gray-100 hover:bg-gray-200 text-gray-600 font-bold rounded-2xl transition-all active:scale-95"
className="py-4 bg-[var(--background)] hover:bg-[var(--background)] text-[var(--text-secondary)] font-bold rounded-2xl transition-all active:scale-95"
>
Hủy bỏ
</button>
+31 -31
View File
@@ -103,18 +103,18 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
return (
<div className="fixed inset-0 z-[2000] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div className="p-5 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h2 className="text-xl font-bold text-gray-900">Tạo Tour mới</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors"></button>
<div className="relative w-full max-w-md bg-[var(--surface)] rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div className="p-5 border-b border-[var(--border)] flex justify-between items-center bg-[var(--background)]/50">
<h2 className="text-xl font-bold text-[var(--text-primary)]">Tạo Tour mới</h2>
<button onClick={onClose} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors"></button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4 overflow-y-auto flex-1">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Tên Tour</label>
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Tên Tour</label>
<input
required
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="VD: Khám phá Đà Lạt"
@@ -122,7 +122,7 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-2 flex items-center gap-2">
<TagIcon className="w-4 h-4" /> Phân loại Tour
</label>
<div className="flex flex-wrap gap-2">
@@ -134,7 +134,7 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all border ${
selectedTags.includes(tag)
? 'bg-blue-600 text-white border-blue-600 shadow-md shadow-blue-100'
: 'bg-white text-gray-500 border-gray-200 hover:border-blue-300'
: 'bg-[var(--surface)] text-[var(--text-secondary)] border-[var(--border)] hover:border-blue-300'
}`}
>
{tag}
@@ -148,7 +148,7 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
onChange={(e) => setCustomTag(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCustomTag())}
placeholder="Thêm nhãn tùy chỉnh..."
className="flex-1 px-3 py-2 bg-gray-50 border border-gray-100 rounded-xl text-xs outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-xl text-xs outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
@@ -161,9 +161,9 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1"> tả chuyến đi</label>
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1"> tả chuyến đi</label>
<textarea
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 resize-none text-sm"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none focus:ring-2 focus:ring-blue-500 resize-none text-sm"
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
@@ -173,19 +173,19 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Bắt đu</label>
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Bắt đu</label>
<input
type="date"
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Kết thúc</label>
<label className="block text-sm font-bold text-[var(--text-secondary)] mb-1">Kết thúc</label>
<input
type="date"
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
@@ -199,18 +199,18 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-[10px] font-bold text-gray-500 uppercase mb-1">Người lớn</label>
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-[10px] font-bold text-[var(--text-muted)] uppercase mb-1">Người lớn</label>
<input type="number" className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
value={adultCount} onChange={e => setAdultCount(Number(e.target.value))} />
</div>
<div>
<label className="block text-[10px] font-bold text-gray-500 uppercase mb-1">Trẻ em</label>
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-[10px] font-bold text-[var(--text-muted)] uppercase mb-1">Trẻ em</label>
<input type="number" className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
value={childCount} onChange={e => setChildCount(Number(e.target.value))} />
</div>
<div>
<label className="block text-[10px] font-bold text-gray-500 uppercase mb-1">Giảm trẻ em %</label>
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
<label className="block text-[10px] font-bold text-[var(--text-muted)] uppercase mb-1">Giảm trẻ em %</label>
<input type="number" className="w-full px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-xl outline-none text-sm"
value={childDiscount} onChange={e => setChildDiscount(Number(e.target.value))} />
</div>
</div>
@@ -218,9 +218,9 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
</div>
<div className="space-y-2">
<label className="block text-sm font-bold text-gray-700">Thành viên tham gia ({members.length})</label>
<label className="block text-sm font-bold text-[var(--text-secondary)]">Thành viên tham gia ({members.length})</label>
{members.length > 0 && (
<div className="flex flex-wrap gap-3 mb-3 p-3 bg-gray-50 rounded-2xl border border-gray-100">
<div className="flex flex-wrap gap-3 mb-3 p-3 bg-[var(--background)] rounded-2xl border border-[var(--border)]">
{members.map((m) => {
const initial = m.name?.charAt(0) || '?';
return (
@@ -232,13 +232,13 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
<button
type="button"
onClick={() => removeMember(m.id)}
className="absolute -top-1 -right-1 w-4 h-4 bg-gray-500 hover:bg-red-600 rounded-full flex items-center justify-center text-white"
className="absolute -top-1 -right-1 w-4 h-4 bg-gray-400 hover:bg-red-600 rounded-full flex items-center justify-center text-white"
aria-label="Remove item"
>
<Trash2 size={10} />
</button>
</div>
<span className="text-[10px] font-semibold text-gray-700 max-w-[72px] truncate">{m.name}</span>
<span className="text-[10px] font-semibold text-[var(--text-secondary)] max-w-[72px] truncate">{m.name}</span>
</div>
);
})}
@@ -247,13 +247,13 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
<div className="relative">
<input
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm font-bold text-gray-800"
className="w-full px-4 py-3 bg-[var(--background)] border border-[var(--border)] rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-sm font-bold text-[var(--text-primary)]"
placeholder="Tìm email hoặc nhập tên thành viên ngoài hệ thống..."
value={query}
onChange={(e) => searchUsers(e.target.value)}
/>
{(results.length > 0 || query.trim()) && (
<div className="absolute bottom-full mb-2 left-0 right-0 bg-white border border-gray-100 rounded-2xl shadow-xl z-20 max-h-48 overflow-y-auto p-2 space-y-1">
<div className="absolute bottom-full mb-2 left-0 right-0 bg-[var(--surface)] border border-[var(--border)] rounded-2xl shadow-xl z-20 max-h-48 overflow-y-auto p-2 space-y-1">
{query.trim() && (
<button
type="button"
@@ -263,7 +263,7 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
setQuery('');
setResults([]);
}}
className="w-full text-left px-3 py-2 text-sm hover:bg-blue-50 rounded-xl text-blue-600 font-bold flex items-center gap-2"
className="w-full text-left px-3 py-2 text-sm hover:bg-[var(--background)] rounded-xl text-blue-600 font-bold flex items-center gap-2"
>
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 text-sm font-black">+</span>
<span>Thêm thành viên ngoài hệ thống: "{query.trim()}"</span>
@@ -274,10 +274,10 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
key={u.id}
type="button"
onClick={() => confirmAddMember(u)}
className="w-full text-left px-3 py-2 text-sm hover:bg-blue-50 rounded-xl flex flex-col"
className="w-full text-left px-3 py-2 text-sm hover:bg-[var(--background)] rounded-xl flex flex-col"
>
<span className="font-bold text-gray-900">{u.name || 'Chưa đặt tên'}</span>
<span className="text-xs text-gray-500">{u.email}</span>
<span className="font-bold text-[var(--text-primary)]">{u.name || 'Chưa đặt tên'}</span>
<span className="text-xs text-[var(--text-muted)]">{u.email}</span>
</button>
))}
</div>
+12 -12
View File
@@ -209,7 +209,7 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
return (
<div className="fixed inset-0 z-[3000] flex items-center justify-center p-4 bg-black/50">
<div className="w-full max-w-md bg-white rounded-[32px] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="w-full max-w-md bg-[var(--surface)] rounded-[32px] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="relative h-32 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 overflow-hidden">
<div className="absolute inset-0 opacity-10">
@@ -228,10 +228,10 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
{/* Content */}
<div className="p-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2 text-center">
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-2 text-center">
Gia nhập tour
</h2>
<p className="text-center text-gray-600 mb-6">
<p className="text-center text-[var(--text-secondary)] mb-6">
Đăng nhập đ tham gia chuyến du lịch này
</p>
@@ -248,45 +248,45 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
<div className="relative mb-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200" />
<div className="w-full border-t border-[var(--border)]" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Hoặc</span>
<span className="px-2 bg-[var(--surface)] text-[var(--text-muted)]">Hoặc</span>
</div>
</div>
{/* Email/Password Form */}
<form onSubmit={handleEmailPasswordJoin} className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<label className="block text-sm font-semibold text-[var(--text-secondary)] mb-2">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
<Mail className="absolute left-3 top-3.5 w-5 h-5 text-[var(--text-muted)]" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
className="w-full pl-10 pr-4 py-2.5 border border-[var(--border)] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition bg-[var(--background)]"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<label className="block text-sm font-semibold text-[var(--text-secondary)] mb-2">
Mật khẩu
</label>
<div className="relative">
<Lock className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
<Lock className="absolute left-3 top-3.5 w-5 h-5 text-[var(--text-muted)]" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Nhập mật khẩu"
required
className="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
className="w-full pl-10 pr-4 py-2.5 border border-[var(--border)] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition bg-[var(--background)]"
/>
</div>
</div>
@@ -311,7 +311,7 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
</form>
{/* Signup Link */}
<div className="mt-6 text-center text-sm text-gray-600">
<div className="mt-6 text-center text-sm text-[var(--text-secondary)]">
Chưa tài khoản?{' '}
<button
onClick={() => {
+14 -14
View File
@@ -172,16 +172,16 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
/>
{/* Modal Content */}
<div className="relative w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden max-h-[90vh] overflow-y-auto animate-in zoom-in-95 duration-300">
<div className="relative w-full max-w-md bg-[var(--surface)] rounded-3xl shadow-2xl overflow-hidden max-h-[90vh] overflow-y-auto animate-in zoom-in-95 duration-300">
<div className="p-8 sm:p-10">
<div className="flex justify-between items-start mb-8">
<div>
<h2 className="text-3xl font-bold text-gray-900">Đăng nhập</h2>
<p className="text-gray-500 mt-2">Chào mừng bạn quay trở lại!</p>
<h2 className="text-3xl font-bold text-[var(--text-primary)]">Đăng nhập</h2>
<p className="text-[var(--text-secondary)] mt-2">Chào mừng bạn quay trở lại!</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-full transition-colors text-gray-400 hover:text-gray-600"
className="p-2 hover:bg-[var(--background)] rounded-full transition-colors text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
>
<X className="w-6 h-6" />
</button>
@@ -195,34 +195,34 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 ml-1">Tài khoản hoặc Email</label>
<label className="text-sm font-semibold text-[var(--text-secondary)] ml-1">Tài khoản hoặc Email</label>
<div className="relative group">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
type="text"
placeholder="admin hoặc email..."
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center px-1">
<label className="text-sm font-semibold text-gray-700">Mật khẩu</label>
<label className="text-sm font-semibold text-[var(--text-secondary)]">Mật khẩu</label>
<button className="text-xs font-bold text-blue-600 hover:text-blue-700">Quên mật khẩu?</button>
</div>
<div className="relative group">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
@@ -239,15 +239,15 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
<div className="relative my-6 flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200"></div>
<div className="w-full border-t border-[var(--border)]"></div>
</div>
<span className="relative px-3 bg-white text-xs font-bold text-gray-400 uppercase">Hoặc</span>
<span className="relative px-3 bg-[var(--surface)] text-xs font-bold text-[var(--text-muted)] uppercase">Hoặc</span>
</div>
<div id="google-signin-btn-login" className="w-full flex justify-center"></div>
<div className="mt-10 pt-8 border-t border-gray-100 text-center">
<p className="text-gray-500">
<div className="mt-10 pt-8 border-t border-[var(--border)] text-center">
<p className="text-[var(--text-secondary)]">
Chưa tài khoản?{' '}
<button
onClick={() => { onClose(); onSwitchToSignup?.(); }}
@@ -39,13 +39,13 @@ export const NotificationModal: React.FC<NotificationModalProps> = ({
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-200" onClick={onConfirm} />
{/* Modal Content */}
<div className="relative w-full max-w-sm bg-white rounded-[32px] shadow-2xl overflow-hidden p-8 text-center animate-in zoom-in-95 duration-200">
<div className="relative w-full max-w-sm bg-[var(--surface)] rounded-[32px] shadow-2xl overflow-hidden p-8 text-center animate-in zoom-in-95 duration-200">
<div className="flex justify-center mb-5">
{icons[type]}
</div>
<h2 className="text-xl font-black text-gray-900 mb-2">{title}</h2>
<p className="text-gray-500 text-sm leading-relaxed mb-8">
<h2 className="text-xl font-black text-[var(--text-primary)] mb-2">{title}</h2>
<p className="text-[var(--text-muted)] text-sm leading-relaxed mb-8">
{message || "Bạn không được phép gỡ bỏ thành viên này!"}
</p>
+113 -17
View File
@@ -1,10 +1,96 @@
@import "tailwindcss";
@layer base {
/* ============ LIGHT THEME (Default) ============ */
:root {
--main-header-height: 56px;
--sub-nav-height: 48px;
--combined-top-height: 104px;
/* Light Theme Variables - Soft Parchment & Yale Blue */
--background: #f6f0ed; /* Parchment */
--background-alt: #f9fafb; /* Light Gray Alt */
--surface: #ffffff; /* Pure White for crisp card layers */
--surface-muted: #f3f4f6; /* Muted Surface */
--surface-hover: #f9fafb; /* Surface Hover */
--border: #e5e7eb; /* Light Border */
--border-light: #f3f4f6; /* Light Border Alt */
--text-primary: #28536b; /* Yale Blue (High contrast text) */
--text-secondary: #7ea8be; /* Steel Blue */
--text-accent: #c2948a; /* Rosy Taupe */
--text-muted: #9ca3af; /* Muted Text */
--text-disabled: #d1d5db; /* Disabled Text */
--primary: #28536b; /* Yale Blue for main buttons */
--primary-hover: #1f4154; /* Darker Yale Blue */
--primary-light: #eff6ff; /* Light Blue Background */
--secondary: #7ea8be; /* Steel Blue */
--secondary-light: #f0f9ff; /* Light Secondary */
--success: #10b981; /* Green */
--success-light: #ecfdf5;
--danger: #ef4444; /* Red */
--danger-light: #fef2f2;
--warning: #f59e0b; /* Amber */
--warning-light: #fffbeb;
--info: #3b82f6; /* Blue */
--info-light: #eff6ff;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
/* Smooth theme transition */
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
/* ============ DARK THEME ============ */
:root.dark {
/* Dark Theme Variables - Vintage Grape & Bondi Blue */
--background: #513b56; /* Vintage Grape */
--background-alt: #434354; /* Dark Alt */
--surface: #525174; /* Dusty Grape */
--surface-muted: #626186; /* Surface Muted */
--surface-hover: #5f6b7e; /* Surface Hover */
--border: #626186; /* Border */
--border-light: #525174; /* Border Light */
--text-primary: #f8fafc; /* Soft White */
--text-secondary: #cbd5e1; /* Secondary Text */
--text-accent: #bce784; /* Lime Cream */
--text-muted: #94a3b8; /* Muted Slate */
--text-disabled: #64748b; /* Disabled Text */
--primary: #348aa7; /* Bondi Blue */
--primary-hover: #296f86; /* Darker Bondi Blue */
--primary-light: #0c2340; /* Dark Blue Background */
--secondary: #5dd39e; /* Emerald */
--secondary-light: #064e3b; /* Dark Secondary */
--success: #10b981; /* Green */
--success-light: #064e3b;
--danger: #ef4444; /* Red */
--danger-light: #3f0f0f;
--warning: #f59e0b; /* Amber */
--warning-light: #3f2009;
--info: #3b82f6; /* Blue */
--info-light: #0c2340;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
/* Smooth theme transition */
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
html, body, #root, .app-container {
@@ -33,7 +119,8 @@
top: 0;
height: var(--main-header-height);
z-index: 50;
background-color: #ffffff;
background-color: var(--surface);
color: var(--text-primary);
}
.sub-nav-menu {
@@ -45,8 +132,9 @@
margin: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
border-bottom: 1px solid #e5e7eb;
background-color: #ffffff;
border-bottom: 1px solid var(--border);
background-color: var(--surface);
color: var(--text-primary);
}
/* Master viewport wrapper - contains entire itinerary */
@@ -71,7 +159,8 @@
position: sticky !important;
top: 0;
z-index: 100 !important;
background-color: #ffffff;
background-color: var(--surface);
color: var(--text-primary);
flex-shrink: 0 !important;
}
@@ -79,8 +168,9 @@
.fixed-top-header-block {
flex-shrink: 0 !important;
width: 100% !important;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
background-color: var(--surface);
color: var(--text-primary);
border-bottom: 1px solid var(--border);
}
/* Timeline scroll container - main scrollable region */
@@ -92,7 +182,8 @@
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
background-color: #f8fafc;
background-color: var(--background);
color: var(--text-primary);
}
.timeline-scroll-container {
@@ -111,7 +202,8 @@
width: 100% !important;
padding: 0 !important;
margin-top: 0px !important;
background-color: #f8fafc;
background-color: var(--background);
color: var(--text-primary);
}
.timeline-scroll-viewport {
@@ -134,17 +226,19 @@
/* Individual folder node wrapper - base stage container */
.folder-node-wrapper {
width: 100% !important;
background-color: #ffffff;
background-color: var(--surface);
color: var(--text-primary);
margin-bottom: 0px !important;
display: flex;
flex-direction: column;
border-bottom: 1px solid #f3f4f6;
border-bottom: 1px solid var(--border);
}
/* Individual stage card block - backwards compatibility */
.stage-card-block {
width: 100% !important;
background-color: #ffffff;
background-color: var(--surface);
color: var(--text-primary);
margin-bottom: 1px !important;
display: flex;
flex-direction: column;
@@ -168,14 +262,15 @@
padding: 12px 16px !important;
display: flex;
align-items: center;
background-color: #ffffff !important;
background-color: var(--surface) !important;
color: var(--text-primary);
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
border-bottom: 1px solid var(--border);
transition: background-color 0.2s;
}
.folder-header-row:hover {
background-color: #f9fafb !important;
background-color: var(--surface-muted) !important;
}
/* Stage header row - sticky positioning for pinned effect */
@@ -187,14 +282,15 @@
padding: 12px 16px !important;
display: flex;
align-items: center;
background-color: #ffffff !important;
background-color: var(--surface) !important;
color: var(--text-primary);
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
border-bottom: 1px solid var(--border);
transition: background-color 0.2s;
}
.stage-header-row:hover {
background-color: #f9fafb !important;
background-color: var(--surface-muted) !important;
}
/* Alternate sticky header styling (kept for backwards compatibility) */
+63 -63
View File
@@ -655,35 +655,35 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
<div className="flex items-center gap-3 pointer-events-auto">
<button
onClick={onBack}
className="w-11 h-11 flex items-center justify-center bg-white rounded-full shadow-xl hover:bg-gray-50 transition-all border border-gray-100 shrink-0"
className="w-11 h-11 flex items-center justify-center bg-[var(--surface)] rounded-full shadow-xl hover:bg-[var(--background)] transition-all border border-[var(--border)] shrink-0"
title="Quay lại"
>
<ChevronLeft className="w-6 h-6 text-gray-800" />
<ChevronLeft className="w-6 h-6 text-[var(--text-primary)]" />
</button>
{/* Nút lọc Tag và Dropdown */}
<div className="relative">
<button
onClick={() => setIsFilterDropdownOpen(prev => !prev)}
className="w-11 h-11 bg-white rounded-full shadow-xl hover:bg-gray-50 transition-all border border-gray-100 flex items-center justify-center shrink-0"
className="w-11 h-11 bg-[var(--surface)] rounded-full shadow-xl hover:bg-[var(--background)] transition-all border border-[var(--border)] flex items-center justify-center shrink-0"
title="Lọc theo loại"
>
<Filter className="w-6 h-6 text-gray-800" />
<Filter className="w-6 h-6 text-[var(--text-primary)]" />
</button>
{/* Filter Dropdown Content */}
{isFilterDropdownOpen && (
<div className="absolute top-full left-0 mt-3 bg-white/90 backdrop-blur-md p-3 rounded-2xl shadow-xl border border-white/20 flex flex-col gap-3 max-w-[220px] z-[1003] animate-in slide-in-from-left-2 duration-200 max-h-[400px] overflow-y-auto">
<div className="absolute top-full left-0 mt-3 bg-[var(--surface)]/90 backdrop-blur-md p-3 rounded-2xl shadow-xl border border-[var(--surface)]/20 flex flex-col gap-3 max-w-[220px] z-[1003] animate-in slide-in-from-left-2 duration-200 max-h-[400px] overflow-y-auto">
{/* Tour Filter Section */}
<div>
<div className="flex items-center gap-2 px-2 py-1 border-b border-gray-100 mb-1.5">
<div className="flex items-center gap-2 px-2 py-1 border-b border-[var(--border)] mb-1.5">
<Filter className="w-3.5 h-3.5 text-blue-600" />
<span className="text-[10px] font-black uppercase text-gray-500 tracking-wider">🧳 Chuyến đi</span>
<span className="text-[10px] font-black uppercase text-[var(--text-muted)] tracking-wider">🧳 Chuyến đi</span>
</div>
<div className="flex flex-col gap-1.5">
<button
onClick={() => { setSelectedFilterTag(null); setIsFilterDropdownOpen(false); }}
className={`px-2.5 py-1 rounded-lg text-[10px] font-bold transition-all ${!selectedFilterTag ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
className={`px-2.5 py-1 rounded-lg text-[10px] font-bold transition-all ${!selectedFilterTag ? 'bg-blue-600 text-white' : 'bg-[var(--background)] text-[var(--text-muted)] hover:bg-[var(--border)]'}`}
>
Tất cả
</button>
@@ -691,7 +691,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
<button
key={tag}
onClick={() => { setSelectedFilterTag(tag === selectedFilterTag ? null : tag); setIsFilterDropdownOpen(false); }}
className={`px-2.5 py-1 rounded-lg text-[10px] font-bold transition-all ${selectedFilterTag === tag ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
className={`px-2.5 py-1 rounded-lg text-[10px] font-bold transition-all ${selectedFilterTag === tag ? 'bg-blue-600 text-white' : 'bg-[var(--background)] text-[var(--text-muted)] hover:bg-[var(--border)]'}`}
>
{tag}
</button>
@@ -701,15 +701,15 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{/* Photo Filter Section */}
{availablePhotoTags.length > 0 && (
<div className="border-t border-gray-200 pt-3">
<div className="flex items-center gap-2 px-2 py-1 border-b border-gray-100 mb-1.5">
<div className="border-t border-[var(--border)] pt-3">
<div className="flex items-center gap-2 px-2 py-1 border-b border-[var(--border)] mb-1.5">
<ImageIcon className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-[10px] font-black uppercase text-gray-500 tracking-wider">📸 nh công khai</span>
<span className="text-[10px] font-black uppercase text-[var(--text-muted)] tracking-wider">📸 nh công khai</span>
</div>
<div className="flex flex-wrap gap-1.5">
<button
onClick={() => { setSelectedPhotoFilterTags([]); setIsFilterDropdownOpen(false); }}
className={`px-2 py-1 rounded-lg text-[9px] font-bold transition-all ${selectedPhotoFilterTags.length === 0 ? 'bg-emerald-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
className={`px-2 py-1 rounded-lg text-[9px] font-bold transition-all ${selectedPhotoFilterTags.length === 0 ? 'bg-emerald-600 text-white' : 'bg-[var(--background)] text-[var(--text-muted)] hover:bg-[var(--border)]'}`}
>
Tất cả
</button>
@@ -725,7 +725,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
: [...prev, tagId]
);
}}
className={`px-2 py-1 rounded-lg text-[9px] font-bold transition-all ${selectedPhotoFilterTags.includes(tagId) ? 'bg-emerald-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
className={`px-2 py-1 rounded-lg text-[9px] font-bold transition-all ${selectedPhotoFilterTags.includes(tagId) ? 'bg-emerald-600 text-white' : 'bg-[var(--background)] text-[var(--text-muted)] hover:bg-[var(--border)]'}`}
title={tagLabel}
>
{tagLabel.length > 13 ? tagLabel.substring(0, 13) + '...' : tagLabel}
@@ -740,37 +740,37 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
</div>
{/* Search Box - Thay thế div "Khám phá khu vực" */}
<div className="relative flex items-center bg-white/90 backdrop-blur-md rounded-2xl shadow-xl border border-white/20 flex-1 max-w-xs sm:max-w-md pointer-events-auto hidden sm:flex px-4 py-3">
<div className="relative flex items-center bg-[var(--surface)]/90 backdrop-blur-md rounded-2xl shadow-xl border border-[var(--surface)]/20 flex-1 max-w-xs sm:max-w-md pointer-events-auto hidden sm:flex px-4 py-3">
<Navigation className="w-4 h-4 text-blue-600 mr-2 flex-shrink-0" />
<input
type="text"
placeholder="Tìm kiếm địa điểm, tour..."
className="flex-1 bg-transparent outline-none text-gray-800 text-sm font-medium"
className="flex-1 bg-transparent outline-none text-[var(--text-primary)] text-sm font-medium"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{isSearchingSuggestions && <Loader2 className="w-4 h-4 animate-spin text-blue-500 mr-2" />}
{searchQuery && (
<button onClick={() => { setSearchQuery(''); setSuggestions([]); }} className="p-1 text-gray-400 hover:text-gray-600 rounded-full">
<button onClick={() => { setSearchQuery(''); setSuggestions([]); }} className="p-1 text-[var(--text-muted)] hover:text-[var(--text-secondary)] rounded-full">
<X className="w-4 h-4" />
</button>
)}
{/* Dropdown danh sách gợi ý */}
{suggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-3 bg-white/95 backdrop-blur-md rounded-2xl shadow-2xl border border-white/20 overflow-hidden z-[1003] animate-in slide-in-from-top-2 duration-200">
<div className="absolute top-full left-0 right-0 mt-3 bg-[var(--surface)]/95 backdrop-blur-md rounded-2xl shadow-2xl border border-[var(--surface)]/20 overflow-hidden z-[1003] animate-in slide-in-from-top-2 duration-200">
{suggestions.map((s, idx) => (
<button
key={`${s.type}-${s.id}-${idx}`}
onClick={() => handleSelectSuggestion(s)}
className="w-full text-left px-4 py-3 hover:bg-blue-50 flex items-center gap-3 transition-colors border-b border-gray-50 last:border-0"
className="w-full text-left px-4 py-3 hover:bg-[var(--background)] flex items-center gap-3 transition-colors border-b border-[var(--border)] last:border-0"
>
<div className={`p-2 rounded-xl flex-shrink-0 ${s.type === 'tour' ? 'bg-blue-50 text-blue-600' : 'bg-green-50 text-green-600'}`}>
{s.type === 'tour' ? <ImageIcon className="w-4 h-4" /> : <MapPin className="w-4 h-4" />}
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-bold text-gray-800 truncate">{s.name}</span>
<span className="text-[10px] font-black uppercase text-gray-400 tracking-wider">
<span className="text-sm font-bold text-[var(--text-primary)] truncate">{s.name}</span>
<span className="text-[10px] font-black uppercase text-[var(--text-muted)] tracking-wider">
{s.type === 'tour' ? 'Chuyến đi của bạn' : 'Địa điểm trên bản đồ'}
</span>
</div>
@@ -790,7 +790,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
console.log("Đang mở Ảnh của tôi...");
onOpenMyPhotos();
}}
className="w-11 h-11 md:w-auto bg-white p-0 md:px-4 md:py-3 rounded-2xl shadow-xl hover:bg-blue-50 text-blue-600 transition-all flex items-center justify-center md:justify-start gap-2 font-bold border border-blue-100 shrink-0"
className="w-11 h-11 md:w-auto bg-[var(--surface)] p-0 md:px-4 md:py-3 rounded-2xl shadow-xl hover:bg-blue-100 text-blue-600 transition-all flex items-center justify-center md:justify-start gap-2 font-bold border border-blue-100 shrink-0"
title="Ảnh của tôi"
>
<ImageIcon className="w-5 h-5" />
@@ -822,31 +822,31 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{/* Lựa chọn Ngôn ngữ */}
<div className="relative group shrink-0">
<button className="w-11 h-11 bg-white dark:bg-slate-800 p-0 rounded-2xl shadow-xl hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-all flex items-center justify-center border border-gray-100 dark:border-slate-700">
<button className="w-11 h-11 bg-[var(--surface)] dark:bg-slate-800 p-0 rounded-2xl shadow-xl hover:bg-[var(--background)] dark:hover:bg-slate-700 text-[var(--text-secondary)] dark:text-slate-200 transition-all flex items-center justify-center border border-[var(--border)] dark:border-slate-700">
<Globe className="w-5 h-5" />
</button>
<div className="absolute right-0 top-12 mt-1 hidden group-hover:block bg-white dark:bg-slate-800 border border-gray-100 dark:border-slate-700 rounded-2xl shadow-2xl p-2 z-[9999] w-32 animate-in fade-in duration-200">
<button onClick={() => changeLanguage('vi')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold ${lang === 'vi' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700'}`}>Tiếng Việt</button>
<button onClick={() => changeLanguage('en')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold ${lang === 'en' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700'}`}>English</button>
<button onClick={() => changeLanguage('zh')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold ${lang === 'zh' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700'}`}></button>
<div className="absolute right-0 top-12 mt-1 hidden group-hover:block bg-[var(--surface)] dark:bg-slate-800 border border-[var(--border)] dark:border-slate-700 rounded-2xl shadow-2xl p-2 z-[9999] w-32 animate-in fade-in duration-200">
<button onClick={() => changeLanguage('vi')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold ${lang === 'vi' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-700'}`}>Tiếng Việt</button>
<button onClick={() => changeLanguage('en')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold ${lang === 'en' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-700'}`}>English</button>
<button onClick={() => changeLanguage('zh')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold ${lang === 'zh' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-700'}`}></button>
</div>
</div>
{/* Lựa chọn Giao diện */}
<div className="relative group shrink-0">
<button className="w-11 h-11 bg-white dark:bg-slate-800 p-0 rounded-2xl shadow-xl hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-all flex items-center justify-center border border-gray-100 dark:border-slate-700">
<button className="w-11 h-11 bg-[var(--surface)] dark:bg-slate-800 p-0 rounded-2xl shadow-xl hover:bg-[var(--background)] dark:hover:bg-slate-700 text-[var(--text-secondary)] dark:text-slate-200 transition-all flex items-center justify-center border border-[var(--border)] dark:border-slate-700">
{theme === 'light' && <Sun className="w-5 h-5 text-amber-500" />}
{theme === 'dark' && <Moon className="w-5 h-5 text-indigo-400" />}
{theme === 'system' && <Laptop className="w-5 h-5 animate-pulse" />}
</button>
<div className="absolute right-0 top-12 mt-1 hidden group-hover:block bg-white dark:bg-slate-800 border border-gray-100 dark:border-slate-700 rounded-2xl shadow-2xl p-2 z-[9999] w-32 animate-in fade-in duration-200">
<button onClick={() => changeTheme('light')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 ${theme === 'light' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700'}`}>
<div className="absolute right-0 top-12 mt-1 hidden group-hover:block bg-[var(--surface)] dark:bg-slate-800 border border-[var(--border)] dark:border-slate-700 rounded-2xl shadow-2xl p-2 z-[9999] w-32 animate-in fade-in duration-200">
<button onClick={() => changeTheme('light')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 ${theme === 'light' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-700'}`}>
<Sun className="w-3.5 h-3.5 text-amber-500" /> {t('themeLight')}
</button>
<button onClick={() => changeTheme('dark')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 ${theme === 'dark' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700'}`}>
<button onClick={() => changeTheme('dark')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 ${theme === 'dark' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-700'}`}>
<Moon className="w-3.5 h-3.5 text-indigo-400" /> {t('themeDark')}
</button>
<button onClick={() => changeTheme('system')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 ${theme === 'system' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700'}`}>
<button onClick={() => changeTheme('system')} className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-2 ${theme === 'system' ? 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400' : 'text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-700'}`}>
<Laptop className="w-3.5 h-3.5" /> {t('themeSystem')}
</button>
</div>
@@ -880,7 +880,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{onLogout && (
<button
onClick={onLogout}
className="w-11 h-11 md:w-auto bg-white p-0 md:px-4 md:py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center justify-center md:justify-start gap-2 font-bold text-gray-700 border border-gray-100 shrink-0"
className="w-11 h-11 md:w-auto bg-[var(--surface)] p-0 md:px-4 md:py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center justify-center md:justify-start gap-2 font-bold text-[var(--text-secondary)] border border-[var(--border)] shrink-0"
title="Đăng xuất"
>
<LogOut className="w-5 h-5" />
@@ -996,7 +996,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
className: 'custom-bubble',
html: `
<div class="relative group w-14 h-14">
<div class="w-14 h-14 rounded-full border-4 ${status.borderClass} shadow-lg overflow-hidden transition-transform group-hover:scale-110 flex items-center justify-center bg-gray-100">
<div class="w-14 h-14 rounded-full border-4 ${status.borderClass} shadow-lg overflow-hidden transition-transform group-hover:scale-110 flex items-center justify-center bg-[var(--background)]">
<img src="${tourImage}" class="w-full h-full object-cover" onerror="this.src='https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=100'"/>
</div>
<div class="absolute -bottom-1 -right-1 bg-blue-600 rounded-full w-5 h-5 border-2 border-white flex items-center justify-center text-[10px] font-black text-white">
@@ -1019,16 +1019,16 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
</div>
)}
{tour.description && (
<div className="text-[10px] text-gray-500 line-clamp-2 leading-tight italic mb-1.5">
<div className="text-[10px] text-[var(--text-muted)] line-clamp-2 leading-tight italic mb-1.5">
{tour.description}
</div>
)}
<div className="flex items-center gap-1.5 pt-1 border-t border-gray-100">
<div className="flex items-center gap-1.5 pt-1 border-t border-[var(--border)]">
<span className={`w-2 h-2 rounded-full ${
status.color === 'green' ? 'bg-emerald-500' :
status.color === 'red' ? 'bg-rose-500' : 'bg-gray-400'
status.color === 'red' ? 'bg-rose-500' : 'bg-[var(--text-muted)]'
}`} />
<span className="text-[9px] font-bold text-gray-600">{status.label}</span>
<span className="text-[9px] font-bold text-[var(--text-secondary)]">{status.label}</span>
</div>
</div>
</Tooltip>
@@ -1146,13 +1146,13 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{item.type === 'RESTAURANT' ? t('businessRestaurant') : item.type === 'HOTEL' ? t('businessHotel') : t('businessHomestay')}
</div>
{item.address && (
<div className="text-[10px] text-gray-500 mb-1 font-semibold">{item.address}</div>
<div className="text-[10px] text-[var(--text-muted)] mb-1 font-semibold">{item.address}</div>
)}
{item.phone && (
<div className="text-[9px] text-gray-400">SĐT: {item.phone}</div>
<div className="text-[9px] text-[var(--text-muted)]">SĐT: {item.phone}</div>
)}
{item.email && (
<div className="text-[9px] text-gray-400">Email: {item.email}</div>
<div className="text-[9px] text-[var(--text-muted)]">Email: {item.email}</div>
)}
<div className="text-[10px] text-slate-600 font-medium italic mt-1 pt-1 border-t border-emerald-100 whitespace-pre-line">
{item.description}
@@ -1167,7 +1167,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{/* Context Menu Chia sẻ */}
{shareMenu && (
<div
className="absolute z-[2000] bg-white rounded-2xl shadow-2xl border border-gray-100 py-2 w-48 animate-in zoom-in-95 duration-200"
className="absolute z-[2000] bg-[var(--surface)] rounded-2xl shadow-2xl border border-[var(--border)] py-2 w-48 animate-in zoom-in-95 duration-200"
style={{ top: shareMenu.y, left: shareMenu.x }}
onClick={(e) => e.stopPropagation()}
>
@@ -1175,19 +1175,19 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
shareMenu.canShare ? (
<button
onClick={() => handleShare(shareMenu.id, shareMenu.title)}
className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-gray-700 flex items-center gap-2 transition-colors"
className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-blue-700 flex items-center gap-2 transition-colors"
>
<Share2 className="w-4 h-4 text-blue-600" /> Sao chép liên kết
</button>
) : (
<div className="px-4 py-2 text-xs text-gray-400 italic font-bold">Bạn đã gia nhập tour này</div>
<div className="px-4 py-2 text-xs text-[var(--text-muted)] italic font-bold">Bạn đã gia nhập tour này</div>
)
) : shareMenu.hasPendingRequest ? (
<button
disabled
className="w-full text-left px-4 py-2 text-sm font-bold text-gray-400 flex items-center gap-2 cursor-not-allowed bg-gray-50/50"
className="w-full text-left px-4 py-2 text-sm font-bold text-[var(--text-muted)] flex items-center gap-2 cursor-not-allowed bg-[var(--background)]/50"
>
<Clock className="w-4 h-4 text-gray-400" /> Đang chờ duyệt...
<Clock className="w-4 h-4 text-[var(--text-muted)]" /> Đang chờ duyệt...
</button>
) : (
<button
@@ -1268,33 +1268,33 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
<div className="absolute bottom-6 left-6 z-[1002] pointer-events-auto flex flex-col items-start gap-2">
<button
onClick={() => setIsLeaderboardOpen(prev => !prev)}
className="bg-white dark:bg-slate-900 border border-gray-150 dark:border-slate-800 text-slate-800 dark:text-slate-100 shadow-2xl rounded-2xl py-2.5 px-4 text-xs font-bold flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-slate-850 transition-all cursor-pointer active:scale-95"
className="bg-[var(--surface)] dark:bg-slate-900 border border-[var(--border)] dark:border-slate-800 text-[var(--text-primary)] dark:text-slate-100 shadow-2xl rounded-2xl py-2.5 px-4 text-xs font-bold flex items-center gap-2 hover:bg-[var(--background)] dark:hover:bg-slate-850 transition-all cursor-pointer active:scale-95"
>
<Users className="w-4 h-4 text-amber-500" />
<span>{t('trustedMembers')} ({trustedUsers.length})</span>
</button>
{isLeaderboardOpen && (
<div className="bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-gray-100 dark:border-slate-800 rounded-3xl shadow-2xl p-4 w-72 max-h-[350px] overflow-y-auto no-scrollbar animate-in slide-in-from-bottom-2 duration-300">
<div className="bg-[var(--surface)]/95 dark:bg-slate-900/95 backdrop-blur-md border border-[var(--border)] dark:border-slate-800 rounded-3xl shadow-2xl p-4 w-72 max-h-[350px] overflow-y-auto no-scrollbar animate-in slide-in-from-bottom-2 duration-300">
<h4 className="text-xs font-black uppercase text-amber-600 dark:text-amber-500 tracking-widest mb-3 flex items-center gap-2">
🏆 {t('trustedMembers')}
</h4>
{trustedUsers.length === 0 ? (
<p className="text-xs text-gray-400 italic">Chưa thành viên nào đưc đánh giá.</p>
<p className="text-xs text-[var(--text-muted)] italic">Chưa thành viên nào đưc đánh giá.</p>
) : (
<div className="space-y-2.5">
{trustedUsers.map((u, idx) => (
<div key={u.id} className="flex items-center justify-between gap-3 p-2 bg-gray-50/50 dark:bg-slate-850/50 rounded-xl border border-gray-100/50 dark:border-slate-800/50">
<div key={u.id} className="flex items-center justify-between gap-3 p-2 bg-[var(--background)]/50 dark:bg-slate-850/50 rounded-xl border border-[var(--border)]/50 dark:border-slate-800/50">
<div className="flex items-center gap-2 min-w-0">
<div className="w-5 h-5 font-bold text-[10px] text-gray-500 flex items-center justify-center bg-gray-100 dark:bg-slate-800 rounded-lg">
<div className="w-5 h-5 font-bold text-[10px] text-[var(--text-muted)] flex items-center justify-center bg-[var(--background)] dark:bg-slate-800 rounded-lg">
{idx + 1}
</div>
<span className="text-xs font-bold text-gray-800 dark:text-slate-200 truncate">{u.name}</span>
<span className="text-xs font-bold text-[var(--text-primary)] dark:text-slate-200 truncate">{u.name}</span>
</div>
<div className="flex items-center gap-1 text-[11px] font-bold text-amber-500 shrink-0">
<span></span>
<span>{u.averageScore}</span>
<span className="text-[9px] text-gray-400 font-medium">({u.ratingCount})</span>
<span className="text-[9px] text-[var(--text-muted)] font-medium">({u.ratingCount})</span>
</div>
</div>
))}
@@ -1312,7 +1312,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
setIsRecommendedOpen(prev => !prev);
setIsBlacklistOpen(false);
}}
className="bg-white dark:bg-slate-900 border border-gray-150 dark:border-slate-800 text-slate-800 dark:text-slate-100 shadow-2xl rounded-2xl py-2.5 px-4 text-xs font-bold flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-slate-850 transition-all cursor-pointer active:scale-95 border-emerald-500/20"
className="bg-[var(--surface)] dark:bg-slate-900 border border-[var(--border)] dark:border-slate-800 text-[var(--text-primary)] dark:text-slate-100 shadow-2xl rounded-2xl py-2.5 px-4 text-xs font-bold flex items-center gap-2 hover:bg-[var(--background)] dark:hover:bg-slate-850 transition-all cursor-pointer active:scale-95 border-emerald-500/20"
>
<Star className="w-4 h-4 text-emerald-500 fill-emerald-500 animate-pulse" />
<span>{t('recommendedTitle') || 'Đề xuất dịch vụ'} ({recommendedLocations.length})</span>
@@ -1324,7 +1324,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
setIsBlacklistOpen(prev => !prev);
setIsRecommendedOpen(false);
}}
className="bg-white dark:bg-slate-900 border border-gray-150 dark:border-slate-800 text-slate-800 dark:text-slate-100 shadow-2xl rounded-2xl py-2.5 px-4 text-xs font-bold flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-slate-850 transition-all cursor-pointer active:scale-95 border-red-500/20"
className="bg-[var(--surface)] dark:bg-slate-900 border border-[var(--border)] dark:border-slate-800 text-[var(--text-primary)] dark:text-slate-100 shadow-2xl rounded-2xl py-2.5 px-4 text-xs font-bold flex items-center gap-2 hover:bg-[var(--background)] dark:hover:bg-slate-850 transition-all cursor-pointer active:scale-95 border-red-500/20"
>
<ShieldAlert className="w-4 h-4 text-red-500 animate-pulse" />
<span>{t('blacklistTitle') || 'Danh sách đen'} ({blacklist.length})</span>
@@ -1332,8 +1332,8 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{/* Recommendations Panel */}
{isRecommendedOpen && (
<div className="bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-gray-100 dark:border-slate-800 rounded-3xl shadow-2xl p-4 w-72 max-h-[350px] overflow-y-auto no-scrollbar animate-in slide-in-from-bottom-2 duration-300">
<h4 className="text-xs font-black uppercase text-emerald-600 dark:text-emerald-500 tracking-widest mb-3 flex items-center gap-2 border-b border-gray-100 dark:border-slate-800 pb-2">
<div className="bg-[var(--surface)]/95 dark:bg-slate-900/95 backdrop-blur-md border border-[var(--border)] dark:border-slate-800 rounded-3xl shadow-2xl p-4 w-72 max-h-[350px] overflow-y-auto no-scrollbar animate-in slide-in-from-bottom-2 duration-300">
<h4 className="text-xs font-black uppercase text-emerald-600 dark:text-emerald-500 tracking-widest mb-3 flex items-center gap-2 border-b border-[var(--border)] dark:border-slate-800 pb-2">
🌟 {t('recommendedTitle') || 'Đề xuất chất lượng'}
</h4>
@@ -1363,7 +1363,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
</div>
{processedRecommendations.length === 0 ? (
<p className="text-xs text-gray-400 italic text-center py-4">Chưa đa điểm đ xuất nào.</p>
<p className="text-xs text-[var(--text-muted)] italic text-center py-4">Chưa đa điểm đ xuất nào.</p>
) : (
<div className="space-y-2.5">
{processedRecommendations.map((item) => {
@@ -1411,7 +1411,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
</button>
)}
<div className="text-[10px] text-slate-600 dark:text-slate-400 font-medium italic mt-1.5 pt-1.5 border-t border-gray-100 dark:border-slate-800 whitespace-pre-line">
<div className="text-[10px] text-[var(--text-secondary)] dark:text-slate-400 font-medium italic mt-1.5 pt-1.5 border-t border-[var(--border)] dark:border-slate-800 whitespace-pre-line">
{item.description}
</div>
</div>
@@ -1424,8 +1424,8 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
{/* Blacklist Panel */}
{isBlacklistOpen && (
<div className="bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-gray-100 dark:border-slate-800 rounded-3xl shadow-2xl p-4 w-72 max-h-[350px] overflow-y-auto no-scrollbar animate-in slide-in-from-bottom-2 duration-300">
<h4 className="text-xs font-black uppercase text-red-600 dark:text-red-500 tracking-widest mb-3 flex items-center gap-2 border-b border-gray-100 dark:border-slate-800 pb-2">
<div className="bg-[var(--surface)]/95 dark:bg-slate-900/95 backdrop-blur-md border border-[var(--border)] dark:border-slate-800 rounded-3xl shadow-2xl p-4 w-72 max-h-[350px] overflow-y-auto no-scrollbar animate-in slide-in-from-bottom-2 duration-300">
<h4 className="text-xs font-black uppercase text-red-600 dark:text-red-500 tracking-widest mb-3 flex items-center gap-2 border-b border-[var(--border)] dark:border-slate-800 pb-2">
{t('blacklistTitle')}
</h4>
@@ -1448,7 +1448,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
</div>
{processedBlacklist.length === 0 ? (
<p className="text-xs text-gray-400 italic text-center py-4">{t('emptyBlacklist')}</p>
<p className="text-xs text-[var(--text-muted)] italic text-center py-4">{t('emptyBlacklist')}</p>
) : (
<div className="space-y-2.5">
{processedBlacklist.map((item) => {
@@ -1488,7 +1488,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
📍 Đnh vị trên bản đ
</button>
)}
<div className="text-[10px] text-red-600 dark:text-red-400 font-medium italic mt-1.5 pt-1.5 border-t border-gray-100 dark:border-slate-850">
<div className="text-[10px] text-red-600 dark:text-red-400 font-medium italic mt-1.5 pt-1.5 border-t border-[var(--border)] dark:border-slate-850">
do: {item.reason}
</div>
</div>
+11 -11
View File
@@ -101,8 +101,8 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 font-sans">
<div className="max-w-md w-full space-y-8 bg-white p-10 rounded-3xl shadow-2xl text-center border border-gray-100">
<div className="min-h-screen flex flex-col items-center justify-center bg-[var(--background)] py-12 px-4 sm:px-6 lg:px-8 font-sans">
<div className="max-w-md w-full space-y-8 bg-[var(--surface)] p-10 rounded-3xl shadow-2xl text-center border border-[var(--border)]">
{/* Logo/Icon */}
<div className="flex justify-center">
@@ -114,16 +114,16 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
{loading ? (
<div className="space-y-4 py-6">
<Loader2 className="w-12 h-12 animate-spin text-blue-600 mx-auto" />
<h2 className="text-xl font-bold text-gray-900">Đang xử tham gia hành trình...</h2>
<p className="text-sm text-gray-500">Vui lòng đi trong giây lát.</p>
<h2 className="text-xl font-bold text-[var(--text-primary)]">Đang xử tham gia hành trình...</h2>
<p className="text-sm text-[var(--text-muted)]">Vui lòng đi trong giây lát.</p>
</div>
) : error ? (
<div className="space-y-4 py-4">
<div className="w-12 h-12 bg-red-50 text-red-500 rounded-full flex items-center justify-center mx-auto">
<AlertCircle className="w-6 h-6" />
</div>
<h2 className="text-xl font-bold text-gray-900">Gia nhập thất bại</h2>
<p className="text-sm text-red-600 bg-red-50 p-4 rounded-2xl font-semibold border border-red-100">{error}</p>
<h2 className="text-xl font-bold text-[var(--text-primary)]">Gia nhập thất bại</h2>
<p className="text-sm text-[var(--text-muted)] bg-red-50 p-4 rounded-2xl font-semibold border border-red-100">{error}</p>
<div className="pt-4 flex flex-col gap-2">
{isLoggedIn ? (
<button
@@ -142,7 +142,7 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
</button>
<button
onClick={onGoToHome}
className="w-full bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold py-3.5 rounded-2xl transition-all"
className="w-full bg-[var(--background)] hover:bg-[var(--background)]/80 text-[var(--text-primary)] font-bold py-3.5 rounded-2xl transition-all border border-[var(--border)]"
>
Về trang chủ
</button>
@@ -155,7 +155,7 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
<div className="w-12 h-12 bg-green-50 text-green-500 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-6 h-6" />
</div>
<h2 className="text-xl font-bold text-gray-900">Thành công!</h2>
<h2 className="text-xl font-bold text-[var(--text-primary)]">Thành công!</h2>
<p className="text-sm text-green-700 bg-green-50 p-4 rounded-2xl font-semibold border border-green-100">{successMsg}</p>
<div className="pt-4">
<button
@@ -171,8 +171,8 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
</div>
) : (
<div className="space-y-6">
<h2 className="text-2xl font-black text-gray-900 tracking-tight">Chào mừng bạn!</h2>
<p className="text-gray-600 text-sm leading-relaxed">
<h2 className="text-2xl font-black text-[var(--text-primary)] tracking-tight">Chào mừng bạn!</h2>
<p className="text-[var(--text-secondary)] text-sm leading-relaxed">
Bạn nhận đưc một lời mời tham gia hành trình du lịch. Vui lòng đăng nhập hoặc tạo tài khoản đ thể join xem các hoạt đng, chi phí của tour.
</p>
@@ -185,7 +185,7 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
</button>
<button
onClick={onGoToSignup}
className="w-full flex items-center justify-center gap-2 bg-gray-50 hover:bg-gray-100 text-gray-700 font-bold py-4 rounded-2xl border border-gray-200 transition-all active:scale-[0.98]"
className="w-full flex items-center justify-center gap-2 bg-[var(--background)] hover:bg-[var(--border)] text-[var(--text-primary)] font-bold py-4 rounded-2xl border border-[var(--border)] transition-all active:scale-[0.98]"
>
<UserPlus className="w-5 h-5" /> Đăng tài khoản mới
</button>
+1 -1
View File
@@ -315,7 +315,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
return (
<div className="h-dvh w-full overflow-hidden font-sans bg-gray-900 relative">
<div className="h-dvh w-full overflow-hidden font-sans bg-[var(--background)] relative">
{/* Background Image with Horizontal Panning */}
<div className="absolute inset-0 z-0">
{/* Active image for panning */}
+19 -19
View File
@@ -339,15 +339,15 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
});
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
<div className="min-h-screen bg-[var(--background)] flex flex-col">
{/* Header */}
<div className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-gray-100 px-4 py-4 flex items-center gap-4">
<button onClick={onBack} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<ChevronLeft className="w-6 h-6 text-gray-600" />
<div className="sticky top-0 z-30 bg-[var(--surface)]/80 backdrop-blur-md border-b border-[var(--border)] px-4 py-4 flex items-center gap-4">
<button onClick={onBack} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
<ChevronLeft className="w-6 h-6 text-[var(--text-secondary)]" />
</button>
<div>
<h1 className="text-xl font-black text-gray-900">Ghi chú của tôi</h1>
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Sổ tay hành trình nhân</p>
<h1 className="text-xl font-black text-[var(--text-primary)]">Ghi chú của tôi</h1>
<p className="text-[10px] font-bold text-[var(--text-muted)] uppercase tracking-widest">Sổ tay hành trình nhân</p>
</div>
</div>
@@ -358,11 +358,11 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
<input
type="text"
placeholder="Tìm kiếm nội dung ghi chú..."
className="w-full pl-10 pr-4 py-3 bg-white rounded-2xl border border-gray-100 shadow-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all text-sm"
className="w-full pl-10 pr-4 py-3 bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all text-sm"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
</div>
<button
onClick={() => setIsCreating(true)}
@@ -374,7 +374,7 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
)}
{isLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-gray-400">
<div className="flex flex-col items-center justify-center py-20 text-[var(--text-muted)]">
<Loader2 className="w-10 h-10 animate-spin mb-4" />
<p className="font-bold">Đang tải ghi chú...</p>
</div>
@@ -383,11 +383,11 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
<input
type="text"
placeholder="Tiêu đề ghi chú..."
className="w-full px-4 py-3 bg-white rounded-2xl border border-gray-100 shadow-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all text-lg font-bold"
className="w-full px-4 py-3 bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all text-lg font-bold"
value={noteForm.title}
onChange={(e) => setNoteForm({ ...noteForm, title: e.target.value })}
/>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden min-h-[500px] flex flex-col">
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm overflow-hidden min-h-[500px] flex flex-col">
<ReactQuill
ref={quillRef}
theme="snow"
@@ -395,7 +395,7 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
onChange={(content) => setNoteForm({ ...noteForm, content })}
modules={quillModules}
placeholder="Bắt đầu viết cảm nhận của bạn tại đây..."
className="flex-1 flex flex-col [&_.ql-container]:flex-1 [&_.ql-container]:flex [&_.ql-container]:flex-col [&_.ql-editor]:flex-1 [&_.ql-editor]:text-base [&_.ql-toolbar]:flex [&_.ql-toolbar]:flex-wrap sm:[&_.ql-toolbar]:flex-nowrap [&_.ql-toolbar]:border-0 [&_.ql-toolbar]:border-b [&_.ql-toolbar]:border-gray-50 [&_.ql-editor_table]:border-collapse [&_.ql-editor_table]:w-full [&_.ql-editor_td]:border [&_.ql-editor_td]:border-gray-200 [&_.ql-editor_td]:p-2 [&_.ql-editor_.ql-align-center]:text-center [&_.ql-editor_.ql-align-right]:text-right [&_.ql-editor_.ql-align-justify]:text-justify [&_.ql-picker-options]:!z-[50] [&_.ql-picker-options]:!shadow-xl [&_.ql-picker-options]:!rounded-xl [&_.ql-picker-options]:!border-gray-100 [&_.ql-stroke]:!stroke-gray-500 [&_.ql-fill]:!fill-gray-500 [&_button:hover_.ql-stroke]:!stroke-amber-500 [&_button:hover_.ql-fill]:!stroke-amber-500 [&_button.ql-active_.ql-stroke]:!stroke-amber-500"
className="flex-1 flex flex-col [&_.ql-container]:flex-1 [&_.ql-container]:flex [&_.ql-container]:flex-col [&_.ql-editor]:flex-1 [&_.ql-editor]:text-base [&_.ql-toolbar]:flex [&_.ql-toolbar]:flex-wrap sm:[&_.ql-toolbar]:flex-nowrap [&_.ql-toolbar]:border-0 [&_.ql-toolbar]:border-b [&_.ql-toolbar]:border-[var(--border)] [&_.ql-editor_table]:border-collapse [&_.ql-editor_table]:w-full [&_.ql-editor_td]:border [&_.ql-editor_td]:border-[var(--border)] [&_.ql-editor_td]:p-2 [&_.ql-editor_.ql-align-center]:text-center [&_.ql-editor_.ql-align-right]:text-right [&_.ql-editor_.ql-align-justify]:text-justify [&_.ql-picker-options]:!z-[50] [&_.ql-picker-options]:!shadow-xl [&_.ql-picker-options]:!rounded-xl [&_.ql-picker-options]:!border-[var(--border)] [&_.ql-stroke]:!stroke-[var(--text-secondary)] [&_.ql-fill]:!fill-[var(--text-secondary)] [&_button:hover_.ql-stroke]:!stroke-amber-500 [&_button:hover_.ql-fill]:!stroke-amber-500 [&_button.ql-active_.ql-stroke]:!stroke-amber-500"
/>
</div>
<div className="flex gap-3">
@@ -416,11 +416,11 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
) : filteredNotes.length > 0 ? (
<div className="grid grid-cols-1 gap-4">
{filteredNotes.map((note) => (
<div key={note.id} className="bg-white p-5 rounded-3xl border border-gray-100 shadow-sm hover:shadow-md transition-all group">
<div key={note.id} className="bg-[var(--surface)] p-5 rounded-3xl border border-[var(--border)] shadow-sm hover:shadow-md transition-all group">
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-bold text-gray-900">{note.title}</h4>
<div className="flex items-center gap-2 text-[10px] text-gray-400 font-bold uppercase mt-1">
<h4 className="font-bold text-[var(--text-primary)]">{note.title}</h4>
<div className="flex items-center gap-2 text-[10px] text-[var(--text-muted)] font-bold uppercase mt-1">
<Calendar className="w-3 h-3" />
{new Date(note.createdAt).toLocaleDateString('vi-VN')}
</div>
@@ -428,14 +428,14 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
<div className="flex gap-1">
<button
onClick={() => handleEditNote(note)}
className="p-2 text-gray-300 hover:text-blue-500 hover:bg-blue-50 rounded-xl transition-all opacity-0 group-hover:opacity-100"
className="p-2 text-[var(--text-muted)] hover:text-blue-500 hover:bg-[var(--background)] rounded-xl transition-all opacity-0 group-hover:opacity-100"
title="Sửa ghi chú"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteNote(note.id)}
className="p-2 text-gray-300 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all opacity-0 group-hover:opacity-100"
className="p-2 text-[var(--text-muted)] hover:text-red-500 hover:bg-[var(--background)] rounded-xl transition-all opacity-0 group-hover:opacity-100"
title="Xóa ghi chú"
>
<Trash2 className="w-4 h-4" />
@@ -443,14 +443,14 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
</div>
</div>
<div
className="text-sm text-gray-600 line-clamp-3 prose prose-sm max-w-none ql-editor !p-0 [&_table]:border-collapse [&_table]:w-full [&_td]:border [&_td]:border-gray-200 [&_td]:p-2 [&_.ql-align-center]:text-center [&_.ql-align-right]:text-right [&_.ql-align-justify]:text-justify"
className="text-sm text-[var(--text-secondary)] line-clamp-3 prose prose-sm max-w-none ql-editor !p-0 [&_table]:border-collapse [&_table]:w-full [&_td]:border [&_td]:border-[var(--border)] [&_td]:p-2 [&_.ql-align-center]:text-center [&_.ql-align-right]:text-right [&_.ql-align-justify]:text-justify"
dangerouslySetInnerHTML={{ __html: note.content }}
/>
</div>
))}
</div>
) : (
<div className="text-center py-24 bg-white rounded-[40px] border-2 border-dashed border-gray-100 shadow-inner">
<div className="text-center py-24 bg-[var(--surface)] rounded-[40px] border-2 border-dashed border-[var(--border)] shadow-inner">
<div className="w-20 h-20 bg-amber-50 rounded-3xl flex items-center justify-center mx-auto mb-6 text-amber-500">
<FileText className="w-10 h-10" />
</div>
+1 -1
View File
@@ -358,7 +358,7 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
title={isLiked ? "Bỏ thích" : "Thích"}
>
<Heart className={`w-4 h-4 transition-colors ${
isLiked ? 'text-rose-500 fill-rose-500' : 'text-gray-300'
isLiked ? 'text-rose-500 fill-rose-500' : 'text-[var(--text-muted)]'
}`} />
<span>{likeCount}</span>
</button>
+29 -29
View File
@@ -239,7 +239,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
if (error || !tour) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-950 text-slate-800 dark:text-slate-100 p-4 transition-colors">
<div className="bg-white dark:bg-slate-900 border border-gray-100 dark:border-slate-800 shadow-2xl rounded-[32px] p-8 max-w-md w-full text-center flex flex-col items-center gap-5">
<div className="bg-[var(--surface)] dark:bg-slate-900 border border-[var(--border)] dark:border-slate-800 shadow-2xl rounded-[32px] p-8 max-w-md w-full text-center flex flex-col items-center gap-5">
<div className="w-16 h-16 bg-rose-50 dark:bg-rose-950/20 border border-rose-100 dark:border-rose-900/30 rounded-full flex items-center justify-center text-rose-500">
<ShieldAlert className="w-8 h-8" />
</div>
@@ -261,11 +261,11 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
return (
<div className="h-screen w-screen flex flex-col bg-slate-50 dark:bg-slate-950 text-slate-800 dark:text-slate-100 transition-colors overflow-hidden">
{/* Header */}
<header className="h-16 shrink-0 bg-white/70 dark:bg-slate-900/70 backdrop-blur-md border-b border-gray-100 dark:border-slate-800 px-4 md:px-6 flex items-center justify-between z-20">
<header className="h-16 shrink-0 bg-[var(--surface)]/70 dark:bg-slate-900/70 backdrop-blur-md border-b border-[var(--border)] dark:border-slate-800 px-4 md:px-6 flex items-center justify-between z-20">
<div className="flex items-center gap-3">
<button
onClick={onGoToHome}
className="p-2 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-xl transition-all active:scale-95 text-slate-500 dark:text-slate-400"
className="p-2 hover:bg-[var(--background)] dark:hover:bg-slate-800 rounded-xl transition-all active:scale-95 text-[var(--text-muted)] dark:text-slate-400"
>
<ChevronLeft className="w-5 h-5" />
</button>
@@ -274,7 +274,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
<span className="w-2.5 h-2.5 bg-rose-500 rounded-full animate-pulse"></span>
<h1 className="text-sm md:text-base font-black uppercase tracking-tight">{t('emergencyJourney')}</h1>
</div>
<p className="text-[10px] text-slate-500 dark:text-slate-400 truncate max-w-[200px] sm:max-w-xs">{tour.title}</p>
<p className="text-[10px] text-[var(--text-muted)] dark:text-slate-400 truncate max-w-[200px] sm:max-w-xs">{tour.title}</p>
</div>
</div>
@@ -283,7 +283,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
<select
value={lang}
onChange={(e) => changeLanguage(e.target.value as any)}
className="bg-gray-50 dark:bg-slate-800 border border-gray-100 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-xl px-2 py-1.5 text-xs font-bold focus:outline-none cursor-pointer"
className="bg-[var(--background)] dark:bg-slate-800 border border-[var(--border)] dark:border-slate-700 text-[var(--text-primary)] dark:text-slate-100 rounded-xl px-2 py-1.5 text-xs font-bold focus:outline-none cursor-pointer"
>
<option value="vi">VI</option>
<option value="en">EN</option>
@@ -294,7 +294,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
<select
value={theme}
onChange={(e) => changeTheme(e.target.value as any)}
className="bg-gray-50 dark:bg-slate-800 border border-gray-100 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-xl px-2 py-1.5 text-xs font-bold focus:outline-none cursor-pointer"
className="bg-[var(--background)] dark:bg-slate-800 border border-[var(--border)] dark:border-slate-700 text-[var(--text-primary)] dark:text-slate-100 rounded-xl px-2 py-1.5 text-xs font-bold focus:outline-none cursor-pointer"
>
<option value="light">{t('themeLight')}</option>
<option value="dark">{t('themeDark')}</option>
@@ -304,7 +304,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
</header>
{/* Mobile Tab Switcher */}
<div className="lg:hidden h-12 shrink-0 bg-white dark:bg-slate-900 border-b border-gray-100 dark:border-slate-800 flex items-center z-10 p-1">
<div className="lg:hidden h-12 shrink-0 bg-[var(--surface)] dark:bg-slate-900 border-b border-[var(--border)] dark:border-slate-800 flex items-center z-10 p-1">
<button
onClick={() => setActiveTab('map')}
className={`flex-1 h-full flex items-center justify-center gap-2 text-xs font-bold rounded-lg transition-all ${activeTab === 'map' ? 'bg-rose-500 text-white shadow-md' : 'text-slate-500 dark:text-slate-400'}`}
@@ -324,13 +324,13 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
{/* Main Layout Area */}
<div className="flex-1 flex flex-col lg:grid lg:grid-cols-12 overflow-hidden">
{/* Left Side Pane (Itinerary and Contacts) */}
<div className={`flex-col lg:col-span-4 bg-white dark:bg-slate-900 border-r border-gray-100 dark:border-slate-800 overflow-y-auto no-scrollbar ${activeTab === 'itinerary' ? 'flex h-full' : 'hidden lg:flex'}`}>
<div className={`flex-col lg:col-span-4 bg-[var(--surface)] dark:bg-slate-900 border-r border-[var(--border)] dark:border-slate-800 overflow-y-auto no-scrollbar ${activeTab === 'itinerary' ? 'flex h-full' : 'hidden lg:flex'}`}>
<div className="p-4 md:p-6 space-y-6">
{/* Tour Meta Info */}
<div className="space-y-2">
<h2 className="text-lg md:text-xl font-black text-slate-900 dark:text-white leading-tight">{tour.title}</h2>
<h2 className="text-lg md:text-xl font-black text-[var(--text-primary)] dark:text-white leading-tight">{tour.title}</h2>
{tour.startDate && (
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
<div className="flex items-center gap-2 text-xs text-[var(--text-muted)] dark:text-slate-400">
<Calendar className="w-4 h-4 text-rose-500" />
<span>
{new Date(tour.startDate).toLocaleDateString()}
@@ -339,7 +339,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
</div>
)}
{tour.description && (
<p className="text-xs text-slate-600 dark:text-slate-400 leading-relaxed bg-slate-50 dark:bg-slate-950 p-3 rounded-2xl border border-gray-100 dark:border-slate-800/40">
<p className="text-xs text-[var(--text-secondary)] dark:text-slate-400 leading-relaxed bg-[var(--background)] dark:bg-slate-950 p-3 rounded-2xl border border-[var(--border)] dark:border-slate-800/40">
{tour.description}
</p>
)}
@@ -358,8 +358,8 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
<div className="bg-rose-50/50 dark:bg-rose-950/10 border border-rose-100/50 dark:border-rose-950/20 p-4 rounded-2xl flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="text-xs text-rose-600 dark:text-rose-400 font-bold uppercase tracking-wider">{t('tourOwner')}</div>
<div className="text-sm font-black text-slate-900 dark:text-white truncate mt-0.5">{ownerContact.name || 'Anonymous'}</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{ownerContact.phone || t('noPhone')}</div>
<div className="text-sm font-black text-[var(--text-primary)] dark:text-white truncate mt-0.5">{ownerContact.name || 'Anonymous'}</div>
<div className="text-xs text-[var(--text-muted)] dark:text-slate-400 mt-0.5">{ownerContact.phone || t('noPhone')}</div>
</div>
{ownerContact.phone && (
<a
@@ -374,11 +374,11 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
{/* Manager contacts */}
{managerContacts.map((mgr, idx) => (
<div key={idx} className="bg-slate-50 dark:bg-slate-950 border border-gray-100 dark:border-slate-800 p-4 rounded-2xl flex items-center justify-between gap-4">
<div key={idx} className="bg-[var(--background)] dark:bg-slate-950 border border-[var(--border)] dark:border-slate-800 p-4 rounded-2xl flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">{t('tourManager')}</div>
<div className="text-sm font-black text-slate-900 dark:text-white truncate mt-0.5">{mgr.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{mgr.phone}</div>
<div className="text-xs text-[var(--text-muted)] dark:text-slate-400 font-bold uppercase tracking-wider">{t('tourManager')}</div>
<div className="text-sm font-black text-[var(--text-primary)] dark:text-white truncate mt-0.5">{mgr.name}</div>
<div className="text-xs text-[var(--text-muted)] dark:text-slate-400 mt-0.5">{mgr.phone}</div>
</div>
<a
href={`tel:${mgr.phone}`}
@@ -390,20 +390,20 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
))}
{!ownerContact?.phone && managerContacts.length === 0 && (
<p className="text-xs text-slate-400 dark:text-slate-500 italic">{t('noPhone')}</p>
<p className="text-xs text-[var(--text-muted)] dark:text-slate-500 italic">{t('noPhone')}</p>
)}
</div>
</div>
{/* Stops Hierarchy List */}
<div className="space-y-3">
<h3 className="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">
<h3 className="text-xs font-black text-[var(--text-muted)] dark:text-slate-500 uppercase tracking-widest">
{t('viewTimeline')}
</h3>
<div className="space-y-4 relative before:absolute before:left-3 before:top-2 before:bottom-2 before:w-[2px] before:bg-gray-100 dark:before:bg-slate-800">
<div className="space-y-4 relative before:absolute before:left-3 before:top-2 before:bottom-2 before:w-[2px] before:bg-[var(--border)] dark:before:bg-slate-800">
{allLocations.length === 0 ? (
<p className="text-xs text-slate-400 dark:text-slate-500 italic pl-6">{t('noStops')}</p>
<p className="text-xs text-[var(--text-muted)] dark:text-slate-500 italic pl-6">{t('noStops')}</p>
) : (
allLocations.map((loc, idx) => {
const isStart = idx === 0;
@@ -430,15 +430,15 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
</div>
{/* Details */}
<div className="flex-1 bg-slate-50/50 hover:bg-slate-50 dark:bg-slate-950/40 dark:hover:bg-slate-950/80 p-3 rounded-2xl border border-gray-100/50 dark:border-slate-800/40 transition-all select-none">
<h4 className="text-xs font-black text-slate-800 dark:text-slate-200 group-hover:text-rose-500 dark:group-hover:text-rose-400 transition-colors">
<div className="flex-1 bg-[var(--background)]/50 hover:bg-[var(--background)] dark:bg-slate-950/40 dark:hover:bg-slate-950/80 p-3 rounded-2xl border border-[var(--border)]/50 dark:border-slate-800/40 transition-all select-none">
<h4 className="text-xs font-black text-[var(--text-primary)] dark:text-slate-200 group-hover:text-rose-500 dark:group-hover:text-rose-400 transition-colors">
{loc.name}
</h4>
{loc.address && (
<p className="text-[10px] text-slate-500 dark:text-slate-400 truncate mt-0.5">{loc.address}</p>
<p className="text-[10px] text-[var(--text-muted)] dark:text-slate-400 truncate mt-0.5">{loc.address}</p>
)}
{loc.plannedStart && (
<p className="text-[9px] text-slate-400 dark:text-slate-500 mt-1">
<p className="text-[9px] text-[var(--text-muted)] dark:text-slate-500 mt-1">
{new Date(loc.plannedStart).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
{loc.plannedEnd && ` - ${new Date(loc.plannedEnd).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
</p>
@@ -454,7 +454,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
</div>
{/* Right Side Map Pane */}
<div className={`relative flex-1 lg:col-span-8 h-full bg-slate-100 dark:bg-slate-950 ${activeTab === 'map' ? 'flex' : 'hidden lg:flex'}`}>
<div className={`relative flex-1 lg:col-span-8 h-full bg-[var(--background)] dark:bg-slate-950 ${activeTab === 'map' ? 'flex' : 'hidden lg:flex'}`}>
{allLocations.length > 0 ? (
<MapContainer
center={[allLocations[0].latitude, allLocations[0].longitude]}
@@ -499,7 +499,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
<Popup>
<div className="p-1">
<div className="font-black text-xs text-slate-900">{loc.name}</div>
{loc.address && <div className="text-[10px] text-gray-500 mt-0.5">{loc.address}</div>}
{loc.address && <div className="text-[10px] text-[var(--text-muted)] mt-0.5">{loc.address}</div>}
</div>
</Popup>
</Marker>
@@ -527,7 +527,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
{/* Locate Me button */}
<button
onClick={handleLocateMe}
className="w-12 h-12 bg-white dark:bg-slate-900 border border-gray-200 dark:border-slate-800 rounded-2xl shadow-xl flex items-center justify-center text-slate-700 dark:text-slate-200 hover:bg-gray-50 dark:hover:bg-slate-800 transition-all active:scale-95"
className="w-12 h-12 bg-[var(--surface)] dark:bg-slate-900 border border-[var(--border)] dark:border-slate-800 rounded-2xl shadow-xl flex items-center justify-center text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-800 transition-all active:scale-95"
title="Định vị của tôi"
>
{isLocating ? (
@@ -541,7 +541,7 @@ export const ShareJourneyPage: React.FC<ShareJourneyPageProps> = ({ token, onGoT
{allLocations.length > 0 && (
<button
onClick={() => setFitBoundsTrigger(prev => prev + 1)}
className="w-12 h-12 bg-white dark:bg-slate-900 border border-gray-200 dark:border-slate-800 rounded-2xl shadow-xl flex items-center justify-center text-slate-700 dark:text-slate-200 hover:bg-gray-50 dark:hover:bg-slate-800 transition-all active:scale-95"
className="w-12 h-12 bg-[var(--surface)] dark:bg-slate-900 border border-[var(--border)] dark:border-slate-800 rounded-2xl shadow-xl flex items-center justify-center text-[var(--text-secondary)] dark:text-slate-200 hover:bg-[var(--background)] dark:hover:bg-slate-800 transition-all active:scale-95"
title="Bao phủ toàn bộ"
>
<Compass className="w-5 h-5 text-violet-500" />
+28 -28
View File
@@ -157,20 +157,20 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-white p-10 rounded-3xl shadow-2xl">
<div className="min-h-screen flex items-center justify-center bg-[var(--background)] py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-[var(--surface)] p-10 rounded-3xl shadow-2xl">
<button
onClick={onBack}
className="absolute top-6 left-6 text-gray-400 hover:text-gray-600 transition-colors"
className="absolute top-6 left-6 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
>
<ChevronLeft className="w-6 h-6" />
</button>
<div className="mb-10">
<h1 className="text-3xl font-extrabold text-gray-900">
<h1 className="text-3xl font-extrabold text-[var(--text-primary)]">
{step === 'form' ? 'Tạo tài khoản mới' : 'Xác thực tài khoản'}
</h1>
<p className="text-gray-500 mt-2 font-medium">
<p className="text-[var(--text-muted)] mt-2 font-medium">
{step === 'form'
? 'Khám phá các tính năng lập kế hoạch chuyên nghiệp.'
: `Vui lòng nhập mã OTP đã được gửi tới ${formData.email}`}
@@ -187,61 +187,61 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
{step === 'form' ? (
<>
<div className="space-y-1.5">
<label className="text-sm font-bold text-gray-700 ml-1">Họ tên</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1">Họ tên</label>
<div className="relative group">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
required
type="text"
placeholder="Nhập họ và tên của bạn"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-bold text-gray-700 ml-1">Email</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1">Email</label>
<div className="relative group">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
required
type="email"
placeholder="Nhập email của bạn"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-sm font-bold text-gray-700 ml-1">Mật khẩu</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1">Mật khẩu</label>
<div className="relative group">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
required
type="password"
placeholder="Nhập mật khẩu"
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-bold text-gray-700 ml-1">Xác nhận mật khẩu</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1">Xác nhận mật khẩu</label>
<div className="relative group">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
required
type="password"
placeholder="Xác nhận mật khẩu"
value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
@@ -249,28 +249,28 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-sm font-bold text-gray-700 ml-1">Số điện thoại</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1">Số điện thoại</label>
<div className="relative group">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
type="tel"
placeholder="Nhập số điện thoại"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-bold text-gray-700 ml-1">Đa chỉ</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1">Đa chỉ</label>
<div className="relative group">
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
type="text"
placeholder="Nhập địa chỉ"
value={formData.address}
onChange={(e) => handleChange('address', e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
@@ -278,9 +278,9 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
</>
) : (
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 ml-1"> xác thực OTP</label>
<label className="text-sm font-bold text-[var(--text-primary)] ml-1"> xác thực OTP</label>
<div className="relative group">
<ShieldCheck className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
<ShieldCheck className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)] group-focus-within:text-blue-500 transition-colors" />
<input
required
type="text"
@@ -288,7 +288,7 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
placeholder="Nhập 6 số mã OTP"
value={otp}
onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))}
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl text-center font-bold tracking-[0.5em] text-lg focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all"
className="w-full pl-12 pr-4 py-4 bg-[var(--background)] border border-[var(--border)] rounded-2xl text-center font-bold tracking-[0.5em] text-lg focus:ring-2 focus:ring-blue-500 focus:bg-[var(--surface)] outline-none transition-all"
/>
</div>
</div>
@@ -308,9 +308,9 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
<>
<div className="relative my-6 flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200"></div>
<div className="w-full border-t border-[var(--border)]"></div>
</div>
<span className="relative px-3 bg-white text-xs font-bold text-gray-400 uppercase">Hoặc</span>
<span className="relative px-3 bg-[var(--surface)] text-xs font-bold text-[var(--text-muted)] uppercase">Hoặc</span>
</div>
<div id="google-signin-btn-signup" className="w-full flex justify-center"></div>
+10 -10
View File
@@ -310,23 +310,23 @@ const MapContextMenu = ({ onAction, onOpen }: { onAction: (action: string, latln
return (
<div
ref={menuRef}
className="absolute z-[2000] bg-white rounded-2xl shadow-2xl border border-gray-100 py-2 w-48 animate-in zoom-in-95 duration-200"
className="absolute z-[2000] bg-[var(--surface)] rounded-2xl shadow-2xl border border-[var(--border)] py-2 w-48 animate-in zoom-in-95 duration-200"
style={{ top: menuPos.y, left: menuPos.x }}
onClick={(e) => e.stopPropagation()}
onContextMenu={(e) => e.preventDefault()}
>
<button onClick={() => { onAction('START', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-gray-700 flex items-center gap-2">
<button onClick={() => { onAction('START', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-[var(--background)] text-sm font-bold text-[var(--text-secondary)] flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-600" /> Bắt đu từ đây
</button>
<button onClick={() => { onAction('END', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-green-50 text-sm font-bold text-gray-700 flex items-center gap-2 border-b border-gray-50">
<button onClick={() => { onAction('END', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-[var(--background)] text-sm font-bold text-[var(--text-secondary)] flex items-center gap-2 border-b border-[var(--border)]">
<div className="w-2 h-2 rounded-full bg-green-600" /> Kết thúc đây
</button>
<div className="px-4 py-2 text-[10px] font-black text-gray-400 uppercase tracking-widest">Thêm vào chặng</div>
<div className="px-4 py-2 text-[10px] font-black text-[var(--text-muted)] uppercase tracking-widest">Thêm vào chặng</div>
{legs.map(leg => (
<button
key={leg.id}
onClick={() => { onAction(`ADD_TO_LEG_${leg.id}`, menuPos.latlng); setMenuPos(null); }}
className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-medium text-gray-600 truncate"
className="w-full text-left px-4 py-2 hover:bg-[var(--background)] text-sm font-medium text-[var(--text-secondary)] truncate"
>
Chặng {leg.sequence}: {leg.note || 'Không có ghi chú'}
</button>
@@ -1706,13 +1706,13 @@ export const TourDetailPage = ({
};
return (
<div className="min-h-screen bg-gray-50 pb-20">
<div className="min-h-screen bg-[var(--background)] pb-20">
{/* Top Navigation Bar */}
<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" />
<div className="sticky top-0 z-30 bg-[var(--surface)]/80 backdrop-blur-md border-b border-[var(--border)] px-4 py-3 flex items-center justify-between">
<button onClick={onBack} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
<ChevronLeft className="w-6 h-6 text-[var(--text-secondary)]" />
</button>
<h1 className="text-lg font-bold text-gray-800 truncate px-4 flex-1 text-center">
<h1 className="text-lg font-bold text-[var(--text-primary)] truncate px-4 flex-1 text-center">
{tourInfo.title}
</h1>
<div className="flex items-center gap-1">
+52 -1
View File
@@ -7,7 +7,58 @@ const config: Config = {
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
extend: {
colors: {
// Light Theme Colors ("Classic Heritage & Soft Sand")
yotripLight: {
bg: '#f6f0ed', // Parchment - Global Page Backgrounds
text: '#28536b', // Yale Blue - Primary Text & Main Titles
steel: '#7ea8be', // Steel Blue - Active Navigation & Secondary Actions
khaki: '#bbb193', // Khaki Beige - Borders, Dividers, Muted States
rosy: '#c2948a', // Rosy Taupe - Special Highlight Badges, Notifications
},
// Dark Theme Colors ("Vintage Velvet & Aquatic Zest")
yotripDark: {
bg: '#513b56', // Vintage Grape - Global Page Backgrounds
surface: '#525174', // Dusty Grape - Component Surfaces (Cards, Bubbles)
lime: '#bce784', // Lime Cream - Brand Text Highlights, Active Icons
bondi: '#348aa7', // Bondi Blue - Primary Action Buttons, Links
emerald: '#5dd39e', // Emerald - Success Utilities & "Tối ưu" badges
},
// CSS Variable based colors for smooth theme support
theme: {
bg: 'var(--background)',
'bg-alt': 'var(--background-alt)',
surface: 'var(--surface)',
'surface-muted': 'var(--surface-muted)',
'surface-hover': 'var(--surface-hover)',
border: 'var(--border)',
'border-light': 'var(--border-light)',
'text': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',
'text-accent': 'var(--text-accent)',
'text-muted': 'var(--text-muted)',
'text-disabled': 'var(--text-disabled)',
primary: 'var(--primary)',
'primary-hover': 'var(--primary-hover)',
'primary-light': 'var(--primary-light)',
secondary: 'var(--secondary)',
'secondary-light': 'var(--secondary-light)',
success: 'var(--success)',
'success-light': 'var(--success-light)',
danger: 'var(--danger)',
'danger-light': 'var(--danger-light)',
warning: 'var(--warning)',
'warning-light': 'var(--warning-light)',
info: 'var(--info)',
'info-light': 'var(--info-light)',
}
},
boxShadow: {
'theme-sm': 'var(--shadow-sm)',
'theme-md': 'var(--shadow-md)',
}
},
},
plugins: [],
};