Compare commits

...

2 Commits

2 changed files with 86 additions and 25 deletions
+84 -25
View File
@@ -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>}
&lt;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>
&lt;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">
<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>
&lt;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' }}>
+ Upload images
<input type="file" accept="image/*" multiple onChange={handleUpload} style={{ display: 'none' }} />
</label>
{images.length === 0 ? (
<p style={{ color: '#777' }}>No images uploaded yet.</p>
) : (
@@ -560,9 +617,9 @@ export default function AdminPage() {
))}
</div>
)}
</Section>
</AccordionSection>
&lt;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>
&lt;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>
&lt;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>
&lt;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>
&lt;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>
&lt;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>
&lt;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>
);
);
}
}
+2
View File
@@ -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()
);
`);