feat(tracker-web): toasts replace inline status divs (UX-6)

Mount the shared ToastProvider in providers.tsx and replace the inline
error/success banners across the overview, items list, board, item detail
and public roadmap with toast() calls — load errors, create, delete, vote,
status/priority/visibility updates, comment add, and idea submission. The
roadmap submit now toasts + closes the dialog instead of an in-modal
banner; the item-detail page keeps a single inline empty-state for the
hard "couldn't load this item" case.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-05-29 07:03:16 -07:00
parent fd7f12513d
commit 3fc559d066
6 changed files with 103 additions and 87 deletions

View File

@ -8,11 +8,11 @@ import {
Badge,
StatusBadge,
StatusDot,
AlertBanner,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
toast,
type StatusTone,
} from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context';
@ -41,14 +41,17 @@ const PRIORITY_TONE: Record<string, StatusTone> = {
export default function BoardPage() {
const { token } = useAuth();
const [items, setItems] = useState<TrackerItem[]>([]);
const [error, setError] = useState('');
const fetchItems = useCallback(async () => {
try {
const res = await listItems({ limit: '200' });
setItems(res.items);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to load');
toast({
type: 'error',
title: 'Failed to load board',
description: err instanceof Error ? err.message : undefined,
});
}
}, []);
@ -63,7 +66,11 @@ export default function BoardPage() {
prev.map(i => (i.id === itemId ? { ...i, status: newStatus as TrackerItem['status'] } : i))
);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to update status');
toast({
type: 'error',
title: 'Failed to update status',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -76,12 +83,6 @@ export default function BoardPage() {
/>
<p className="-mt-4 text-sm text-muted-foreground">Kanban view of all items</p>
{error && (
<AlertBanner tone="error" title="Something went wrong">
{error}
</AlertBanner>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{COLUMNS.map(col => {
const colItems = items.filter(i => i.status === col.key);

View File

@ -10,7 +10,7 @@ import {
Input,
Textarea,
Select,
AlertBanner,
toast,
} from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context';
import {
@ -58,7 +58,11 @@ export default function ItemDetailPage() {
const updated = await updateItemStatus(id, status);
setItem(updated);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to update status');
toast({
type: 'error',
title: 'Failed to update status',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -68,7 +72,11 @@ export default function ItemDetailPage() {
const updated = await updateItem(id, { priority } as Partial<TrackerItem>);
setItem(updated);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to update priority');
toast({
type: 'error',
title: 'Failed to update priority',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -78,7 +86,11 @@ export default function ItemDetailPage() {
const updated = await updateItem(id, { visibility } as Partial<TrackerItem>);
setItem(updated);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to update visibility');
toast({
type: 'error',
title: 'Failed to update visibility',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -91,8 +103,13 @@ export default function ItemDetailPage() {
} as Partial<TrackerItem>);
setItem(updated);
setEditing(false);
toast({ type: 'success', title: 'Item updated' });
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to update');
toast({
type: 'error',
title: 'Failed to update',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -104,7 +121,11 @@ export default function ItemDetailPage() {
setComments(prev => [...prev, comment]);
setNewComment('');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to add comment');
toast({
type: 'error',
title: 'Failed to add comment',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -114,7 +135,11 @@ export default function ItemDetailPage() {
const res = await toggleVote(id);
setItem(prev => (prev ? { ...prev, voteCount: res.voteCount } : prev));
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to vote');
toast({
type: 'error',
title: 'Failed to vote',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -123,9 +148,14 @@ export default function ItemDetailPage() {
if (!confirm('Delete this item? This cannot be undone.')) return;
try {
await deleteItem(id);
toast({ type: 'success', title: 'Item deleted' });
router.push('/dashboard/items');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to delete');
toast({
type: 'error',
title: 'Failed to delete',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -165,12 +195,6 @@ export default function ItemDetailPage() {
}
/>
{error && (
<AlertBanner tone="error" title="Something went wrong">
{error}
</AlertBanner>
)}
{/* Title/description editor */}
<div className="space-y-2">
{editing ? (

View File

@ -14,7 +14,7 @@ import {
Modal,
ConfirmDialog,
StatusBadge,
AlertBanner,
toast,
type StatusTone,
} from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context';
@ -46,7 +46,6 @@ export default function ItemsListPage() {
const [items, setItems] = useState<TrackerItem[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// Filters
const [typeFilter, setTypeFilter] = useState('');
@ -66,7 +65,6 @@ export default function ItemsListPage() {
const fetchItems = useCallback(async () => {
setLoading(true);
setError('');
try {
const params: Record<string, string> = {};
if (typeFilter) params.type = typeFilter;
@ -77,7 +75,11 @@ export default function ItemsListPage() {
setItems(res.items);
setTotal(res.total);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to load items');
toast({
type: 'error',
title: 'Failed to load items',
description: err instanceof Error ? err.message : undefined,
});
} finally {
setLoading(false);
}
@ -112,9 +114,14 @@ export default function ItemsListPage() {
setShowCreate(false);
setNewTitle('');
setNewDescription('');
toast({ type: 'success', title: 'Item created' });
fetchItems();
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to create item');
toast({
type: 'error',
title: 'Failed to create item',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -125,10 +132,15 @@ export default function ItemsListPage() {
try {
await deleteItem(deleteId);
setDeleteId(null);
toast({ type: 'success', title: 'Item deleted' });
fetchItems();
} catch (err: unknown) {
setDeleteId(null);
setError(err instanceof Error ? err.message : 'Failed to delete');
toast({
type: 'error',
title: 'Failed to delete',
description: err instanceof Error ? err.message : undefined,
});
}
}, [deleteId, fetchItems]);
@ -225,12 +237,6 @@ export default function ItemsListPage() {
/>
<p className="-mt-4 text-sm text-muted-foreground">{total} items total</p>
{error && (
<AlertBanner tone="error" title="Something went wrong">
{error}
</AlertBanner>
)}
{/* Filters */}
<div className="flex flex-wrap gap-3">
<Input

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
import { KpiCard } from '@bytelyst/data-viz';
import { AlertBanner, Skeleton } from '@/components/ui/Primitives';
import { Skeleton, toast } from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context';
import { getStats, type TrackerStats } from '@/lib/tracker-client';
import { overviewKpis } from '@/lib/overview-charts';
@ -18,13 +18,14 @@ const OverviewCharts = dynamic(() => import('@/components/overview-charts'), {
export default function DashboardOverview() {
const { token } = useAuth();
const [stats, setStats] = useState<TrackerStats | null>(null);
const [error, setError] = useState('');
useEffect(() => {
if (!token) return;
getStats()
.then(setStats)
.catch(err => setError(err.message));
.catch(err =>
toast({ type: 'error', title: 'Failed to load stats', description: err.message })
);
}, [token]);
const kpis = stats ? overviewKpis(stats) : null;
@ -34,12 +35,6 @@ export default function DashboardOverview() {
<PageHeader title="Dashboard" />
<p className="-mt-4 text-sm text-muted-foreground">Overview of all tracked items</p>
{error && (
<AlertBanner tone="error" title="Something went wrong">
{error}
</AlertBanner>
)}
{stats && kpis ? (
<div className="space-y-4">
{/* KPI row */}
@ -53,11 +48,11 @@ export default function DashboardOverview() {
{/* Charts */}
<OverviewCharts stats={stats} />
</div>
) : !error ? (
) : (
<div className="flex justify-center py-10">
<LoadingSpinner />
</div>
) : null}
)}
</div>
);
}

View File

@ -3,6 +3,7 @@
import { useEffect, type ReactNode } from 'react';
import dynamic from 'next/dynamic';
import { CommandRegistryProvider } from '@bytelyst/command-palette';
import { ToastProvider } from '@/components/ui/Primitives';
import { AuthProvider } from '@/lib/auth-context';
import { ThemeProvider } from '@/lib/theme-context';
import { ProductProvider } from '@/lib/product-context';
@ -23,10 +24,12 @@ export function Providers({ children }: { children: ReactNode }) {
<ThemeProvider>
<ProductProvider>
<AuthProvider>
<CommandRegistryProvider>
{children}
<CommandMenu />
</CommandRegistryProvider>
<ToastProvider>
<CommandRegistryProvider>
{children}
<CommandMenu />
</CommandRegistryProvider>
</ToastProvider>
</AuthProvider>
</ProductProvider>
</ThemeProvider>

View File

@ -11,7 +11,7 @@ import {
Badge,
StatusDot,
MetricCard,
AlertBanner,
toast,
type BadgeProps,
type StatusTone,
} from '@/components/ui/Primitives';
@ -50,7 +50,6 @@ export default function RoadmapPage() {
const [items, setItems] = useState<TrackerItem[]>([]);
const [stats, setStats] = useState<PublicRoadmapStats | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState('');
const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [view, setView] = useState<'board' | 'list'>('board');
@ -65,14 +64,12 @@ export default function RoadmapPage() {
name: '',
});
const [submitting, setSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState('');
// Vote email (persisted in localStorage)
const [voteEmail, setVoteEmail] = useState('');
const [showEmailPrompt, setShowEmailPrompt] = useState(false);
const [pendingVoteId, setPendingVoteId] = useState<string | null>(null);
const [votedItems, setVotedItems] = useState<Set<string>>(new Set());
const [voteError, setVoteError] = useState('');
useEffect(() => {
if (typeof window !== 'undefined') {
@ -85,7 +82,6 @@ export default function RoadmapPage() {
const fetchData = useCallback(async () => {
setLoading(true);
setLoadError('');
try {
const params: Record<string, string> = {
sortBy: 'voteCount',
@ -98,7 +94,11 @@ export default function RoadmapPage() {
setItems(itemsRes.items);
setStats(statsRes);
} catch (err) {
setLoadError(err instanceof Error ? err.message : 'Failed to load roadmap');
toast({
type: 'error',
title: "Couldn't load the roadmap",
description: err instanceof Error ? err.message : undefined,
});
} finally {
setLoading(false);
}
@ -118,7 +118,6 @@ export default function RoadmapPage() {
};
const doVote = async (itemId: string, email: string) => {
setVoteError('');
try {
const res = await publicVote(itemId, email);
setItems(prev => prev.map(i => (i.id === itemId ? { ...i, voteCount: res.voteCount } : i)));
@ -131,7 +130,11 @@ export default function RoadmapPage() {
setVotedItems(newVoted);
localStorage.setItem('roadmap_voted', JSON.stringify([...newVoted]));
} catch (err) {
setVoteError(err instanceof Error ? err.message : 'Vote failed');
toast({
type: 'error',
title: 'Vote failed',
description: err instanceof Error ? err.message : undefined,
});
}
};
@ -148,12 +151,14 @@ export default function RoadmapPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setSubmitSuccess('');
try {
const res = await submitPublicItem(submitForm);
setSubmitSuccess(
`Thanks! Your ${submitForm.type} request "${res.title}" has been submitted for review.`
);
toast({
type: 'success',
title: 'Idea submitted',
description: `Your ${submitForm.type} request "${res.title}" has been submitted for review.`,
});
setShowSubmit(false);
setSubmitForm({
title: '',
description: '',
@ -169,7 +174,11 @@ export default function RoadmapPage() {
// Refresh data to show new item
fetchData();
} catch (err) {
setSubmitSuccess(`Error: ${err instanceof Error ? err.message : 'Submission failed'}`);
toast({
type: 'error',
title: 'Submission failed',
description: err instanceof Error ? err.message : undefined,
});
} finally {
setSubmitting(false);
}
@ -201,17 +210,6 @@ export default function RoadmapPage() {
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 py-6">
{loadError ? (
<AlertBanner tone="error" title="Couldn't load the roadmap" className="mb-4">
{loadError}
</AlertBanner>
) : null}
{voteError ? (
<AlertBanner tone="error" title="Vote failed" className="mb-4">
{voteError}
</AlertBanner>
) : null}
{/* Stats bar */}
{stats && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
@ -395,21 +393,10 @@ export default function RoadmapPage() {
<Modal
open={showSubmit}
onOpenChange={open => {
if (!open) {
setShowSubmit(false);
setSubmitSuccess('');
}
if (!open) setShowSubmit(false);
}}
title="Submit an Idea"
>
{submitSuccess ? (
<AlertBanner
tone={submitSuccess.startsWith('Error') ? 'error' : 'success'}
className="mb-4"
>
{submitSuccess}
</AlertBanner>
) : null}
<form onSubmit={handleSubmit} className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<Input