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,
|
Badge,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
StatusDot,
|
StatusDot,
|
||||||
AlertBanner,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
|
toast,
|
||||||
type StatusTone,
|
type StatusTone,
|
||||||
} from '@/components/ui/Primitives';
|
} from '@/components/ui/Primitives';
|
||||||
import { useAuth } from '@/lib/auth-context';
|
import { useAuth } from '@/lib/auth-context';
|
||||||
@ -41,14 +41,17 @@ const PRIORITY_TONE: Record<string, StatusTone> = {
|
|||||||
export default function BoardPage() {
|
export default function BoardPage() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const [items, setItems] = useState<TrackerItem[]>([]);
|
const [items, setItems] = useState<TrackerItem[]>([]);
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const fetchItems = useCallback(async () => {
|
const fetchItems = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await listItems({ limit: '200' });
|
const res = await listItems({ limit: '200' });
|
||||||
setItems(res.items);
|
setItems(res.items);
|
||||||
} catch (err: unknown) {
|
} 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))
|
prev.map(i => (i.id === itemId ? { ...i, status: newStatus as TrackerItem['status'] } : i))
|
||||||
);
|
);
|
||||||
} catch (err: unknown) {
|
} 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>
|
<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">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
{COLUMNS.map(col => {
|
{COLUMNS.map(col => {
|
||||||
const colItems = items.filter(i => i.status === col.key);
|
const colItems = items.filter(i => i.status === col.key);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
Select,
|
Select,
|
||||||
AlertBanner,
|
toast,
|
||||||
} from '@/components/ui/Primitives';
|
} from '@/components/ui/Primitives';
|
||||||
import { useAuth } from '@/lib/auth-context';
|
import { useAuth } from '@/lib/auth-context';
|
||||||
import {
|
import {
|
||||||
@ -58,7 +58,11 @@ export default function ItemDetailPage() {
|
|||||||
const updated = await updateItemStatus(id, status);
|
const updated = await updateItemStatus(id, status);
|
||||||
setItem(updated);
|
setItem(updated);
|
||||||
} catch (err: unknown) {
|
} 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>);
|
const updated = await updateItem(id, { priority } as Partial<TrackerItem>);
|
||||||
setItem(updated);
|
setItem(updated);
|
||||||
} catch (err: unknown) {
|
} 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>);
|
const updated = await updateItem(id, { visibility } as Partial<TrackerItem>);
|
||||||
setItem(updated);
|
setItem(updated);
|
||||||
} catch (err: unknown) {
|
} 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>);
|
} as Partial<TrackerItem>);
|
||||||
setItem(updated);
|
setItem(updated);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
|
toast({ type: 'success', title: 'Item updated' });
|
||||||
} catch (err: unknown) {
|
} 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]);
|
setComments(prev => [...prev, comment]);
|
||||||
setNewComment('');
|
setNewComment('');
|
||||||
} catch (err: unknown) {
|
} 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);
|
const res = await toggleVote(id);
|
||||||
setItem(prev => (prev ? { ...prev, voteCount: res.voteCount } : prev));
|
setItem(prev => (prev ? { ...prev, voteCount: res.voteCount } : prev));
|
||||||
} catch (err: unknown) {
|
} 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;
|
if (!confirm('Delete this item? This cannot be undone.')) return;
|
||||||
try {
|
try {
|
||||||
await deleteItem(id);
|
await deleteItem(id);
|
||||||
|
toast({ type: 'success', title: 'Item deleted' });
|
||||||
router.push('/dashboard/items');
|
router.push('/dashboard/items');
|
||||||
} catch (err: unknown) {
|
} 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 */}
|
{/* Title/description editor */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
AlertBanner,
|
toast,
|
||||||
type StatusTone,
|
type StatusTone,
|
||||||
} from '@/components/ui/Primitives';
|
} from '@/components/ui/Primitives';
|
||||||
import { useAuth } from '@/lib/auth-context';
|
import { useAuth } from '@/lib/auth-context';
|
||||||
@ -46,7 +46,6 @@ export default function ItemsListPage() {
|
|||||||
const [items, setItems] = useState<TrackerItem[]>([]);
|
const [items, setItems] = useState<TrackerItem[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [typeFilter, setTypeFilter] = useState('');
|
const [typeFilter, setTypeFilter] = useState('');
|
||||||
@ -66,7 +65,6 @@ export default function ItemsListPage() {
|
|||||||
|
|
||||||
const fetchItems = useCallback(async () => {
|
const fetchItems = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
|
||||||
try {
|
try {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (typeFilter) params.type = typeFilter;
|
if (typeFilter) params.type = typeFilter;
|
||||||
@ -77,7 +75,11 @@ export default function ItemsListPage() {
|
|||||||
setItems(res.items);
|
setItems(res.items);
|
||||||
setTotal(res.total);
|
setTotal(res.total);
|
||||||
} catch (err: unknown) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -112,9 +114,14 @@ export default function ItemsListPage() {
|
|||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
setNewTitle('');
|
setNewTitle('');
|
||||||
setNewDescription('');
|
setNewDescription('');
|
||||||
|
toast({ type: 'success', title: 'Item created' });
|
||||||
fetchItems();
|
fetchItems();
|
||||||
} catch (err: unknown) {
|
} 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 {
|
try {
|
||||||
await deleteItem(deleteId);
|
await deleteItem(deleteId);
|
||||||
setDeleteId(null);
|
setDeleteId(null);
|
||||||
|
toast({ type: 'success', title: 'Item deleted' });
|
||||||
fetchItems();
|
fetchItems();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setDeleteId(null);
|
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]);
|
}, [deleteId, fetchItems]);
|
||||||
|
|
||||||
@ -225,12 +237,6 @@ export default function ItemsListPage() {
|
|||||||
/>
|
/>
|
||||||
<p className="-mt-4 text-sm text-muted-foreground">{total} items total</p>
|
<p className="-mt-4 text-sm text-muted-foreground">{total} items total</p>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<AlertBanner tone="error" title="Something went wrong">
|
|
||||||
{error}
|
|
||||||
</AlertBanner>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
||||||
import { KpiCard } from '@bytelyst/data-viz';
|
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 { useAuth } from '@/lib/auth-context';
|
||||||
import { getStats, type TrackerStats } from '@/lib/tracker-client';
|
import { getStats, type TrackerStats } from '@/lib/tracker-client';
|
||||||
import { overviewKpis } from '@/lib/overview-charts';
|
import { overviewKpis } from '@/lib/overview-charts';
|
||||||
@ -18,13 +18,14 @@ const OverviewCharts = dynamic(() => import('@/components/overview-charts'), {
|
|||||||
export default function DashboardOverview() {
|
export default function DashboardOverview() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const [stats, setStats] = useState<TrackerStats | null>(null);
|
const [stats, setStats] = useState<TrackerStats | null>(null);
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
getStats()
|
getStats()
|
||||||
.then(setStats)
|
.then(setStats)
|
||||||
.catch(err => setError(err.message));
|
.catch(err =>
|
||||||
|
toast({ type: 'error', title: 'Failed to load stats', description: err.message })
|
||||||
|
);
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
const kpis = stats ? overviewKpis(stats) : null;
|
const kpis = stats ? overviewKpis(stats) : null;
|
||||||
@ -34,12 +35,6 @@ export default function DashboardOverview() {
|
|||||||
<PageHeader title="Dashboard" />
|
<PageHeader title="Dashboard" />
|
||||||
<p className="-mt-4 text-sm text-muted-foreground">Overview of all tracked items</p>
|
<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 ? (
|
{stats && kpis ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* KPI row */}
|
{/* KPI row */}
|
||||||
@ -53,11 +48,11 @@ export default function DashboardOverview() {
|
|||||||
{/* Charts */}
|
{/* Charts */}
|
||||||
<OverviewCharts stats={stats} />
|
<OverviewCharts stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
) : !error ? (
|
) : (
|
||||||
<div className="flex justify-center py-10">
|
<div className="flex justify-center py-10">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useEffect, type ReactNode } from 'react';
|
import { useEffect, type ReactNode } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { CommandRegistryProvider } from '@bytelyst/command-palette';
|
import { CommandRegistryProvider } from '@bytelyst/command-palette';
|
||||||
|
import { ToastProvider } from '@/components/ui/Primitives';
|
||||||
import { AuthProvider } from '@/lib/auth-context';
|
import { AuthProvider } from '@/lib/auth-context';
|
||||||
import { ThemeProvider } from '@/lib/theme-context';
|
import { ThemeProvider } from '@/lib/theme-context';
|
||||||
import { ProductProvider } from '@/lib/product-context';
|
import { ProductProvider } from '@/lib/product-context';
|
||||||
@ -23,10 +24,12 @@ export function Providers({ children }: { children: ReactNode }) {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ProductProvider>
|
<ProductProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<CommandRegistryProvider>
|
<ToastProvider>
|
||||||
{children}
|
<CommandRegistryProvider>
|
||||||
<CommandMenu />
|
{children}
|
||||||
</CommandRegistryProvider>
|
<CommandMenu />
|
||||||
|
</CommandRegistryProvider>
|
||||||
|
</ToastProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ProductProvider>
|
</ProductProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
StatusDot,
|
StatusDot,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
AlertBanner,
|
toast,
|
||||||
type BadgeProps,
|
type BadgeProps,
|
||||||
type StatusTone,
|
type StatusTone,
|
||||||
} from '@/components/ui/Primitives';
|
} from '@/components/ui/Primitives';
|
||||||
@ -50,7 +50,6 @@ export default function RoadmapPage() {
|
|||||||
const [items, setItems] = useState<TrackerItem[]>([]);
|
const [items, setItems] = useState<TrackerItem[]>([]);
|
||||||
const [stats, setStats] = useState<PublicRoadmapStats | null>(null);
|
const [stats, setStats] = useState<PublicRoadmapStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loadError, setLoadError] = useState('');
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [typeFilter, setTypeFilter] = useState('');
|
const [typeFilter, setTypeFilter] = useState('');
|
||||||
const [view, setView] = useState<'board' | 'list'>('board');
|
const [view, setView] = useState<'board' | 'list'>('board');
|
||||||
@ -65,14 +64,12 @@ export default function RoadmapPage() {
|
|||||||
name: '',
|
name: '',
|
||||||
});
|
});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [submitSuccess, setSubmitSuccess] = useState('');
|
|
||||||
|
|
||||||
// Vote email (persisted in localStorage)
|
// Vote email (persisted in localStorage)
|
||||||
const [voteEmail, setVoteEmail] = useState('');
|
const [voteEmail, setVoteEmail] = useState('');
|
||||||
const [showEmailPrompt, setShowEmailPrompt] = useState(false);
|
const [showEmailPrompt, setShowEmailPrompt] = useState(false);
|
||||||
const [pendingVoteId, setPendingVoteId] = useState<string | null>(null);
|
const [pendingVoteId, setPendingVoteId] = useState<string | null>(null);
|
||||||
const [votedItems, setVotedItems] = useState<Set<string>>(new Set());
|
const [votedItems, setVotedItems] = useState<Set<string>>(new Set());
|
||||||
const [voteError, setVoteError] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@ -85,7 +82,6 @@ export default function RoadmapPage() {
|
|||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadError('');
|
|
||||||
try {
|
try {
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string> = {
|
||||||
sortBy: 'voteCount',
|
sortBy: 'voteCount',
|
||||||
@ -98,7 +94,11 @@ export default function RoadmapPage() {
|
|||||||
setItems(itemsRes.items);
|
setItems(itemsRes.items);
|
||||||
setStats(statsRes);
|
setStats(statsRes);
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -118,7 +118,6 @@ export default function RoadmapPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doVote = async (itemId: string, email: string) => {
|
const doVote = async (itemId: string, email: string) => {
|
||||||
setVoteError('');
|
|
||||||
try {
|
try {
|
||||||
const res = await publicVote(itemId, email);
|
const res = await publicVote(itemId, email);
|
||||||
setItems(prev => prev.map(i => (i.id === itemId ? { ...i, voteCount: res.voteCount } : i)));
|
setItems(prev => prev.map(i => (i.id === itemId ? { ...i, voteCount: res.voteCount } : i)));
|
||||||
@ -131,7 +130,11 @@ export default function RoadmapPage() {
|
|||||||
setVotedItems(newVoted);
|
setVotedItems(newVoted);
|
||||||
localStorage.setItem('roadmap_voted', JSON.stringify([...newVoted]));
|
localStorage.setItem('roadmap_voted', JSON.stringify([...newVoted]));
|
||||||
} catch (err) {
|
} 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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setSubmitSuccess('');
|
|
||||||
try {
|
try {
|
||||||
const res = await submitPublicItem(submitForm);
|
const res = await submitPublicItem(submitForm);
|
||||||
setSubmitSuccess(
|
toast({
|
||||||
`Thanks! Your ${submitForm.type} request "${res.title}" has been submitted for review.`
|
type: 'success',
|
||||||
);
|
title: 'Idea submitted',
|
||||||
|
description: `Your ${submitForm.type} request "${res.title}" has been submitted for review.`,
|
||||||
|
});
|
||||||
|
setShowSubmit(false);
|
||||||
setSubmitForm({
|
setSubmitForm({
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
@ -169,7 +174,11 @@ export default function RoadmapPage() {
|
|||||||
// Refresh data to show new item
|
// Refresh data to show new item
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@ -201,17 +210,6 @@ export default function RoadmapPage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 py-6">
|
<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 bar */}
|
||||||
{stats && (
|
{stats && (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
||||||
@ -395,21 +393,10 @@ export default function RoadmapPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={showSubmit}
|
open={showSubmit}
|
||||||
onOpenChange={open => {
|
onOpenChange={open => {
|
||||||
if (!open) {
|
if (!open) setShowSubmit(false);
|
||||||
setShowSubmit(false);
|
|
||||||
setSubmitSuccess('');
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title="Submit an Idea"
|
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">
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user