Implement organized image upload functionality: Added category, room, and location target fields for image uploads

This commit is contained in:
2026-06-27 21:12:07 -05:00
parent 69cb389ecf
commit 52f6668f02
2 changed files with 74 additions and 13 deletions
+72 -13
View File
@@ -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' }}>
+ Upload images <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem' }}>
<input type="file" accept="image/*" multiple onChange={handleUpload} style={{ display: 'none' }} /> <label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '14px', color: '#555' }}>
</label> 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 ? ( {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>
); );
);
}
} }
+2
View File
@@ -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()
); );
`); `);