'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; interface ImageRecord { id: number; filename: string; data: string; uploaded_at: string; } interface Room { id: number; name: string; description: string; price: string; photos: string[]; active: boolean; sort_order: number; } interface RoomForm { id?: number; name: string; description: string; price: string; photos: string | string[]; active: boolean; sort_order: number; } interface MenuItem { id: number; category: string; name: string; description: string; price: string; is_daily_special: boolean; active: boolean; sort_order: number; } interface Place { id: number; name: string; description: string; photo: string | null; distance_km: string | null; visit_time: string | null; suggestion: string | null; active: boolean; sort_order: number; } interface Review { id: number; author_name: string; rating: number; text: string; approved: boolean; } interface User { id: number; username: string; role: 'admin' | 'user'; } interface Slide { id: number; title: string | null; subtitle: string | null; image_id: number | null; image_url: string | null; active: boolean; sort_order: number; } interface CalendarEvent { id: number; date: string; title: string; type: 'holiday' | 'season' | 'weather' | 'event'; description: string | null; color: string | null; active: boolean; } const gold = '#E8A849'; const navy = '#001321'; const warm = '#a6683c'; export default function AdminPage() { const router = useRouter(); const [images, setImages] = useState([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [message, setMessage] = useState(null); const [logoUrl, setLogoUrl] = useState(''); const [faviconUrl, setFaviconUrl] = useState(''); const [tagline, setTagline] = useState('Hotel · Restaurant · Andean Highlands'); const [rooms, setRooms] = useState([]); const [roomForm, setRoomForm] = useState({ name: '', description: '', price: '', photos: '', active: true, sort_order: 0 }); const [editingRoom, setEditingRoom] = useState(null); const [menu, setMenu] = useState([]); const [menuForm, setMenuForm] = useState>({ category: '', name: '', description: '', price: '', is_daily_special: false, active: true, sort_order: 0 }); const [editingMenu, setEditingMenu] = useState(null); const [places, setPlaces] = useState([]); const [placeForm, setPlaceForm] = useState>({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 }); const [editingPlace, setEditingPlace] = useState(null); const [reviews, setReviews] = useState([]); const [users, setUsers] = useState([]); const [userForm, setUserForm] = useState({ username: '', password: '', role: 'user' as 'admin' | 'user' }); const [slides, setSlides] = useState([]); const [slideForm, setSlideForm] = useState>({ title: '', subtitle: '', image_id: null, image_url: '', active: true, sort_order: 0 }); const [editingSlide, setEditingSlide] = useState(null); const [events, setEvents] = useState([]); const [eventForm, setEventForm] = useState>({ date: '', title: '', type: 'event', description: '', color: '#E8A849', active: true }); const [editingEvent, setEditingEvent] = useState(null); const [footer, setFooter] = useState({ facebook_url: '', instagram_url: '', whatsapp_url: '', address: '', phone: '', email: '', }); useEffect(() => { fetch('/api/admin/images', { credentials: 'same-origin' }) .then(async (res) => { if (!res.ok) { if (res.status === 401) { router.push('/login'); return; } throw new Error('Failed to load images'); } const data = await res.json(); setImages(data.images || []); setCount(data.count || 0); }) .catch((err) => setError(err.message)); fetch('/api/rooms').then((r) => r.json()).then((j) => setRooms(j.data || [])).catch(() => {}); fetch('/api/menu').then((r) => r.json()).then((j) => setMenu(j.data || [])).catch(() => {}); fetch('/api/places').then((r) => r.json()).then((j) => setPlaces(j.data || [])).catch(() => {}); fetch('/api/admin/reviews', { credentials: 'same-origin' }).then((r) => r.json()).then((j) => setReviews(j.data || [])).catch(() => {}); fetch('/api/admin/users', { credentials: 'same-origin' }).then((r) => r.json()).then((j) => setUsers(j.data || [])).catch(() => {}); fetch('/api/slides').then((r) => r.json()).then((j) => setSlides(j.data || [])).catch(() => {}); fetch('/api/calendar_events').then((r) => r.json()).then((j) => setEvents(j.data || [])).catch(() => {}); fetch('/api/site-assets').then((res) => res.json()).then((json) => { const c = json.data || {}; setFooter({ facebook_url: c.facebook_url || '', instagram_url: c.instagram_url || '', whatsapp_url: c.whatsapp_url || '', address: c.address || '', phone: c.phone || '', email: c.email || '', }); if (c.logo) setLogoUrl(c.logo); if (c.favicon) setFaviconUrl(c.favicon); if (c.tagline) setTagline(c.tagline); }).catch(() => {}); setLoading(false); }, [router]); const notify = (msg: string) => { setMessage(msg); setTimeout(() => setMessage(null), 4000); }; const updateAsset = async (key: string, value: string) => { const res = await fetch('/api/site-assets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, value }), credentials: 'same-origin', }); if (!res.ok) { setError('Failed to save ' + key); return; } notify(`${key} updated.`); }; const saveFooter = async () => { const res = await fetch('/api/site-assets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ values: footer }), credentials: 'same-origin', }); if (!res.ok) { setError('Failed to save footer'); return; } notify('Footer info updated.'); }; const handleUpload = (e: React.ChangeEvent) => { const files = e.target.files; if (!files) return; Array.from(files).forEach((file) => { const reader = new FileReader(); reader.onload = async () => { const data = reader.result as string; const res = await fetch('/api/admin/images', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: file.name, data }), credentials: 'same-origin', }); if (!res.ok) { setError('Upload failed'); return; } const result = await res.json(); setImages((prev) => [{ ...result.image, data }, ...prev]); setCount((prev) => prev + 1); }; reader.readAsDataURL(file); }); e.target.value = ''; }; const handleDelete = async (id: number) => { const res = await fetch(`/api/admin/images?id=${id}`, { method: 'DELETE', credentials: 'same-origin' }); if (res.ok) { setImages((prev) => prev.filter((img) => img.id !== id)); setCount((prev) => prev - 1); } }; const api = async (url: string, method: string, body?: object) => { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.error || 'Request failed'); } return res.json(); }; const saveRoom = async (e: React.FormEvent) => { e.preventDefault(); const photosValue = roomForm.photos || []; const photosArray: string[] = typeof photosValue === 'string' ? (photosValue as unknown as string).split(',').map((s: string) => s.trim()).filter(Boolean) : photosValue; const payload = { ...roomForm, photos: photosArray, active: roomForm.active !== false, sort_order: roomForm.sort_order || 0 }; if (editingRoom) { await api(`/api/rooms/${editingRoom}`, 'PUT', payload); setRooms((prev) => prev.map((r) => (r.id === editingRoom ? { ...r, ...payload } as Room : r))); setEditingRoom(null); } else { const json = await api('/api/rooms', 'POST', payload); setRooms((prev) => [...prev, json.data]); } setRoomForm({ name: '', description: '', price: '', photos: '', active: true, sort_order: 0 }); notify('Room saved.'); }; const editRoom = (r: Room) => { setRoomForm({ ...r, photos: r.photos?.join(', ') || '' }); setEditingRoom(r.id); }; const deleteRoom = async (id: number) => { await api(`/api/rooms/${id}`, 'DELETE'); setRooms((prev) => prev.filter((r) => r.id !== id)); }; const saveMenu = async (e: React.FormEvent) => { e.preventDefault(); const payload = { ...menuForm, active: menuForm.active !== false, sort_order: menuForm.sort_order || 0 }; if (editingMenu) { await api(`/api/menu/${editingMenu}`, 'PUT', payload); setMenu((prev) => prev.map((m) => (m.id === editingMenu ? { ...m, ...payload } as MenuItem : m))); setEditingMenu(null); } else { const json = await api('/api/menu', 'POST', payload); setMenu((prev) => [...prev, json.data]); } setMenuForm({ category: '', name: '', description: '', price: '', is_daily_special: false, active: true, sort_order: 0 }); notify('Menu item saved.'); }; const editMenu = (m: MenuItem) => { setMenuForm({ ...m }); setEditingMenu(m.id); }; const deleteMenu = async (id: number) => { await api(`/api/menu/${id}`, 'DELETE'); setMenu((prev) => prev.filter((m) => m.id !== id)); }; const savePlace = async (e: React.FormEvent) => { e.preventDefault(); const payload = { ...placeForm, active: placeForm.active !== false, sort_order: placeForm.sort_order || 0 }; if (editingPlace) { await api(`/api/places/${editingPlace}`, 'PUT', payload); setPlaces((prev) => prev.map((p) => (p.id === editingPlace ? { ...p, ...payload } as Place : p))); setEditingPlace(null); } else { const json = await api('/api/places', 'POST', payload); setPlaces((prev) => [...prev, json.data]); } setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 }); notify('Place saved.'); }; const editPlace = (p: Place) => { setPlaceForm({ ...p }); setEditingPlace(p.id); }; const deletePlace = async (id: number) => { await api(`/api/places/${id}`, 'DELETE'); setPlaces((prev) => prev.filter((p) => p.id !== id)); }; const approveReview = async (id: number) => { await api(`/api/admin/reviews/${id}`, 'PUT', { approved: true }); setReviews((prev) => prev.map((r) => (r.id === id ? { ...r, approved: true } : r))); }; const deleteReview = async (id: number) => { await api(`/api/admin/reviews/${id}`, 'DELETE'); setReviews((prev) => prev.filter((r) => r.id !== id)); }; const saveUser = async (e: React.FormEvent) => { e.preventDefault(); const json = await api('/api/admin/users', 'POST', userForm); setUsers((prev) => [...prev, json.user]); setUserForm({ username: '', password: '', role: 'user' }); notify('User created.'); }; const deleteUser = async (id: number) => { await api(`/api/admin/users/${id}`, 'DELETE'); setUsers((prev) => prev.filter((u) => u.id !== id)); }; const saveSlide = async (e: React.FormEvent) => { e.preventDefault(); const payload = { ...slideForm, active: slideForm.active !== false, sort_order: slideForm.sort_order || 0 }; if (editingSlide) { await api(`/api/slides/${editingSlide}`, 'PUT', payload); setSlides((prev) => prev.map((s) => (s.id === editingSlide ? { ...s, ...payload } as Slide : s))); setEditingSlide(null); } else { const json = await api('/api/slides', 'POST', payload); setSlides((prev) => [...prev, json.data]); } setSlideForm({ title: '', subtitle: '', image_id: null, image_url: '', active: true, sort_order: 0 }); notify('Slide saved.'); }; const editSlide = (s: Slide) => { setSlideForm({ ...s }); setEditingSlide(s.id); }; const deleteSlide = async (id: number) => { await api(`/api/slides/${id}`, 'DELETE'); setSlides((prev) => prev.filter((s) => s.id !== id)); }; const saveEvent = async (e: React.FormEvent) => { e.preventDefault(); const payload = { ...eventForm, active: eventForm.active !== false }; if (editingEvent) { await api(`/api/calendar_events/${editingEvent}`, 'PUT', payload); setEvents((prev) => prev.map((ev) => (ev.id === editingEvent ? { ...ev, ...payload } as CalendarEvent : ev))); setEditingEvent(null); } else { const json = await api('/api/calendar_events', 'POST', payload); setEvents((prev) => [...prev, json.data]); } setEventForm({ date: '', title: '', type: 'event', description: '', color: '#E8A849', active: true }); notify('Calendar event saved.'); }; const editEvent = (ev: CalendarEvent) => { setEventForm({ ...ev }); setEditingEvent(ev.id); }; const deleteEvent = async (id: number) => { await api(`/api/calendar_events/${id}`, 'DELETE'); setEvents((prev) => prev.filter((ev) => ev.id !== id)); }; if (loading) { return (

