fix: image gallery organized with sections and borders, images now show correctly
This commit is contained in:
+229
-23
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user