Implement organized image upload functionality: Added category, room, and location target fields for image uploads
This commit is contained in:
+69
-10
@@ -246,6 +246,10 @@ export default function AdminPage() {
|
|||||||
notify('Footer info updated.');
|
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 handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
@@ -253,15 +257,17 @@ export default function AdminPage() {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
const data = reader.result as string;
|
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', {
|
const res = await fetch('/api/admin/images', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ filename: file.name, data }),
|
body: JSON.stringify(body),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
if (!res.ok) { setError('Upload failed'); return; }
|
if (!res.ok) { setError('Upload failed'); return; }
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
setImages((prev) => [{ ...result.image, data }, ...prev]);
|
setImages((prev) => [{ ...result.image, data, category: uploadCategory }, ...prev]);
|
||||||
setCount((prev) => prev + 1);
|
setCount((prev) => prev + 1);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@@ -525,10 +531,61 @@ export default function AdminPage() {
|
|||||||
</AccordionSection>
|
</AccordionSection>
|
||||||
|
|
||||||
<AccordionSection title="Image Gallery">
|
<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' }}>
|
<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
|
+ Upload images
|
||||||
<input type="file" accept="image/*" multiple onChange={handleUpload} style={{ display: 'none' }} />
|
<input type="file" accept="image/*" multiple onChange={handleUpload} style={{ display: 'none' }} />
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{images.length === 0 ? (
|
{images.length === 0 ? (
|
||||||
<p style={{ color: '#777' }}>No images uploaded yet.</p>
|
<p style={{ color: '#777' }}>No images uploaded yet.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -560,7 +617,7 @@ export default function AdminPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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' }}>
|
<form onSubmit={saveSlide} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||||
@@ -587,7 +644,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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)} />
|
<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' }}>
|
<form onSubmit={saveEvent} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||||
@@ -625,7 +682,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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)} />
|
<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' }}>
|
<form onSubmit={saveSocialEvent} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||||
@@ -690,7 +747,7 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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' }}>
|
<form onSubmit={saveRoom} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||||
@@ -745,7 +802,7 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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' }}>
|
<form onSubmit={saveMenu} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||||
@@ -768,7 +825,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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)} />
|
<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' }}>
|
<form onSubmit={savePlace} style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', marginBottom: '1.5rem' }}>
|
||||||
@@ -795,7 +852,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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)} />
|
<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> : (
|
{reviews.length === 0 ? <p style={{ color: '#777' }}>No pending reviews.</p> : (
|
||||||
@@ -1205,4 +1262,6 @@ function StatsCards({ stats }: { stats: { label: string; value: string | number
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export async function initSchema() {
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
filename VARCHAR(255) NOT NULL,
|
filename VARCHAR(255) NOT NULL,
|
||||||
data TEXT 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()
|
uploaded_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|||||||
Reference in New Issue
Block a user