fix: lỗi các thành viên không thể upload ảnh và không có nút xóa ảnh của user

This commit is contained in:
2026-06-16 18:54:55 +07:00
parent 00554224a1
commit 2fabdb79df
7 changed files with 1151 additions and 16 deletions
+92 -12
View File
@@ -8,6 +8,7 @@ import { AddMemberModal } from '../components/AddMemberModal';
import { useConfirm } from '@/hooks/useConfirm';
import { useNotification } from '@/hooks/useNotification';
import { CommentModal } from '@/components/CommentModal';
import { AddPhotoModal } from '@/components/AddPhotoModal';
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline, useMap } from 'react-leaflet';
import _MarkerClusterGroup from 'react-leaflet-cluster';
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
@@ -15,6 +16,7 @@ import {
Map as MapIcon,
Wallet,
Image as ImageIcon,
Upload,
Calendar,
Users,
ChevronLeft,
@@ -32,7 +34,8 @@ import {
X,
MessageSquare,
Share2,
Tag as TagIcon
Tag as TagIcon,
Trash2
} from 'lucide-react';
import L from 'leaflet';
@@ -180,10 +183,21 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline');
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
const [isAddMemberOpen, setIsAddMemberOpen] = useState(false);
const [isAddPhotoOpen, setIsAddPhotoOpen] = useState(false);
const [targetLegId, setTargetLegId] = useState<string | null>(null);
const [editingLocation, setEditingLocation] = useState<any>(null);
const [selectedMember, setSelectedMember] = useState<any>(null);
const [isMemberDetailOpen, setIsMemberDetailOpen] = useState(false);
const [selectedPhoto, setSelectedPhoto] = useState<any | null>(null);
const currentUserId = useMemo(() => {
const token = localStorage.getItem('token');
if (!token) return null;
try {
return JSON.parse(atob(token.split('.')[1])).sub;
} catch (e) {
return null;
}
}, []);
const [joinRequests, setJoinRequests] = useState<any[]>([]);
const [titleInput, setTitleInput] = useState(currentTour?.title ?? '');
const [descriptionInput, setDescriptionInput] = useState(currentTour?.description ?? '');
@@ -223,6 +237,32 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
}
};
const handleDeletePhoto = async (photoId: string) => {
const isConfirmed = await confirm({
title: 'Xóa ảnh này?',
message: 'Bạn có chắc chắn muốn xóa ảnh này khỏi chuyến đi? Hành động này không thể hoàn tác.'
});
if (!isConfirmed) return;
try {
const response = await fetch(`/api/v1/photos/${photoId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) throw new Error('Không thể xóa ảnh');
notify({ title: 'Thành công', message: 'Đã xóa ảnh.', type: 'success' });
fetchTour(tourId);
setSelectedPhoto(null);
} catch (error) {
notify({ title: 'Lỗi', message: 'Không thể xóa ảnh. Vui lòng thử lại.', type: 'error' });
}
};
const fetchTour = useTourStore(state => state.fetchTour);
const fetchPublicTourDetails = useTourStore(state => state.fetchPublicTourDetails); // New action
@@ -288,6 +328,7 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
// Nếu là public view, không có quyền chỉnh sửa
const canEdit = isPublicView ? false : ['OWNER', 'MANAGER'].includes(userRole || '');
const canInvite = isPublicView ? false : ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
const canUploadPhoto = isPublicView ? false : ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE'].includes(userRole || '');
const isOwner = isPublicView ? false : userRole === 'OWNER';
const canShare = isPublicView || ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
@@ -944,17 +985,45 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
)}
{activeTab === 'photo' && (
<div className="grid grid-cols-3 gap-1.5 animate-in fade-in">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white">
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
<img
src={`https://picsum.photos/seed/${i + 10}/400/400`}
alt="Tour photo"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="grid grid-cols-3 gap-2 animate-in fade-in">
{currentTour?.photos && currentTour.photos.length > 0 ? (
currentTour.photos.map((photo: any) => {
const isUploader = currentUserId === photo.uploaderId;
return (
<div
key={photo.id}
onClick={() => setSelectedPhoto(photo)}
className="aspect-square bg-gray-200 rounded-2xl overflow-hidden relative group border border-white shadow-sm cursor-pointer"
>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors z-10" />
<img
src={photo.imageUrl}
alt="Tour photo"
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
{isUploader && !isPublicView && (
<button
onClick={(e) => {
e.stopPropagation();
handleDeletePhoto(photo.id);
}}
className="absolute top-2 right-2 p-1.5 bg-red-500/80 backdrop-blur-sm text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity z-20 hover:bg-red-600"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
);
})
) : (
<div className="col-span-3 py-24 text-center bg-white rounded-[40px] border-2 border-dashed border-gray-100 animate-in zoom-in-95">
<div className="w-20 h-20 bg-gray-50 rounded-3xl flex items-center justify-center mx-auto mb-6">
<ImageIcon className="w-10 h-10 text-gray-200" />
</div>
<h4 className="text-lg font-bold text-gray-400">Khoảnh khắc trống</h4>
<p className="text-sm text-gray-300 mt-2 max-w-[200px] mx-auto">Hãy người đu tiên chia sẻ những hình nh tuyệt vời của chuyến đi này!</p>
</div>
))}
)}
</div>
)}
@@ -1172,13 +1241,14 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
</div>
{/* Floating Action Button (Mobile) */}
{canEdit && !isPublicView && ( // Hide floating action button in public view
{((activeTab === 'plan' && canEdit) || (activeTab === 'photo' && canUploadPhoto)) && !isPublicView && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
<button
onClick={() => {
setTargetLegId(null); // Reset khi nhấn nút floating tổng quát
setEditingLocation(null);
if (activeTab === 'plan') setIsAddLocationOpen(true);
if (activeTab === 'photo' && canUploadPhoto) setIsAddPhotoOpen(true);
}}
className="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold">
{activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh'}
@@ -1213,6 +1283,16 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
/>
)}
{/* Add Photo Modal */}
{currentTour && (
<AddPhotoModal
isOpen={isAddPhotoOpen}
onClose={() => setIsAddPhotoOpen(false)}
tourId={currentTour.id}
onSuccess={() => fetchTour(currentTour.id)}
/>
)}
{/* Member Detail Popover */}
{isMemberDetailOpen && selectedMember && (
<div className="fixed inset-0 z-[2100] flex items-center justify-center p-4">