Compare commits
2 Commits
ba920406c8
...
52f6668f02
| Author | SHA1 | Date | |
|---|---|---|---|
| 52f6668f02 | |||
| 69cb389ecf |
+81
-22
@@ -246,6 +246,10 @@ export default function AdminPage() {
|
||||
notify('Footer info updated.');
|
||||
};
|
||||
|
||||
const [uploadCategory, setUploadCategory] = useState<string>('other');
|
||||
const [uploadRoomId, setUploadRoomId] = useState<string>('');
|
||||
const [uploadLocationTarget, setUploadLocationTarget] = useState<string>('gallery');
|
||||
|
||||
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
@@ -253,15 +257,17 @@ export default function AdminPage() {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const data = reader.result as string;
|
||||
const body: Record<string, unknown> = { 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({ filename: file.name, data }),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!res.ok) { setError('Upload failed'); return; }
|
||||
const result = await res.json();
|
||||
setImages((prev) => [{ ...result.image, data }, ...prev]);
|
||||
setImages((prev) => [{ ...result.image, data, category: uploadCategory }, ...prev]);
|
||||
setCount((prev) => prev + 1);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
@@ -496,7 +502,7 @@ export default function AdminPage() {
|
||||
{error && <p style={{ color: '#c23b22', marginBottom: '1rem' }}>{error}</p>}
|
||||
{message && <p style={{ color: '#3b7a22', marginBottom: '1rem' }}>{message}</p>}
|
||||
|
||||
<AccordionSection title="Brand Assets">
|
||||
<AccordionSection title="Brand Assets">
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: '1fr auto', alignItems: 'end' }}>
|
||||
<Text label="Navbar logo" value={logoUrl} onChange={setLogoUrl} />
|
||||
<Button onClick={() => updateAsset('logo', logoUrl)} disabled={!logoUrl}>Save logo</Button>
|
||||
@@ -508,9 +514,9 @@ export default function AdminPage() {
|
||||
<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.
|
||||
</p>
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Footer / Contact">
|
||||
<AccordionSection title="Footer / Contact">
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))' }}>
|
||||
<Text label="Facebook URL" value={footer.facebook_url} onChange={(v) => setFooter({ ...footer, facebook_url: v })} />
|
||||
<Text label="Instagram URL" value={footer.instagram_url} onChange={(v) => setFooter({ ...footer, instagram_url: v })} />
|
||||
@@ -522,13 +528,64 @@ export default function AdminPage() {
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Button onClick={saveFooter}>Save footer info</Button>
|
||||
</div>
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Image Gallery">
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', padding: '0.75rem 1.25rem', background: gold, color: navy, borderRadius: '999px', cursor: 'pointer', fontWeight: 600, marginBottom: '1.5rem' }}>
|
||||
<AccordionSection title="Image Gallery">
|
||||
<div style={{ display: 'grid', gap: '1rem', marginBottom: '1.5rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem' }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
|
||||
Category
|
||||
<select
|
||||
value={uploadCategory}
|
||||
onChange={(e) => setUploadCategory(e.target.value)}
|
||||
style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}
|
||||
>
|
||||
<option value="rooms">Rooms</option>
|
||||
<option value="coffee">Coffee</option>
|
||||
<option value="landscape">Landscape</option>
|
||||
<option value="social">Social</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{uploadCategory === 'rooms' && (
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
|
||||
Room
|
||||
<select
|
||||
value={uploadRoomId}
|
||||
onChange={(e) => setUploadRoomId(e.target.value)}
|
||||
style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}
|
||||
>
|
||||
<option value="">Select a room</option>
|
||||
{rooms.map((room) => (
|
||||
<option key={room.id} value={room.id}>{room.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
|
||||
Location Target
|
||||
<select
|
||||
value={uploadLocationTarget}
|
||||
onChange={(e) => setUploadLocationTarget(e.target.value)}
|
||||
style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}
|
||||
>
|
||||
<option value="gallery">Gallery</option>
|
||||
<option value="homepage">Homepage</option>
|
||||
<option value="rooms">Rooms</option>
|
||||
<option value="coffee">Coffee</option>
|
||||
<option value="social">Social</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', padding: '0.75rem 1.25rem', background: gold, color: navy, borderRadius: '999px', cursor: 'pointer', fontWeight: 600 }}>
|
||||
+ Upload images
|
||||
<input type="file" accept="image/*" multiple onChange={handleUpload} style={{ display: 'none' }} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{images.length === 0 ? (
|
||||
<p style={{ color: '#777' }}>No images uploaded yet.</p>
|
||||
) : (
|
||||
@@ -560,9 +617,9 @@ export default function AdminPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Homepage Slideshow">
|
||||
<AccordionSection 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 })} />
|
||||
@@ -587,9 +644,9 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={slides.map((s) => [s.title || '(untitled)', s.image_url ? '🖼️ image' : 'no image', s.active ? 'On' : 'Off', `#${s.sort_order}`])} onEdit={(i) => editSlide(slides[i])} onDelete={(i) => deleteSlide(slides[i].id)} />
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Calendar Events">
|
||||
<AccordionSection 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 })} />
|
||||
@@ -625,9 +682,9 @@ export default function AdminPage() {
|
||||
</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>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Social Events">
|
||||
<AccordionSection title="Social Events">
|
||||
<form onSubmit={saveSocialEvent} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||
<Text label="Name" value={socialEventForm.name || ''} onChange={(v) => setSocialEventForm({ ...socialEventForm, name: v })} />
|
||||
<Text label="Price" value={socialEventForm.price || ''} onChange={(v) => setSocialEventForm({ ...socialEventForm, price: v })} />
|
||||
@@ -690,9 +747,9 @@ export default function AdminPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Rooms">
|
||||
<AccordionSection 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 })} />
|
||||
@@ -745,9 +802,9 @@ export default function AdminPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Menu">
|
||||
<AccordionSection title="Menu">
|
||||
<form onSubmit={saveMenu} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||
<Text label="Category" value={menuForm.category || ''} onChange={(v) => setMenuForm({ ...menuForm, category: v })} />
|
||||
<Text label="Name" value={menuForm.name || ''} onChange={(v) => setMenuForm({ ...menuForm, name: v })} />
|
||||
@@ -768,9 +825,9 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</form>
|
||||
<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>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Landscape Places">
|
||||
<AccordionSection title="Landscape Places">
|
||||
<form onSubmit={savePlace} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||
<Text label="Name" value={placeForm.name || ''} onChange={(v) => setPlaceForm({ ...placeForm, name: v })} />
|
||||
<Text label="Distance (km)" value={placeForm.distance_km || ''} onChange={(v) => setPlaceForm({ ...placeForm, distance_km: v })} />
|
||||
@@ -795,9 +852,9 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</form>
|
||||
<ListTable rows={places.map((p) => [p.name, `${p.distance_km || ''} km`, p.description.slice(0, 60), p.photos.length > 0 ? `🖼️ ${p.photos.length}` : 'no image', p.active ? 'On' : 'Off', `#${p.sort_order}`])} onEdit={(i) => editPlace(places[i])} onDelete={(i) => deletePlace(places[i].id)} />
|
||||
</Section>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection title="Pending Reviews">
|
||||
<AccordionSection title="Pending Reviews">
|
||||
{reviews.length === 0 ? <p style={{ color: '#777' }}>No pending reviews.</p> : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{reviews.map((r) => (
|
||||
@@ -1205,4 +1262,6 @@ function StatsCards({ stats }: { stats: { label: string; value: string | number
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export async function initSchema() {
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
category VARCHAR(100) DEFAULT 'other',
|
||||
room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL,
|
||||
uploaded_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user