From 3fc559d066033b4997dbecab08b3065e7afab412 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 29 May 2026 07:03:16 -0700 Subject: [PATCH] feat(tracker-web): toasts replace inline status divs (UX-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../src/app/dashboard/board/page.tsx | 21 +++---- .../src/app/dashboard/items/[id]/page.tsx | 52 +++++++++++----- .../src/app/dashboard/items/page.tsx | 30 ++++++---- .../tracker-web/src/app/dashboard/page.tsx | 17 ++---- dashboards/tracker-web/src/app/providers.tsx | 11 ++-- .../tracker-web/src/app/roadmap/page.tsx | 59 ++++++++----------- 6 files changed, 103 insertions(+), 87 deletions(-) diff --git a/dashboards/tracker-web/src/app/dashboard/board/page.tsx b/dashboards/tracker-web/src/app/dashboard/board/page.tsx index e390bdca..d7c589e9 100644 --- a/dashboards/tracker-web/src/app/dashboard/board/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/board/page.tsx @@ -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 = { export default function BoardPage() { const { token } = useAuth(); const [items, setItems] = useState([]); - 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() { />

Kanban view of all items

- {error && ( - - {error} - - )} -
{COLUMNS.map(col => { const colItems = items.filter(i => i.status === col.key); diff --git a/dashboards/tracker-web/src/app/dashboard/items/[id]/page.tsx b/dashboards/tracker-web/src/app/dashboard/items/[id]/page.tsx index b00029c5..1c045017 100644 --- a/dashboards/tracker-web/src/app/dashboard/items/[id]/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/items/[id]/page.tsx @@ -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); 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); 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); 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 && ( - - {error} - - )} - {/* Title/description editor */}
{editing ? ( diff --git a/dashboards/tracker-web/src/app/dashboard/items/page.tsx b/dashboards/tracker-web/src/app/dashboard/items/page.tsx index b138db08..fa8f4539 100644 --- a/dashboards/tracker-web/src/app/dashboard/items/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/items/page.tsx @@ -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([]); 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 = {}; 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() { />

{total} items total

- {error && ( - - {error} - - )} - {/* Filters */}
import('@/components/overview-charts'), { export default function DashboardOverview() { const { token } = useAuth(); const [stats, setStats] = useState(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() {

Overview of all tracked items

- {error && ( - - {error} - - )} - {stats && kpis ? (
{/* KPI row */} @@ -53,11 +48,11 @@ export default function DashboardOverview() { {/* Charts */}
- ) : !error ? ( + ) : (
- ) : null} + )}
); } diff --git a/dashboards/tracker-web/src/app/providers.tsx b/dashboards/tracker-web/src/app/providers.tsx index 2bbe92ec..2b59d4e7 100644 --- a/dashboards/tracker-web/src/app/providers.tsx +++ b/dashboards/tracker-web/src/app/providers.tsx @@ -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 }) { - - {children} - - + + + {children} + + + diff --git a/dashboards/tracker-web/src/app/roadmap/page.tsx b/dashboards/tracker-web/src/app/roadmap/page.tsx index 4abf0206..e6bb124c 100644 --- a/dashboards/tracker-web/src/app/roadmap/page.tsx +++ b/dashboards/tracker-web/src/app/roadmap/page.tsx @@ -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([]); const [stats, setStats] = useState(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(null); const [votedItems, setVotedItems] = useState>(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 = { 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() {
- {loadError ? ( - - {loadError} - - ) : null} - {voteError ? ( - - {voteError} - - ) : null} - {/* Stats bar */} {stats && (
@@ -395,21 +393,10 @@ export default function RoadmapPage() { { - if (!open) { - setShowSubmit(false); - setSubmitSuccess(''); - } + if (!open) setShowSubmit(false); }} title="Submit an Idea" > - {submitSuccess ? ( - - {submitSuccess} - - ) : null}