'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
type Image = { id: number; filename: string; data: string; category: string; room_id: number | null; location_target: string; uploaded_at: string };
type Slide = { id: number; title: string; subtitle: string; image_url: string | null; image_id: number | null; active: boolean; sort_order: number };
type Room = { id: number; name: string; description: string; price: string; photos: string[]; featured_photo: string | null; active: boolean; sort_order: number };
type CalendarEvent = { id: number; date: string; title: string; type: string; color: string; description: string | null; active: boolean };
type SocialEvent = { id: number; name: string; description: string; price: string | null; date: string | null; photos: string[]; featured_photo: string | null; active: boolean; sort_order: number };
type MenuItem = { id: number; category: string; name: string; description: string; price: string; is_daily_special: boolean; active: boolean; sort_order: number };
type Place = { id: number; name: string; description: string; photos: string[]; featured_photo: string | null; distance_km: string; visit_time: string; suggestion: string; active: boolean; sort_order: number };
type Review = { id: number; author_name: string; author_email: string | null; rating: number; text: string; approved: boolean };
type FooterConfig = { logo: string; favicon: string; tagline: string; facebook_url: string; instagram_url: string; whatsapp_url: string; address: string; phone: string; email: string };
type SocialRes = { id: number; event_id: number; username: string; event_name: string; guests: number; notes: string; status: string; created_at: string };
const C = {
gold: '#D4A853', goldLight: 'rgba(212,168,83,0.12)', navy: '#010D1E',
warm: '#7A5A3A', bg: '#F7F4EF', card: '#FFFFFF', border: 'rgba(1,13,30,0.08)',
text: '#444444', textLight: '#888888', danger: '#D32F2F', success: '#388E3C',
shadow: '0 1px 4px rgba(1,13,30,0.06)', shadowHover: '0 6px 20px rgba(1,13,30,0.12)',
};
const NAV = [
{ id: 'brand', label: 'Brand', icon: 'π¨' },
{ id: 'gallery', label: 'Images', icon: 'πΌοΈ' },
{ id: 'slides', label: 'Slideshow', icon: 'ποΈ' },
{ id: 'calendar', label: 'Calendar', icon: 'π
' },
{ id: 'social', label: 'Social Events', icon: 'πͺ' },
{ id: 'rooms', label: 'Rooms', icon: 'π¨' },
{ id: 'menu', label: 'Menu', icon: 'π½οΈ' },
{ id: 'places', label: 'Places', icon: 'πΊοΈ' },
{ id: 'reviews', label: 'Reviews', icon: 'β' },
{ id: 'users', label: 'Users', icon: 'π₯' },
{ id: 'weather', label: 'Weather', icon: 'π€οΈ' },
];
function I({ label, value, onChange, type = 'text', placeholder, style }: any) {
return (
{label && }
onChange(e.target.value)} placeholder={placeholder}
style={{ padding: '8px 12px', borderRadius: 8, border: `1px solid ${C.border}`, fontSize: '14px', outline: 'none', background: '#FAFAFA', ...style }}
onFocus={e => { (e.target.style as any).borderColor = C.gold; (e.target.style as any).background = '#FFFFFF'; }}
onBlur={e => { (e.target.style as any).borderColor = C.border; (e.target.style as any).background = '#FAFAFA'; }} />
);
}
function TA({ label, value, onChange, rows = 3 }: any) {
return (
{label && }
);
}
function S({ label, value, onChange, options, style }: any) {
return (
{label && }
);
}
function Tgl({ label, checked, onChange }: any) {
return (
);
}
function Btn({ children, onClick, variant = 'primary', size = 'md', disabled }: any) {
const sz = size === 'sm' ? { padding: '5px 10px', fontSize: '12px' } : { padding: '8px 16px', fontSize: '14px' };
const bg = variant === 'danger' ? C.danger : variant === 'secondary' ? '#E8E4DF' : C.gold;
const color = variant === 'secondary' ? C.text : '#fff';
return (
);
}
function Badge({ children, color: c }: any) {
return (
{children}
);
}
function Toast({ message, type, onClose }: any) {
useEffect(() => { const t = setTimeout(onClose, 3500); return () => clearTimeout(t); }, [onClose]);
return (
{message}
);
}
function StatCard({ icon, value, label }: any) {
return (
);
}
function Empty({ icon, title, desc }: any) {
return (
{icon}
{title}
{desc &&
{desc}
}
);
}
function SecHead({ title, count, action }: any) {
return (
{title} ({count})
{action}
);
}
/* βββ Image helpers βββ */
function readFileBase64(file: File): Promise {
return new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(r.result as string);
r.onerror = rej;
r.readAsDataURL(file);
});
}
/* βββ Entity thumbnail βββ */
function Thumb({ urls, w = 200, h = 140 }: { urls: string[]; w?: number; h?: number }) {
if (!urls?.length) return No image
;
return
;
}
/* βββ Thumbnail grid (inline, small) βββ */
function MiniThumbs({ urls, max = 3 }: { urls: string[]; max?: number }) {
if (!urls?.length) return null;
return (
{urls.slice(0, max).map((u, i) => (

))}
{urls.length > max &&
+{urls.length - max}}
);
}
/* βββ Section uploader (per-section image upload) βββ */
function SectionUploader({ category, room_id, label, onUploaded }: { category: string; room_id: number | null; label: string; onUploaded?: () => void }) {
const ref = useRef(null);
return (
<>
ref.current?.click()} size="sm">{label}
{
for (const file of Array.from(e.target.files || [])) {
const b64 = await readFileBase64(file);
try {
const res = await fetch('/api/admin/images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, data: b64, category, room_id, location_target: category }),
});
if (res.ok) onUploaded?.();
} catch (err) { console.error(err); }
}
e.target.value = '';
}} />
>
);
}
/* βββ Organized image gallery with sections and borders βββ */
function ImageGallery({ images, onDelete }: { images: Image[]; onDelete: (id: number) => void }) {
const [selectedCategory, setSelectedCategory] = useState('all');
const [hoveredCard, setHoveredCard] = useState(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 = 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);
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 (
setHoveredCard(img.id)}
onMouseLeave={() => setHoveredCard(null)}
>
{img.filename}
{/* Category label overlay on hover */}
{isHov && img.location_target && (
{img.location_target}
)}
);
};
const SectionBox = ({ title, count, children }: { title: string; count: number; children: React.ReactNode }) => (
{title}
{count}
{children}
);
return (
{/* Category filter bar with borders */}
{['all', 'slides', 'calendar', 'social', 'rooms', 'other'].map((cat) => (
setSelectedCategory(cat)}
variant={selectedCategory === cat ? 'primary' : 'secondary'}
size="sm"
>
{cat === 'all' ? 'All Images' : cat.charAt(0).toUpperCase() + cat.slice(1)}
))}
{selectedCategory === 'all' ? (
{/* Static sections */}
{sectionCategories.map((section) => {
const imgs = images.filter(section.filter);
if (imgs.length === 0) return null;
return (
{imgs.map((img) => )}
);
})}
{/* Rooms section with room number subdivision */}
{Object.keys(roomsByNumber).length > 0 && (
{Object.entries(roomsByNumber).map(([roomKey, imgs]) => (
ποΈ Room {roomKey}
{imgs.length} photo{imgs.length !== 1 ? 's' : ''}
{imgs.map((img) => )}
))}
)}
{/* 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 (
{otherImages.map((img) => )}
);
})()}
) : (
{filteredImages.length === 0 ? (
No images in this category
) : (
{filteredImages.map((img) => )}
)}
)}
);
}
/* βββ Entity card βββ */
/* βββ Entity card βββ */
function EntityCard({ title, desc, meta, thumb, actions, photoUrls }: any) {
return (
{ e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = C.shadowHover; }}
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = C.shadow; }}>
{thumb ? (

) : photoUrls?.length ?
:
π·
}
{title}
{desc &&
{desc}
}
{meta &&
{meta}
}
{photoUrls?.length ?
: null}
{actions &&
{actions}
}
);
}
/* βββ Confirm dialog βββ */
function Confirm({ message, onConfirm, onCancel }: any) {
return (
);
}
/* βββ Slides Section βββ */
function SlidesSection({ slides, setSlides, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const saveSlide = async (slide: Slide) => {
try {
const res = await fetch(`/api/slides/${slide.id}`, {
method: slide.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(slide),
});
if (res.ok) { setEditing(null); setSlides((s: any) => s.map((s: Slide) => s.id === slide.id ? slide : s)); onToast('Slide saved!', 'success'); }
} catch { onToast('Error saving slide', 'error'); }
};
const delSlide = async (id: number) => {
try {
await fetch(`/api/slides/${id}`, { method: 'DELETE' });
setSlides((s: any) => s.filter((x: Slide) => x.id !== id));
onToast('Slide deleted', 'success');
} catch { onToast('Error deleting slide', 'error'); }
setConfirmDel(null);
};
return (
setEditing({ id: 0, title: '', subtitle: '', image_url: null, image_id: null, active: true, sort_order: slides.length })} size="sm">+ New Slide} />
{slides.length === 0 ? : (
{slides.map((s: Slide) => (
i.id === s.image_id)?.data) : null}
actions={
<>
setEditing(s)}>Edit
setConfirmDel(s.id)}>Del
>
} />
))}
)}
{editing && (
setEditing(null)} />
)}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => delSlide(confirmDel)} />}
);
}
function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) {
const [showPicker, setShowPicker] = useState(false);
const s = { ...slide };
return (
{s.id ? 'Edit Slide' : 'New Slide'}
setSlide({ ...s, title: v })} />
setSlide({ ...s, subtitle: v })} />
setShowPicker(!showPicker)} size="sm" variant="secondary">{showPicker ? 'Hide picker' : 'Pick from gallery'}
{s.image_id && images.find((i: Image) => i.id === s.image_id) && (

i.id === s.image_id)!.data} style={{ width: 80, height: 56, objectFit: 'cover', borderRadius: 8, border: `2px solid ${C.gold}` }} />
)}
{showPicker && (
{images.filter((i: Image) => i.category === 'slides').map((img: Image) => (

{ setSlide({ ...s, image_id: img.id, image_url: img.data }); setShowPicker(false); }}
style={{ width: 64, height: 48, objectFit: 'cover', borderRadius: 6, cursor: 'pointer', border: s.image_id === img.id ? `2px solid ${C.gold}` : '1px solid #eee' }} />
))}
{images.filter((i: Image) => i.category === 'slides').length === 0 &&
No images uploaded}
)}
setSlide({ ...s, active: v })} />
setSlide({ ...s, sort_order: Number(v) })} style={{ width: 80 }} />
Cancel
onSave(s)}>Save
);
}
/* βββ Calendar Section βββ */
function CalendarSection({ events, setEvents, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const save = async (ev: CalendarEvent) => {
try {
const res = await fetch(`/api/calendar_events/${ev.id}`, {
method: ev.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ev),
});
if (res.ok) { setEditing(null); setEvents((e: any) => e.map((x: CalendarEvent) => x.id === ev.id ? ev : x)); onToast('Event saved!', 'success'); }
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/calendar_events/${id}`, { method: 'DELETE' });
setEvents((e: any) => e.filter((x: CalendarEvent) => x.id !== id));
onToast('Event deleted', 'success');
setConfirmDel(null);
};
return (
setEditing({ id: 0, date: '', title: '', type: 'default', color: '#D4A853', description: null, active: true })} size="sm">+ New Event} />
{events.length === 0 ? : (
{events.map((ev: CalendarEvent) => (
setEditing(ev)}>Edit
setConfirmDel(ev.id)}>Del
>
} />
))}
)}
{editing && setEditing(null)} />}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
function CalendarEditor({ event, setEvent, onSave, onCancel }: any) {
const e = { ...event };
return (
{e.id ? 'Edit Event' : 'New Event'}
setEvent({ ...e, title: v })} />
setEvent({ ...e, date: v })} />
setEvent({ ...e, type: v })}
options={[{ value: 'default', label: 'Default' }, { value: 'special', label: 'Special' }, { value: 'holiday', label: 'Holiday' }]} />
setEvent({ ...e, color: v })} />
setEvent({ ...e, description: v || null })} />
setEvent({ ...e, active: v })} />
Cancel
onSave(e)}>Save
);
}
/* βββ Social Events Section βββ */
function SocialSection({ events, setEvents, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const save = async (ev: SocialEvent) => {
try {
const res = await fetch(`/api/social_events/${ev.id}`, {
method: ev.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ev),
});
if (res.ok) { setEditing(null); setEvents((e: any) => e.map((x: SocialEvent) => x.id === ev.id ? ev : x)); onToast('Event saved!', 'success'); }
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/social_events/${id}`, { method: 'DELETE' });
setEvents((e: any) => e.filter((x: SocialEvent) => x.id !== id));
onToast('Event deleted', 'success');
setConfirmDel(null);
};
return (
setEditing({ id: 0, name: '', description: '', price: null, date: null, photos: [], featured_photo: null, active: true, sort_order: events.length })} size="sm">+ New Event} />
{events.length === 0 ? : (
{events.map((ev: SocialEvent) => (
setEditing(ev)}>Edit
setConfirmDel(ev.id)}>Del
>
} />
))}
)}
{editing && setEditing(null)} />}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
function SocialEditor({ event, setEvent, images, onSave, onCancel }: any) {
const [showPicker, setShowPicker] = useState(false);
const [localPhotos, setLocalPhotos] = useState(event.photos || []);
const ev = { ...event, photos: localPhotos };
const addPhotos = async (files: FileList) => {
const newPhotos: string[] = [];
for (const f of Array.from(files)) {
const b64 = await readFileBase64(f);
try {
const r = await fetch('/api/admin/images', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: f.name, data: b64, category: 'social', room_id: null, location_target: 'social' }),
});
if (r.ok) { const d = await r.json(); newPhotos.push(d.data); }
} catch { const b = await readFileBase64(f); newPhotos.push(b); }
}
setLocalPhotos(prev => [...prev, ...newPhotos]);
};
return (
{ev.id ? 'Edit Event' : 'New Event'}
setEvent({ ...ev, name: v })} />
setEvent({ ...ev, description: v })} />
setEvent({ ...ev, price: v || null })} />
setEvent({ ...ev, date: v || null })} />
document.getElementById('social-photos')?.click()} size="sm" variant="secondary">+ Add Photos
{ addPhotos(e.target.files!); e.target.value = ''; }} />
{localPhotos.map((p, i) => (
))}
setEvent({ ...ev, active: v })} />
Cancel
onSave(ev)}>Save
);
}
/* βββ Rooms Section βββ */
function RoomsSection({ rooms, setRooms, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const save = async (room: Room) => {
try {
const res = await fetch(`/api/rooms/${room.id}`, {
method: room.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(room),
});
if (res.ok) { setEditing(null); setRooms((r: any) => r.map((x: Room) => x.id === room.id ? room : x)); onToast('Room saved!', 'success'); }
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/rooms/${id}`, { method: 'DELETE' });
setRooms((r: any) => r.filter((x: Room) => x.id !== id));
onToast('Room deleted', 'success');
setConfirmDel(null);
};
return (
setEditing({ id: 0, name: '', description: '', price: '', photos: [], featured_photo: null, active: true, sort_order: rooms.length })} size="sm">+ New Room} />
{rooms.length === 0 ? : (
{rooms.map((r: Room) => (
setEditing(r)}>Edit
setConfirmDel(r.id)}>Del
>
} />
))}
)}
{editing && setEditing(null)} />}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
const [showPicker, setShowPicker] = useState(false);
const [localPhotos, setLocalPhotos] = useState(room.photos || []);
const r = { ...room, photos: localPhotos };
const addPhotos = async (files: FileList) => {
const newPhotos: string[] = [];
for (const f of Array.from(files)) {
const b64 = await readFileBase64(f);
try {
const res = await fetch('/api/admin/images', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: f.name, data: b64, category: 'rooms', room_id: r.id || null, location_target: `room-${r.id || 'new'}` }),
});
if (res.ok) { const d = await res.json(); newPhotos.push(d.data); }
} catch { const b = await readFileBase64(f); newPhotos.push(b); }
}
setLocalPhotos(prev => [...prev, ...newPhotos]);
};
return (
{r.id ? 'Edit Room' : 'New Room'}
setRoom({ ...r, name: v })} />
setRoom({ ...r, description: v })} />
setRoom({ ...r, price: v })} />
document.getElementById(`room-photos-${r.id}`)?.click()} size="sm" variant="secondary">+ Add Photos
{ addPhotos(e.target.files!); e.target.value = ''; }} />
{localPhotos.map((p, i) => (
))}
setRoom({ ...r, active: v })} />
Cancel
onSave(r)}>Save
);
}
/* βββ Menu Section βββ */
function MenuSection({ items, setItems, images, onToast }: any) {
const [editing, setEditing] = useState