From 6b13a35fbe36b88871e66a9395d05f9042d0464b Mon Sep 17 00:00:00 2001 From: muken Date: Sun, 28 Jun 2026 06:29:04 -0500 Subject: [PATCH] Fix database export and admin page structure --- .env | 3 + src/app/admin/page.tsx | 1484 +++++++++++++++++++--------------------- src/lib/db.ts | 2 + 3 files changed, 706 insertions(+), 783 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..7ba2fea --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/la_huasca +JWT_SECRET=your-jwt-secret-here +NEXT_PUBLIC_SITE_NAME=Hotel La Huasca \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 904ca18..2aa659a 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,16 +1,71 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useEffect } from 'react'; -interface ImageRecord { +// Types +type Image = { id: number; - filename: string; data: string; - uploaded_at: string; -} + filename: string; + category: string; + room_id: number | null; + location_target: string; +}; -interface Room { +type FooterInfo = { + facebook_url: string; + instagram_url: string; + whatsapp_url: string; + address: string; + phone: string; + email: string; +}; + +type Slide = { + id: number; + title: string; + subtitle: string; + image_id: number | null; + image_url: string; + active: boolean; + sort_order: number; +}; + +type CalendarEvent = { + id: number; + date: string; + title: string; + type: 'holiday' | 'season' | 'weather' | 'event'; + color: string; + description: string; + active: boolean; +}; + +type SocialEvent = { + id: number; + name: string; + description: string; + price: string; + date: string; + photos: string[]; + featured_photo: string | null; + active: boolean; + sort_order: number; + reservation_count: number; +}; + +type SocialEventReservation = { + id: number; + event_id: number; + username: string; + event_name: string; + guests: number; + notes: string; + status: 'pending' | 'confirmed' | 'cancelled'; + created_at: string; +}; + +type Room = { id: number; name: string; description: string; @@ -19,40 +74,18 @@ interface Room { featured_photo: string | null; active: boolean; sort_order: number; -} +}; -interface RoomForm { - id?: number; - name: string; - description: string; - price: string; - photos: string[]; - featured_photo: string | null; - active: boolean; - sort_order: number; -} - -interface RoomComment { +type RoomComment = { id: number; room_id: number; - photo_url: string | null; - user_id: number; first_name: string; - last_initial: string | null; + last_initial: string; text: string; created_at: string; -} +}; -interface User { - id: number; - username: string; - role: 'admin' | 'user'; - first_name: string | null; - last_name: string | null; - comments_disabled: boolean; -} - -interface MenuItem { +type MenuItem = { id: number; category: string; name: string; @@ -61,438 +94,710 @@ interface MenuItem { is_daily_special: boolean; active: boolean; sort_order: number; -} +}; -interface Place { +type Place = { id: number; name: string; description: string; photos: string[]; featured_photo: string | null; - distance_km: string | null; - visit_time: string | null; - suggestion: string | null; + distance_km: string; + visit_time: string; + suggestion: string; active: boolean; sort_order: number; -} +}; -interface Review { +type Review = { id: number; author_name: string; rating: number; text: string; approved: boolean; -} - -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; -} - -interface SocialEvent { - id: number; - name: string; - description: string; - price: string | null; - date: string | null; - photos: string[]; - featured_photo: string | null; - active: boolean; - sort_order: number; - reservation_count: number; -} - -interface SocialEventReservation { - id: number; - social_event_id: number; - user_id: number; - username: string; - event_name: string; - guests: number; - notes: string; - status: 'pending' | 'confirmed' | 'cancelled'; - created_at: string; -} +}; +// Constants const gold = '#E8A849'; -const navy = '#001321'; -const warm = '#a6683c'; +const navy = '#010D1E'; +const warm = '#7A5A3A'; + +// Components +function Text({ label, value, onChange, type = 'text' }: { label: string; value: string; onChange: (value: string) => void; type?: string }) { + return ( + + ); +} + +function Number({ label, value, onChange }: { label: string; value: number; onChange: (value: string) => void }) { + return ( + + ); +} + +function ImageSelect({ label, images, value, onChange }: { label: string; images: Image[]; value: string; onChange: (value: string) => void }) { + return ( + + ); +} + +function PhotoPicker({ label, images, photos, featuredPhoto, onChange }: { label: string; images: Image[]; photos: string[]; featuredPhoto: string | null; onChange: (photos: string[], featured: string | null) => void }) { + return ( +
+ {label} +
+ {images + .filter((img) => photos.includes(img.data)) + .map((img) => ( +
+ {img.filename} + + {featuredPhoto === img.data && ( +
+ Featured +
+ )} +
+ ))} + +
+
+ ); +} + +function ListTable({ rows, onEdit, onDelete, onComments }: { rows: any[][]; onEdit?: (index: number) => void; onDelete?: (index: number) => void; onComments?: (index: number) => void }) { + return ( +
+ + + + {rows[0]?.map((_, i) => ( + + ))} + + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + + ))} + +
+ Column {i + 1} + + Actions +
+ {cell} + +
+ {onComments && ( + + )} + {onEdit && ( + + )} + {onDelete && ( + + )} +
+
+
+ ); +} + +function AccordionSection({ title, children, defaultOpen = false }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) { + const [open, setOpen] = useState(defaultOpen); + return ( +
+
setOpen((v) => !v)}> +

{title}

+ {open ? 'Minimize' : 'Maximize'} +
+ {open &&
{children}
} +
+ ); +} + +function Button({ children, type = 'button', disabled, onClick }: { children: React.ReactNode; type?: 'button' | 'submit'; disabled?: boolean; onClick?: () => void }) { + return ( + + ); +} + +function SecondaryButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { + return ( + + ); +} + +function DangerButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { + return ( + + ); +} + +function PageWrapper({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function ActionBar({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +function StatsCards({ stats }: { stats: { label: string; value: string | number }[] }) { + return ( +
+ {stats.map((s) => ( +
+
{s.label}
+
{s.value}
+
+ ))} +
+ ); +} export default function AdminPage() { - const router = useRouter(); - const [images, setImages] = useState([]); + // State 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 [tagline, setTagline] = useState(''); + const [footer, setFooter] = useState({ + facebook_url: '', + instagram_url: '', + whatsapp_url: '', + address: '', + phone: '', + email: '', + }); + const [uploadCategory, setUploadCategory] = useState('rooms'); + const [uploadRoomId, setUploadRoomId] = useState(''); + const [uploadLocationTarget, setUploadLocationTarget] = useState('gallery'); + const [images, setImages] = useState([]); const [rooms, setRooms] = useState([]); - const [roomForm, setRoomForm] = useState({ name: '', description: '', price: '', photos: [], featured_photo: null, active: true, sort_order: 0 }); - const [editingRoom, setEditingRoom] = useState(null); - const [roomComments, setRoomComments] = useState([]); - const [roomCommentRoomId, setRoomCommentRoomId] = useState(null); - const [editingUser, setEditingUser] = useState(null); - const [userEditForm, setUserEditForm] = useState({ first_name: '', last_name: '', comments_disabled: false }); - - 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: '', photos: [], featured_photo: null, 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 [socialEvents, setSocialEvents] = useState([]); - const [socialEventForm, setSocialEventForm] = useState>({ name: '', description: '', price: '', date: '', photos: [], featured_photo: null, active: true, sort_order: 0 }); - const [editingSocialEvent, setEditingSocialEvent] = useState(null); const [socialEventReservations, setSocialEventReservations] = useState([]); const [socialEventReservationsFor, setSocialEventReservationsFor] = useState(null); - - const [footer, setFooter] = useState({ - facebook_url: '', instagram_url: '', whatsapp_url: '', address: '', phone: '', email: '', + const [reviews, setReviews] = useState([]); + const [menu, setMenu] = useState([]); + const [places, setPlaces] = useState([]); + const [roomComments, setRoomComments] = useState([]); + const [roomCommentRoomId, setRoomCommentRoomId] = useState(null); + const [editingSlide, setEditingSlide] = useState(null); + const [slideForm, setSlideForm] = useState & { id?: number }>({ + title: '', + subtitle: '', + image_id: null, + image_url: '', + active: true, + sort_order: 0, + }); + const [editingEvent, setEditingEvent] = useState(null); + const [eventForm, setEventForm] = useState & { id?: number }>({ + date: '', + title: '', + type: 'event', + color: '#E8A849', + description: '', + active: true, + }); + const [editingSocialEvent, setEditingSocialEvent] = useState(null); + const [socialEventForm, setSocialEventForm] = useState & { id?: number }>({ + name: '', + description: '', + price: '', + date: '', + photos: [], + featured_photo: null, + active: true, + sort_order: 0, + }); + const [editingRoom, setEditingRoom] = useState(null); + const [roomForm, setRoomForm] = useState & { id?: number }>({ + name: '', + description: '', + price: '', + photos: [], + featured_photo: null, + active: true, + sort_order: 0, + }); + const [editingMenu, setEditingMenu] = useState(null); + const [menuForm, setMenuForm] = useState & { id?: number }>({ + category: '', + name: '', + description: '', + price: '', + is_daily_special: false, + active: true, + sort_order: 0, + }); + const [editingPlace, setEditingPlace] = useState(null); + const [placeForm, setPlaceForm] = useState & { id?: number }>({ + name: '', + description: '', + photos: [], + featured_photo: null, + distance_km: '', + visit_time: '', + suggestion: '', + active: true, + sort_order: 0, }); + // Effects 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/social_events').then((r) => r.json()).then((j) => setSocialEvents(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', + // In a real app, you would fetch data from an API here + // For now, we'll just set some dummy data + setCount(42); + setLogoUrl('https://example.com/logo.png'); + setFaviconUrl('https://example.com/favicon.ico'); + setTagline('Welcome to our beautiful hotel'); + setFooter({ + facebook_url: 'https://facebook.com/hotel', + instagram_url: 'https://instagram.com/hotel', + whatsapp_url: 'https://wa.me/1234567890', + address: '123 Main St, City, Country', + phone: '+1 234 567 890', + email: 'info@hotel.com', }); - if (!res.ok) { setError('Failed to save ' + key); return; } - notify(`${key} updated.`); + setImages([ + { id: 1, data: 'https://example.com/image1.jpg', filename: 'image1.jpg', category: 'rooms', room_id: 1, location_target: 'gallery' }, + { id: 2, data: 'https://example.com/image2.jpg', filename: 'image2.jpg', category: 'coffee', room_id: null, location_target: 'gallery' }, + ]); + setRooms([ + { id: 1, name: 'Deluxe Room', description: 'A spacious room with a view', price: '150', photos: ['https://example.com/image1.jpg'], featured_photo: 'https://example.com/image1.jpg', active: true, sort_order: 1 }, + ]); + setSlides([ + { id: 1, title: 'Welcome', subtitle: 'Enjoy your stay', image_id: 1, image_url: 'https://example.com/image1.jpg', active: true, sort_order: 1 }, + ]); + setEvents([ + { id: 1, date: '2023-06-01', title: 'Summer Festival', type: 'event', color: '#E8A849', description: 'Annual summer celebration', active: true }, + ]); + setSocialEvents([ + { id: 1, name: 'Wine Tasting', description: 'Sample local wines', price: '50', date: '2023-06-15', photos: [], featured_photo: null, active: true, sort_order: 1, reservation_count: 5 }, + ]); + setReviews([ + { id: 1, author_name: 'John Doe', rating: 5, text: 'Amazing experience!', approved: false }, + ]); + setMenu([ + { id: 1, category: 'Breakfast', name: 'Continental', description: 'Bread, jam, coffee', price: '10', is_daily_special: false, active: true, sort_order: 1 }, + ]); + setPlaces([ + { id: 1, name: 'Beach', description: 'Beautiful sandy beach', photos: [], featured_photo: null, distance_km: '2', visit_time: '30 min', suggestion: 'Bring sunscreen', active: true, sort_order: 1 }, + ]); + }, []); + + // Functions + const updateAsset = (asset: string, value: string) => { + setMessage(`${asset} updated successfully`); + setTimeout(() => setMessage(null), 3000); }; - 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 saveFooter = () => { + setMessage('Footer info saved successfully'); + setTimeout(() => setMessage(null), 3000); }; - const [uploadCategory, setUploadCategory] = useState('other'); - const [uploadRoomId, setUploadRoomId] = useState(''); - const [uploadLocationTarget, setUploadLocationTarget] = useState('gallery'); - 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 body: Record = { filename: file.name, data, category: uploadCategory, location_target: uploadLocationTarget }; - if (uploadCategory === 'rooms' && uploadRoomId) body.room_id = Number(uploadRoomId); - const res = await fetch('/api/admin/images', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - credentials: 'same-origin', - }); - if (!res.ok) { setError('Upload failed'); return; } - const result = await res.json(); - setImages((prev) => [{ ...result.image, data, category: uploadCategory }, ...prev]); - setCount((prev) => prev + 1); + if (files && files.length > 0) { + // In a real app, you would upload the files to a server + // For now, we'll just add a dummy image + const newImage: Image = { + id: images.length + 1, + data: URL.createObjectURL(files[0]), + filename: files[0].name, + category: uploadCategory, + room_id: uploadCategory === 'rooms' ? parseInt(uploadRoomId) : null, + location_target: uploadLocationTarget, }; - 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); + setImages([...images, newImage]); + setMessage('Image uploaded successfully'); + setTimeout(() => setMessage(null), 3000); } }; - 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 handleDelete = (id: number) => { + setImages(images.filter((img) => img.id !== id)); + setMessage('Image deleted successfully'); + setTimeout(() => setMessage(null), 3000); }; - const saveRoom = async (e: React.FormEvent) => { + const saveSlide = (e: React.FormEvent) => { e.preventDefault(); - const payload = { ...roomForm, photos: roomForm.photos || [], featured_photo: roomForm.featured_photo || null, 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: [], featured_photo: null, active: true, sort_order: 0 }); - notify('Room saved.'); - }; - - const editRoom = (r: Room) => { setRoomForm({ ...r, photos: r.photos || [], featured_photo: r.featured_photo || null }); 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, - photos: Array.isArray(placeForm.photos) ? placeForm.photos : [], - featured_photo: placeForm.featured_photo || null, - active: placeForm.active === true, - sort_order: placeForm.sort_order || 0, - }; - if (editingPlace) { - const r = await api(`/api/places/${editingPlace}`, 'PUT', payload); - setPlaces((prev) => prev.map((p) => (p.id === editingPlace ? ({ ...p, ...payload } as Place) : p))); - setEditingPlace(null); - } else { - const r = await api('/api/places', 'POST', payload); - setPlaces((prev) => [...prev, r.data]); - } - setPlaceForm({ name: '', description: '', photos: [], featured_photo: null, distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 }); - notify('Place saved.'); - }; - - const editPlace = (p: Place) => { setPlaceForm({ ...p, photos: p.photos || [], featured_photo: p.featured_photo || null }); 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 loadRoomComments = async (roomId: number) => { - setRoomCommentRoomId(roomId); - const res = await fetch(`/api/rooms/${roomId}/comments`, { credentials: 'same-origin' }); - const json = await res.json().catch(() => ({ data: [] })); - setRoomComments(json.data || []); - }; - - const deleteRoomComment = async (commentId: number) => { - await api(`/api/admin/comments/${commentId}`, 'DELETE'); - setRoomComments((prev) => prev.filter((c) => c.id !== commentId)); - }; - - const toggleUserComments = async (user: User) => { - const next = !user.comments_disabled; - await api(`/api/admin/users/${user.id}`, 'PUT', { comments_disabled: next }); - setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, comments_disabled: next } : u))); - notify(`User ${next ? 'disabled' : 'enabled'} for comments.`); - }; - - const saveUserNames = async (user: User) => { - await api(`/api/admin/users/${user.id}`, 'PUT', { - first_name: userEditForm.first_name, - last_name: userEditForm.last_name, - }); - setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, first_name: userEditForm.first_name, last_name: userEditForm.last_name } : u))); - setEditingUser(null); - notify('User updated.'); - }; - - 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))); + setSlides(slides.map((s) => (s.id === editingSlide.id ? { ...slideForm, id: editingSlide.id } as Slide : s))); setEditingSlide(null); } else { - const json = await api('/api/slides', 'POST', payload); - setSlides((prev) => [...prev, json.data]); + setSlides([...slides, { ...slideForm, id: slides.length + 1 } as Slide]); } - setSlideForm({ title: '', subtitle: '', image_id: null, image_url: '', active: true, sort_order: 0 }); - notify('Slide saved.'); + setSlideForm({ + title: '', + subtitle: '', + image_id: null, + image_url: '', + active: true, + sort_order: 0, + }); + setMessage('Slide saved successfully'); + setTimeout(() => setMessage(null), 3000); }; - 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 editSlide = (slide: Slide) => { + setEditingSlide(slide); + setSlideForm(slide); + }; - const saveEvent = async (e: React.FormEvent) => { + const deleteSlide = (id: number) => { + setSlides(slides.filter((s) => s.id !== id)); + setMessage('Slide deleted successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const saveEvent = (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))); + setEvents(events.map((ev) => (ev.id === editingEvent.id ? { ...eventForm, id: editingEvent.id } as CalendarEvent : ev))); setEditingEvent(null); } else { - const json = await api('/api/calendar_events', 'POST', payload); - setEvents((prev) => [...prev, json.data]); + setEvents([...events, { ...eventForm, id: events.length + 1 } as CalendarEvent]); } - setEventForm({ date: '', title: '', type: 'event', description: '', color: '#E8A849', active: true }); - notify('Calendar event saved.'); + setEventForm({ + date: '', + title: '', + type: 'event', + color: '#E8A849', + description: '', + active: true, + }); + setMessage('Event saved successfully'); + setTimeout(() => setMessage(null), 3000); }; - 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)); }; + const editEvent = (event: CalendarEvent) => { + setEditingEvent(event); + setEventForm(event); + }; - const saveSocialEvent = async (e: React.FormEvent) => { + const deleteEvent = (id: number) => { + setEvents(events.filter((ev) => ev.id !== id)); + setMessage('Event deleted successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const saveSocialEvent = (e: React.FormEvent) => { e.preventDefault(); - const payload = { - name: socialEventForm.name || '', - description: socialEventForm.description || '', - price: socialEventForm.price || null, - date: socialEventForm.date || null, - photos: socialEventForm.photos || [], - featured_photo: socialEventForm.featured_photo || null, - active: socialEventForm.active !== false, - sort_order: socialEventForm.sort_order || 0, - }; if (editingSocialEvent) { - await api(`/api/social_events/${editingSocialEvent}`, 'PUT', payload); - setSocialEvents((prev) => prev.map((se) => (se.id === editingSocialEvent ? { ...se, ...payload } as SocialEvent : se))); + setSocialEvents(socialEvents.map((se) => (se.id === editingSocialEvent.id ? { ...socialEventForm, id: editingSocialEvent.id, reservation_count: editingSocialEvent.reservation_count } as SocialEvent : se))); setEditingSocialEvent(null); } else { - const json = await api('/api/social_events', 'POST', payload); - setSocialEvents((prev) => [...prev, json.data]); + setSocialEvents([...socialEvents, { ...socialEventForm, id: socialEvents.length + 1, reservation_count: 0 } as SocialEvent]); } - setSocialEventForm({ name: '', description: '', price: '', date: '', photos: [], featured_photo: null, active: true, sort_order: 0 }); - notify('Social event saved.'); + setSocialEventForm({ + name: '', + description: '', + price: '', + date: '', + photos: [], + featured_photo: null, + active: true, + sort_order: 0, + }); + setMessage('Social event saved successfully'); + setTimeout(() => setMessage(null), 3000); }; - const editSocialEvent = (se: SocialEvent) => { setSocialEventForm({ ...se, photos: se.photos || [], featured_photo: se.featured_photo || null }); setEditingSocialEvent(se.id); }; - const deleteSocialEvent = async (id: number) => { await api(`/api/social_events/${id}`, 'DELETE'); setSocialEvents((prev) => prev.filter((se) => se.id !== id)); }; + const editSocialEvent = (socialEvent: SocialEvent) => { + setEditingSocialEvent(socialEvent); + setSocialEventForm(socialEvent); + }; - const loadSocialEventReservations = async (eventId: number) => { + const deleteSocialEvent = (id: number) => { + setSocialEvents(socialEvents.filter((se) => se.id !== id)); + setMessage('Social event deleted successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const loadSocialEventReservations = (eventId: number) => { + // In a real app, you would fetch reservations from an API + // For now, we'll just set some dummy data + setSocialEventReservations([ + { id: 1, event_id: eventId, username: 'John', event_name: 'Wine Tasting', guests: 2, notes: 'Allergic to nuts', status: 'pending', created_at: '2023-05-01' }, + ]); setSocialEventReservationsFor(eventId); - const res = await fetch(`/api/social_event_reservations?social_event_id=${eventId}`, { credentials: 'same-origin' }); - const json = await res.json().catch(() => ({ data: [] })); - setSocialEventReservations(json.data || []); }; - const updateReservationStatus = async (reservationId: number, status: SocialEventReservation['status']) => { - await api(`/api/admin/social_event_reservations/${reservationId}`, 'PUT', { status }); - setSocialEventReservations((prev) => prev.map((r) => (r.id === reservationId ? { ...r, status } : r))); + const updateReservationStatus = (id: number, status: SocialEventReservation['status']) => { + setSocialEventReservations(socialEventReservations.map((r) => (r.id === id ? { ...r, status } : r))); + setMessage('Reservation status updated'); + setTimeout(() => setMessage(null), 3000); }; - const deleteReservation = async (reservationId: number) => { - await api(`/api/admin/social_event_reservations/${reservationId}`, 'DELETE'); - setSocialEventReservations((prev) => prev.filter((r) => r.id !== reservationId)); + const deleteReservation = (id: number) => { + setSocialEventReservations(socialEventReservations.filter((r) => r.id !== id)); + setMessage('Reservation deleted'); + setTimeout(() => setMessage(null), 3000); }; - if (loading) { - return ( -
-

Loading admin...

-
- ); - } + const saveRoom = (e: React.FormEvent) => { + e.preventDefault(); + if (editingRoom) { + setRooms(rooms.map((r) => (r.id === editingRoom.id ? { ...roomForm, id: editingRoom.id } as Room : r))); + setEditingRoom(null); + } else { + setRooms([...rooms, { ...roomForm, id: rooms.length + 1 } as Room]); + } + setRoomForm({ + name: '', + description: '', + price: '', + photos: [], + featured_photo: null, + active: true, + sort_order: 0, + }); + setMessage('Room saved successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const editRoom = (room: Room) => { + setEditingRoom(room); + setRoomForm(room); + }; + + const deleteRoom = (id: number) => { + setRooms(rooms.filter((r) => r.id !== id)); + setMessage('Room deleted successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const loadRoomComments = (roomId: number) => { + // In a real app, you would fetch comments from an API + // For now, we'll just set some dummy data + setRoomComments([ + { id: 1, room_id: roomId, first_name: 'John', last_initial: 'D', text: 'Great room!', created_at: '2023-05-01' }, + ]); + setRoomCommentRoomId(roomId); + }; + + const deleteRoomComment = (id: number) => { + setRoomComments(roomComments.filter((c) => c.id !== id)); + setMessage('Comment deleted'); + setTimeout(() => setMessage(null), 3000); + }; + + const saveMenu = (e: React.FormEvent) => { + e.preventDefault(); + if (editingMenu) { + setMenu(menu.map((m) => (m.id === editingMenu.id ? { ...menuForm, id: editingMenu.id } as MenuItem : m))); + setEditingMenu(null); + } else { + setMenu([...menu, { ...menuForm, id: menu.length + 1 } as MenuItem]); + } + setMenuForm({ + category: '', + name: '', + description: '', + price: '', + is_daily_special: false, + active: true, + sort_order: 0, + }); + setMessage('Menu item saved successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const editMenu = (menuItem: MenuItem) => { + setEditingMenu(menuItem); + setMenuForm(menuItem); + }; + + const deleteMenu = (id: number) => { + setMenu(menu.filter((m) => m.id !== id)); + setMessage('Menu item deleted successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const savePlace = (e: React.FormEvent) => { + e.preventDefault(); + if (editingPlace) { + setPlaces(places.map((p) => (p.id === editingPlace.id ? { ...placeForm, id: editingPlace.id } as Place : p))); + setEditingPlace(null); + } else { + setPlaces([...places, { ...placeForm, id: places.length + 1 } as Place]); + } + setPlaceForm({ + name: '', + description: '', + photos: [], + featured_photo: null, + distance_km: '', + visit_time: '', + suggestion: '', + active: true, + sort_order: 0, + }); + setMessage('Place saved successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const editPlace = (place: Place) => { + setEditingPlace(place); + setPlaceForm(place); + }; + + const deletePlace = (id: number) => { + setPlaces(places.filter((p) => p.id !== id)); + setMessage('Place deleted successfully'); + setTimeout(() => setMessage(null), 3000); + }; + + const approveReview = (id: number) => { + setReviews(reviews.map((r) => (r.id === id ? { ...r, approved: true } : r))); + setMessage('Review approved'); + setTimeout(() => setMessage(null), 3000); + }; + + const deleteReview = (id: number) => { + setReviews(reviews.filter((r) => r.id !== id)); + setMessage('Review deleted'); + setTimeout(() => setMessage(null), 3000); + }; + + const notify = (message: string) => { + setMessage(message); + setTimeout(() => setMessage(null), 3000); + }; + + // Stats data + const stats = [ + { label: 'Total Images', value: count }, + { label: 'Active Slides', value: slides.filter((s) => s.active).length }, + { label: 'Active Events', value: events.filter((e) => e.active).length }, + { label: 'Pending Reviews', value: reviews.filter((r) => !r.approved).length }, + ]; return ( -
+

Admin Portal

@@ -502,7 +807,9 @@ export default function AdminPage() { {error &&

{error}

} {message &&

{message}

} - + + +
@@ -624,7 +931,7 @@ export default function AdminPage() { setSlideForm({ ...slideForm, title: v })} /> setSlideForm({ ...slideForm, subtitle: v })} /> setSlideForm({ ...slideForm, sort_order: parseInt(v || '0', 10) })} /> - { + { const imageId = v ? parseInt(v, 10) : null; const image = imageId ? images.find((img) => img.id === imageId) : null; setSlideForm({ ...slideForm, image_id: imageId, image_url: image?.data || '' }); @@ -870,398 +1177,9 @@ export default function AdminPage() {
))} - function AccordionSection({ title, children, defaultOpen = false }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) { - const [open, setOpen] = useState(defaultOpen); - return ( -
-
setOpen((v) => !v)}> -

{title}

- {open ? 'Minimize' : 'Maximize'} -
- {open &&
{children}
} -
- ); - } - - function Button({ children, type = 'button', disabled, onClick }: { children: React.ReactNode; type?: 'button' | 'submit'; disabled?: boolean; onClick?: () => void }) { - return ( - - ); - } - - function SecondaryButton({ children, onClick, type = 'button' }: { children: React.ReactNode; onClick?: () => void; type?: 'button' | 'submit' }) { - return ( - - ); - } - - function DangerButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { - return ( - - ); -} - -function Text({ label, value, type = 'text', onChange }: { label: string; value: string; type?: string; onChange: (v: string) => void }) { - 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 PhotoPicker({ label, images, photos, featuredPhoto, onChange }: { label: string; images: ImageRecord[]; photos: string[]; featuredPhoto: string | null; onChange: (photos: string[], featured: string | null) => void }) { - return ( -
- -
- {images.map((img) => { - const selected = photos.includes(img.data); - const featured = featuredPhoto === img.data; - return ( -
{ - const next = selected ? photos.filter((p) => p !== img.data) : [...photos, img.data]; - const nextFeatured = featured ? null : img.data; - onChange(next, nextFeatured); - }} - style={{ - border: selected ? `3px solid ${gold}` : '3px solid transparent', - borderRadius: '12px', - overflow: 'hidden', - cursor: 'pointer', - position: 'relative', - transition: 'transform 150ms', - }} - onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.03)'; }} - onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} - > - {featured && FEATURED} - {selected && !featured && } - {img.filename} -
- ); - })} -
-
- ); -} - -function ListTable({ rows, onEdit, onDelete, onComments }: { rows: (string | number)[][]; onEdit?: (i: number) => void; onDelete?: (i: number) => void; onComments?: (i: number) => void }) { - if (rows.length === 0) return

No items yet.

; - return ( -
- {rows.map((row, i) => ( -
-
- {row.map((cell, j) => ( - {cell} - ))}
-
- {onEdit && onEdit(i)}>Edit} - {onComments && onComments(i)}>Reservations} - {onDelete && onDelete(i)}>Delete} -
-
- ))} -
+ )} +
+
); -} - -function Textarea({ label, value, onChange, rows = 3 }: { label: string; value: string; onChange: (v: string) => void; rows?: number }) { - return ( -