bugs: lỗi khi người dùng link để tham gia tour nhưng không xuất hiện trong danh sách thành viên

This commit is contained in:
2026-06-20 20:59:01 +07:00
parent 52706cab7d
commit 36157bd53b
18 changed files with 1419 additions and 188 deletions
+184
View File
@@ -0,0 +1,184 @@
import React, { useEffect, useState, useRef } from 'react';
import { Compass, Loader2, LogIn, UserPlus, AlertCircle, CheckCircle } from 'lucide-react';
import { LoginModal } from '../components/LoginModal';
interface JoinTourPageProps {
onLoginSuccess: (user: any) => void;
onGoToSignup: () => void;
onViewTour: (tourId: string) => void;
onGoToHome: () => void;
}
export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGoToSignup, onViewTour, onGoToHome }) => {
const [inviteToken, setInviteToken] = useState<string | null>(null);
const [isLoginOpen, setIsLoginOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [successMsg, setSuccessMsg] = useState('');
const [tourId, setTourId] = useState<string | null>(null);
// Guard chống React StrictMode chạy effect 2 lần trong dev mode
const hasJoinedRef = useRef(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
setInviteToken(token);
if (token) {
localStorage.setItem('pendingInviteToken', token);
// Nếu đã đăng nhập, tự động thực hiện join
const systemToken = localStorage.getItem('token');
if (systemToken && !hasJoinedRef.current) {
hasJoinedRef.current = true;
handleJoinTour(token, systemToken);
}
} else {
setError('Mã lời mời không tồn tại hoặc không hợp lệ.');
}
}, []);
const handleJoinTour = async (token: string, authToken: string) => {
setLoading(true);
setError('');
try {
const res = await fetch('/api/v1/tours/join-by-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`
},
body: JSON.stringify({ token })
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message || 'Không thể gia nhập tour.');
}
setSuccessMsg(data.message || 'Bạn đã tham gia tour thành công!');
setTourId(data.tourId);
localStorage.removeItem('pendingInviteToken');
} catch (err: any) {
setError(err.message || 'Đã xảy ra lỗi.');
} finally {
setLoading(false);
}
};
const handleSuccessLogin = (user: any) => {
onLoginSuccess(user);
const token = localStorage.getItem('token');
const pendingToken = localStorage.getItem('pendingInviteToken') || inviteToken;
if (token && pendingToken) {
handleJoinTour(pendingToken, token);
}
};
const isLoggedIn = !!localStorage.getItem('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">
{/* Logo/Icon */}
<div className="flex justify-center">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600 shadow-md">
<Compass className="w-10 h-10 animate-spin-slow" />
</div>
</div>
{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>
</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>
<div className="pt-4 flex flex-col gap-2">
{isLoggedIn ? (
<button
onClick={onGoToHome}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3.5 rounded-2xl transition-all shadow-lg"
>
Về trang khám phá
</button>
) : (
<>
<button
onClick={() => setIsLoginOpen(true)}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3.5 rounded-2xl transition-all flex items-center justify-center gap-2"
>
<LogIn className="w-5 h-5" /> Thử đăng nhập lại
</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"
>
Về trang chủ
</button>
</>
)}
</div>
</div>
) : successMsg ? (
<div className="space-y-4 py-4">
<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>
<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
onClick={() => {
if (tourId) onViewTour(tourId);
else onGoToHome();
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3.5 rounded-2xl transition-all shadow-lg"
>
Xem chi tiết Tour
</button>
</div>
</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">
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>
<div className="space-y-3 pt-4">
<button
onClick={() => setIsLoginOpen(true)}
className="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-2xl shadow-lg transition-all active:scale-[0.98]"
>
<LogIn className="w-5 h-5" /> Đăng nhập hệ thống
</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]"
>
<UserPlus className="w-5 h-5" /> Đăng tài khoản mới
</button>
</div>
</div>
)}
</div>
<LoginModal
isOpen={isLoginOpen}
onClose={() => setIsLoginOpen(false)}
onSwitchToSignup={onGoToSignup}
onLoginSuccess={handleSuccessLogin}
/>
</div>
);
};