Sửa lỗi người nhận được liên kết chia sẻ không xem được nội dung Tour
This commit is contained in:
+86
-69
@@ -1,88 +1,105 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { LandingPage } from '@/pages/LandingPage';
|
||||
import { TourDetailPage } from '@/pages/TourDetailPage';
|
||||
import { ExploreMap } from '@/pages/ExploreMap';
|
||||
import { SignupPage } from '@/pages/SignupPage';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
import { LandingPage } from './pages/LandingPage';
|
||||
import { ExploreMap } from './pages/ExploreMap';
|
||||
import { TourDetailPage } from './pages/TourDetailPage';
|
||||
import { SignupPage } from './pages/SignupPage';
|
||||
import { useTourStore } from './store/useTourStore';
|
||||
|
||||
function App() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const viewTourId = params.get('viewTour');
|
||||
|
||||
const App = () => {
|
||||
type View = 'landing' | 'explore' | 'detail' | 'signup';
|
||||
const [view, setView] = useState<View>('landing');
|
||||
const [isInitialSetup, setIsInitialSetup] = useState(false);
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [isUserLoaded, setIsUserLoaded] = useState(false); // Trạng thái để biết user đã được load từ localStorage chưa
|
||||
const [currentPage, setCurrentPage] = useState<'landing' | 'explore' | 'tourDetail' | 'signup'>(viewTourId ? 'tourDetail' : 'landing');
|
||||
const [currentTourId, setCurrentTourId] = useState<string | null>(viewTourId);
|
||||
const [isPublicTourView, setIsPublicTourView] = useState(!!viewTourId);
|
||||
|
||||
// Lấy action từ store
|
||||
const fetchTour = useTourStore(state => state.fetchTour);
|
||||
const fetchPublicTourDetails = useTourStore(state => state.fetchPublicTourDetails);
|
||||
|
||||
useEffect(() => {
|
||||
// Khôi phục phiên đăng nhập từ localStorage
|
||||
const savedUser = localStorage.getItem('user');
|
||||
if (savedUser) {
|
||||
const parsedUser = JSON.parse(savedUser);
|
||||
setUser(parsedUser);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const viewTourId = params.get('viewTour');
|
||||
|
||||
if (viewTourId) {
|
||||
// Không gọi replaceState ngay để tránh mất ID khi component re-render hoặc refresh
|
||||
} else {
|
||||
// Kiểm tra đăng nhập bình thường nếu không có tham số viewTour
|
||||
const token = localStorage.getItem('token');
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (token && storedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser));
|
||||
setCurrentPage('explore'); // Chuyển đến bản đồ khám phá nếu đã đăng nhập
|
||||
} catch (e) {
|
||||
console.error("Lỗi khi phân tích dữ liệu người dùng từ localStorage", e);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setCurrentPage('landing'); // Quay về trang Landing nếu dữ liệu lỗi
|
||||
}
|
||||
} else {
|
||||
setCurrentPage('landing'); // Quay về trang Landing nếu chưa đăng nhập
|
||||
}
|
||||
}
|
||||
setIsUserLoaded(true); // Đánh dấu user đã được load
|
||||
}, []); // Chỉ chạy một lần khi component mount
|
||||
|
||||
// Kiểm tra xem hệ thống đã được cài đặt chưa
|
||||
fetch(`/api/v1/auth/status`)
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(data => setIsInitialSetup(!!data.isInitialSetup))
|
||||
.catch(() => setIsInitialSetup(false));
|
||||
}, []); // Chạy một lần khi component mount
|
||||
|
||||
// Effect để xử lý chuyển hướng nếu user đã đăng nhập và đang ở trang landing
|
||||
useEffect(() => {
|
||||
if (isUserLoaded && user && view === 'landing') {
|
||||
setView('explore');
|
||||
}
|
||||
}, [isUserLoaded, user, view]);
|
||||
|
||||
const handleLoginSuccess = (userData: any) => {
|
||||
setUser(userData);
|
||||
const handleLoginSuccess = (loggedInUser: any) => {
|
||||
setUser(loggedInUser);
|
||||
setCurrentPage('explore');
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setUser(null);
|
||||
setView('landing');
|
||||
setCurrentPage('landing');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
{view === 'landing' && (
|
||||
<LandingPage
|
||||
isInitialSetup={isInitialSetup}
|
||||
onContinue={() => setView('explore')}
|
||||
onGoToSignup={() => setView('signup')}
|
||||
onGoToMap={() => setView('explore')}
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
/>
|
||||
)}
|
||||
const handleViewTour = (tourId: string) => {
|
||||
setCurrentTourId(tourId);
|
||||
setIsPublicTourView(false); // Không phải chế độ công khai nếu đến từ bản đồ khám phá
|
||||
setCurrentPage('tourDetail');
|
||||
};
|
||||
|
||||
{view === 'signup' && (
|
||||
<SignupPage
|
||||
onBack={() => setView('landing')}
|
||||
onSuccess={() => setView('landing')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view === 'explore' && (
|
||||
<ExploreMap
|
||||
onBack={() => setView('landing')}
|
||||
onLogout={user ? handleLogout : undefined}
|
||||
user={user}
|
||||
onViewTour={(id) => {
|
||||
fetchTour(id);
|
||||
setView('detail');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
const handleBackFromTourDetail = () => {
|
||||
setCurrentTourId(null);
|
||||
setIsPublicTourView(false);
|
||||
// Quay về trang khám phá nếu đã đăng nhập, ngược lại quay về Landing
|
||||
if (user) {
|
||||
setCurrentPage('explore');
|
||||
} else {
|
||||
setCurrentPage('landing');
|
||||
}
|
||||
};
|
||||
|
||||
{view === 'detail' && (
|
||||
<TourDetailPage onBack={() => setView('explore')} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const handleBackFromSignup = () => {
|
||||
setCurrentPage('landing');
|
||||
};
|
||||
|
||||
const handleSignupSuccess = () => {
|
||||
setCurrentPage('landing'); // Quay về trang Landing sau khi đăng ký thành công
|
||||
};
|
||||
|
||||
if (currentPage === 'tourDetail') {
|
||||
return (
|
||||
<TourDetailPage
|
||||
tourId={currentTourId!} // tourId được đảm bảo không null ở đây
|
||||
onBack={handleBackFromTourDetail}
|
||||
isPublicView={isPublicTourView}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentPage === 'explore') {
|
||||
return <ExploreMap onBack={handleBackFromTourDetail} onLogout={handleLogout} user={user} onViewTour={handleViewTour} />;
|
||||
}
|
||||
|
||||
if (currentPage === 'signup') {
|
||||
return <SignupPage onBack={handleBackFromSignup} onSuccess={handleSignupSuccess} />;
|
||||
}
|
||||
|
||||
return <LandingPage onContinue={() => setCurrentPage('explore')} onGoToSignup={() => setCurrentPage('signup')} onGoToMap={() => setCurrentPage('explore')} onLoginSuccess={handleLoginSuccess} isInitialSetup={false} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -26,8 +26,12 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
|
||||
const fetchComments = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/v1/locations/${locationId}/comments`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
|
||||
@@ -240,7 +240,7 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
|
||||
const canEdit = isPublicView ? false : ['OWNER', 'MANAGER'].includes(userRole || '');
|
||||
const canInvite = isPublicView ? false : ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
||||
const isOwner = isPublicView ? false : userRole === 'OWNER';
|
||||
const canShare = ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
||||
const canShare = isPublicView || ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (isPublicView) {
|
||||
|
||||
@@ -28,6 +28,7 @@ interface TourState {
|
||||
setActiveLegId: (id: string | null) => void;
|
||||
setMapCenter: (pos: [number, number]) => void;
|
||||
fetchTour: (id: string) => Promise<void>;
|
||||
fetchPublicTourDetails: (tourId: string) => Promise<void>;
|
||||
fetchPublicTours: () => Promise<void>;
|
||||
createJoinRequest: (tourId: string, userId?: string) => Promise<any>;
|
||||
fetchJoinRequests: (tourId: string) => Promise<any[]>;
|
||||
@@ -47,10 +48,9 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
setActiveLegId: (id) => set({ activeLegId: id }),
|
||||
setMapCenter: (pos) => set({ mapCenter: pos }),
|
||||
fetchTour: async (id: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const token = localStorage.getItem('token');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
|
||||
const response = await fetch(`/api/v1/tours/${id}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
@@ -75,11 +75,10 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
fetchPublicTours: async () => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/explore`, {
|
||||
const response = await fetch(`/api/v1/tours/explore`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
@@ -89,9 +88,27 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
const data = await response.json();
|
||||
set({ publicTours: data });
|
||||
},
|
||||
fetchPublicTourDetails: async (tourId: string) => {
|
||||
try {
|
||||
// Reset state cũ trước khi tải dữ liệu mới
|
||||
set({ currentTour: null, legs: [], userRole: 'VIEWER_ONLY' });
|
||||
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/public`);
|
||||
if (!response.ok) throw new Error('Không thể tải tour công khai');
|
||||
const data = await response.json();
|
||||
const legs = data.legs || [];
|
||||
set({
|
||||
currentTour: data,
|
||||
legs,
|
||||
userRole: 'VIEWER_ONLY',
|
||||
activeLegId: legs.length > 0 ? legs[0].id : null
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('Lỗi khi tải tour công khai:', err);
|
||||
}
|
||||
},
|
||||
createTour: async (tourData: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours`, {
|
||||
const response = await fetch(`/api/v1/tours`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -108,9 +125,8 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
return tour;
|
||||
},
|
||||
updateTourDetails: async (tourId: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -125,8 +141,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
updateTour: async (id: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
|
||||
const response = await fetch(`/api/v1/tours/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -140,8 +155,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
get().fetchPublicTours();
|
||||
},
|
||||
deleteTour: async (id: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
|
||||
const response = await fetch(`/api/v1/tours/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
@@ -157,8 +171,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
get().fetchPublicTours();
|
||||
},
|
||||
addLeg: async (tourId: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/legs`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/legs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -170,8 +183,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
get().fetchTour(tourId);
|
||||
},
|
||||
initializeLegs: async (tourId: string, count: number) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/legs/batch`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/legs/batch`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -183,8 +195,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
await get().fetchTour(tourId);
|
||||
},
|
||||
updateLeg: async (legId: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/legs/${legId}`, {
|
||||
const response = await fetch(`/api/v1/legs/${legId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -198,8 +209,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
deleteLeg: async (legId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/legs/${legId}`, {
|
||||
const response = await fetch(`/api/v1/legs/${legId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
@@ -214,8 +224,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
addLocation: async (tourId: string, locationData: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/locations`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/locations`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -228,8 +237,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
get().fetchTour(tourId);
|
||||
},
|
||||
updateLocation: async (locationId: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/locations/${locationId}`, {
|
||||
const response = await fetch(`/api/v1/locations/${locationId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -243,8 +251,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
deleteLocation: async (locationId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/locations/${locationId}`, {
|
||||
const response = await fetch(`/api/v1/locations/${locationId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
@@ -256,8 +263,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
updateTourStartPoint: async (tourId: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/start-point`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/start-point`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -271,8 +277,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
await get().fetchTour(tourId);
|
||||
},
|
||||
updateTourEndPoint: async (tourId: string, data: any) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/end-point`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/end-point`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -284,8 +289,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
await get().fetchTour(tourId);
|
||||
},
|
||||
optimizeRouting: async (legId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/routing/optimize/${legId}`, {
|
||||
const response = await fetch(`/api/v1/routing/optimize/${legId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const { locations, totalDistance } = await response.json();
|
||||
@@ -299,8 +303,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
removeMember: async (tourId: string, userId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/members/${userId}`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/members/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
@@ -311,8 +314,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
addMember: async (tourId: string, member: { userId: string; role?: string }) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/members`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/members`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -328,8 +330,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
createJoinRequest: async (tourId: string, userId?: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/join-requests`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -344,8 +345,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
return response.json();
|
||||
},
|
||||
fetchJoinRequests: async (tourId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/join-requests`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
@@ -357,8 +357,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
return response.json();
|
||||
},
|
||||
acceptJoinRequest: async (tourId: string, requestId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests/${requestId}/accept`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/join-requests/${requestId}/accept`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
@@ -372,8 +371,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
if (currentTour) get().fetchTour(currentTour.id);
|
||||
},
|
||||
rejectJoinRequest: async (tourId: string, requestId: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests/${requestId}/reject`, {
|
||||
const response = await fetch(`/api/v1/tours/${tourId}/join-requests/${requestId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
|
||||
Reference in New Issue
Block a user