Replace admin alerts with inline errors and toasts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
await api.request(url, { method: editingId ? 'PATCH' : 'POST', body: fd });
|
||||
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();
|
||||
await onSave(form, file, editingId ?? undefined);
|
||||
reset();
|
||||
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();
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user