feat(landscape): multi-photo gallery like rooms with uploads and ordering

This commit is contained in:
2026-06-27 19:29:08 -05:00
parent cfd0c533a4
commit 71ae322b3f
5 changed files with 174 additions and 76 deletions
+18 -11
View File
@@ -67,7 +67,8 @@ interface Place {
id: number;
name: string;
description: string;
photo: string | null;
photos: string[];
featured_photo: string | null;
distance_km: string | null;
visit_time: string | null;
suggestion: string | null;
@@ -140,7 +141,7 @@ export default function AdminPage() {
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: '', active: true, sort_order: 0 });
const [placeForm, setPlaceForm] = useState<Partial<Place>>({ name: '', description: '', photos: [], featured_photo: null, distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 });
const [editingPlace, setEditingPlace] = useState<number | null>(null);
const [reviews, setReviews] = useState<Review[]>([]);
@@ -305,7 +306,7 @@ 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 };
const payload = { ...placeForm, photos: placeForm.photos || [], featured_photo: placeForm.featured_photo || null, active: placeForm.active !== false, sort_order: placeForm.sort_order || 0 };
if (editingPlace) {
await api(`/api/places/${editingPlace}`, 'PUT', payload);
setPlaces((prev) => prev.map((p) => (p.id === editingPlace ? { ...p, ...payload } as Place : p)));
@@ -314,11 +315,11 @@ export default function AdminPage() {
const json = await api('/api/places', 'POST', payload);
setPlaces((prev) => [...prev, json.data]);
}
setPlaceForm({ name: '', description: '', photo: '', distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 });
setPlaceForm({ name: '', description: '', photos: [], featured_photo: null, distance_km: '', visit_time: '', suggestion: '', active: true, sort_order: 0 });
notify('Place saved.');
};
const editPlace = (p: Place) => { setPlaceForm({ ...p }); setEditingPlace(p.id); };
const editPlace = (p: Place) => { setPlaceForm({ ...p, photos: p.photos || [], featured_photo: p.featured_photo || null }); setEditingPlace(p.id); };
const deletePlace = async (id: number) => { await api(`/api/places/${id}`, 'DELETE'); setPlaces((prev) => prev.filter((p) => p.id !== id)); };
const approveReview = async (id: number) => {
@@ -558,7 +559,7 @@ export default function AdminPage() {
<Text label="Price / night" value={roomForm.price || ''} onChange={(v) => setRoomForm({ ...roomForm, price: v })} />
<Text label="Description" value={roomForm.description || ''} onChange={(v) => setRoomForm({ ...roomForm, description: v })} />
<Number label="Sort order" value={roomForm.sort_order ?? 0} onChange={(v) => setRoomForm({ ...roomForm, sort_order: parseInt(v || '0', 10) })} />
<RoomPhotoPicker
<PhotoPicker
label="Room photos"
images={images}
photos={roomForm.photos || []}
@@ -635,8 +636,14 @@ export default function AdminPage() {
<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 })} />
<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) })} />
<PhotoPicker
label="Photos"
images={images}
photos={placeForm.photos || []}
featuredPhoto={placeForm.featured_photo || null}
onChange={(photos: string[], featured: string | null) => setPlaceForm({ ...placeForm, photos, featured_photo: featured })}
/>
<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' }}>
@@ -645,10 +652,10 @@ export default function AdminPage() {
</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: '', active: true, sort_order: 0 }); }}>Cancel</SecondaryButton>}
{editingPlace && <SecondaryButton onClick={() => { setEditingPlace(null); setPlaceForm({ name: '', description: '', photos: ([] as string[]), featured_photo: null, 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), 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>
<Section title="Pending Reviews">
@@ -823,7 +830,7 @@ function ListTable({ rows, onEdit, onDelete, onComments }: { rows: string[][]; o
);
}
function RoomPhotoPicker({
function PhotoPicker({
label, images, photos, featuredPhoto, onChange,
}: {
label: string;
@@ -873,7 +880,7 @@ function RoomPhotoPicker({
</div>
{allPhotos.length > 0 && (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<span style={{ fontSize: '13px', color: '#777' }}>Featured photo:</span>
<span style={{ fontSize: '13px', color: '#777' }}>Featured:</span>
<select
value={featuredPhoto || ''}
onChange={(e) => onChange(photos, e.target.value || null)}