Replace admin alerts with inline errors and toasts
All checks were successful
docker / test (push) Successful in 37s
docker / build-and-push (push) Successful in 59s

This commit is contained in:
2026-05-21 16:25:43 +00:00
parent fd874c9499
commit 7cdf9b95f7
2 changed files with 55 additions and 14 deletions

View File

@@ -16,6 +16,7 @@ export default function App() {
const [links, setLinks] = useState<LinkItem[]>([]);
const [page, setPage] = useState<Page>('loading');
const [query, setQuery] = useState('');
const [toast, setToast] = useState<{ message: string; tone: 'success' | 'error' } | null>(null);
async function refresh() {
const current = await api.request<SetupState>('/api/me');
@@ -33,6 +34,13 @@ export default function App() {
setLinks(await api.request<LinkItem[]>('/api/links'));
}
function showToast(message: string, tone: 'success' | 'error' = 'success') {
setToast({ message, tone });
window.setTimeout(() => {
setToast((current) => (current?.message === message ? null : current));
}, 2500);
}
useEffect(() => {
refresh().catch(() => setPage('setup'));
}, []);
@@ -67,22 +75,16 @@ export default function App() {
onSave={async (payload, file, editingId) => {
const fd = toFormData(payload, file);
const url = editingId ? `/api/links/${editingId}` : '/api/links';
try {
await api.request(url, { method: editingId ? 'PATCH' : 'POST', body: fd });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('Link name already exists')) {
alert('Link names must be unique.');
return;
}
throw err;
}
showToast(editingId ? 'Link updated.' : 'Link created.');
await refresh();
}}
onDelete={async (id) => {
await api.request(`/api/links/${id}`, { method: 'DELETE' });
showToast('Link deleted.');
await refresh();
}}
showToast={showToast}
/>
</div>
</Shell>
@@ -91,6 +93,7 @@ export default function App() {
return (
<Shell>
{toast ? <Toast message={toast.message} tone={toast.tone} /> : null}
<Dashboard
links={links}
query={query}
@@ -127,6 +130,14 @@ function Shell({ children }: { children: ReactNode }) {
return <div className="min-h-screen">{children}</div>;
}
function Toast({ message, tone }: { message: string; tone: 'success' | 'error' }) {
return (
<div className="toast-wrap" role="status" aria-live="polite">
<div className={`toast ${tone === 'error' ? 'toast-error' : 'toast-success'}`}>{message}</div>
</div>
);
}
function Centered({ children }: { children: ReactNode }) {
return <div className="grid min-h-screen place-items-center p-8 text-slate-500">{children}</div>;
}
@@ -300,15 +311,18 @@ function AdminPage({
links,
onSave,
onDelete,
showToast,
}: {
links: LinkItem[];
onSave: (payload: LinkForm, file?: File | null, editingId?: number) => Promise<void>;
onDelete: (id: number) => Promise<void>;
showToast: (message: string, tone?: 'success' | 'error') => void;
}) {
const [form, setForm] = useState<LinkForm>(emptyForm());
const [file, setFile] = useState<File | null>(null);
const [editingId, setEditingId] = useState<number | null>(null);
const [restoreText, setRestoreText] = useState('');
const [submitError, setSubmitError] = useState<string | null>(null);
const startEdit = (link: LinkItem) => {
setEditingId(link.id);
@@ -336,8 +350,18 @@ function AdminPage({
className="panel space-y-2"
onSubmit={async (event) => {
event.preventDefault();
setSubmitError(null);
try {
await onSave(form, file, editingId ?? undefined);
reset();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('Link name already exists')) {
setSubmitError('Link names must be unique.');
return;
}
setSubmitError(message || 'Saving failed. Please try again.');
}
}}
>
<input className="input" value={form.name} placeholder="name" onChange={(e) => setForm({ ...form, name: e.target.value })} required />
@@ -354,6 +378,7 @@ function AdminPage({
<button type="submit" className="btn-subtle">{editingId ? 'Update' : 'Add'}</button>
{editingId ? <button type="button" className="btn-subtle" onClick={reset}>Cancel</button> : null}
</div>
{submitError ? <p className="text-xs text-rose-400">{submitError}</p> : null}
</form>
<div className="panel space-y-2">
<div className="text-xs text-slate-400">Backup / Restore</div>
@@ -396,7 +421,7 @@ function AdminPage({
onClick={async () => {
const data = JSON.parse(restoreText);
await api.request('/api/admin/restore?dry_run=true', { method: 'POST', body: JSON.stringify({ data, confirm: false }) });
alert('Restore dry-run passed.');
showToast('Restore dry-run passed.');
}}
>
Validate (dry-run)
@@ -408,7 +433,7 @@ function AdminPage({
if (!confirm('Apply restore now? This replaces existing users, sessions, and links.')) return;
const data = JSON.parse(restoreText);
await api.request('/api/admin/restore?dry_run=false', { method: 'POST', body: JSON.stringify({ data, confirm: true }) });
alert('Restore applied.');
showToast('Restore applied.');
window.location.reload();
}}
>

View File

@@ -74,3 +74,19 @@ body {
.admin-row {
@apply flex items-center justify-between gap-3 py-2;
}
.toast-wrap {
@apply pointer-events-none fixed right-4 top-4 z-50;
}
.toast {
@apply rounded-md border px-3 py-2 text-xs shadow-lg;
}
.toast-success {
@apply border-emerald-800 bg-emerald-950/95 text-emerald-200;
}
.toast-error {
@apply border-rose-800 bg-rose-950/95 text-rose-200;
}