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:
parent
fd7f12513d
commit
3fc559d066
@ -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);
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user