diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 03f3ef7..95fd8a5 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -473,18 +473,24 @@ function Confirm({ message, onConfirm, onCancel }: any) { } /* ─── Slides Section ─── */ -function SlidesSection({ slides, setSlides, images, onToast }: any) { +function SlidesSection({ slides, setSlides, images, onToast, onRefreshImages }: 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}`, { + const url = slide.id ? `/api/slides/${slide.id}` : '/api/slides'; + const res = await fetch(url, { 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'); } + if (res.ok) { + const data = await res.json(); + setEditing(null); + setSlides((s: any) => slide.id ? s.map((x: Slide) => x.id === slide.id ? data.data : x) : [...s, data.data]); + onToast('Slide saved!', 'success'); + } } catch { onToast('Error saving slide', 'error'); } }; @@ -516,16 +522,41 @@ function SlidesSection({ slides, setSlides, images, onToast }: any) { )} {editing && ( - setEditing(null)} /> + setEditing(null)} /> )} {confirmDel !== null && setConfirmDel(null)} onConfirm={() => delSlide(confirmDel)} />} ); } -function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) { +function SlideEditor({ slide, setSlide, images, onRefreshImages, onSave, onCancel }: any) { const [showPicker, setShowPicker] = useState(false); + const [uploading, setUploading] = useState(false); const s = { ...slide }; + + const handleUpload = async (files: FileList) => { + setUploading(true); + try { + for (const file of Array.from(files)) { + const b64 = await readFileBase64(file); + const res = await fetch('/api/admin/images', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: file.name, data: b64, category: 'slides', room_id: null, location_target: 'slides' }), + }); + if (res.ok) { + const data = await res.json(); + onRefreshImages?.(); + setSlide({ ...s, image_id: data.data.id, image_url: data.data.data }); + } + } + } catch (err) { + console.error('Upload failed:', err); + } finally { + setUploading(false); + } + }; + return (
@@ -537,6 +568,10 @@ function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) {
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}` }} /> )} @@ -547,7 +582,7 @@ function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) { { 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} + {images.filter((i: Image) => i.category === 'slides').length === 0 && No images uploaded. Use "Upload from computer" above.}
)}
@@ -572,12 +607,18 @@ function CalendarSection({ events, setEvents, images, onToast }: any) { const save = async (ev: CalendarEvent) => { try { - const res = await fetch(`/api/calendar_events/${ev.id}`, { + const url = ev.id ? `/api/calendar_events/${ev.id}` : '/api/calendar_events'; + const res = await fetch(url, { 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'); } + if (res.ok) { + const data = await res.json(); + setEditing(null); + setEvents((e: any) => ev.id ? e.map((x: CalendarEvent) => x.id === ev.id ? data.data : x) : [...e, data.data]); + onToast('Event saved!', 'success'); + } } catch { onToast('Error', 'error'); } }; @@ -642,12 +683,18 @@ function SocialSection({ events, setEvents, images, onToast }: any) { const save = async (ev: SocialEvent) => { try { - const res = await fetch(`/api/social_events/${ev.id}`, { + const url = ev.id ? `/api/social_events/${ev.id}` : '/api/social_events'; + const res = await fetch(url, { 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'); } + if (res.ok) { + const data = await res.json(); + setEditing(null); + setEvents((e: any) => ev.id ? e.map((x: SocialEvent) => x.id === ev.id ? data.data : x) : [...e, data.data]); + onToast('Event saved!', 'success'); + } } catch { onToast('Error', 'error'); } }; @@ -748,12 +795,18 @@ function RoomsSection({ rooms, setRooms, images, onToast }: any) { const save = async (room: Room) => { try { - const res = await fetch(`/api/rooms/${room.id}`, { + const url = room.id ? `/api/rooms/${room.id}` : '/api/rooms'; + const res = await fetch(url, { 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'); } + if (res.ok) { + const data = await res.json(); + setEditing(null); + setRooms((r: any) => room.id ? r.map((x: Room) => x.id === room.id ? data.data : x) : [...r, data.data]); + onToast('Room saved!', 'success'); + } } catch { onToast('Error', 'error'); } }; @@ -1079,12 +1132,18 @@ function PlacesSection({ places, setPlaces, images, onToast }: any) { const save = async (place: Place) => { try { - const res = await fetch(`/api/places/${place.id}`, { + const url = place.id ? `/api/places/${place.id}` : '/api/places'; + const res = await fetch(url, { method: place.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(place), }); - if (res.ok) { setEditing(null); setPlaces((p: any) => p.map((x: Place) => x.id === place.id ? place : x)); onToast('Place saved!', 'success'); } + if (res.ok) { + const data = await res.json(); + setEditing(null); + setPlaces((p: any) => place.id ? p.map((x: Place) => x.id === place.id ? data.data : x) : [...p, data.data]); + onToast('Place saved!', 'success'); + } } catch { onToast('Error', 'error'); } }; @@ -1302,15 +1361,148 @@ function Dashboard({ rooms, slides, events, reviews, places }: any) { } /* ─── Users Section ─── */ -function UsersSection() { +function UsersSection({ onToast }: { onToast: (msg: string, type: string) => void }) { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(null); + const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user' }); + const [showAdd, setShowAdd] = useState(false); + + useEffect(() => { + loadUsers(); + }, []); + + const loadUsers = async () => { + try { + const res = await fetch('/api/admin/users'); + if (res.ok) { + const data = await res.json(); + setUsers(data.data || []); + } + } catch (err) { + console.error('Failed to load users:', err); + } finally { + setLoading(false); + } + }; + + const createUser = async () => { + if (!newUser.username || !newUser.password) { + onToast('Username and password required', 'error'); + return; + } + try { + const res = await fetch('/api/admin/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newUser), + }); + if (res.ok) { + onToast('User created!', 'success'); + setNewUser({ username: '', password: '', role: 'user' }); + setShowAdd(false); + loadUsers(); + } else { + const err = await res.json(); + onToast(err.error || 'Failed to create user', 'error'); + } + } catch { + onToast('Error creating user', 'error'); + } + }; + + const updateUser = async (user: any) => { + try { + const res = await fetch('/api/admin/users', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(user), + }); + if (res.ok) { + onToast('User updated!', 'success'); + setEditing(null); + loadUsers(); + } else { + const err = await res.json(); + onToast(err.error || 'Failed to update user', 'error'); + } + } catch { + onToast('Error updating user', 'error'); + } + }; + + const deleteUser = async (id: number) => { + try { + const res = await fetch(`/api/admin/users?id=${id}`, { method: 'DELETE' }); + if (res.ok) { + onToast('User deleted', 'success'); + loadUsers(); + } + } catch { + onToast('Error deleting user', 'error'); + } + }; + + if (loading) { + return
Loading...
; + } + return (
- -
-
👥
-
User management
-

Manage staff accounts and permissions.

+ setShowAdd(!showAdd)} size="sm">+ Add User} /> + {showAdd && ( +
+
+ setNewUser({ ...newUser, username: v })} /> + setNewUser({ ...newUser, password: v })} /> +
+ + +
+
+
+ Create User + setShowAdd(false)} size="sm">Cancel +
+
+ )} +
+ {users.map((u: any) => ( +
+
+
{u.username} {u.role}
+
{u.first_name || ''} {u.last_name || ''} {u.comments_disabled ? '• Comments disabled' : ''}
+
+
+ setEditing(u)}>Edit +
+
+ ))}
+ {editing && ( +
+
+

Edit User

+
+ setEditing({ ...editing, first_name: v })} /> + setEditing({ ...editing, last_name: v })} /> +
+ setEditing({ ...editing, comments_disabled: e.target.checked })} /> + +
+
+
+ updateUser(editing)} size="sm">Save + setEditing(null)} size="sm">Cancel +
+
+
+ )}
); } @@ -1518,6 +1710,13 @@ export default function AdminPage() { }, []); /* ── Data fetching ── */ + const loadImages = async () => { + try { + const res = await fetch('/api/admin/images'); + if (res.ok) { const d = await res.json(); setImages(Array.isArray(d) ? d : (d.images || [])); } + } catch (err) { console.error('Failed to load images:', err); } + }; + useEffect(() => { if (!isAuthenticated) return; const fetchAll = async () => { @@ -1563,14 +1762,14 @@ export default function AdminPage() {
); - case 'slides': return ; + case 'slides': return ; case 'calendar': return ; case 'social': return ; case 'rooms': return ; case 'menu': return ; case 'places': return ; case 'reviews': return ; - case 'users': return ; + case 'users': return ; case 'weather': return ; default: return ; }