Loading admin...

); } return (

Admin Portal

Total uploaded images: {count}

{error &&

{error}

} {message &&

{message}

}

Tip: click an uploaded gallery image to preview, copy its data URL, and paste it above.

setFooter({ ...footer, facebook_url: v })} /> setFooter({ ...footer, instagram_url: v })} /> setFooter({ ...footer, whatsapp_url: v })} /> setFooter({ ...footer, address: v })} /> setFooter({ ...footer, phone: v })} /> setFooter({ ...footer, email: v })} />
{images.length === 0 ? (

No images uploaded yet.

) : (
{images.map((img) => (
{ e.currentTarget.style.transform = 'scale(1.03)'; e.currentTarget.style.boxShadow = '0 8px 22px rgba(0,0,0,0.12)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; }} onClick={() => { navigator.clipboard.writeText(img.data); notify('Image URL copied.'); }} > {img.filename}
))}
)}
setSlideForm({ ...slideForm, title: v })} /> setSlideForm({ ...slideForm, subtitle: v })} /> setSlideForm({ ...slideForm, image_url: v })} /> setSlideForm({ ...slideForm, sort_order: parseInt(v || '0', 10) })} /> setSlideForm({ ...slideForm, image_id: v ? parseInt(v, 10) : null })} />
{editingSlide && { setEditingSlide(null); setSlideForm({ title: '', subtitle: '', image_id: null, image_url: '', active: true, sort_order: 0 }); }}>Cancel}
[s.title || '(untitled)', s.active ? 'On' : 'Off', `#${s.sort_order}`])} onEdit={(i) => editSlide(slides[i])} onDelete={(i) => deleteSlide(slides[i].id)} />
setEventForm({ ...eventForm, date: v })} /> setEventForm({ ...eventForm, title: v })} /> setEventForm({ ...eventForm, color: v })} /> setEventForm({ ...eventForm, description: v })} />
{editingEvent && { setEditingEvent(null); setEventForm({ date: '', title: '', type: 'event', description: '', color: '#E8A849', active: true }); }}>Cancel}
[ev.date, ev.title, ev.type, ev.active ? 'On' : 'Off'])} onEdit={(i) => editEvent(events[i])} onDelete={(i) => deleteEvent(events[i].id)} />
setRoomForm({ ...roomForm, name: v })} /> setRoomForm({ ...roomForm, price: v })} /> setRoomForm({ ...roomForm, photos: v })} /> setRoomForm({ ...roomForm, sort_order: parseInt(v || '0', 10) })} /> setRoomForm({ ...roomForm, description: v })} />
{editingRoom && { setEditingRoom(null); setRoomForm({ name: '', description: '', price: '', photos: '', active: true, sort_order: 0 }); }}>Cancel}
[r.name, `$${r.price}`, r.description.slice(0, 60), r.active ? 'On' : 'Off', `#${r.sort_order}`])} onEdit={(i) => editRoom(rooms[i])} onDelete={(i) => deleteRoom(rooms[i].id)} />
setMenuForm({ ...menuForm, category: v })} /> setMenuForm({ ...menuForm, name: v })} /> setMenuForm({ ...menuForm, price: v })} /> setMenuForm({ ...menuForm, sort_order: parseInt(v || '0', 10) })} /> setMenuForm({ ...menuForm, description: v })} />
{editingMenu && { setEditingMenu(null); setMenuForm({ category: '', name: '', description: '', price: '', is_daily_special: false, active: true, sort_order: 0 }); }}>Cancel}
[m.category, m.name, `$${m.price}`, m.is_daily_special ? '★ Daily' : '', m.active ? 'On' : 'Off', `#${m.sort_order}`])} onEdit={(i) => editMenu(menu[i])} onDelete={(i) => deleteMenu(menu[i].id)} />
setPlaceForm({ ...placeForm, name: v })} /> setPlaceForm({ ...placeForm, distance_km: v })} /> setPlaceForm({ ...placeForm, visit_time: v })} /> setPlaceForm({ ...placeForm, photo: v })} /> setPlaceForm({ ...placeForm, sort_order: parseInt(v || '0', 10) })} /> setPlaceForm({ ...placeForm, description: v })} /> setPlaceForm({ ...placeForm, suggestion: v })} />
{editingPlace && { setEditingPlace(null); setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 }); }}>Cancel}
[p.name, `${p.distance_km || ''} km`, p.description.slice(0, 60), p.active ? 'On' : 'Off', `#${p.sort_order}`])} onEdit={(i) => editPlace(places[i])} onDelete={(i) => deletePlace(places[i].id)} />
{reviews.length === 0 ?

No pending reviews.

: (
{reviews.map((r) => (
{r.author_name} {'★'.repeat(r.rating)}{'☆'.repeat(5 - r.rating)}

{r.text}

{!r.approved && } deleteReview(r.id)}>Delete
))}
)}
setUserForm({ ...userForm, username: v })} /> setUserForm({ ...userForm, password: v })} />
[u.username, u.role])} onDelete={(i) => deleteUser(users[i].id)} />
); } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function Text({ label, value, onChange, type = 'text' }: { label: string; value: string; onChange: (v: string) => void; type?: string }) { return ( ); } function Number({ label, value, onChange }: { label: string; value: number; onChange: (v: string) => void }) { return ( ); } function ImageSelect({ label, images, value, onChange }: { label: string; images: ImageRecord[]; value: string | number; onChange: (v: string) => void }) { return ( ); } function Button({ children, onClick, type = 'button', disabled = false }: { children: React.ReactNode; onClick?: () => void; type?: 'button' | 'submit'; disabled?: boolean }) { return ( ); } function SecondaryButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { return ( ); } function DangerButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { return ( ); } function ListTable({ rows, onEdit, onDelete }: { rows: string[][]; onEdit?: (index: number) => void; onDelete: (index: number) => void }) { return (
{rows.map((row, i) => (
{row.map((cell, j) => {cell})}
{onEdit && } onDelete(i)}>Delete
))}
); }