Files
travelplanning/frontend/src/pages/JoinTourPage.tsx
T

208 lines
9.0 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react';
import { Compass, Loader2, LogIn, UserPlus, AlertCircle, CheckCircle } from 'lucide-react';
import { JoinTourLoginModal } from '../components/JoinTourLoginModal';
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);
// Sync inviteToken from URL when modal opens to ensure fresh value
useEffect(() => {
if (isLoginOpen) {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
if (token) {
setInviteToken(token);
localStorage.setItem('pendingInviteToken', token);
}
}
}, [isLoginOpen]);
// Check for real authenticated user (not guest)
const hasRealToken = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
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 (có token thực, không phải guest), tự động thực hiện join
const systemToken = localStorage.getItem('token');
const guestToken = localStorage.getItem('guest_token');
if (systemToken && !guestToken && !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 {
console.log('[JoinTour] Attempting to join with token');
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) {
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) {
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 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 isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
return (
<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">
<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-[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-[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
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-[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>
</>
)}
</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-[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
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-[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>
<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-[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>
</div>
</div>
)}
</div>
<JoinTourLoginModal
isOpen={isLoginOpen}
onClose={() => setIsLoginOpen(false)}
inviteToken={inviteToken || ''}
onSwitchToSignup={onGoToSignup}
onJoinSuccess={handleJoinSuccess}
/>
</div>
);
};