@@ -80,65 +80,17 @@ export function KanbanBoard({
8080 /* ── Search state ── */
8181 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
8282 const debouncedSearchTerm = useDebounce ( searchTerm , 500 ) ;
83- const [ searchMatchedTaskIds , setSearchMatchedTaskIds ] = useState < Set < number > | null > ( null ) ;
8483 const [ isSearching , setIsSearching ] = useState ( false ) ;
8584
85+ // We keep isSearching for UI feedback if needed,
86+ // though client-side search is nearly instantaneous.
8687 useEffect ( ( ) => {
87- let active = true ;
88-
89- if ( ! debouncedSearchTerm . trim ( ) ) {
90- setSearchMatchedTaskIds ( null ) ;
88+ if ( searchTerm !== debouncedSearchTerm ) {
89+ setIsSearching ( true ) ;
90+ } else {
9191 setIsSearching ( false ) ;
92- return ;
9392 }
94-
95- const fetchSearchResults = async ( ) => {
96- setIsSearching ( true ) ;
97- try {
98- const supabase = createClient ( ) ;
99- const colIds = columns . map ( c => c . id ) ;
100- if ( colIds . length === 0 ) {
101- if ( active ) {
102- setSearchMatchedTaskIds ( new Set ( ) ) ;
103- setIsSearching ( false ) ;
104- }
105- return ;
106- }
107-
108- // 1. Escape characters used as wildcards in ilike: \ , % , _
109- // 2. Escape double quotes for PostgREST: " -> ""
110- const safeTerm = debouncedSearchTerm
111- . replace ( / \\ / g, "\\\\" ) // Escape backslash first
112- . replace ( / % / g, "\\%" )
113- . replace ( / _ / g, "\\_" )
114- . replace ( / " / g, '""' ) ;
115-
116- // 3. Wrap in double quotes to handle reserved characters in .or() like , ( )
117- const filterVal = `"%${ safeTerm } %"` ;
118-
119- const { data, error } = await supabase
120- . from ( "tasks" )
121- . select ( "id" )
122- . in ( "column_id" , colIds )
123- . or ( `title.ilike.${ filterVal } ,description.ilike.${ filterVal } ` ) ;
124-
125- if ( ! active ) return ;
126-
127- if ( ! error && data ) {
128- setSearchMatchedTaskIds ( new Set ( data . map ( t => t . id ) ) ) ;
129- }
130- } catch ( e ) {
131- if ( active ) console . error ( e ) ;
132- } finally {
133- if ( active ) setIsSearching ( false ) ;
134- }
135- } ;
136-
137- fetchSearchResults ( ) ;
138- return ( ) => {
139- active = false ;
140- } ;
141- } , [ debouncedSearchTerm , columns ] ) ;
93+ } , [ searchTerm , debouncedSearchTerm ] ) ;
14294
14395 const lastSyncedColumnsRef = useRef ( columns ) ;
14496 const lastSyncedTasksRef = useRef ( tasks ) ;
@@ -530,16 +482,27 @@ export function KanbanBoard({
530482 return map ;
531483 } , [ boardLabels ] ) ;
532484
533- // Filter tasks by assignee and labels (client-side) using useMemo for performance
485+ // Filter tasks by assignee, labels, and search term (client-side) using useMemo for performance
534486 const { effectiveLabelIds, filteredTasks } = React . useMemo ( ( ) => {
535487 let tasks = localTasks ;
536488
489+ // 1. Filter by Search Term
490+ if ( debouncedSearchTerm . trim ( ) ) {
491+ const term = debouncedSearchTerm . toLowerCase ( ) ;
492+ tasks = tasks . filter ( ( t ) =>
493+ t . title . toLowerCase ( ) . includes ( term ) ||
494+ ( t . description && t . description . toLowerCase ( ) . includes ( term ) )
495+ ) ;
496+ }
497+
498+ // 2. Filter by Assignee
537499 if ( filterUserId ) {
538500 tasks = tasks . filter ( ( t ) =>
539501 t . assignees ?. some ( ( a ) => a . user_id === filterUserId ) ,
540502 ) ;
541503 }
542504
505+ // 3. Filter by Labels
543506 let effectiveIds = filterLabelIds || [ ] ;
544507 if ( filterLabelIds && filterLabelIds . length > 0 ) {
545508 const selectedColors = new Set (
@@ -563,7 +526,7 @@ export function KanbanBoard({
563526 }
564527
565528 return { effectiveLabelIds : effectiveIds , filteredTasks : tasks } ;
566- } , [ localTasks , filterUserId , filterLabelIds , boardLabels ] ) ;
529+ } , [ localTasks , filterUserId , filterLabelIds , boardLabels , debouncedSearchTerm ] ) ;
567530
568531 // Don't render on server to avoid hydration mismatch
569532 if ( ! isMounted ) {
@@ -591,7 +554,7 @@ export function KanbanBoard({
591554 }
592555
593556 const isFiltering =
594- ! ! filterUserId || effectiveLabelIds . length > 0 || searchMatchedTaskIds !== null ;
557+ ! ! filterUserId || effectiveLabelIds . length > 0 || ! ! debouncedSearchTerm . trim ( ) ;
595558
596559 return (
597560 < div className = "flex flex-col h-full" >
@@ -608,11 +571,10 @@ export function KanbanBoard({
608571 filterUserId === currentUserId ? null : currentUserId ,
609572 )
610573 }
611- className = { `flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold border-2 transition-all ${
612- filterUserId === currentUserId
574+ className = { `flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold border-2 transition-all ${ filterUserId === currentUserId
613575 ? "bg-[#28B8FA] border-[#28B8FA] text-white shadow-md shadow-cyan-200"
614576 : "bg-white border-slate-200 text-slate-500 hover:border-[#28B8FA] hover:text-[#28B8FA]"
615- } `}
577+ } `}
616578 title = "Chỉ hiện nhiệm vụ của tôi"
617579 >
618580 < svg
@@ -644,11 +606,10 @@ export function KanbanBoard({
644606 onFilterChange ?.( isActive ? null : member . user_id )
645607 }
646608 title = { `Lọc theo: ${ member . display_name } ` }
647- className = { `relative rounded-full transition-all focus:outline-none ${
648- isActive
609+ className = { `relative rounded-full transition-all focus:outline-none ${ isActive
649610 ? "ring-3 ring-[#28B8FA] ring-offset-2 scale-110"
650611 : "ring-2 ring-transparent hover:ring-[#28B8FA]/40 hover:ring-offset-1 hover:scale-105"
651- } `}
612+ } `}
652613 >
653614 < UserAvatar
654615 avatarUrl = { member . avatar_url }
@@ -692,11 +653,10 @@ export function KanbanBoard({
692653 < div className = "relative" ref = { labelFilterPopoverRef } >
693654 < button
694655 onClick = { ( ) => setShowLabelFilterPopover ( ( v ) => ! v ) }
695- className = { `flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold border-2 transition-all ${
696- filterLabelIds . length > 0 || showLabelFilterPopover
656+ className = { `flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold border-2 transition-all ${ filterLabelIds . length > 0 || showLabelFilterPopover
697657 ? "bg-[#28B8FA] border-[#28B8FA] text-white shadow-md shadow-cyan-200"
698658 : "bg-white border-slate-200 text-slate-500 hover:border-[#28B8FA] hover:text-[#28B8FA]"
699- } `}
659+ } `}
700660 title = "Lọc theo nhãn"
701661 >
702662 < svg
@@ -735,8 +695,8 @@ export function KanbanBoard({
735695 // Find all IDs with this same color
736696 const relatedIds = label . color_hex
737697 ? colorToLabelIdsMap . get ( label . color_hex ) || [
738- label . id ,
739- ]
698+ label . id ,
699+ ]
740700 : [ label . id ] ;
741701
742702 if ( isActive ) {
@@ -898,7 +858,6 @@ export function KanbanBoard({
898858 column = { col }
899859 colIndex = { index }
900860 tasks = { columnTasks }
901- searchMatchedTaskIds = { searchMatchedTaskIds }
902861 onTaskClick = { onTaskClick }
903862 onAddTask = { onAddTask }
904863 onUpdateColumn = { onUpdateColumn }
0 commit comments