fix: image gallery organized with sections and borders, images now show correctly

This commit is contained in:
2026-06-28 13:50:32 -05:00
parent e1b091b25d
commit 295c024ced
6 changed files with 521 additions and 122 deletions
+229 -23
View File
@@ -203,33 +203,239 @@ function SectionUploader({ category, room_id, label, onUploaded }: { category: s
);
}
/* ─── Image gallery filter buttons ─── */
function GalleryFilter({ images, onDelete, filter, onFilter }: { images: Image[]; onDelete: (id: number) => void; filter: string; onFilter: (f: string) => void }) {
const cats = ['all', 'rooms', 'slides', 'social', 'landscape', 'other'];
return (
<div>
<div style={{ display: 'flex', gap: '6px', marginBottom: '16px', flexWrap: 'wrap' }}>
{cats.map(c => (
<Btn key={c} onClick={() => onFilter(c)} variant={filter === c ? 'primary' : 'secondary'} size="sm">{c === 'all' ? 'All' : c.charAt(0).toUpperCase() + c.slice(1)}</Btn>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '12px' }}>
{images.filter(img => filter === 'all' || img.category === filter).map(img => (
<div key={img.id} style={{ position: 'relative', borderRadius: 12, overflow: 'hidden', boxShadow: C.shadow }}>
<img src={img.data} alt={img.filename} style={{ width: '100%', height: '90px', objectFit: 'cover', display: 'block' }} />
<div style={{ padding: '6px 8px', fontSize: '11px', color: C.text, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', background: C.card }}>
{img.filename}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '2px' }}>
<button onClick={() => onDelete(img.id)} style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: C.danger, fontSize: '16px', padding: '2px 6px' }}>×</button>
</div>
/* ─── Organized image gallery with sections and borders ─── */
function ImageGallery({ images, onDelete }: { images: Image[]; onDelete: (id: number) => void }) {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
const sectionCategories = [
{ key: 'slides', label: '🎞️ Slideshow Gallery', filter: (i: Image) => i.category === 'slides' || i.location_target === 'slides' },
{ key: 'calendar', label: '📅 Calendar Events Gallery', filter: (i: Image) => i.category === 'calendar' || i.location_target === 'calendar' },
{ key: 'social', label: '🎪 Social Events Gallery', filter: (i: Image) => i.category === 'social' || i.location_target === 'social' },
];
// Group room images by room location_target (e.g. "room-1", "room-2")
const roomImages = images.filter(
(i) => i.category === 'rooms' || i.location_target?.startsWith('room-')
);
const roomsByNumber: Record<string, Image[]> = roomImages.reduce((acc, img) => {
const key = img.location_target?.replace('room-', '') || 'uncategorized';
if (!acc[key]) acc[key] = [];
acc[key].push(img);
return acc;
}, {} as Record<string, Image[]>);
const filteredImages = images.filter((img) => {
if (selectedCategory === 'all') return true;
if (selectedCategory === 'rooms') return img.category === 'rooms' || img.location_target?.startsWith('room-');
return img.category === selectedCategory || img.location_target === selectedCategory;
});
const ImageCard = ({ img }: { img: Image }) => {
const isHov = hoveredCard === img.id;
return (
<div
style={{
position: 'relative', borderRadius: '10px', overflow: 'hidden',
boxShadow: isHov ? C.shadowHover : C.shadow,
border: `1px solid ${isHov ? C.gold : C.border}`,
transform: isHov ? 'translateY(-2px)' : 'none',
transition: 'all 200ms ease',
cursor: 'default',
}}
onMouseEnter={() => setHoveredCard(img.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<img
src={img.data}
alt={img.filename}
style={{
width: '100%', height: '110px', objectFit: 'cover', display: 'block',
}}
/>
<div style={{
padding: '6px 8px', fontSize: '11px', color: C.text,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
background: C.card,
}}>
{img.filename}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '4px' }}>
<button
onClick={(e) => { e.stopPropagation(); onDelete(img.id); }}
style={{
width: '24px', height: '24px', borderRadius: '6px',
border: '1px solid rgba(211,47,47,0.2)',
background: isHov ? 'rgba(211,47,47,0.15)' : 'transparent',
color: C.danger, fontSize: '14px', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 200ms',
}}
>
×
</button>
</div>
{/* Category label overlay on hover */}
{isHov && img.location_target && (
<div style={{
position: 'absolute', bottom: '30px', left: '4px', right: '4px',
padding: '3px 6px', fontSize: '10px', color: '#fff',
background: 'rgba(1,13,30,0.75)', borderRadius: '4px',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
textAlign: 'center',
}}>
{img.location_target}
</div>
))}
)}
</div>
);
};
const SectionBox = ({ title, count, children }: { title: string; count: number; children: React.ReactNode }) => (
<div style={{
borderRadius: '12px', overflow: 'hidden',
border: `1px solid ${C.border}`, background: '#fff',
boxShadow: C.shadow,
}}>
<div style={{
padding: '10px 16px',
background: C.goldLight,
borderBottom: `1px solid ${C.border}`,
fontWeight: 700, color: C.navy, fontSize: '14px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<span>{title}</span>
<span style={{
fontSize: '11px', padding: '2px 10px', borderRadius: '999px',
background: C.success, color: '#fff', fontWeight: 600,
}}>{count}</span>
</div>
<div style={{ padding: '16px' }}>
{children}
</div>
</div>
);
return (
<div>
{/* Category filter bar with borders */}
<div style={{
display: 'flex', gap: '8px', marginBottom: '20px', flexWrap: 'wrap',
padding: '12px 16px', borderRadius: '12px',
border: `1px solid ${C.border}`, background: '#fff',
boxShadow: C.shadow,
}}>
{['all', 'slides', 'calendar', 'social', 'rooms', 'other'].map((cat) => (
<Btn
key={cat}
onClick={() => setSelectedCategory(cat)}
variant={selectedCategory === cat ? 'primary' : 'secondary'}
size="sm"
>
{cat === 'all' ? 'All Images' : cat.charAt(0).toUpperCase() + cat.slice(1)}
</Btn>
))}
</div>
{selectedCategory === 'all' ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Static sections */}
{sectionCategories.map((section) => {
const imgs = images.filter(section.filter);
if (imgs.length === 0) return null;
return (
<SectionBox key={section.key} title={section.label} count={imgs.length}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: '12px',
}}>
{imgs.map((img) => <ImageCard key={img.id} img={img} />)}
</div>
</SectionBox>
);
})}
{/* Rooms section with room number subdivision */}
{Object.keys(roomsByNumber).length > 0 && (
<SectionBox title="🏨 Rooms Gallery" count={roomImages.length}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{Object.entries(roomsByNumber).map(([roomKey, imgs]) => (
<div key={roomKey}>
<div style={{
display: 'flex', alignItems: 'center', gap: '8px',
marginBottom: '10px', padding: '8px 12px',
background: '#FAFAFA', borderRadius: '8px',
border: `1px solid ${C.border}`,
}}>
<span style={{ fontSize: '13px', fontWeight: 600, color: C.navy }}>
🛏 Room {roomKey}
</span>
<span style={{
fontSize: '11px', padding: '2px 8px', borderRadius: '999px',
background: C.goldLight, color: C.gold, fontWeight: 600,
}}>{imgs.length} photo{imgs.length !== 1 ? 's' : ''}</span>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: '12px',
}}>
{imgs.map((img) => <ImageCard key={img.id} img={img} />)}
</div>
</div>
))}
</div>
</SectionBox>
)}
{/* Uncategorized/other */}
{(() => {
const otherImages = images.filter(
(i) => !sectionCategories.some(s => s.filter(i)) &&
!(i.category === 'rooms' || i.location_target?.startsWith('room-'))
);
if (otherImages.length === 0) return null;
return (
<SectionBox title="📁 Uncategorized" count={otherImages.length}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: '12px',
}}>
{otherImages.map((img) => <ImageCard key={img.id} img={img} />)}
</div>
</SectionBox>
);
})()}
</div>
) : (
<SectionBox title={
selectedCategory === 'rooms' ? '🏨 Rooms' :
selectedCategory === 'other' ? '📁 Other' :
selectedCategory.charAt(0).toUpperCase() + selectedCategory.slice(1)
} count={filteredImages.length}>
{filteredImages.length === 0 ? (
<div style={{ padding: '40px 20px', textAlign: 'center', color: C.textLight }}>
No images in this category
</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: '12px',
}}>
{filteredImages.map((img) => <ImageCard key={img.id} img={img} />)}
</div>
)}
</SectionBox>
)}
</div>
);
}
/* ─── Entity card ─── */
/* ─── Entity card ─── */
function EntityCard({ title, desc, meta, thumb, actions, photoUrls }: any) {
return (
@@ -769,7 +975,7 @@ function MenuSection({ items, setItems, images, onToast }: any) {
>
{/* Drag handle */}
<span style={{ fontSize: '18px', color: C.textLight, userSelect: 'none', flexShrink: 0, cursor: 'grab' }}
onMouseDown={(e) => { e.dataTransfer!.effectAllowed = 'move'; }}></span>
onMouseDown={() => {}}></span>
{/* Position number */}
<span style={{ fontSize: '13px', fontWeight: 700, color: C.textLight, width: '22px', textAlign: 'center', flexShrink: 0 }}>
@@ -1159,7 +1365,7 @@ export default function AdminPage() {
if (menuRes.ok) { const d = await menuRes.json(); setMenuItems(Array.isArray(d) ? d : (d.data || [])); }
if (placesRes.ok) { const d = await placesRes.json(); setPlaces(Array.isArray(d) ? d : []); }
if (reviewsRes.ok) { const d = await reviewsRes.json(); setReviews(Array.isArray(d) ? d : []); }
if (imagesRes?.ok) { const d = await imagesRes.json(); setImages(Array.isArray(d) ? d : []); }
if (imagesRes?.ok) { const d = await imagesRes.json(); setImages(Array.isArray(d) ? d : (d.images || [])); }
} catch (err) { console.error('Failed to load admin data:', err); }
};
fetchAll();