@@ -13,9 +13,18 @@ import {
1313 DialogTitle ,
1414} from "@/components/ui/dialog" ;
1515import { Icon , IconName } from "@/components/ui/icon-picker" ;
16+ import { Input } from "@/components/ui/input" ;
17+ import {
18+ Pagination ,
19+ PaginationContent ,
20+ PaginationItem ,
21+ PaginationLink ,
22+ PaginationNext ,
23+ PaginationPrevious ,
24+ } from "@/components/ui/pagination" ;
1625import { Category } from "@/lib/models/category" ;
17- import { Tag , Trash2 } from "lucide-react" ;
18- import { useState } from "react" ;
26+ import { Search , Tag , Trash2 } from "lucide-react" ;
27+ import { useEffect , useMemo , useState } from "react" ;
1928
2029interface ViewCategoriesModalProps {
2130 isOpen : boolean ;
@@ -36,6 +45,29 @@ export function ViewCategoriesModal({
3645 const [ confirmDeleteCategory , setConfirmDeleteCategory ] =
3746 useState < Category | null > ( null ) ;
3847
48+ // Frontend-only search + pagination
49+ const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
50+ const [ debouncedSearch , setDebouncedSearch ] = useState ( "" ) ;
51+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
52+ const pageSize = 5 ;
53+
54+ useEffect ( ( ) => {
55+ const t = setTimeout ( ( ) => setDebouncedSearch ( searchTerm . trim ( ) ) , 300 ) ;
56+ return ( ) => clearTimeout ( t ) ;
57+ } , [ searchTerm ] ) ;
58+
59+ const filtered = useMemo ( ( ) => {
60+ if ( ! debouncedSearch ) return categories ;
61+ const s = debouncedSearch . toLowerCase ( ) ;
62+ return categories . filter ( ( c ) => c . name . toLowerCase ( ) . includes ( s ) ) ;
63+ } , [ categories , debouncedSearch ] ) ;
64+
65+ const totalPages = Math . ceil ( filtered . length / pageSize ) || 1 ;
66+ const pagedCategories = useMemo ( ( ) => {
67+ const start = ( currentPage - 1 ) * pageSize ;
68+ return filtered . slice ( start , start + pageSize ) ;
69+ } , [ filtered , currentPage ] ) ;
70+
3971 const handleDeleteCategory = async ( category : Category ) => {
4072 deleteCategoryMutation . mutate ( category . id , {
4173 onSuccess : ( ) => {
@@ -47,14 +79,26 @@ export function ViewCategoriesModal({
4779 return (
4880 < >
4981 < Dialog open = { isOpen } onOpenChange = { onOpenChange } >
50- < DialogContent className = "sm:max-w-[425px ] max-h-[80vh] overflow-y-auto" >
82+ < DialogContent className = "sm:max-w-[600px ] max-h-[80vh] overflow-y-auto" >
5183 < DialogHeader >
5284 < DialogTitle className = "flex items-center gap-2" >
5385 < Tag className = "h-5 w-5" />
5486 Categories
5587 </ DialogTitle >
5688 </ DialogHeader >
57- < div className = "space-y-4" >
89+ < div className = "space-y-4 py-2" >
90+ < div className = "relative" >
91+ < Search className = "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
92+ < Input aria-label = "Search categories"
93+ placeholder = "Search categories by name..."
94+ value = { searchTerm }
95+ onChange = { ( e ) => {
96+ setSearchTerm ( e . target . value ) ;
97+ setCurrentPage ( 1 ) ;
98+ } }
99+ className = "pl-10"
100+ />
101+ </ div >
58102 { isLoading ? (
59103 < div className = "space-y-2" >
60104 { [ ...Array ( 3 ) ] . map ( ( _ , i ) => (
@@ -64,9 +108,15 @@ export function ViewCategoriesModal({
64108 />
65109 ) ) }
66110 </ div >
111+ ) : pagedCategories . length === 0 ? (
112+ < div className = "text-center py-8 text-muted-foreground" >
113+ { debouncedSearch
114+ ? "No categories match your search."
115+ : "No categories found. Add your first category to get started." }
116+ </ div >
67117 ) : (
68118 < div className = "space-y-2" >
69- { categories . map ( ( category ) => (
119+ { pagedCategories . map ( ( category ) => (
70120 < div
71121 key = { category . id }
72122 className = "flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50"
@@ -97,9 +147,50 @@ export function ViewCategoriesModal({
97147 </ div >
98148 </ div >
99149 ) ) }
100- { categories . length === 0 && (
101- < div className = "text-center py-8 text-muted-foreground" >
102- No categories found. Add your first category to get started.
150+ { totalPages > 1 && (
151+ < div className = "flex items-center justify-between text-sm text-muted-foreground pt-2" >
152+ < Pagination >
153+ < PaginationContent >
154+ < PaginationItem >
155+ < PaginationPrevious
156+ onClick = { ( ) =>
157+ setCurrentPage ( ( p ) => Math . max ( 1 , p - 1 ) )
158+ }
159+ className = {
160+ currentPage <= 1
161+ ? "pointer-events-none opacity-50"
162+ : "cursor-pointer"
163+ }
164+ />
165+ </ PaginationItem >
166+ { Array . from (
167+ { length : totalPages } ,
168+ ( _ , i ) => i + 1
169+ ) . map ( ( page ) => (
170+ < PaginationItem key = { page } >
171+ < PaginationLink
172+ onClick = { ( ) => setCurrentPage ( page ) }
173+ isActive = { currentPage === page }
174+ className = "cursor-pointer"
175+ >
176+ { page }
177+ </ PaginationLink >
178+ </ PaginationItem >
179+ ) ) }
180+ < PaginationItem >
181+ < PaginationNext
182+ onClick = { ( ) =>
183+ setCurrentPage ( ( p ) => Math . min ( totalPages , p + 1 ) )
184+ }
185+ className = {
186+ currentPage >= totalPages
187+ ? "pointer-events-none opacity-50"
188+ : "cursor-pointer"
189+ }
190+ />
191+ </ PaginationItem >
192+ </ PaginationContent >
193+ </ Pagination >
103194 </ div >
104195 ) }
105196 </ div >
0 commit comments