fix: admin panel save issues - POST/PUT URLs, users list, image upload
This commit is contained in:
+221
-22
@@ -473,18 +473,24 @@ function Confirm({ message, onConfirm, onCancel }: any) {
|
||||
}
|
||||
|
||||
/* ─── Slides Section ─── */
|
||||
function SlidesSection({ slides, setSlides, images, onToast }: any) {
|
||||
function SlidesSection({ slides, setSlides, images, onToast, onRefreshImages }: any) {
|
||||
const [editing, setEditing] = useState<Slide | null>(null);
|
||||
const [confirmDel, setConfirmDel] = useState<number | null>(null);
|
||||
|
||||
const saveSlide = async (slide: Slide) => {
|
||||
try {
|
||||
const res = await fetch(`/api/slides/${slide.id}`, {
|
||||
const url = slide.id ? `/api/slides/${slide.id}` : '/api/slides';
|
||||
const res = await fetch(url, {
|
||||
method: slide.id ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(slide),
|
||||
});
|
||||
if (res.ok) { setEditing(null); setSlides((s: any) => s.map((s: Slide) => s.id === slide.id ? slide : s)); onToast('Slide saved!', 'success'); }
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEditing(null);
|
||||
setSlides((s: any) => slide.id ? s.map((x: Slide) => x.id === slide.id ? data.data : x) : [...s, data.data]);
|
||||
onToast('Slide saved!', 'success');
|
||||
}
|
||||
} catch { onToast('Error saving slide', 'error'); }
|
||||
};
|
||||
|
||||
@@ -516,16 +522,41 @@ function SlidesSection({ slides, setSlides, images, onToast }: any) {
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<SlideEditor slide={editing} setSlide={setEditing} images={images} onSave={saveSlide} onCancel={() => setEditing(null)} />
|
||||
<SlideEditor slide={editing} setSlide={setEditing} images={images} onRefreshImages={onRefreshImages} onSave={saveSlide} onCancel={() => setEditing(null)} />
|
||||
)}
|
||||
{confirmDel !== null && <Confirm message="Delete this slide?" onCancel={() => setConfirmDel(null)} onConfirm={() => delSlide(confirmDel)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) {
|
||||
function SlideEditor({ slide, setSlide, images, onRefreshImages, onSave, onCancel }: any) {
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const s = { ...slide };
|
||||
|
||||
const handleUpload = async (files: FileList) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
for (const file of Array.from(files)) {
|
||||
const b64 = await readFileBase64(file);
|
||||
const res = await fetch('/api/admin/images', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: file.name, data: b64, category: 'slides', room_id: null, location_target: 'slides' }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
onRefreshImages?.();
|
||||
setSlide({ ...s, image_id: data.data.id, image_url: data.data.data });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9998 }}>
|
||||
<div style={{ background: '#FFFFFF', borderRadius: 16, padding: '24px 28px', maxWidth: 500, width: '90%', maxHeight: '90vh', overflowY: 'auto' }}>
|
||||
@@ -537,6 +568,10 @@ function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) {
|
||||
<label style={{ fontSize: '13px', fontWeight: 600, color: C.text }}>Image</label>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<Btn onClick={() => setShowPicker(!showPicker)} size="sm" variant="secondary">{showPicker ? 'Hide picker' : 'Pick from gallery'}</Btn>
|
||||
<label style={{ cursor: 'pointer' }}>
|
||||
<input type="file" accept="image/*" multiple style={{ display: 'none' }} onChange={(e) => { if (e.target.files?.length) handleUpload(e.target.files); e.target.value = ''; }} />
|
||||
<Btn size="sm" variant="secondary" disabled={uploading}>{uploading ? 'Uploading...' : 'Upload from computer'}</Btn>
|
||||
</label>
|
||||
{s.image_id && images.find((i: Image) => i.id === s.image_id) && (
|
||||
<img src={images.find((i: Image) => i.id === s.image_id)!.data} style={{ width: 80, height: 56, objectFit: 'cover', borderRadius: 8, border: `2px solid ${C.gold}` }} />
|
||||
)}
|
||||
@@ -547,7 +582,7 @@ function SlideEditor({ slide, setSlide, images, onSave, onCancel }: any) {
|
||||
<img key={img.id} src={img.data} alt="" onClick={() => { setSlide({ ...s, image_id: img.id, image_url: img.data }); setShowPicker(false); }}
|
||||
style={{ width: 64, height: 48, objectFit: 'cover', borderRadius: 6, cursor: 'pointer', border: s.image_id === img.id ? `2px solid ${C.gold}` : '1px solid #eee' }} />
|
||||
))}
|
||||
{images.filter((i: Image) => i.category === 'slides').length === 0 && <span style={{ fontSize: '12px', color: C.textLight, padding: '8px' }}>No images uploaded</span>}
|
||||
{images.filter((i: Image) => i.category === 'slides').length === 0 && <span style={{ fontSize: '12px', color: C.textLight, padding: '8px' }}>No images uploaded. Use "Upload from computer" above.</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -572,12 +607,18 @@ function CalendarSection({ events, setEvents, images, onToast }: any) {
|
||||
|
||||
const save = async (ev: CalendarEvent) => {
|
||||
try {
|
||||
const res = await fetch(`/api/calendar_events/${ev.id}`, {
|
||||
const url = ev.id ? `/api/calendar_events/${ev.id}` : '/api/calendar_events';
|
||||
const res = await fetch(url, {
|
||||
method: ev.id ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ev),
|
||||
});
|
||||
if (res.ok) { setEditing(null); setEvents((e: any) => e.map((x: CalendarEvent) => x.id === ev.id ? ev : x)); onToast('Event saved!', 'success'); }
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEditing(null);
|
||||
setEvents((e: any) => ev.id ? e.map((x: CalendarEvent) => x.id === ev.id ? data.data : x) : [...e, data.data]);
|
||||
onToast('Event saved!', 'success');
|
||||
}
|
||||
} catch { onToast('Error', 'error'); }
|
||||
};
|
||||
|
||||
@@ -642,12 +683,18 @@ function SocialSection({ events, setEvents, images, onToast }: any) {
|
||||
|
||||
const save = async (ev: SocialEvent) => {
|
||||
try {
|
||||
const res = await fetch(`/api/social_events/${ev.id}`, {
|
||||
const url = ev.id ? `/api/social_events/${ev.id}` : '/api/social_events';
|
||||
const res = await fetch(url, {
|
||||
method: ev.id ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ev),
|
||||
});
|
||||
if (res.ok) { setEditing(null); setEvents((e: any) => e.map((x: SocialEvent) => x.id === ev.id ? ev : x)); onToast('Event saved!', 'success'); }
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEditing(null);
|
||||
setEvents((e: any) => ev.id ? e.map((x: SocialEvent) => x.id === ev.id ? data.data : x) : [...e, data.data]);
|
||||
onToast('Event saved!', 'success');
|
||||
}
|
||||
} catch { onToast('Error', 'error'); }
|
||||
};
|
||||
|
||||
@@ -748,12 +795,18 @@ function RoomsSection({ rooms, setRooms, images, onToast }: any) {
|
||||
|
||||
const save = async (room: Room) => {
|
||||
try {
|
||||
const res = await fetch(`/api/rooms/${room.id}`, {
|
||||
const url = room.id ? `/api/rooms/${room.id}` : '/api/rooms';
|
||||
const res = await fetch(url, {
|
||||
method: room.id ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(room),
|
||||
});
|
||||
if (res.ok) { setEditing(null); setRooms((r: any) => r.map((x: Room) => x.id === room.id ? room : x)); onToast('Room saved!', 'success'); }
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEditing(null);
|
||||
setRooms((r: any) => room.id ? r.map((x: Room) => x.id === room.id ? data.data : x) : [...r, data.data]);
|
||||
onToast('Room saved!', 'success');
|
||||
}
|
||||
} catch { onToast('Error', 'error'); }
|
||||
};
|
||||
|
||||
@@ -1079,12 +1132,18 @@ function PlacesSection({ places, setPlaces, images, onToast }: any) {
|
||||
|
||||
const save = async (place: Place) => {
|
||||
try {
|
||||
const res = await fetch(`/api/places/${place.id}`, {
|
||||
const url = place.id ? `/api/places/${place.id}` : '/api/places';
|
||||
const res = await fetch(url, {
|
||||
method: place.id ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(place),
|
||||
});
|
||||
if (res.ok) { setEditing(null); setPlaces((p: any) => p.map((x: Place) => x.id === place.id ? place : x)); onToast('Place saved!', 'success'); }
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEditing(null);
|
||||
setPlaces((p: any) => place.id ? p.map((x: Place) => x.id === place.id ? data.data : x) : [...p, data.data]);
|
||||
onToast('Place saved!', 'success');
|
||||
}
|
||||
} catch { onToast('Error', 'error'); }
|
||||
};
|
||||
|
||||
@@ -1302,16 +1361,149 @@ function Dashboard({ rooms, slides, events, reviews, places }: any) {
|
||||
}
|
||||
|
||||
/* ─── Users Section ─── */
|
||||
function UsersSection() {
|
||||
function UsersSection({ onToast }: { onToast: (msg: string, type: string) => void }) {
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState<any | null>(null);
|
||||
const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user' });
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/users');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUsers(data.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load users:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createUser = async () => {
|
||||
if (!newUser.username || !newUser.password) {
|
||||
onToast('Username and password required', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newUser),
|
||||
});
|
||||
if (res.ok) {
|
||||
onToast('User created!', 'success');
|
||||
setNewUser({ username: '', password: '', role: 'user' });
|
||||
setShowAdd(false);
|
||||
loadUsers();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
onToast(err.error || 'Failed to create user', 'error');
|
||||
}
|
||||
} catch {
|
||||
onToast('Error creating user', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const updateUser = async (user: any) => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/users', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
if (res.ok) {
|
||||
onToast('User updated!', 'success');
|
||||
setEditing(null);
|
||||
loadUsers();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
onToast(err.error || 'Failed to update user', 'error');
|
||||
}
|
||||
} catch {
|
||||
onToast('Error updating user', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUser = async (id: number) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users?id=${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
onToast('User deleted', 'success');
|
||||
loadUsers();
|
||||
}
|
||||
} catch {
|
||||
onToast('Error deleting user', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ textAlign: 'center', padding: '40px' }}>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SecHead title="👥 Users" />
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: C.textLight }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '8px' }}>👥</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600, color: C.text }}>User management</div>
|
||||
<p style={{ fontSize: '13px' }}>Manage staff accounts and permissions.</p>
|
||||
<SecHead title="👥 Users" count={users.length}
|
||||
action={<Btn onClick={() => setShowAdd(!showAdd)} size="sm">+ Add User</Btn>} />
|
||||
{showAdd && (
|
||||
<div style={{ background: C.card, borderRadius: 12, padding: 16, marginBottom: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12 }}>
|
||||
<I label="Username" value={newUser.username} onChange={(v: string) => setNewUser({ ...newUser, username: v })} />
|
||||
<I label="Password" type="password" value={newUser.password} onChange={(v: string) => setNewUser({ ...newUser, password: v })} />
|
||||
<div>
|
||||
<label style={{ fontSize: 13, fontWeight: 600, color: C.text, display: 'block', marginBottom: 6 }}>Role</label>
|
||||
<select value={newUser.role} onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: `1px solid ${C.border}`, fontSize: 14 }}>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
|
||||
<Btn onClick={createUser} size="sm">Create User</Btn>
|
||||
<Btn variant="secondary" onClick={() => setShowAdd(false)} size="sm">Cancel</Btn>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
{users.map((u: any) => (
|
||||
<div key={u.id} style={{ background: C.card, borderRadius: 12, padding: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: C.text }}>{u.username} <span style={{ fontSize: 12, background: u.role === 'admin' ? '#dc2626' : '#059669', color: '#fff', padding: '2px 8px', borderRadius: 4, marginLeft: 8 }}>{u.role}</span></div>
|
||||
<div style={{ fontSize: 12, color: C.textLight }}>{u.first_name || ''} {u.last_name || ''} {u.comments_disabled ? '• Comments disabled' : ''}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Btn size="sm" variant="secondary" onClick={() => setEditing(u)}>Edit</Btn>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{editing && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9998 }}>
|
||||
<div style={{ background: '#fff', borderRadius: 16, padding: 24, maxWidth: 400, width: '90%' }}>
|
||||
<h3 style={{ margin: '0 0 20px', fontSize: 18, fontWeight: 700 }}>Edit User</h3>
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<I label="First Name" value={editing.first_name || ''} onChange={(v: string) => setEditing({ ...editing, first_name: v })} />
|
||||
<I label="Last Name" value={editing.last_name || ''} onChange={(v: string) => setEditing({ ...editing, last_name: v })} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" checked={editing.comments_disabled || false} onChange={(e) => setEditing({ ...editing, comments_disabled: e.target.checked })} />
|
||||
<label style={{ fontSize: 13, color: C.text }}>Disable comments</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
|
||||
<Btn onClick={() => updateUser(editing)} size="sm">Save</Btn>
|
||||
<Btn variant="secondary" onClick={() => setEditing(null)} size="sm">Cancel</Btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1518,6 +1710,13 @@ export default function AdminPage() {
|
||||
}, []);
|
||||
|
||||
/* ── Data fetching ── */
|
||||
const loadImages = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/images');
|
||||
if (res.ok) { const d = await res.json(); setImages(Array.isArray(d) ? d : (d.images || [])); }
|
||||
} catch (err) { console.error('Failed to load images:', err); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
const fetchAll = async () => {
|
||||
@@ -1563,14 +1762,14 @@ export default function AdminPage() {
|
||||
<ImageGallery images={images} onDelete={deleteImage} />
|
||||
</div>
|
||||
);
|
||||
case 'slides': return <SlidesSection slides={slides} setSlides={setSlides} images={images} onToast={showToast} />;
|
||||
case 'slides': return <SlidesSection slides={slides} setSlides={setSlides} images={images} onToast={showToast} onRefreshImages={loadImages} />;
|
||||
case 'calendar': return <CalendarSection events={events} setEvents={setEvents} images={images} onToast={showToast} />;
|
||||
case 'social': return <SocialSection events={socialEvents} setEvents={setSocialEvents} images={images} onToast={showToast} />;
|
||||
case 'rooms': return <RoomsSection rooms={rooms} setRooms={setRooms} images={images} onToast={showToast} />;
|
||||
case 'menu': return <MenuSection items={menuItems} setItems={setMenuItems} images={images} onToast={showToast} />;
|
||||
case 'places': return <PlacesSection places={places} setPlaces={setPlaces} images={images} onToast={showToast} />;
|
||||
case 'reviews': return <ReviewsSection reviews={reviews} setReviews={setReviews} onToast={showToast} />;
|
||||
case 'users': return <UsersSection />;
|
||||
case 'users': return <UsersSection onToast={showToast} />;
|
||||
case 'weather': return <WeatherSection onToast={showToast} />;
|
||||
default: return <Dashboard rooms={rooms} slides={slides} events={events} reviews={reviews} places={places} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user