fix: sửa UI photoview hiển thị ở public
This commit is contained in:
Vendored
+2
-2
@@ -21,7 +21,7 @@
|
||||
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
|
||||
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
|
||||
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
|
||||
<script type="module" crossorigin src="/assets/index-CSiax1SI.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BtRV7JVM.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CMxvf4Kt.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-others-CNhtyHGs.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-BDwQQzB8.js">
|
||||
@@ -30,7 +30,7 @@
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-C3XQY6t9.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/vendor-maps-CcXJxNtP.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CIipVipb.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CLipHKhu.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -427,358 +427,308 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Left Side: Photo Detail */}
|
||||
<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>
|
||||
{/* ============================================================
|
||||
TIER 3: FIXED PHOTO FRAME VIEWPORT (z-20 / sticky on mobile)
|
||||
Aspect-square container. Does NOT resize on image swap.
|
||||
Desktop: left 3/5 column, absolute-fill with overlay metadata.
|
||||
============================================================ */}
|
||||
<div className="w-full aspect-square bg-slate-950 flex items-center justify-center sticky top-0 z-20 shrink-0 shadow-2xl border-b border-slate-900/60 md:relative md:aspect-auto md:w-3/5 md:h-full md:overflow-hidden md:border-b-0 group">
|
||||
|
||||
{/* Image fill */}
|
||||
<a
|
||||
href={`${window.location.origin}/api/v1/public-photos/${photo.id}/share`}
|
||||
className="absolute inset-0 flex items-center justify-center cursor-zoom-in"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsFullscreen(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo.imageUrl}
|
||||
alt="Public Map Upload"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className="w-full h-full object-contain select-none pointer-events-none"
|
||||
draggable={false}
|
||||
/>
|
||||
{(!isAuthorized || !isLoggedIn) && (
|
||||
<div className="absolute inset-0 bg-transparent select-none z-10" />
|
||||
)}
|
||||
</a>
|
||||
|
||||
{/* TIER 3.1: LOCATION & TIMESTAMP OVERLAY — bottom-left inside image (z-30) */}
|
||||
<div className="absolute bottom-3 left-3 z-30 flex flex-col gap-0.5 max-w-[70vw] md:max-w-[45%] pointer-events-none drop-shadow-[0_1px_3px_rgba(0,0,0,0.9)]">
|
||||
<div className="flex items-center gap-1.5 text-[10px] font-bold text-blue-300">
|
||||
<MapPin className="w-3.5 h-3.5 text-red-500 shrink-0" />
|
||||
<span className="truncate leading-tight">{resolvedAddress}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[9px] text-blue-200/80 pl-5">
|
||||
<Calendar className="w-2.5 h-2.5 text-blue-300/70 shrink-0" />
|
||||
<span>{new Date(photo.capturedAt).toLocaleDateString('vi-VN', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
})}</span>
|
||||
</div>
|
||||
|
||||
{/* Like Button Overlay */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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>
|
||||
|
||||
<a
|
||||
href={`${window.location.origin}/api/v1/public-photos/${photo.id}/share`}
|
||||
className="w-full h-auto max-h-[85vh] object-contain cursor-zoom-in md:h-full md:max-h-none flex items-center justify-center relative"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsFullscreen(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo.imageUrl}
|
||||
alt="Public Map Upload"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className={`w-full h-auto max-h-[85vh] object-contain md:h-full md:max-h-none select-none ${
|
||||
!isLoggedIn ? 'pointer-events-none' : ''
|
||||
}`}
|
||||
draggable={false}
|
||||
/>
|
||||
{/* Shield overlay to prevent Save Image As on right-click / long press for non-owners */}
|
||||
{(!isAuthorized || !isLoggedIn) && (
|
||||
<div className="absolute inset-0 bg-transparent select-none z-10" />
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Info & Timeline overlay inside photo panel */}
|
||||
<div className="relative z-20 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 */}
|
||||
{/* Like button — bottom-right on mobile, top-left on desktop */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleLike();
|
||||
}}
|
||||
className="absolute bottom-3 right-3 z-30 flex items-center gap-1 bg-black/55 backdrop-blur-sm border border-white/10 rounded-xl px-2.5 py-1.5 text-[11px] font-bold transition-all active:scale-95 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-3.5 h-3.5 transition-colors ${
|
||||
isLiked ? 'text-rose-500 fill-rose-500' : 'text-white'
|
||||
}`} />
|
||||
<span className="text-white">{likeCount}</span>
|
||||
</button>
|
||||
|
||||
{/* Uploader pill — top-left on mobile */}
|
||||
<div className="absolute top-[calc(0.75rem+env(safe-area-inset-top,0px))] left-3 z-30 md:hidden flex items-center gap-1.5 bg-black/55 backdrop-blur-sm px-2.5 py-1 rounded-full border border-white/10">
|
||||
<div className="w-4 h-4 rounded-full bg-emerald-500/30 flex items-center justify-center">
|
||||
<User className="w-2.5 h-2.5 text-emerald-300" />
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-200 max-w-[110px] truncate font-semibold">
|
||||
{photo.uploader?.name || 'Ẩn danh'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop: overlay metadata gradient at bottom of photo column */}
|
||||
<div className="hidden md:flex absolute bottom-0 left-0 right-0 z-20 px-6 pb-5 pt-12 bg-gradient-to-t from-black/70 via-black/20 to-transparent flex-col gap-1 pointer-events-none">
|
||||
<h4 className="text-sm font-black text-white tracking-tight leading-snug drop-shadow">
|
||||
{photo.metadata?.title || ''}
|
||||
</h4>
|
||||
{photo.metadata?.description && (
|
||||
<p className="text-xs text-slate-300 leading-snug line-clamp-2 drop-shadow">{photo.metadata.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-slate-300 mt-1">
|
||||
<MapPin className="w-3 h-3 text-rose-400 shrink-0" />
|
||||
<span className="truncate">{resolvedAddress}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-slate-400">
|
||||
<Calendar className="w-2.5 h-2.5 shrink-0" />
|
||||
<span>{new Date(photo.capturedAt).toLocaleDateString('vi-VN', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
})}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop thumbnail timeline carousel at the bottom */}
|
||||
{photoGroup && photoGroup.length > 1 && (
|
||||
<div className="hidden md:flex absolute bottom-0 left-0 right-0 z-20 px-4 pb-3 gap-2 overflow-x-auto no-scrollbar justify-end items-end pointer-events-auto">
|
||||
{photoGroup.map((p) => {
|
||||
const isActive = p.id === photo.id;
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={(e) => { e.stopPropagation(); onSelectPhoto?.(p); }}
|
||||
className={`relative w-10 h-10 rounded-xl overflow-hidden transition-all active:scale-95 shrink-0 ${
|
||||
isActive ? 'border-2 border-emerald-400 scale-105 shadow-lg' : 'border border-slate-600/50 opacity-70 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<img src={p.imageUrl} alt="thumb" className="w-full h-full object-cover pointer-events-none" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ============================================================
|
||||
TIER 2: SCROLLABLE COMMENTS PANEL (z-10)
|
||||
Slides BEHIND the sticky image frame when swiping up on mobile.
|
||||
Desktop: right 2/5 column with its own overflow-y-auto.
|
||||
============================================================ */}
|
||||
<div className="flex-1 overflow-y-auto relative z-10 flex flex-col bg-slate-900 border-t md:border-t-0 md:border-l border-slate-800 md:w-2/5 md:flex-1 md:min-h-0">
|
||||
|
||||
{/* ---- TITLE HEADER: replaces old "Lịch sử ảnh tại vị trí này" strip ---- */}
|
||||
<div className="px-5 pt-4 pb-0 bg-slate-900/95 backdrop-blur-md border-b border-slate-800/60 sticky top-0 z-10 flex flex-col gap-2">
|
||||
|
||||
{/* Row 1: Title + Edit button */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-sm font-black text-slate-100 tracking-wide leading-snug">
|
||||
{photo.metadata?.title || 'Chưa có tiêu đề'}
|
||||
</h2>
|
||||
{photo.metadata?.description && (
|
||||
<p className="text-[11px] text-slate-400 leading-relaxed mt-0.5 line-clamp-2">
|
||||
{photo.metadata.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isAuthorized && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
||||
className="p-1.5 bg-slate-800 hover:bg-slate-700 border border-slate-700/50 rounded-xl text-slate-300 hover:text-white transition-all shrink-0"
|
||||
title="Chỉnh sửa thông tin"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2: Thumbnail timeline strip — below title, mobile only */}
|
||||
{photoGroup && photoGroup.length > 1 && (
|
||||
<div className="flex flex-col gap-2 border-b border-slate-800/80 pb-3 relative z-20">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-400 flex items-center gap-1.5">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-500 animate-ping"></span>
|
||||
Lịch sử ảnh tại vị trí này ({photoGroup.length})
|
||||
</span>
|
||||
<div className="flex gap-3 overflow-x-auto no-scrollbar py-1 relative z-20">
|
||||
{photoGroup.map((p) => {
|
||||
const isActive = p.id === photo.id;
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPhoto?.(p);
|
||||
}}
|
||||
className={`relative w-12 h-12 rounded-xl overflow-hidden border-2 transition-all active:scale-95 shrink-0 ${
|
||||
isActive ? 'border-emerald-500 scale-110 shadow-lg' : 'border-slate-700 hover:border-slate-500'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={p.imageUrl}
|
||||
alt="Timeline thumbnail"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className={`w-full h-full object-cover ${
|
||||
!isLoggedIn ? 'pointer-events-none' : ''
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute bottom-0 inset-x-0 bg-slate-950/70 text-[8px] text-center font-bold text-slate-300 py-0.5">
|
||||
{new Date(p.capturedAt).toLocaleDateString('vi-VN', { month: '2-digit', day: '2-digit' })}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex gap-2 overflow-x-auto no-scrollbar pb-3 md:hidden">
|
||||
{photoGroup.map((p) => {
|
||||
const isActive = p.id === photo.id;
|
||||
const dateObj = new Date(p.capturedAt);
|
||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={(e) => { e.stopPropagation(); onSelectPhoto?.(p); }}
|
||||
className={`relative w-12 h-12 rounded-2xl overflow-hidden transition-all active:scale-95 shrink-0 ${
|
||||
isActive
|
||||
? 'border-[3px] border-emerald-500 shadow-lg shadow-emerald-500/25'
|
||||
: 'border-2 border-slate-600 hover:border-slate-400'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={p.imageUrl}
|
||||
alt="thumb"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className="w-full h-full object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="absolute bottom-0 inset-x-0 bg-slate-950/70 text-[7px] text-center font-black text-slate-200 py-px tracking-wide">
|
||||
{day}-{month}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
{/* Edit form (when editing) */}
|
||||
{isEditing && (
|
||||
<div className="px-5 py-4 border-b border-slate-800/60 bg-slate-900/95">
|
||||
<div className="flex flex-col gap-3 bg-slate-900/95 border border-slate-800 p-4 rounded-2xl animate-in slide-in-from-bottom-2">
|
||||
<h4 className="text-xs font-black uppercase tracking-wider text-emerald-400">Chỉnh sửa thông tin ảnh</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="block text-[9px] uppercase font-bold tracking-wider text-slate-400 mb-1">Tiêu đề</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
<input type="text" 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-base md: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>
|
||||
<label className="block text-[9px] uppercase font-bold tracking-wider text-slate-400 mb-1">Mô tả</label>
|
||||
<textarea
|
||||
value={editDescription}
|
||||
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-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500 resize-none"
|
||||
/>
|
||||
<textarea value={editDescription} 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-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500 resize-none" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-[9px] uppercase font-bold tracking-wider text-slate-400 mb-1">Vĩ độ</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={editLat}
|
||||
<input type="number" step="any" 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-base md: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>
|
||||
<label className="block text-[9px] uppercase font-bold tracking-wider text-slate-400 mb-1">Kinh độ</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={editLng}
|
||||
<input type="number" step="any" 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-base md: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>
|
||||
|
||||
<div className="flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsMapOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-800 hover:bg-slate-750 border border-slate-700/50 text-slate-300 hover:text-white rounded-xl text-[10px] font-bold transition-all"
|
||||
>
|
||||
<MapPin className="w-3.5 h-3.5 text-rose-500" />
|
||||
Chọn trên bản đồ
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setIsMapOpen(true); }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-800 hover:bg-slate-750 border border-slate-700/50 text-slate-300 hover:text-white rounded-xl text-[10px] font-bold transition-all">
|
||||
<MapPin className="w-3.5 h-3.5 text-rose-500" />
|
||||
Chọn trên bản đồ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
disabled={isSavingEdit}
|
||||
className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-bold transition-all"
|
||||
>
|
||||
<div className="flex justify-end gap-2 mt-1">
|
||||
<button onClick={(e) => { e.stopPropagation(); setIsEditing(false); }} disabled={isSavingEdit}
|
||||
className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-bold transition-all">
|
||||
Hủy
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSaveEdit();
|
||||
}}
|
||||
disabled={isSavingEdit}
|
||||
className="flex items-center gap-1 px-4 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 disabled:opacity-50 text-white rounded-lg text-xs font-bold transition-all"
|
||||
>
|
||||
{isSavingEdit ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
Đang lưu...
|
||||
</>
|
||||
) : (
|
||||
'Lưu lại'
|
||||
)}
|
||||
<button onClick={(e) => { e.stopPropagation(); handleSaveEdit(); }} disabled={isSavingEdit}
|
||||
className="flex items-center gap-1 px-4 py-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 disabled:opacity-50 text-white rounded-lg text-xs font-bold transition-all">
|
||||
{isSavingEdit ? (<><Loader2 className="w-3 h-3 animate-spin" />Đang lưu...</>) : 'Lưu lại'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Title & Description display */}
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
{photo.metadata?.title ? (
|
||||
<h4 className="text-sm font-black text-white tracking-tight leading-snug break-words">
|
||||
{photo.metadata.title}
|
||||
</h4>
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-500 italic block mb-1">Chưa có tiêu đề</span>
|
||||
)}
|
||||
{photo.metadata?.description ? (
|
||||
<p className="text-xs text-slate-300 leading-relaxed mt-1 max-h-20 overflow-y-auto no-scrollbar break-words">
|
||||
{photo.metadata.description}
|
||||
</p>
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-500 italic block mt-1">Chưa có mô tả</span>
|
||||
)}
|
||||
</div>
|
||||
{isAuthorized && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className="p-1.5 bg-slate-800 hover:bg-slate-700 border border-slate-700/50 rounded-xl text-slate-300 hover:text-white transition-all shrink-0"
|
||||
title="Chỉnh sửa thông tin"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Photo Metadata */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-t border-slate-800/80 pt-3 text-xs text-slate-350">
|
||||
<div className="space-y-1 text-left">
|
||||
<div className="flex items-center gap-1.5 text-slate-400">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
Ngày chụp: {new Date(photo.capturedAt).toLocaleDateString('vi-VN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-slate-400" title={photo.metadata?.lat && photo.metadata?.lng ? `${photo.metadata.lat.toFixed(6)}, ${photo.metadata.lng.toFixed(6)}` : ''}>
|
||||
<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 text-xs text-emerald-400">
|
||||
<User className="w-3.5 h-3.5" />
|
||||
Người đăng: {photo.uploader?.name || 'Ẩn danh'}
|
||||
</div>
|
||||
</div>
|
||||
{isAuthorized && photo.originalUrl && (
|
||||
<a
|
||||
href={photo.originalUrl}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Comments */}
|
||||
<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="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" />
|
||||
{t('commentSectionTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">Ảnh chia sẻ công khai trên bản đồ</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download original (authorized) */}
|
||||
{isAuthorized && photo.originalUrl && (
|
||||
<div className="px-5 py-2 border-b border-slate-800/40 flex justify-end">
|
||||
<a href={photo.originalUrl} download target="_blank" rel="noopener noreferrer"
|
||||
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
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---- COMMENTS HEADER (desktop) ---- */}
|
||||
<div className="hidden md:block px-6 py-4 border-b border-slate-800">
|
||||
<h3 className="text-base font-black tracking-tight text-white flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4 text-emerald-500" />
|
||||
{t('commentSectionTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-0.5">Ảnh chia sẻ công khai trên bản đồ</p>
|
||||
</div>
|
||||
|
||||
{/* Comments list scroll area */}
|
||||
<div className="md:flex-1 md:overflow-y-auto p-6 space-y-4 bg-slate-900/50">
|
||||
{/* ---- COMMENT FEED ---- */}
|
||||
<div className="p-4 space-y-4 flex-1">
|
||||
{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" />
|
||||
<span className="text-xs font-semibold">{t('loading')}</span>
|
||||
</div>
|
||||
) : comments.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500 gap-3">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-slate-500 gap-3">
|
||||
<div className="p-4 bg-slate-800/40 rounded-full text-slate-600">
|
||||
<MessageSquare className="w-8 h-8" />
|
||||
<MessageSquare className="w-7 h-7" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold italic">Chưa có bình luận nào. Hãy bắt đầu cuộc trò chuyện!</span>
|
||||
</div>
|
||||
) : (
|
||||
comments.map((c) => {
|
||||
return (
|
||||
<div key={c.id} className="flex gap-3 items-start animate-in fade-in slide-in-from-bottom-2 duration-200">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 border border-slate-700/60">
|
||||
<User className="w-4.5 h-4.5 text-slate-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="bg-slate-800/55 p-3.5 rounded-2xl rounded-tl-none border border-slate-800 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs font-black text-slate-200 truncate">{c.userName}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[9px] font-medium text-slate-500">
|
||||
{new Date(c.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
{(currentUser?.isAdmin ||
|
||||
currentUser?.id === c.userId ||
|
||||
currentUser?.id === photo.uploaderId ||
|
||||
currentUser?.id === photo.uploader?.id) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteComment(c.id);
|
||||
}}
|
||||
className="text-slate-500 hover:text-rose-500 transition-colors p-0.5"
|
||||
title={t('delete') || "Xóa"}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
comments.map((c) => (
|
||||
<div key={c.id} className="flex gap-3 items-start animate-in fade-in slide-in-from-bottom-2 duration-200">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center flex-shrink-0 border border-slate-700/60">
|
||||
<User className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="bg-slate-800/55 p-3.5 rounded-2xl rounded-tl-none border border-slate-800 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs font-black text-slate-200 truncate">{c.userName}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[9px] font-medium text-slate-500">
|
||||
{new Date(c.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
{(currentUser?.isAdmin || currentUser?.id === c.userId ||
|
||||
currentUser?.id === photo.uploaderId || currentUser?.id === photo.uploader?.id) && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteComment(c.id); }}
|
||||
className="text-slate-500 hover:text-rose-500 transition-colors p-0.5"
|
||||
title={t('delete') || "Xóa"}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-300 leading-relaxed break-words">{c.content}</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-300 leading-relaxed break-words">{c.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
<div ref={commentsEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Comment Input Area */}
|
||||
<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">
|
||||
{/* ---- STICKY COMMENT INPUT TRAY BAR (z-20 stays above comments) ---- */}
|
||||
<div className="sticky bottom-0 z-20 p-4 bg-slate-950 border-t border-slate-800/80 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] md:pb-4">
|
||||
<div className="relative flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -790,22 +740,19 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
disabled={isSending}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSend();
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
||||
disabled={!newComment.trim() || isSending}
|
||||
className="p-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 disabled:opacity-40 text-white rounded-2xl transition-all active:scale-95 shadow-lg shadow-emerald-950/30 flex items-center justify-center"
|
||||
className="p-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 disabled:opacity-40 text-white rounded-2xl transition-all active:scale-95 shadow-lg flex items-center justify-center"
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="w-4.5 h-4.5 animate-spin" />
|
||||
) : (
|
||||
<Send className="w-4.5 h-4.5" />
|
||||
)}
|
||||
{isSending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user