Add admin slideshow, horizontal animated calendar, and weather strip
This commit is contained in:
+214
-24
@@ -16,6 +16,8 @@ interface Room {
|
||||
description: string;
|
||||
price: string;
|
||||
photos: string[];
|
||||
active: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
interface RoomForm {
|
||||
@@ -24,6 +26,8 @@ interface RoomForm {
|
||||
description: string;
|
||||
price: string;
|
||||
photos: string | string[];
|
||||
active: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
@@ -33,6 +37,8 @@ interface MenuItem {
|
||||
description: string;
|
||||
price: string;
|
||||
is_daily_special: boolean;
|
||||
active: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
interface Place {
|
||||
@@ -43,6 +49,8 @@ interface Place {
|
||||
distance_km: string | null;
|
||||
visit_time: string | null;
|
||||
suggestion: string | null;
|
||||
active: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
interface Review {
|
||||
@@ -59,6 +67,26 @@ interface User {
|
||||
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';
|
||||
@@ -72,17 +100,18 @@ export default function AdminPage() {
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [logoUrl, setLogoUrl] = useState('');
|
||||
const [faviconUrl, setFaviconUrl] = useState('');
|
||||
const [tagline, setTagline] = useState('Hotel · Restaurant · Andean Highlands');
|
||||
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [roomForm, setRoomForm] = useState<RoomForm>({ name: '', description: '', price: '', photos: '' });
|
||||
const [roomForm, setRoomForm] = useState<RoomForm>({ name: '', description: '', price: '', photos: '', active: true, sort_order: 0 });
|
||||
const [editingRoom, setEditingRoom] = useState<number | null>(null);
|
||||
|
||||
const [menu, setMenu] = useState<MenuItem[]>([]);
|
||||
const [menuForm, setMenuForm] = useState<Partial<MenuItem>>({ category: '', name: '', description: '', price: '', is_daily_special: false });
|
||||
const [menuForm, setMenuForm] = useState<Partial<MenuItem>>({ category: '', name: '', description: '', price: '', is_daily_special: false, active: true, sort_order: 0 });
|
||||
const [editingMenu, setEditingMenu] = useState<number | null>(null);
|
||||
|
||||
const [places, setPlaces] = useState<Place[]>([]);
|
||||
const [placeForm, setPlaceForm] = useState<Partial<Place>>({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '' });
|
||||
const [placeForm, setPlaceForm] = useState<Partial<Place>>({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 });
|
||||
const [editingPlace, setEditingPlace] = useState<number | null>(null);
|
||||
|
||||
const [reviews, setReviews] = useState<Review[]>([]);
|
||||
@@ -90,6 +119,14 @@ export default function AdminPage() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [userForm, setUserForm] = useState({ username: '', password: '', role: 'user' as 'admin' | 'user' });
|
||||
|
||||
const [slides, setSlides] = useState<Slide[]>([]);
|
||||
const [slideForm, setSlideForm] = useState<Partial<Slide>>({ title: '', subtitle: '', image_id: null, image_url: '', active: true, sort_order: 0 });
|
||||
const [editingSlide, setEditingSlide] = useState<number | null>(null);
|
||||
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const [eventForm, setEventForm] = useState<Partial<CalendarEvent>>({ date: '', title: '', type: 'event', description: '', color: '#E8A849', active: true });
|
||||
const [editingEvent, setEditingEvent] = useState<number | null>(null);
|
||||
|
||||
const [footer, setFooter] = useState({
|
||||
facebook_url: '', instagram_url: '', whatsapp_url: '', address: '', phone: '', email: '',
|
||||
});
|
||||
@@ -112,6 +149,8 @@ export default function AdminPage() {
|
||||
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 || {};
|
||||
@@ -121,6 +160,7 @@ export default function AdminPage() {
|
||||
});
|
||||
if (c.logo) setLogoUrl(c.logo);
|
||||
if (c.favicon) setFaviconUrl(c.favicon);
|
||||
if (c.tagline) setTagline(c.tagline);
|
||||
}).catch(() => {});
|
||||
|
||||
setLoading(false);
|
||||
@@ -168,7 +208,7 @@ export default function AdminPage() {
|
||||
});
|
||||
if (!res.ok) { setError('Upload failed'); return; }
|
||||
const result = await res.json();
|
||||
setImages((prev) => [result.image, ...prev]);
|
||||
setImages((prev) => [{ ...result.image, data }, ...prev]);
|
||||
setCount((prev) => prev + 1);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
@@ -201,8 +241,8 @@ export default function AdminPage() {
|
||||
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()) : photosValue;
|
||||
const payload = { ...roomForm, photos: photosArray };
|
||||
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)));
|
||||
@@ -211,24 +251,25 @@ export default function AdminPage() {
|
||||
const json = await api('/api/rooms', 'POST', payload);
|
||||
setRooms((prev) => [...prev, json.data]);
|
||||
}
|
||||
setRoomForm({ name: '', description: '', price: '', photos: [] });
|
||||
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 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', menuForm);
|
||||
setMenu((prev) => prev.map((m) => (m.id === editingMenu ? { ...m, ...menuForm } as MenuItem : m)));
|
||||
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', menuForm);
|
||||
const json = await api('/api/menu', 'POST', payload);
|
||||
setMenu((prev) => [...prev, json.data]);
|
||||
}
|
||||
setMenuForm({ category: '', name: '', description: '', price: '', is_daily_special: false });
|
||||
setMenuForm({ category: '', name: '', description: '', price: '', is_daily_special: false, active: true, sort_order: 0 });
|
||||
notify('Menu item saved.');
|
||||
};
|
||||
|
||||
@@ -237,15 +278,16 @@ export default function AdminPage() {
|
||||
|
||||
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', placeForm);
|
||||
setPlaces((prev) => prev.map((p) => (p.id === editingPlace ? { ...p, ...placeForm } as Place : p)));
|
||||
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', placeForm);
|
||||
const json = await api('/api/places', 'POST', payload);
|
||||
setPlaces((prev) => [...prev, json.data]);
|
||||
}
|
||||
setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '' });
|
||||
setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 });
|
||||
notify('Place saved.');
|
||||
};
|
||||
|
||||
@@ -269,6 +311,42 @@ export default function AdminPage() {
|
||||
|
||||
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 (
|
||||
<main style={{ padding: '2rem', color: warm, fontSize: '18px' }}>
|
||||
@@ -294,6 +372,8 @@ export default function AdminPage() {
|
||||
<Button onClick={() => updateAsset('logo', logoUrl)} disabled={!logoUrl}>Save logo</Button>
|
||||
<Text label="Favicon" value={faviconUrl} onChange={setFaviconUrl} />
|
||||
<Button onClick={() => updateAsset('favicon', faviconUrl)} disabled={!faviconUrl}>Save favicon</Button>
|
||||
<Text label="Homepage tagline" value={tagline} onChange={setTagline} />
|
||||
<Button onClick={() => updateAsset('tagline', tagline)} disabled={!tagline}>Save tagline</Button>
|
||||
</div>
|
||||
<p style={{ margin: '1rem 0 0', fontSize: '13px', color: '#777' }}>
|
||||
Tip: click an uploaded gallery image to preview, copy its data URL, and paste it above.
|
||||
@@ -324,27 +404,97 @@ export default function AdminPage() {
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '1.25rem' }}>
|
||||
{images.map((img) => (
|
||||
<div key={img.id} style={{ borderRadius: '16px', overflow: 'hidden', display: 'flex', flexDirection: 'column', background: '#f5f0e8' }}>
|
||||
<div
|
||||
key={img.id}
|
||||
style={{
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#f5f0e8',
|
||||
transition: 'transform 200ms, box-shadow 200ms',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={(e) => { 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 src={img.data} alt={img.filename} style={{ width: '100%', height: '180px', objectFit: 'cover' }} />
|
||||
<button onClick={() => handleDelete(img.id)} style={{ padding: '0.65rem', background: '#c23b22', color: '#fff', border: 'none', cursor: 'pointer', fontSize: '14px', fontWeight: 600 }}>Delete</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(img.id); }}
|
||||
style={{ padding: '0.65rem', background: '#c23b22', color: '#fff', border: 'none', cursor: 'pointer', fontSize: '14px', fontWeight: 600 }}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title="Homepage Slideshow">
|
||||
<form onSubmit={saveSlide} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||
<Text label="Title" value={slideForm.title || ''} onChange={(v) => setSlideForm({ ...slideForm, title: v })} />
|
||||
<Text label="Subtitle" value={slideForm.subtitle || ''} onChange={(v) => setSlideForm({ ...slideForm, subtitle: v })} />
|
||||
<Text label="Image URL (or pick image ID below)" value={slideForm.image_url || ''} onChange={(v) => setSlideForm({ ...slideForm, image_url: v })} />
|
||||
<Number label="Sort order" value={slideForm.sort_order ?? 0} onChange={(v) => setSlideForm({ ...slideForm, sort_order: parseInt(v || '0', 10) })} />
|
||||
<ImageSelect label="Gallery image" images={images} value={slideForm.image_id ?? ''} onChange={(v) => setSlideForm({ ...slideForm, image_id: v ? parseInt(v, 10) : null })} />
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '14px', color: '#555' }}>
|
||||
<input type="checkbox" checked={slideForm.active !== false} onChange={(e) => setSlideForm({ ...slideForm, active: e.target.checked })} />
|
||||
Active
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'end' }}>
|
||||
<Button type="submit">{editingSlide ? 'Update slide' : 'Add slide'}</Button>
|
||||
{editingSlide && <SecondaryButton onClick={() => { setEditingSlide(null); setSlideForm({ title: '', subtitle: '', image_id: null, image_url: '', active: true, sort_order: 0 }); }}>Cancel</SecondaryButton>}
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={slides.map((s) => [s.title || '(untitled)', s.active ? 'On' : 'Off', `#${s.sort_order}`])} onEdit={(i) => editSlide(slides[i])} onDelete={(i) => deleteSlide(slides[i].id)} />
|
||||
</Section>
|
||||
|
||||
<Section title="Calendar Events">
|
||||
<form onSubmit={saveEvent} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||
<Text label="Date" type="date" value={eventForm.date || ''} onChange={(v) => setEventForm({ ...eventForm, date: v })} />
|
||||
<Text label="Title" value={eventForm.title || ''} onChange={(v) => setEventForm({ ...eventForm, title: v })} />
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
|
||||
Type
|
||||
<select value={eventForm.type || 'event'} onChange={(e) => setEventForm({ ...eventForm, type: e.target.value as CalendarEvent['type'] })} style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}>
|
||||
<option value="holiday">Holiday</option>
|
||||
<option value="season">Season</option>
|
||||
<option value="weather">Weather</option>
|
||||
<option value="event">Event</option>
|
||||
</select>
|
||||
</label>
|
||||
<Text label="Color" value={eventForm.color || '#E8A849'} onChange={(v) => setEventForm({ ...eventForm, color: v })} />
|
||||
<Text label="Description" value={eventForm.description || ''} onChange={(v) => setEventForm({ ...eventForm, description: v })} />
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '14px', color: '#555' }}>
|
||||
<input type="checkbox" checked={eventForm.active !== false} onChange={(e) => setEventForm({ ...eventForm, active: e.target.checked })} />
|
||||
Active
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'end' }}>
|
||||
<Button type="submit">{editingEvent ? 'Update event' : 'Add event'}</Button>
|
||||
{editingEvent && <SecondaryButton onClick={() => { setEditingEvent(null); setEventForm({ date: '', title: '', type: 'event', description: '', color: '#E8A849', active: true }); }}>Cancel</SecondaryButton>}
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={events.map((ev) => [ev.date, ev.title, ev.type, ev.active ? 'On' : 'Off'])} onEdit={(i) => editEvent(events[i])} onDelete={(i) => deleteEvent(events[i].id)} />
|
||||
</Section>
|
||||
|
||||
<Section title="Rooms">
|
||||
<form onSubmit={saveRoom} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||
<Text label="Name" value={roomForm.name || ''} onChange={(v) => setRoomForm({ ...roomForm, name: v })} />
|
||||
<Text label="Price / night" value={roomForm.price || ''} onChange={(v) => setRoomForm({ ...roomForm, price: v })} />
|
||||
<Text label="Photos (comma-separated URLs)" value={typeof roomForm.photos === 'string' ? roomForm.photos : roomForm.photos.join(', ')} onChange={(v) => setRoomForm({ ...roomForm, photos: v })} />
|
||||
<Number label="Sort order" value={roomForm.sort_order ?? 0} onChange={(v) => setRoomForm({ ...roomForm, sort_order: parseInt(v || '0', 10) })} />
|
||||
<Text label="Description" value={roomForm.description || ''} onChange={(v) => setRoomForm({ ...roomForm, description: v })} />
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '14px', color: '#555' }}>
|
||||
<input type="checkbox" checked={roomForm.active !== false} onChange={(e) => setRoomForm({ ...roomForm, active: e.target.checked })} />
|
||||
Active
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'end' }}>
|
||||
<Button type="submit">{editingRoom ? 'Update room' : 'Add room'}</Button>
|
||||
{editingRoom && <SecondaryButton onClick={() => { setEditingRoom(null); setRoomForm({ name: '', description: '', price: '', photos: [] }); }}>Cancel</SecondaryButton>}
|
||||
{editingRoom && <SecondaryButton onClick={() => { setEditingRoom(null); setRoomForm({ name: '', description: '', price: '', photos: '', active: true, sort_order: 0 }); }}>Cancel</SecondaryButton>}
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={rooms.map((r) => [r.name, `$${r.price}`, r.description.slice(0, 60)])} onEdit={(i) => editRoom(rooms[i])} onDelete={(i) => deleteRoom(rooms[i].id)} />
|
||||
<ListTable rows={rooms.map((r) => [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)} />
|
||||
</Section>
|
||||
|
||||
<Section title="Menu">
|
||||
@@ -352,17 +502,22 @@ export default function AdminPage() {
|
||||
<Text label="Category" value={menuForm.category || ''} onChange={(v) => setMenuForm({ ...menuForm, category: v })} />
|
||||
<Text label="Name" value={menuForm.name || ''} onChange={(v) => setMenuForm({ ...menuForm, name: v })} />
|
||||
<Text label="Price" value={menuForm.price || ''} onChange={(v) => setMenuForm({ ...menuForm, price: v })} />
|
||||
<Number label="Sort order" value={menuForm.sort_order ?? 0} onChange={(v) => setMenuForm({ ...menuForm, sort_order: parseInt(v || '0', 10) })} />
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '14px', color: '#555' }}>
|
||||
<input type="checkbox" checked={menuForm.is_daily_special || false} onChange={(e) => setMenuForm({ ...menuForm, is_daily_special: e.target.checked })} />
|
||||
Daily special
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '14px', color: '#555' }}>
|
||||
<input type="checkbox" checked={menuForm.active !== false} onChange={(e) => setMenuForm({ ...menuForm, active: e.target.checked })} />
|
||||
Active
|
||||
</label>
|
||||
<Text label="Description" value={menuForm.description || ''} onChange={(v) => setMenuForm({ ...menuForm, description: v })} />
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'end' }}>
|
||||
<Button type="submit">{editingMenu ? 'Update item' : 'Add item'}</Button>
|
||||
{editingMenu && <SecondaryButton onClick={() => { setEditingMenu(null); setMenuForm({ category: '', name: '', description: '', price: '', is_daily_special: false }); }}>Cancel</SecondaryButton>}
|
||||
{editingMenu && <SecondaryButton onClick={() => { setEditingMenu(null); setMenuForm({ category: '', name: '', description: '', price: '', is_daily_special: false, active: true, sort_order: 0 }); }}>Cancel</SecondaryButton>}
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={menu.map((m) => [m.category, m.name, `$${m.price}`, m.is_daily_special ? '★ Daily' : ''])} onEdit={(i) => editMenu(menu[i])} onDelete={(i) => deleteMenu(menu[i].id)} />
|
||||
<ListTable rows={menu.map((m) => [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)} />
|
||||
</Section>
|
||||
|
||||
<Section title="Landscape Places">
|
||||
@@ -371,14 +526,19 @@ export default function AdminPage() {
|
||||
<Text label="Distance (km)" value={placeForm.distance_km || ''} onChange={(v) => setPlaceForm({ ...placeForm, distance_km: v })} />
|
||||
<Text label="Visit time" value={placeForm.visit_time || ''} onChange={(v) => setPlaceForm({ ...placeForm, visit_time: v })} />
|
||||
<Text label="Photo URL" value={placeForm.photo || ''} onChange={(v) => setPlaceForm({ ...placeForm, photo: v })} />
|
||||
<Number label="Sort order" value={placeForm.sort_order ?? 0} onChange={(v) => setPlaceForm({ ...placeForm, sort_order: parseInt(v || '0', 10) })} />
|
||||
<Text label="Description" value={placeForm.description || ''} onChange={(v) => setPlaceForm({ ...placeForm, description: v })} />
|
||||
<Text label="Suggestion" value={placeForm.suggestion || ''} onChange={(v) => setPlaceForm({ ...placeForm, suggestion: v })} />
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '14px', color: '#555' }}>
|
||||
<input type="checkbox" checked={placeForm.active !== false} onChange={(e) => setPlaceForm({ ...placeForm, active: e.target.checked })} />
|
||||
Active
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'end' }}>
|
||||
<Button type="submit">{editingPlace ? 'Update place' : 'Add place'}</Button>
|
||||
{editingPlace && <SecondaryButton onClick={() => { setEditingPlace(null); setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '' }); }}>Cancel</SecondaryButton>}
|
||||
{editingPlace && <SecondaryButton onClick={() => { setEditingPlace(null); setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 }); }}>Cancel</SecondaryButton>}
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={places.map((p) => [p.name, `${p.distance_km || ''} km`, p.description.slice(0, 60)])} onEdit={(i) => editPlace(places[i])} onDelete={(i) => deletePlace(places[i].id)} />
|
||||
<ListTable rows={places.map((p) => [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)} />
|
||||
</Section>
|
||||
|
||||
<Section title="Pending Reviews">
|
||||
@@ -445,6 +605,36 @@ function Text({ label, value, onChange, type = 'text' }: { label: string; value:
|
||||
);
|
||||
}
|
||||
|
||||
function Number({ label, value, onChange }: { label: string; value: number; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
|
||||
{label}
|
||||
<input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageSelect({ label, images, value, onChange }: { label: string; images: ImageRecord[]; value: string | number; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
|
||||
{label}
|
||||
<select value={value} onChange={(e) => onChange(e.target.value)} style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}>
|
||||
<option value="">None (use URL)</option>
|
||||
{images.map((img) => (
|
||||
<option key={img.id} value={img.id}>
|
||||
#{img.id} {img.filename}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({ children, onClick, type = 'button', disabled = false }: { children: React.ReactNode; onClick?: () => void; type?: 'button' | 'submit'; disabled?: boolean }) {
|
||||
return (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user