fix: sửa lỗi hiển thị trên browser của điện thoại
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+42
-42
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<title>Travel Planner</title>
|
||||
<script type="module" crossorigin src="/assets/index-UWqs1Nt4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-I6bJ9cOX.css">
|
||||
<script type="module" crossorigin src="/assets/index-DvhesUei.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-i0kJVU1C.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -355,7 +355,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[5000] flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 z-[5000] md:flex md:items-center md:justify-center md:p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-950/80 backdrop-blur-md animate-in fade-in duration-300"
|
||||
@@ -363,38 +363,53 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
/>
|
||||
|
||||
{/* Container */}
|
||||
<div className="relative w-full max-w-5xl h-[90vh] md:h-[85vh] bg-slate-900 border border-slate-800 rounded-[32px] shadow-2xl overflow-y-auto md:overflow-hidden flex flex-col md:flex-row animate-in zoom-in-95 duration-300 text-slate-100">
|
||||
<div className="fixed inset-0 overflow-y-auto flex flex-col bg-slate-900 text-slate-100 md:relative md:inset-auto md:overflow-hidden md:rounded-[32px] md:shadow-2xl md:border md:border-slate-800 md:w-full md:max-w-5xl md:h-[85vh] md:flex-row animate-in zoom-in-95 duration-300">
|
||||
|
||||
{/* Close Button Mobile/Desktop */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="fixed md:absolute top-6 right-6 md:top-4 md:right-4 z-50 p-2 bg-slate-950/60 hover:bg-slate-800/80 border border-slate-700/50 rounded-full text-slate-300 hover:text-white transition-colors"
|
||||
className="fixed md:absolute top-[calc(1rem+env(safe-area-inset-top,0px))] right-4 md:top-4 md:right-4 z-50 p-2 bg-slate-950/60 hover:bg-slate-800/80 border border-slate-700/50 rounded-full text-slate-300 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Left Side: Photo Detail */}
|
||||
<div className="relative w-full md:w-3/5 h-[45vh] md:h-full bg-slate-950 flex items-center justify-center overflow-hidden group shrink-0">
|
||||
{/* Like Button Overlay */}
|
||||
<button
|
||||
onClick={handleToggleLike}
|
||||
className="absolute top-4 left-4 z-40 flex items-center gap-1.5 bg-slate-950/70 hover:bg-slate-900/80 border border-slate-700/50 text-slate-200 hover: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-slate-350 hover:text-rose-450'
|
||||
}`} />
|
||||
<span>{likeCount}</span>
|
||||
</button>
|
||||
<div className="relative w-full md:w-3/5 md:h-full bg-slate-950 flex flex-col overflow-hidden group shrink-0">
|
||||
|
||||
{/* Photo wrapper for mobile view (handles top overlay name and bottom-right like) */}
|
||||
<div className="relative w-full flex items-center justify-center md:absolute md:inset-0 md:flex md:items-center md:justify-center bg-slate-950">
|
||||
{/* Mobile Only: Uploader details overlay */}
|
||||
<div className="absolute top-[calc(0.75rem+env(safe-area-inset-top,0px))] left-4 z-40 md:hidden flex items-center gap-2 bg-slate-950/70 backdrop-blur-md px-2.5 py-1.5 rounded-full border border-slate-700/50">
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-500/20 flex items-center justify-center">
|
||||
<User className="w-3 h-3 text-emerald-400" />
|
||||
</div>
|
||||
<span className="text-xs text-slate-200 max-w-[120px] truncate">
|
||||
{photo.uploader?.name || 'Ẩn danh'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Like Button Overlay */}
|
||||
<button
|
||||
onClick={handleToggleLike}
|
||||
className="absolute bottom-4 right-4 z-40 flex items-center gap-1.5 bg-slate-950/70 hover:bg-slate-900/80 border border-slate-700/50 text-slate-200 hover:text-white font-bold py-1.5 px-3 rounded-full transition-all active:scale-95 text-[11px] backdrop-blur-md md:absolute md:top-[calc(1rem+env(safe-area-inset-top,0px))] md:left-4 md:bottom-auto md:right-auto"
|
||||
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-slate-350 hover:text-rose-450'
|
||||
}`} />
|
||||
<span>{likeCount}</span>
|
||||
</button>
|
||||
|
||||
<img
|
||||
src={photo.imageUrl}
|
||||
alt="Public Map Upload"
|
||||
className="w-full h-auto max-h-[85vh] object-contain cursor-zoom-in md:h-full md:max-h-none"
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={photo.imageUrl}
|
||||
alt="Public Map Upload"
|
||||
className="w-full h-full object-contain cursor-zoom-in"
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
/>
|
||||
{/* Info & Timeline overlay inside photo panel */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/60 via-black/20 to-transparent flex flex-col gap-4">
|
||||
<div className="relative p-6 bg-slate-900 border-b border-slate-800/60 md:absolute md:bottom-0 md:left-0 md:right-0 md:bg-gradient-to-t md:from-black/60 md:via-black/20 md:to-transparent md:border-b-0 flex flex-col gap-4">
|
||||
|
||||
{/* Timeline scroll */}
|
||||
{photoGroup && photoGroup.length > 1 && (
|
||||
@@ -437,7 +452,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
placeholder="Nhập tiêu đề cho ảnh..."
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -448,7 +463,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
onChange={(e) => setEditDescription(e.target.value)}
|
||||
placeholder="Mô tả bức ảnh này..."
|
||||
rows={2}
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500 resize-none"
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -461,7 +476,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
value={editLat}
|
||||
onChange={(e) => setEditLat(e.target.value === '' ? '' : parseFloat(e.target.value))}
|
||||
placeholder="Vĩ độ..."
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -472,7 +487,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
value={editLng}
|
||||
onChange={(e) => setEditLng(e.target.value === '' ? '' : parseFloat(e.target.value))}
|
||||
placeholder="Kinh độ..."
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -561,7 +576,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
<MapPin className="w-3.5 h-3.5 text-rose-500" />
|
||||
Địa điểm: {resolvedAddress}
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-1.5 font-semibold text-emerald-400">
|
||||
<div className="hidden md:flex items-center gap-1.5 text-xs text-emerald-400">
|
||||
<User className="w-3.5 h-3.5" />
|
||||
Người đăng: {photo.uploader?.name || 'Ẩn danh'}
|
||||
</div>
|
||||
@@ -572,7 +587,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden md:flex items-center gap-1.5 bg-slate-800/80 hover:bg-slate-700/80 border border-slate-700/50 text-slate-200 hover:text-white font-bold py-1 px-3 rounded-full transition-all active:scale-95 text-[10px]"
|
||||
className="flex items-center gap-1.5 bg-slate-800/80 hover:bg-slate-700/80 border border-slate-700/50 text-slate-200 hover:text-white font-bold py-1 px-3 rounded-full transition-all active:scale-95 text-[10px]"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 text-emerald-500" />
|
||||
Tải ảnh gốc
|
||||
@@ -594,39 +609,11 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Only: Uploader details & divider */}
|
||||
<div className="block md:hidden p-6 bg-slate-900 border-t border-slate-800/60 space-y-4 shrink-0">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2.5 text-left">
|
||||
<div className="p-2.5 bg-emerald-500/10 rounded-xl text-emerald-400">
|
||||
<User className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-450 block uppercase font-bold tracking-wider">Người đăng ảnh</span>
|
||||
<span className="text-sm font-semibold text-white">{photo.uploader?.name || 'Ẩn danh'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{isAuthorized && photo.originalUrl && (
|
||||
<a
|
||||
href={photo.originalUrl}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 bg-slate-850 hover:bg-slate-800 border border-slate-700 text-slate-200 hover:text-white font-bold py-2 px-4 rounded-xl transition-all text-xs"
|
||||
>
|
||||
<Download className="w-4 h-4 text-emerald-500" />
|
||||
Tải ảnh gốc
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t border-slate-800/60 pt-2" />
|
||||
</div>
|
||||
|
||||
{/* Right Side: Comments */}
|
||||
<div className="w-full md:w-2/5 h-auto md:h-full flex flex-col bg-slate-900 border-t md:border-t-0 md:border-l border-slate-800">
|
||||
<div className="w-full md:w-2/5 md:flex-1 md:min-h-0 flex flex-col bg-slate-900 border-t md:border-t-0 md:border-l border-slate-800">
|
||||
|
||||
{/* Comments Header */}
|
||||
<div className="p-6 border-b border-slate-800 flex items-center justify-between">
|
||||
<div className="hidden md:block p-6 border-b border-slate-800">
|
||||
<div>
|
||||
<h3 className="text-lg font-black tracking-tight text-white flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5 text-emerald-500" />
|
||||
@@ -637,7 +624,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Comments list scroll area */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4 bg-slate-900/50">
|
||||
<div className="md:flex-1 md:overflow-y-auto p-6 space-y-4 bg-slate-900/50">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-slate-500 gap-2">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-emerald-500" />
|
||||
@@ -676,7 +663,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Comment Input Area */}
|
||||
<div className="p-4 bg-slate-950/40 border-t border-slate-800/80">
|
||||
<div className="sticky bottom-0 md:static p-4 bg-slate-950 md:bg-slate-950/40 border-t border-slate-800/80 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] md:pb-4 z-40">
|
||||
<div className="relative flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -684,7 +671,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && !isSending && handleSend()}
|
||||
placeholder="Viết bình luận công khai..."
|
||||
className="flex-1 bg-slate-800/65 border border-slate-700/70 text-slate-100 placeholder-slate-500 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all"
|
||||
className="flex-1 bg-slate-800/65 border border-slate-700/70 text-slate-100 placeholder-slate-500 rounded-2xl px-4 py-3 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all"
|
||||
disabled={isSending}
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -94,6 +94,14 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
||||
groups[key].push(photo);
|
||||
}
|
||||
});
|
||||
// Sort each group by likes descending
|
||||
Object.values(groups).forEach((group) => {
|
||||
group.sort((a, b) => {
|
||||
const likesA = a.metadata?.likedUserIds?.length || 0;
|
||||
const likesB = b.metadata?.likedUserIds?.length || 0;
|
||||
return likesB - likesA;
|
||||
});
|
||||
});
|
||||
return Object.values(groups);
|
||||
}, [publicPhotos]);
|
||||
|
||||
|
||||
@@ -75,7 +75,12 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
const res = await fetch('/api/v1/public-photos');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setPublicPhotos(data);
|
||||
const sortedData = data.sort((a: any, b: any) => {
|
||||
const likesA = a.metadata?.likedUserIds?.length || 0;
|
||||
const likesB = b.metadata?.likedUserIds?.length || 0;
|
||||
return likesB - likesA;
|
||||
});
|
||||
setPublicPhotos(sortedData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Lỗi khi tải ảnh công khai:', e);
|
||||
|
||||
@@ -4,6 +4,15 @@ import { useNotification } from '@/hooks/useNotification';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { CoordinateSelectModal } from '../components/CoordinateSelectModal';
|
||||
|
||||
const getMostLikedPhoto = (photos: any[]) => {
|
||||
if (!photos || photos.length === 0) return null;
|
||||
return [...photos].sort((a, b) => {
|
||||
const likesA = a.metadata?.likedUserIds?.length || 0;
|
||||
const likesB = b.metadata?.likedUserIds?.length || 0;
|
||||
return likesB - likesA;
|
||||
})[0];
|
||||
};
|
||||
|
||||
export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
|
||||
const [photos, setPhotos] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -195,7 +204,7 @@ export const MyPhotosPage = ({ onBack }: { onBack: () => void }) => {
|
||||
// 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]);
|
||||
setSelectedPhotoForDisplay(getMostLikedPhoto(filteredPhotos));
|
||||
} else if (filteredPhotos.length === 0) {
|
||||
setSelectedPhotoForDisplay(null);
|
||||
} else if (selectedPhotoForDisplay) {
|
||||
|
||||
@@ -329,6 +329,15 @@ const MapContextMenu = ({ onAction }: { onAction: (action: string, latlng: L.Lat
|
||||
);
|
||||
};
|
||||
|
||||
const getMostLikedPhoto = (photos: any[]) => {
|
||||
if (!photos || photos.length === 0) return null;
|
||||
return [...photos].sort((a, b) => {
|
||||
const likesA = a.metadata?.likedUserIds?.length || 0;
|
||||
const likesB = b.metadata?.likedUserIds?.length || 0;
|
||||
return likesB - likesA;
|
||||
})[0];
|
||||
};
|
||||
|
||||
export const TourDetailPage = ({
|
||||
onBack,
|
||||
tourId,
|
||||
@@ -682,9 +691,9 @@ export const TourDetailPage = ({
|
||||
// Effect to set initial selected photo for display or reset if current one is no longer in filtered list
|
||||
useEffect(() => {
|
||||
if (filteredPhotos.length > 0 && !selectedPhotoForDisplay) {
|
||||
setSelectedPhotoForDisplay(filteredPhotos[0]);
|
||||
setSelectedPhotoForDisplay(getMostLikedPhoto(filteredPhotos));
|
||||
} else if (selectedPhotoForDisplay && !filteredPhotos.some(p => p.id === selectedPhotoForDisplay.id)) {
|
||||
setSelectedPhotoForDisplay(filteredPhotos.length > 0 ? filteredPhotos[0] : null);
|
||||
setSelectedPhotoForDisplay(filteredPhotos.length > 0 ? getMostLikedPhoto(filteredPhotos) : null);
|
||||
}
|
||||
}, [filteredPhotos, selectedPhotoForDisplay]);
|
||||
const [isCommentModalOpen, setIsCommentModalOpen] = useState(false);
|
||||
@@ -835,7 +844,7 @@ export const TourDetailPage = ({
|
||||
if (currentTour && currentTour.photos && currentTour.photos.length > 0) {
|
||||
// If there are photos, and no leg is selected, default to 'all' and first photo
|
||||
if (selectedLegIdForPhoto === 'all' && !selectedPhotoForDisplay) {
|
||||
setSelectedPhotoForDisplay(currentTour.photos[0]);
|
||||
setSelectedPhotoForDisplay(getMostLikedPhoto(currentTour.photos));
|
||||
} else if (selectedLegIdForPhoto !== 'all') {
|
||||
// If a specific leg is selected, try to find a photo for that leg
|
||||
const targetLeg = legs.find(l => l.id === selectedLegIdForPhoto);
|
||||
@@ -843,9 +852,9 @@ export const TourDetailPage = ({
|
||||
const locationIdsInLeg = targetLeg.locations.map((loc: any) => loc.id);
|
||||
const photosInLeg = currentTour.photos.filter((p: any) => p.locationId && locationIdsInLeg.includes(p.locationId));
|
||||
if (photosInLeg.length > 0 && !selectedPhotoForDisplay) {
|
||||
setSelectedPhotoForDisplay(photosInLeg[0]);
|
||||
setSelectedPhotoForDisplay(getMostLikedPhoto(photosInLeg));
|
||||
} else if (selectedPhotoForDisplay && !photosInLeg.some(p => p.id === selectedPhotoForDisplay.id)) {
|
||||
setSelectedPhotoForDisplay(photosInLeg.length > 0 ? photosInLeg[0] : null);
|
||||
setSelectedPhotoForDisplay(photosInLeg.length > 0 ? getMostLikedPhoto(photosInLeg) : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1345,7 +1354,7 @@ export const TourDetailPage = ({
|
||||
date: tourDateDisplay,
|
||||
membersCount: currentTour?.participants?.length || 0,
|
||||
budget: currentTour?.totalCost ? `${Number(currentTour.totalCost).toLocaleString()} VND` : "0 VND",
|
||||
coverImage: currentTour?.photos?.[0]?.imageUrl || "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?q=80&w=2070&auto=format&fit=crop"
|
||||
coverImage: getMostLikedPhoto(currentTour?.photos || [])?.imageUrl || "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?q=80&w=2070&auto=format&fit=crop"
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user