diff --git a/frontend/src/components/EmptyState.tsx b/frontend/src/components/EmptyState.tsx
new file mode 100644
index 0000000..2d36a29
--- /dev/null
+++ b/frontend/src/components/EmptyState.tsx
@@ -0,0 +1,21 @@
+import { Button } from '@/components/ui/button'
+
+interface EmptyStateProps {
+ icon: React.ElementType
+ heading: string
+ subtext: string
+ action?: { label: string; onClick: () => void }
+}
+
+export function EmptyState({ icon: Icon, heading, subtext, action }: EmptyStateProps) {
+ return (
+
+
+
{heading}
+
{subtext}
+ {action && (
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/CategoriesPage.tsx b/frontend/src/pages/CategoriesPage.tsx
index b26926b..1408eb8 100644
--- a/frontend/src/pages/CategoriesPage.tsx
+++ b/frontend/src/pages/CategoriesPage.tsx
@@ -6,8 +6,11 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'
import { categories as categoriesApi, type Category, type CategoryType } from '@/lib/api'
+import { EmptyState } from '@/components/EmptyState'
+import { Spinner } from '@/components/ui/spinner'
+import { FolderOpen } from 'lucide-react'
const CATEGORY_TYPES: CategoryType[] = ['income', 'bill', 'variable_expense', 'debt', 'saving', 'investment']
@@ -23,15 +26,23 @@ const TYPE_COLORS: Record = {
export function CategoriesPage() {
const { t } = useTranslation()
const [list, setList] = useState([])
+ const [loading, setLoading] = useState(true)
const [dialogOpen, setDialogOpen] = useState(false)
const [editing, setEditing] = useState(null)
const [name, setName] = useState('')
const [type, setType] = useState('bill')
const [sortOrder, setSortOrder] = useState(0)
+ const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null)
+ const [deleting, setDeleting] = useState(false)
+ const [deleteError, setDeleteError] = useState(null)
const fetchCategories = async () => {
- const data = await categoriesApi.list()
- setList(data)
+ try {
+ const data = await categoriesApi.list()
+ setList(data)
+ } finally {
+ setLoading(false)
+ }
}
useEffect(() => {
@@ -64,9 +75,19 @@ export function CategoriesPage() {
fetchCategories()
}
- const handleDelete = async (id: string) => {
- await categoriesApi.delete(id)
- fetchCategories()
+ const confirmDelete = async () => {
+ if (!pendingDelete) return
+ setDeleting(true)
+ setDeleteError(null)
+ try {
+ await categoriesApi.delete(pendingDelete.id)
+ setPendingDelete(null)
+ fetchCategories()
+ } catch (err) {
+ setDeleteError(err instanceof Error ? err.message : 'Failed to delete category')
+ } finally {
+ setDeleting(false)
+ }
}
const grouped = CATEGORY_TYPES.map((ct) => ({
@@ -81,7 +102,16 @@ export function CategoriesPage() {
- {grouped.map((group) => (
+ {!loading && list.length === 0 && (
+
+ )}
+
+ {grouped.length > 0 && grouped.map((group) => (
@@ -106,7 +136,7 @@ export function CategoriesPage() {
-