fix: UI của trang MyPhotoPage lỗi

This commit is contained in:
2026-06-16 21:25:08 +07:00
parent 1eed9b00de
commit 6026c2227a
3 changed files with 134 additions and 132 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

+134 -132
View File
@@ -6,9 +6,9 @@ import { useConfirm } from '@/hooks/useConfirm';
export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
const [photos, setPhotos] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [filterTourId, setFilterTourId] = useState<string>('');
const [selectedTourIdForPhoto, setSelectedTourIdForPhoto] = useState<string | 'all'>('all'); // Changed from filterTourId
const [filterDate, setFilterDate] = useState<string>('');
const [selectedPhoto, setSelectedPhoto] = useState<any | null>(null); // State cho lightbox
const [selectedPhotoForDisplay, setSelectedPhotoForDisplay] = useState<any | null>(null); // New state for large photo display
const [sortOrder, setSortOrder] = useState<'newest' | 'oldest'>('newest'); // 'newest' by default
const notify = useNotification();
const confirm = useConfirm();
@@ -34,7 +34,7 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
}, []);
// Lấy danh sách các Tour duy nhất để hiển thị trong bộ lọc
const uniqueTours = useMemo(() => {
const toursWithPhotos = useMemo(() => {
const tourMap = new Map();
photos.forEach(p => {
if (p.tourId && p.tour) {
@@ -46,14 +46,14 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
// Logic lọc ảnh tại Frontend
const filteredPhotos = useMemo(() => {
let sortedPhotos = photos.filter(p => {
const matchTour = !filterTourId || p.tourId === filterTourId;
let photosToFilter = photos.filter(p => {
const matchTour = selectedTourIdForPhoto === 'all' || p.tourId === selectedTourIdForPhoto;
// So sánh ngày định dạng YYYY-MM-DD
const photoDate = p.capturedAt ? p.capturedAt.split('T')[0] : '';
const matchDate = !filterDate || photoDate === filterDate;
return matchTour && matchDate;
});
let sortedPhotos = photosToFilter;
// Sắp xếp ảnh
if (sortOrder === 'newest') {
sortedPhotos.sort((a, b) => new Date(b.capturedAt).getTime() - new Date(a.capturedAt).getTime());
@@ -61,7 +61,21 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
sortedPhotos.sort((a, b) => new Date(a.capturedAt).getTime() - new Date(b.capturedAt).getTime());
}
return sortedPhotos;
}, [photos, filterTourId, filterDate, sortOrder]);
}, [photos, selectedTourIdForPhoto, filterDate, sortOrder]);
// Effect để thiết lập ảnh được chọn hiển thị hoặc reset nếu ảnh hiện tại không còn trong danh sách lọc
useEffect(() => {
if (filteredPhotos.length > 0 && (!selectedPhotoForDisplay || !filteredPhotos.some(p => p.id === selectedPhotoForDisplay.id))) {
setSelectedPhotoForDisplay(filteredPhotos[0]);
} else if (filteredPhotos.length === 0) {
setSelectedPhotoForDisplay(null);
} else if (selectedPhotoForDisplay) {
// Nếu ảnh đang chọn vẫn còn trong danh sách lọc, không làm gì cả
} else {
// Nếu không có ảnh nào để hiển thị
setSelectedPhotoForDisplay(null); // No photos to display
}
}, [filteredPhotos, selectedPhotoForDisplay]);
const handleDeletePhoto = async (photoId: string) => {
const isConfirmed = await confirm({
@@ -84,18 +98,12 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
notify({ title: 'Thành công', message: 'Ảnh đã được xóa.', type: 'success' });
// Cập nhật lại danh sách ảnh sau khi xóa
setPhotos(prev => prev.filter(p => p.id !== photoId));
setSelectedPhoto(null); // Đóng lightbox
setSelectedPhotoForDisplay(null); // Reset ảnh đang hiển thị
} catch (error) {
notify({ title: 'Lỗi', message: 'Không thể xóa ảnh. Vui lòng thử lại.', type: 'error' });
}
};
// Đóng lightbox khi nhấn ESC
useEffect(() => {
const handleEsc = (event: KeyboardEvent) => event.key === 'Escape' && setSelectedPhoto(null);
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, []);
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
@@ -116,21 +124,11 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
<Filter className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider text-gray-400">Bộ lọc:</span>
</div>
{/* Lọc theo Tour */}
<div className="relative min-w-[160px]">
<select
value={filterTourId}
onChange={(e) => setFilterTourId(e.target.value)}
className="w-full pl-3 pr-8 py-2 bg-gray-50 border border-gray-100 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-blue-500 transition-all appearance-none cursor-pointer text-gray-700"
>
<option value="">Tất cả chuyến đi</option>
{uniqueTours.map(t => (
<option key={t.id} value={t.id}>{t.title}</option>
))}
</select>
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400">
<ChevronLeft className="w-3 h-3 -rotate-90" />
{/* Chỉ báo Tour hiện tại */}
<div className="relative min-w-[150px]">
<div className="px-3 py-2 bg-blue-50 text-blue-600 rounded-xl text-xs font-bold border border-blue-100 truncate max-w-[200px]">
{selectedTourIdForPhoto === 'all' ? 'Tất cả hành trình' : toursWithPhotos.find(t => t.id === selectedTourIdForPhoto)?.title || 'Tour đã chọn'}
</div>
</div>
@@ -161,9 +159,9 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
{/* Reset Filters - Nút xóa nhanh lọc */}
{(filterTourId || filterDate) && (
{(selectedTourIdForPhoto !== 'all' || filterDate) && (
<button
onClick={() => { setFilterTourId(''); setFilterDate(''); }}
onClick={() => { setSelectedTourIdForPhoto('all'); setFilterDate(''); }}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-bold text-red-500 hover:bg-red-50 rounded-xl transition-all"
>
<X className="w-3.5 h-3.5" />
@@ -184,116 +182,120 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
<Loader2 className="w-10 h-10 animate-spin mb-4" />
<p className="font-bold">Đang tải kho nh...</p>
</div>
) : filteredPhotos.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredPhotos.map((photo) => (
<div
key={photo.id}
onClick={() => setSelectedPhoto(photo)}
className="group relative bg-white rounded-3xl overflow-hidden shadow-sm border border-gray-100 transition-all hover:shadow-xl hover:-translate-y-1 cursor-pointer"
>
{/* Image Preview */}
<div className="aspect-square relative overflow-hidden bg-gray-100">
<img
src={photo.imageUrl || photo.originalUrl}
alt="My memory"
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
{!photo.imageUrl && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<span className="text-[10px] font-black text-white uppercase bg-red-500 px-2 py-1 rounded-lg">Tour đã xóa</span>
</div>
)}
{/* Overlay Actions */}
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
{photo.originalUrl && (
<a
onClick={(e) => e.stopPropagation()} // Ngăn chặn mở lightbox khi bấm tải xuống
href={photo.originalUrl}
download
className="p-3 bg-white text-blue-600 rounded-2xl shadow-xl hover:bg-blue-600 hover:text-white transition-all transform hover:scale-110"
title="Tải xuống ảnh gốc"
>
<Download className="w-5 h-5" />
</a>
)}
</div>
</div>
{/* Info */}
<div className="p-3">
<div className="flex items-center gap-1.5 mb-1 text-gray-400">
<MapPin className="w-3 h-3" />
<span className="text-[10px] font-bold truncate">
{photo.tour?.title || 'Không rõ hành trình'}
</span>
</div>
<div className="flex items-center gap-1.5 text-gray-300">
<Calendar className="w-3 h-3" />
<span className="text-[9px] font-medium italic">
{new Date(photo.capturedAt).toLocaleDateString('vi-VN')}
</span>
</div>
) : (
<div className="animate-in fade-in">
<div className="flex flex-col md:flex-row gap-4">
{/* Left Column: Tour List */}
<div className="md:w-1/4 bg-white rounded-2xl shadow-lg border border-gray-100 p-4 flex-shrink-0">
<h3 className="text-sm font-bold text-gray-800 mb-3">Tour của bạn</h3>
<div className="space-y-2">
<button
onClick={() => { setSelectedTourIdForPhoto('all'); setSelectedPhotoForDisplay(null); }}
className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold transition-all ${
selectedTourIdForPhoto === 'all' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Tất cả nh
</button>
{toursWithPhotos.map(tour => (
<button
key={tour.id}
onClick={() => { setSelectedTourIdForPhoto(tour.id); setSelectedPhotoForDisplay(null); }}
className={`w-full text-left px-3 py-2 rounded-xl text-xs font-bold transition-all ${
selectedTourIdForPhoto === tour.id ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{tour.title}
</button>
))}
</div>
</div>
))}
{/* Right Column: Large Photo Display */}
<div className="md:flex-1 bg-white rounded-2xl shadow-lg border border-gray-100 p-4 flex flex-col items-center justify-center min-h-[300px]">
{selectedPhotoForDisplay ? (
<div className="relative w-full h-full flex flex-col items-center justify-center">
<img
src={selectedPhotoForDisplay.imageUrl || selectedPhotoForDisplay.originalUrl}
alt="Selected Photo"
className="max-w-full max-h-[calc(100vh-350px)] object-contain rounded-xl shadow-md"
/>
<button
onClick={(e) => {
e.stopPropagation();
handleDeletePhoto(selectedPhotoForDisplay.id);
}}
className="absolute top-4 right-4 p-2 bg-red-500/80 backdrop-blur-sm text-white rounded-full shadow-lg hover:bg-red-600 transition-all active:scale-90"
title="Xóa ảnh này"
>
<Trash2 className="w-5 h-5" />
</button>
<div className="mt-6 w-full flex items-center justify-between px-2">
<div className="space-y-1">
<div className="flex items-center gap-2 text-gray-900 font-bold">
<MapPin className="w-4 h-4 text-blue-500" />
{selectedPhotoForDisplay.tour?.title || 'Không rõ hành trình'}
</div>
<div className="flex items-center gap-2 text-gray-400 text-xs font-medium uppercase tracking-wider">
<Calendar className="w-3.5 h-3.5" />
{new Date(selectedPhotoForDisplay.capturedAt).toLocaleDateString('vi-VN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</div>
</div>
{selectedPhotoForDisplay.originalUrl && (
<a
href={selectedPhotoForDisplay.originalUrl}
download
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-black uppercase tracking-widest text-[10px] transition-all shadow-lg shadow-blue-900/20 active:scale-95"
>
<Download className="w-4 h-4" /> Tải xuống nh gốc
</a>
)}
</div>
</div>
) : (
<div className="text-center text-gray-400 py-20">
<ImageIcon className="w-16 h-16 mx-auto mb-4 opacity-20" />
<p className="text-lg font-bold">Chọn một tấm nh đ xem</p>
</div>
)}
</div>
</div>
{/* Bottom Row: Thumbnails */}
<div className="mt-4 bg-white rounded-2xl shadow-lg border border-gray-100 p-4">
<div className="flex items-center justify-between mb-4 px-1">
<h3 className="text-xs font-black text-gray-400 uppercase tracking-widest">Kho nh ({filteredPhotos.length})</h3>
</div>
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 gap-3 max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
{filteredPhotos.map((photo: any) => (
<div
key={photo.id}
onClick={() => setSelectedPhotoForDisplay(photo)}
className={`aspect-square bg-gray-100 rounded-xl overflow-hidden relative group border-2 transition-all cursor-pointer ${
selectedPhotoForDisplay?.id === photo.id ? 'border-blue-500 scale-[0.98]' : 'border-transparent hover:border-blue-200'
}`}
>
<img
src={photo.imageUrl || photo.originalUrl}
alt="thumbnail"
className="w-full h-full object-cover"
/>
</div>
))}
</div>
</div>
</div>
) : (
) }
<div className="h-4"></div>
<div className="py-32 text-center bg-white rounded-[40px] border-2 border-dashed border-gray-100">
<ImageIcon className="w-16 h-16 text-gray-200 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-400">Chưa nh nào</h3>
<p className="text-sm text-gray-300">Hãy tham gia các chuyến đi lưu lại khoảnh khắc nhé!</p>
</div>
)}
{}
</div>
{/* Lightbox - Xem ảnh toàn màn hình */}
{selectedPhoto && (
<div
className="fixed inset-0 z-[7000] flex items-center justify-center bg-black/95 backdrop-blur-md p-4 animate-in fade-in duration-300"
onClick={() => setSelectedPhoto(null)}
>
<button
onClick={() => setSelectedPhoto(null)}
className="absolute top-6 right-6 p-3 bg-white/10 hover:bg-white/20 text-white rounded-full transition-all z-10"
>
<X className="w-6 h-6" />
</button>
{/* Nút xóa ảnh */}
<button
onClick={(e) => { e.stopPropagation(); handleDeletePhoto(selectedPhoto.id); }}
className="absolute top-6 left-6 p-3 bg-red-500/10 hover:bg-red-500/20 text-white rounded-full transition-all z-10"
>
<Trash2 className="w-6 h-6" />
</button>
<div className="relative max-w-5xl w-full max-h-[90vh] flex flex-col items-center" onClick={(e) => e.stopPropagation()}>
<img
src={selectedPhoto.originalUrl || selectedPhoto.imageUrl}
alt="Fullscreen view"
className="max-w-full max-h-[75vh] object-contain rounded-2xl shadow-2xl animate-in zoom-in-95 duration-300"
/>
<div className="mt-6 text-center text-white">
<h2 className="text-xl font-bold">{selectedPhoto.tour?.title || 'Không rõ hành trình'}</h2>
<p className="text-sm opacity-60 italic mt-1">{new Date(selectedPhoto.capturedAt).toLocaleDateString('vi-VN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</p>
{selectedPhoto.originalUrl && (
<a
href={selectedPhoto.originalUrl}
download
className="mt-6 inline-flex items-center gap-2 px-8 py-3.5 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-black uppercase tracking-widest text-xs transition-all shadow-lg shadow-blue-900/20 active:scale-95"
>
<Download className="w-4 h-4" />
Tải xuống nh gốc
</a>
)}
</div>
</div>
</div>
)}
</div>
);
};