feat(landscape): multi-photo gallery like rooms with uploads and ordering
This commit is contained in:
+18
-11
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user