fix: guest and admin logic
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { UserManagementModal } from '../components/UserManagementModal';
|
||||
|
||||
interface AdminDashboardProps {
|
||||
user: any;
|
||||
onNavigate: (page: string) => void;
|
||||
}
|
||||
|
||||
export const AdminDashboard: React.FC<AdminDashboardProps> = ({ user, onNavigate }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
|
||||
if (!user?.isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-black text-white mb-4">Quyền Truy Cập Bị Từ Chối</h1>
|
||||
<p className="text-slate-400 mb-6">Bạn không có quyền truy cập trang admin này.</p>
|
||||
<button
|
||||
onClick={() => onNavigate('dashboard')}
|
||||
className="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl font-bold transition-all"
|
||||
>
|
||||
Quay lại Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// When admin closes the modal, navigate back to user dashboard
|
||||
const handleCloseModal = () => {
|
||||
onNavigate('dashboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
||||
{/* Header with toggle button */}
|
||||
<div className="fixed top-0 left-0 right-0 z-[60] bg-slate-900/95 backdrop-blur-md border-b border-slate-800 px-4 md:px-6 py-4 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="p-2 hover:bg-slate-800 rounded-xl transition-colors"
|
||||
title="Quay lại User Dashboard"
|
||||
>
|
||||
<ArrowLeft className="w-6 h-6 text-slate-300" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-black text-white">
|
||||
🛡️ Admin Dashboard
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="px-4 py-2 text-sm font-bold text-slate-300 hover:text-white hover:bg-slate-800 rounded-xl transition-all"
|
||||
>
|
||||
Switch to User Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal shown full screen */}
|
||||
<div className="pt-20">
|
||||
<UserManagementModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -61,7 +61,11 @@ function MapTracker() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos, onLoginSuccess }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void, onOpenMyPhotos: () => void, onLoginSuccess?: (user: any) => void }) => {
|
||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos, onLoginSuccess, onGoToDashboard }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void, onOpenMyPhotos: () => void, onLoginSuccess?: (user: any) => void, onGoToDashboard?: () => void }) => {
|
||||
// Check if user is logged in (real user) or is a guest
|
||||
const guestToken = localStorage.getItem('guest_token');
|
||||
const isLoggedInOrGuest = user || guestToken;
|
||||
|
||||
// Tối ưu hóa việc lấy dữ liệu từ store bằng selectors để tránh re-render thừa
|
||||
const publicTours = useTourStore(state => state.publicTours);
|
||||
const fetchPublicTours = useTourStore(state => state.fetchPublicTours);
|
||||
@@ -701,7 +705,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
{/* Nhóm bên phải: Các thao tác người dùng */}
|
||||
<div className="flex items-center gap-2 pointer-events-auto">
|
||||
{/* Nút Ảnh của tôi */}
|
||||
{user && (
|
||||
{isLoggedInOrGuest && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log("Đang mở Ảnh của tôi...");
|
||||
@@ -716,7 +720,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
)}
|
||||
|
||||
{/* Nút tạo Tour mới */}
|
||||
{user && (
|
||||
{user && !localStorage.getItem('guest_token') && (
|
||||
<button
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
className="w-11 h-11 md:w-auto bg-green-600 p-0 md:px-4 md:py-3 rounded-2xl shadow-xl hover:bg-green-700 text-white transition-all flex items-center justify-center md:justify-start gap-2 font-bold shrink-0"
|
||||
@@ -770,28 +774,29 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
</div>
|
||||
|
||||
{/* Nút quản lý người dùng cho Admin */}
|
||||
{user && (
|
||||
{user?.isAdmin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (user.isAdmin) {
|
||||
setIsAdminModalOpen(true);
|
||||
} else {
|
||||
const key = window.prompt(t('enterSecretKey'));
|
||||
if (key) {
|
||||
handlePromoteAdmin(key);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`w-11 h-11 md:w-auto p-0 md:px-4 md:py-3 rounded-2xl shadow-xl transition-all flex items-center justify-center md:justify-start gap-2 font-bold shrink-0 ${
|
||||
user.isAdmin ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-slate-700 hover:bg-slate-800 text-slate-300 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-750 dark:border-slate-700 border border-slate-600'
|
||||
}`}
|
||||
onClick={() => setIsAdminModalOpen(true)}
|
||||
className="w-11 h-11 md:w-auto p-0 md:px-4 md:py-3 rounded-2xl shadow-xl transition-all flex items-center justify-center md:justify-start gap-2 font-bold shrink-0 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
title={t('systemBtn')}
|
||||
>
|
||||
{user.isAdmin ? <Settings className="w-5 h-5" /> : <Lock className="w-5 h-5" />}
|
||||
<Settings className="w-5 h-5" />
|
||||
<span className="hidden md:inline text-sm">{t('systemBtn')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Nút Bảng điều khiển của tôi - chỉ hiển thị cho người dùng đã đăng nhập (không phải khách) */}
|
||||
{user && !localStorage.getItem('guest_token') && onGoToDashboard && (
|
||||
<button
|
||||
onClick={onGoToDashboard}
|
||||
className="w-11 h-11 md:w-auto p-0 md:px-4 md:py-3 rounded-2xl shadow-xl transition-all flex items-center justify-center md:justify-start gap-2 font-bold shrink-0 bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
title="Bảng điều khiển của tôi"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
<span className="hidden md:inline text-sm">Bảng điều khiển của tôi</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Nút đăng xuất */}
|
||||
{onLogout && (
|
||||
<button
|
||||
@@ -1059,7 +1064,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[10px] bg-emerald-50 text-emerald-700 px-1.5 py-0.5 rounded font-black uppercase tracking-wider mb-1 w-max">
|
||||
{item.type === 'RESTAURANT' ? 'Nhà hàng' : item.type === 'HOTEL' ? 'Khách sạn' : 'Homestay'}
|
||||
{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>
|
||||
@@ -1291,7 +1296,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
<div className="flex items-start justify-between gap-1.5">
|
||||
<span className="text-xs font-bold text-gray-800 dark:text-slate-200 truncate">{item.name}</span>
|
||||
<span className="text-[8px] bg-emerald-50 dark:bg-emerald-950/40 text-emerald-600 dark:text-emerald-400 px-1.5 py-0.5 rounded font-black shrink-0">
|
||||
{item.type === 'RESTAURANT' ? 'Nhà hàng' : item.type === 'HOTEL' ? 'Khách sạn' : 'Homestay'}
|
||||
{item.type === 'RESTAURANT' ? t('businessRestaurant') : item.type === 'HOTEL' ? t('businessHotel') : t('businessHomestay')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1446,9 +1451,9 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
onChange={(e) => setProposeForm(prev => ({ ...prev, type: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-slate-800 rounded-xl focus:outline-none focus:ring-1 focus:ring-emerald-500 bg-gray-50/20 dark:bg-slate-950/40 text-gray-900 dark:text-slate-100"
|
||||
>
|
||||
<option value="RESTAURANT">Nhà hàng</option>
|
||||
<option value="HOTEL">Khách sạn</option>
|
||||
<option value="HOMESTAY">Homestay</option>
|
||||
<option value="RESTAURANT">{t('businessRestaurant')}</option>
|
||||
<option value="HOTEL">{t('businessHotel')}</option>
|
||||
<option value="HOMESTAY">{t('businessHomestay')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Compass, Loader2, LogIn, UserPlus, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { LoginModal } from '../components/LoginModal';
|
||||
import { JoinTourLoginModal } from '../components/JoinTourLoginModal';
|
||||
|
||||
interface JoinTourPageProps {
|
||||
onLoginSuccess: (user: any) => void;
|
||||
@@ -43,6 +43,7 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
console.log('[JoinTour] Attempting to join with token');
|
||||
const res = await fetch('/api/v1/tours/join-by-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -54,28 +55,31 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message || 'Không thể gia nhập tour.');
|
||||
console.error('[JoinTour] Join failed with status', res.status, 'message:', data.message);
|
||||
// Show real backend error message
|
||||
throw new Error(data.message || `Không thể gia nhập tour (${res.status}). Vui lòng kiểm tra lại mã lời mời.`);
|
||||
}
|
||||
|
||||
console.log('[JoinTour] Join successful, message:', data.message);
|
||||
setSuccessMsg(data.message || 'Bạn đã tham gia tour thành công!');
|
||||
setTourId(data.tourId);
|
||||
// Đợi 500ms để đảm bảo các thay đổi cache ở backend hoàn tất trước khi tiếp tục
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
localStorage.removeItem('pendingInviteToken');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Đã xảy ra lỗi.');
|
||||
console.error('[JoinTour] Error:', err.message);
|
||||
setError(err.message || 'Đã xảy ra lỗi khi gia nhập tour.');
|
||||
// Keep the pendingInviteToken in localStorage so user can retry
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccessLogin = (user: any) => {
|
||||
const handleJoinSuccess = (user: any, tourData: any) => {
|
||||
console.log('[JoinTour] Join successful via modal, tourId:', tourData.tourId);
|
||||
setTourId(tourData.tourId);
|
||||
setSuccessMsg(tourData.message || 'Bạn đã tham gia tour thành công!');
|
||||
onLoginSuccess(user);
|
||||
const token = localStorage.getItem('token');
|
||||
const pendingToken = localStorage.getItem('pendingInviteToken') || inviteToken;
|
||||
if (token && pendingToken) {
|
||||
handleJoinTour(pendingToken, token);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoggedIn = !!localStorage.getItem('token');
|
||||
@@ -175,11 +179,12 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
|
||||
|
||||
</div>
|
||||
|
||||
<LoginModal
|
||||
<JoinTourLoginModal
|
||||
isOpen={isLoginOpen}
|
||||
onClose={() => setIsLoginOpen(false)}
|
||||
inviteToken={inviteToken || ''}
|
||||
onSwitchToSignup={onGoToSignup}
|
||||
onLoginSuccess={handleSuccessLogin}
|
||||
onJoinSuccess={handleJoinSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,6 +45,21 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
onExploreTours,
|
||||
onViewTour
|
||||
}) => {
|
||||
// GUARD: Prevent guest users from accessing dashboard
|
||||
const guestToken = localStorage.getItem('guest_token');
|
||||
const token = localStorage.getItem('token');
|
||||
if (guestToken && !token) {
|
||||
// This is a guest user - redirect them by triggering onLogout
|
||||
// which will clear everything and redirect to landing
|
||||
console.warn('Guest user attempted to access MemberDashboard - redirecting to home');
|
||||
// Clear guest tokens
|
||||
localStorage.removeItem('guest_token');
|
||||
localStorage.removeItem('guest_user');
|
||||
// Redirect to home
|
||||
window.location.href = '/';
|
||||
return null;
|
||||
}
|
||||
|
||||
const notify = useNotification();
|
||||
const confirm = useConfirm();
|
||||
const { t, lang, changeLanguage } = useTranslation();
|
||||
@@ -130,7 +145,7 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
setShareStatus(data);
|
||||
notify({
|
||||
title: 'Thành công',
|
||||
message: isEnabled ? 'Đã bật chia sẻ hành trình cứu hộ.' : 'Đã tắt chia sẻ.',
|
||||
message: isEnabled ? t('emergencySharingEnabled') : t('emergencySharingDisabled'),
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
@@ -337,9 +352,15 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
|
||||
// Fetch connections, tours, and photos on mount
|
||||
useEffect(() => {
|
||||
fetchPublicTours();
|
||||
fetchConnections();
|
||||
fetchPhotos();
|
||||
Promise.all([
|
||||
fetchPublicTours().catch(err => {
|
||||
console.error('[MemberDashboard] fetchPublicTours failed:', err.message);
|
||||
// If fetch fails due to anonymous user rejection, don't crash
|
||||
// The page is already loaded
|
||||
}),
|
||||
fetchConnections(),
|
||||
fetchPhotos()
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const fetchConnectionsRef = useRef<() => Promise<void>>(null as any);
|
||||
@@ -560,7 +581,7 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
if (res.ok) {
|
||||
notify({
|
||||
title: 'Đã cập nhật',
|
||||
message: `Mối quan hệ đã được chuyển sang nhóm: ${type === 'FAMILY' ? 'Gia đình' : 'Bạn bè'}.`,
|
||||
message: `${t('changeConnectionType')}${type === 'FAMILY' ? t('familyGroup') : t('friends')}.`,
|
||||
type: 'success'
|
||||
});
|
||||
fetchConnections();
|
||||
@@ -573,8 +594,8 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
// Remove connection
|
||||
const handleRemoveConnection = async (connId: string, targetName: string) => {
|
||||
const isConfirmed = await confirm({
|
||||
title: 'Hủy kết nối',
|
||||
message: `Bạn có chắc chắn muốn hủy kết nối với ${targetName}?`
|
||||
title: t('disconnectTitle'),
|
||||
message: `${t('disconnectConfirm')} ${targetName}?`
|
||||
});
|
||||
|
||||
if (isConfirmed) {
|
||||
@@ -586,7 +607,7 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
if (res.ok) {
|
||||
notify({
|
||||
title: 'Thành công',
|
||||
message: 'Đã hủy kết nối thành công.',
|
||||
message: t('disconnectSuccess'),
|
||||
type: 'success'
|
||||
});
|
||||
fetchConnections();
|
||||
@@ -595,7 +616,7 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Lỗi hủy kết nối:', err);
|
||||
console.error(t('disconnectError'), err);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -684,7 +705,7 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
const getConnectionStatusText = (targetId: string) => {
|
||||
const isConnected = connections.find(c => c.targetUser?.id === targetId);
|
||||
if (isConnected) {
|
||||
return isConnected.type === 'FAMILY' ? 'Gia đình' : 'Bạn bè';
|
||||
return isConnected.type === 'FAMILY' ? t('familyGroup') : t('friends');
|
||||
}
|
||||
const isPendingReceived = receivedRequests.find(r => r.requester?.id === targetId);
|
||||
if (isPendingReceived) return 'Chờ bạn duyệt';
|
||||
@@ -805,7 +826,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
className="w-full py-4 px-4 bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl font-bold flex items-center justify-center gap-2 shadow-lg active:scale-98 transition-all"
|
||||
>
|
||||
<Compass className="w-5 h-5 animate-spin-slow" />
|
||||
Khám phá Bản đồ Tour
|
||||
{t('exploreTourMap')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -823,12 +844,12 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<Compass className="w-5 h-5 text-indigo-400" />
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-sm flex items-center gap-1.5">
|
||||
Hành trình của tôi
|
||||
{t('myItineraries')}
|
||||
{(unreadTourSenders.length > 0 || unreadTourChats.length > 0) && (
|
||||
<span className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" title="Có tin nhắn mới"></span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[10px] font-medium text-slate-400">Xem và quản lý các chuyến đi</span>
|
||||
<span className="text-[10px] font-medium text-slate-400">{t('toursManagementDesc')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -850,8 +871,8 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<div className="flex items-center gap-3">
|
||||
<ImageIcon className="w-5 h-5 text-indigo-400" />
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-sm">Thư viện ảnh</span>
|
||||
<span className="text-[10px] font-medium text-slate-400">Kho ảnh gốc của bạn từ các tour</span>
|
||||
<span className="text-sm">{t('photoGallery')}</span>
|
||||
<span className="text-[10px] font-medium text-slate-400">{t('photoGalleryDesc')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -876,8 +897,8 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="w-5 h-5 text-indigo-400" />
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-sm">Danh sách bạn bè</span>
|
||||
<span className="text-[10px] font-medium text-slate-400">Bạn bè, gia đình, yêu cầu chờ duyệt</span>
|
||||
<span className="text-sm">{t('friendsList')}</span>
|
||||
<span className="text-[10px] font-medium text-slate-400">{t('manageFriendsDesc')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -962,8 +983,8 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
</button>
|
||||
<div>
|
||||
<h2 className="text-sm font-black uppercase text-white tracking-wider">
|
||||
{activeTab === 'tours' && 'Hành trình của tôi'}
|
||||
{activeTab === 'connections' && 'Danh sách bạn bè'}
|
||||
{activeTab === 'tours' && t('myItineraries')}
|
||||
{activeTab === 'connections' && t('friendsList')}
|
||||
{activeTab === 'chats' && 'Trò chuyện trực tiếp'}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -1096,7 +1117,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
<span>Thư viện ảnh</span>
|
||||
<span>{t('photoGallery')}</span>
|
||||
</div>
|
||||
<span className="bg-slate-800/80 px-2 py-0.5 text-xs rounded-full text-slate-400 font-semibold border border-slate-700">
|
||||
{photos.length}
|
||||
@@ -1135,7 +1156,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>Danh sách bạn bè</span>
|
||||
<span>{t('friendsList')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{receivedRequests.length > 0 && (
|
||||
@@ -1188,7 +1209,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black uppercase tracking-tight flex items-center gap-2 text-white">
|
||||
<Compass className="w-6 h-6 text-indigo-500" />
|
||||
Hành trình của tôi
|
||||
{t('myItineraries')}
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 mt-1">Danh sách các chuyến đi bạn tham gia (với vai trò chủ sở hữu, quản lý hoặc thành viên).</p>
|
||||
</div>
|
||||
@@ -1331,9 +1352,9 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-black uppercase tracking-tight flex items-center gap-2 text-white">
|
||||
<Users className="w-6 h-6 text-indigo-500" />
|
||||
Danh sách bạn bè
|
||||
{t('friendsList')}
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 mt-1">Quản lý các mối quan hệ bạn bè, gia đình, duyệt các yêu cầu kết nối từ thành viên khác.</p>
|
||||
<p className="text-xs text-slate-400 mt-1">{t('manageRelations')}</p>
|
||||
</div>
|
||||
|
||||
{/* Sub-tabs for connections */}
|
||||
@@ -1382,7 +1403,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<div className="bg-slate-900/40 border border-slate-800/60 rounded-3xl p-12 text-center max-w-md mx-auto my-6">
|
||||
<Users className="w-12 h-12 text-indigo-500 mx-auto mb-4" />
|
||||
<h3 className="text-base font-bold mb-1">Chưa có kết nối nào</h3>
|
||||
<p className="text-xs text-slate-400 mb-6">Bạn chưa kết nối với ai. Hãy chuyển sang tìm kiếm để gửi lời mời.</p>
|
||||
<p className="text-xs text-slate-400 mb-6">{t('noConnections')}</p>
|
||||
<button
|
||||
onClick={() => setConnectionSubTab('search')}
|
||||
className="py-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-xs font-bold transition-all"
|
||||
@@ -1418,7 +1439,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
? 'bg-rose-950/40 text-rose-300 border-rose-900/50'
|
||||
: 'bg-indigo-950/40 text-indigo-300 border-indigo-900/50'
|
||||
}`}>
|
||||
{conn.type === 'FAMILY' ? 'Gia đình' : 'Bạn bè'}
|
||||
{conn.type === 'FAMILY' ? t('familyGroup') : t('friends')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1431,7 +1452,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
onChange={(e) => handleChangeConnectionType(conn.id, e.target.value as any)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700/80 rounded-md text-[10px] px-2 py-1 outline-none font-bold cursor-pointer hover:bg-slate-750 transition-colors"
|
||||
>
|
||||
<option value="FRIEND">Bạn bè</option>
|
||||
<option value="FRIEND">{t('friends')}</option>
|
||||
<option value="FAMILY">Gia đình</option>
|
||||
</select>
|
||||
|
||||
@@ -1452,7 +1473,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<button
|
||||
onClick={() => handleRemoveConnection(conn.id, connUser.name)}
|
||||
className="p-1.5 bg-rose-950/50 hover:bg-rose-600 text-rose-300 hover:text-white border border-rose-800/40 hover:border-rose-500 rounded-lg text-xs font-bold transition-all"
|
||||
title="Hủy kết nối"
|
||||
title={t('disconnectTitle')}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -1474,7 +1495,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<Search className="absolute left-4 top-3.5 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tìm kiếm theo Tên hoặc Email (nhập tối thiểu 2 ký tự)..."
|
||||
placeholder={t('searchMembers')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800/80 rounded-2xl py-3 pl-11 pr-4 text-sm text-white placeholder-slate-500 outline-none focus:border-indigo-500/80 transition-all duration-150"
|
||||
@@ -1483,11 +1504,11 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
|
||||
{searchingUsers ? (
|
||||
<div className="text-center py-8 text-slate-400 text-xs font-bold flex items-center justify-center gap-1.5">
|
||||
<Clock className="w-4 h-4 animate-spin text-indigo-500" /> Đang tìm kiếm...
|
||||
<Clock className="w-4 h-4 animate-spin text-indigo-500" /> {t('searching')}...
|
||||
</div>
|
||||
) : searchQuery.trim().length < 2 ? (
|
||||
<div className="text-center py-12 text-slate-500 text-xs italic">
|
||||
Vui lòng nhập tối thiểu 2 ký tự để tìm kiếm thành viên.
|
||||
{t('minCharsRequired')}
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400 text-xs">
|
||||
@@ -1522,7 +1543,7 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
||||
<div>
|
||||
{statusText ? (
|
||||
<span className={`px-3 py-1 rounded-lg text-xs font-bold border ${
|
||||
statusText === 'Bạn bè' || statusText === 'Gia đình'
|
||||
statusText === t('friends') || statusText === t('familyGroup')
|
||||
? 'bg-emerald-950/30 border-emerald-900/50 text-emerald-300'
|
||||
: 'bg-slate-800/80 border-slate-700 text-slate-400'
|
||||
}`}>
|
||||
|
||||
@@ -3170,6 +3170,7 @@ export const TourDetailPage = ({
|
||||
onClose={() => setIsAddPhotoOpen(false)}
|
||||
tourId={currentTour.id}
|
||||
onSuccess={() => fetchTour(currentTour.id)}
|
||||
isPublicView={isPublicView}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user