fix: bố cục lại cách hiển thị thông tin ảnh

This commit is contained in:
2026-06-20 08:04:12 +07:00
parent 5081546cdf
commit ac11bb56db
13 changed files with 749 additions and 393 deletions
+167 -76
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react';
import { ChevronLeft, Image as ImageIcon, Download, Calendar, MapPin, Loader2, Filter, X, Trash2, Edit } from 'lucide-react';
import { ChevronLeft, Image as ImageIcon, Download, Calendar, MapPin, Loader2, Filter, X, Trash2, Edit, Heart } from 'lucide-react';
import { useNotification } from '@/hooks/useNotification';
import { useConfirm } from '@/hooks/useConfirm';
import { CoordinateSelectModal } from '../components/CoordinateSelectModal';
@@ -21,6 +21,33 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
const [editLng, setEditLng] = useState<number | ''>('');
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [isMapOpen, setIsMapOpen] = useState(false);
const [resolvedAddress, setResolvedAddress] = useState<string>('');
useEffect(() => {
const lat = selectedPhotoForDisplay?.metadata?.lat;
const lng = selectedPhotoForDisplay?.metadata?.lng;
if (typeof lat === 'number' && typeof lng === 'number') {
setResolvedAddress('Đang xác định địa điểm...');
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=vi`)
.then(res => {
if (!res.ok) throw new Error();
return res.json();
})
.then(data => {
if (data && data.display_name) {
const shortAddress = data.display_name.split(',').slice(0, 3).join(',').trim();
setResolvedAddress(shortAddress || data.display_name);
} else {
setResolvedAddress(`${lat.toFixed(4)}, ${lng.toFixed(4)}`);
}
})
.catch(() => {
setResolvedAddress(`${lat.toFixed(4)}, ${lng.toFixed(4)}`);
});
} else {
setResolvedAddress('Chưa xác định tọa độ');
}
}, [selectedPhotoForDisplay?.id, selectedPhotoForDisplay?.metadata?.lat, selectedPhotoForDisplay?.metadata?.lng]);
useEffect(() => {
if (selectedPhotoForDisplay) {
@@ -75,6 +102,45 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
}
};
const currentUser = useMemo(() => {
const userStr = localStorage.getItem('user');
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch (e) {
return null;
}
}, []);
const likedUserIds = selectedPhotoForDisplay && selectedPhotoForDisplay.metadata && Array.isArray(selectedPhotoForDisplay.metadata.likedUserIds)
? selectedPhotoForDisplay.metadata.likedUserIds
: [];
const isLiked = currentUser && likedUserIds.includes(currentUser.id);
const likeCount = likedUserIds.length;
const handleToggleLike = async () => {
if (!selectedPhotoForDisplay) return;
const token = localStorage.getItem('token');
if (!token) return;
try {
const response = await fetch(`/api/v1/photos/${selectedPhotoForDisplay.id}/toggle-like`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const updatedPhoto = await response.json();
setPhotos(prev => prev.map(p => p.id === updatedPhoto.id ? { ...p, metadata: updatedPhoto.metadata } : p));
setSelectedPhotoForDisplay((prev: any) => prev ? ({ ...prev, metadata: updatedPhoto.metadata }) : null);
}
} catch (error) {
console.error('Lỗi khi thích ảnh:', error);
}
};
useEffect(() => {
const fetchPhotos = async () => {
try {
@@ -277,24 +343,105 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
<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="relative overflow-hidden rounded-xl shadow-md max-w-full max-h-[calc(100vh-350px)] group">
<img
src={selectedPhotoForDisplay.imageUrl || selectedPhotoForDisplay.originalUrl}
alt="Selected Photo"
className="max-w-full max-h-[calc(100vh-350px)] object-contain"
/>
{/* Overlays (Only show when not editing) */}
{!isEditing && (
<>
{/* Like (Heart) button overlay */}
<button
onClick={handleToggleLike}
className="absolute top-4 left-4 z-10 flex items-center gap-1.5 bg-black/60 hover:bg-black/75 border border-white/10 text-white font-bold py-1.5 px-3 rounded-full transition-all active:scale-95 text-[11px] backdrop-blur-md"
title={isLiked ? "Bỏ thích" : "Thích"}
>
<Heart className={`w-4 h-4 transition-colors ${
isLiked ? 'text-rose-500 fill-rose-500 animate-in zoom-in-75' : 'text-gray-300 hover:text-rose-450'
}`} />
<span>{likeCount}</span>
</button>
<div className="mt-6 w-full flex flex-col gap-4 px-2">
{isEditing ? (
{/* Delete button overlay */}
<button
onClick={(e) => {
e.stopPropagation();
handleDeletePhoto(selectedPhotoForDisplay.id);
}}
className="absolute top-4 right-4 z-10 p-2 bg-black/60 hover:bg-red-600 border border-white/10 text-white rounded-full transition-all active:scale-95 backdrop-blur-md"
title="Xóa ảnh này"
>
<Trash2 className="w-4 h-4" />
</button>
{/* Bottom metadata details gradient panel overlay */}
<div className="absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/95 via-black/50 to-transparent p-6 text-white flex flex-col gap-2">
<div className="flex justify-between items-start gap-4">
<div className="flex-1 min-w-0">
{selectedPhotoForDisplay.metadata?.title ? (
<h3 className="text-base font-extrabold text-white break-words drop-shadow-md">
{selectedPhotoForDisplay.metadata.title}
</h3>
) : (
<span className="text-xs text-gray-350 italic block mb-1 drop-shadow-md">Chưa tiêu đ</span>
)}
{selectedPhotoForDisplay.metadata?.description ? (
<p className="text-xs text-gray-250 leading-relaxed mt-1 break-words drop-shadow-sm max-h-16 overflow-y-auto no-scrollbar">
{selectedPhotoForDisplay.metadata.description}
</p>
) : (
<span className="text-[11px] text-gray-350 italic block mt-1 drop-shadow-sm">Chưa tả</span>
)}
</div>
{/* Edit Button Overlay */}
<button
onClick={() => setIsEditing(true)}
className="p-2 bg-white/10 hover:bg-white/20 border border-white/15 rounded-xl text-white hover:text-gray-200 transition-all shrink-0 backdrop-blur-sm"
title="Chỉnh sửa thông tin"
>
<Edit className="w-4 h-4" />
</button>
</div>
{/* Additional metadata info inside bottom overlay */}
<div className="flex flex-wrap items-center justify-between gap-4 border-t border-white/10 pt-3 text-xs text-gray-200">
<div className="space-y-1">
<div className="flex items-center gap-2 text-gray-300 text-[10px] font-bold uppercase tracking-wider">
<Calendar className="w-3.5 h-3.5" />
Ngày chụp: {new Date(selectedPhotoForDisplay.capturedAt).toLocaleDateString('vi-VN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</div>
<div className="flex items-center gap-2 font-bold" title={selectedPhotoForDisplay.metadata?.lat && selectedPhotoForDisplay.metadata?.lng ? `${selectedPhotoForDisplay.metadata.lat.toFixed(6)}, ${selectedPhotoForDisplay.metadata.lng.toFixed(6)}` : ''}>
<MapPin className="w-3.5 h-3.5 text-rose-500" />
Đa điểm: {resolvedAddress}
</div>
{selectedPhotoForDisplay.tour?.title && (
<div className="text-[10px] text-gray-400">
Hành trình: {selectedPhotoForDisplay.tour.title}
</div>
)}
</div>
{selectedPhotoForDisplay.originalUrl && (
<a
href={selectedPhotoForDisplay.originalUrl}
download
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold uppercase tracking-widest text-[9px] transition-all shadow-md active:scale-95 shrink-0"
>
<Download className="w-3.5 h-3.5 animate-pulse" /> Tải nh gốc
</a>
)}
</div>
</div>
</>
)}
</div>
{isEditing && (
<div className="mt-6 w-full flex flex-col gap-4 px-2">
<div className="space-y-3 bg-gray-50 border border-gray-150 p-4 rounded-2xl w-full">
<h4 className="text-xs font-black uppercase tracking-wider text-blue-605">Chỉnh sửa thông tin nh</h4>
@@ -382,64 +529,8 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
</button>
</div>
</div>
) : (
<div className="flex flex-col gap-3 w-full">
<div className="flex justify-between items-start gap-4">
<div className="flex-1">
{selectedPhotoForDisplay.metadata?.title ? (
<h3 className="text-base font-extrabold text-gray-900 break-words">
{selectedPhotoForDisplay.metadata.title}
</h3>
) : (
<span className="text-xs text-gray-400 italic block mb-1">Chưa tiêu đ</span>
)}
{selectedPhotoForDisplay.metadata?.description ? (
<p className="text-xs text-gray-600 leading-relaxed mt-1 break-words">
{selectedPhotoForDisplay.metadata.description}
</p>
) : (
<span className="text-[11px] text-gray-400 italic block mt-1">Chưa tả</span>
)}
</div>
<button
onClick={() => setIsEditing(true)}
className="p-2 bg-gray-100 hover:bg-gray-200 border border-gray-200 rounded-xl text-gray-500 hover:text-gray-700 transition-all shrink-0"
title="Chỉnh sửa thông tin"
>
<Edit className="w-4 h-4" />
</button>
</div>
<div className="flex flex-wrap items-center justify-between gap-4 border-t border-gray-100 pt-3">
<div className="space-y-1">
<div className="flex items-center gap-2 text-gray-900 font-bold text-xs">
<MapPin className="w-4 h-4 text-blue-500" />
{selectedPhotoForDisplay.tour?.title || 'Không rõ hành trình'}
{selectedPhotoForDisplay.metadata?.lat && selectedPhotoForDisplay.metadata?.lng && (
<span className="text-[11px] text-gray-400 font-normal ml-1">
({selectedPhotoForDisplay.metadata.lat.toFixed(4)}, {selectedPhotoForDisplay.metadata.lng.toFixed(4)})
</span>
)}
</div>
<div className="flex items-center gap-2 text-gray-450 text-[10px] font-bold 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-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold uppercase tracking-widest text-[9px] transition-all shadow-md active:scale-95 shrink-0"
>
<Download className="w-3.5 h-3.5" /> Tải nh gốc
</a>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
) : (
<div className="text-center text-gray-400 py-20">