1
1
'use client' ;
2
2
3
+ import { FileIcon } from "@/components/ui/fileIcon" ;
3
4
import { Repository , SearchResultFile } from "@/lib/types" ;
4
5
import { cn , getRepoCodeHostInfo } from "@/lib/utils" ;
5
- import { SetStateAction , useCallback , useEffect , useState } from "react" ;
6
+ import { LaptopIcon } from "@radix-ui/react-icons" ;
7
+ import Image from "next/image" ;
8
+ import { useRouter , useSearchParams } from "next/navigation" ;
9
+ import { useEffect , useMemo } from "react" ;
6
10
import { Entry } from "./entry" ;
7
11
import { Filter } from "./filter" ;
8
- import Image from "next/image" ;
9
- import { LaptopIcon } from "@radix-ui/react-icons" ;
10
- import { FileIcon } from "@/components/ui/fileIcon" ;
11
12
12
13
interface FilePanelProps {
13
14
matches : SearchResultFile [ ] ;
14
15
onFilterChanged : ( filteredMatches : SearchResultFile [ ] ) => void ,
15
16
repoMetadata : Record < string , Repository > ;
16
17
}
17
18
19
+ const LANGUAGES_QUERY_PARAM = "langs" ;
20
+ const REPOS_QUERY_PARAM = "repos" ;
21
+
18
22
export const FilterPanel = ( {
19
23
matches,
20
24
onFilterChanged,
21
25
repoMetadata,
22
26
} : FilePanelProps ) => {
23
- const [ repos , setRepos ] = useState < Record < string , Entry > > ( { } ) ;
24
- const [ languages , setLanguages ] = useState < Record < string , Entry > > ( { } ) ;
25
-
26
- useEffect ( ( ) => {
27
- const _repos = aggregateMatches (
27
+ const router = useRouter ( ) ;
28
+ const searchParams = useSearchParams ( ) ;
29
+
30
+ // Helper to parse query params into sets
31
+ const getSelectedFromQuery = ( param : string ) => {
32
+ const value = searchParams . get ( param ) ;
33
+ return value ? new Set ( value . split ( ',' ) ) : new Set ( ) ;
34
+ } ;
35
+
36
+ const repos = useMemo ( ( ) => {
37
+ const selectedRepos = getSelectedFromQuery ( REPOS_QUERY_PARAM ) ;
38
+ return aggregateMatches (
28
39
"Repository" ,
29
40
matches ,
30
41
( key ) => {
@@ -44,17 +55,16 @@ export const FilterPanel = ({
44
55
key,
45
56
displayName : info ?. displayName ?? key ,
46
57
count : 0 ,
47
- isSelected : false ,
58
+ isSelected : selectedRepos . has ( key ) ,
48
59
Icon,
49
60
} ;
50
61
}
51
- ) ;
52
-
53
- setRepos ( _repos ) ;
54
- } , [ matches , repoMetadata , setRepos ] ) ;
62
+ )
63
+ } , [ searchParams ] ) ;
55
64
56
- useEffect ( ( ) => {
57
- const _languages = aggregateMatches (
65
+ const languages = useMemo ( ( ) => {
66
+ const selectedLanguages = getSelectedFromQuery ( LANGUAGES_QUERY_PARAM ) ;
67
+ return aggregateMatches (
58
68
"Language" ,
59
69
matches ,
60
70
( key ) => {
@@ -66,67 +76,76 @@ export const FilterPanel = ({
66
76
key,
67
77
displayName : key ,
68
78
count : 0 ,
69
- isSelected : false ,
79
+ isSelected : selectedLanguages . has ( key ) ,
70
80
Icon : Icon ,
71
81
} satisfies Entry ;
72
82
}
73
- )
74
-
75
- setLanguages ( _languages ) ;
76
- } , [ matches , setLanguages ] ) ;
77
-
78
- const onEntryClicked = useCallback ( (
79
- key : string ,
80
- setter : ( value : SetStateAction < Record < string , Entry > > ) => void ,
81
- ) => {
82
- setter ( ( values ) => ( {
83
- ...values ,
84
- [ key ] : {
85
- ...values [ key ] ,
86
- isSelected : ! values [ key ] . isSelected ,
87
- } ,
88
- } ) ) ;
89
- } , [ ] ) ;
90
-
91
- useEffect ( ( ) => {
92
- const selectedRepos = new Set (
93
- Object . entries ( repos )
94
- . filter ( ( [ _ , { isSelected } ] ) => isSelected )
95
- . map ( ( [ key ] ) => key )
96
83
) ;
84
+ } , [ searchParams ] ) ;
97
85
98
- const selectedLanguages = new Set (
99
- Object . entries ( languages )
100
- . filter ( ( [ _ , { isSelected } ] ) => isSelected )
101
- . map ( ( [ key ] ) => key )
102
- ) ;
86
+ // Calls `onFilterChanged` with the filtered list of matches
87
+ // whenever the filter state changes.
88
+ useEffect ( ( ) => {
89
+ const selectedRepos = new Set ( Object . keys ( repos ) . filter ( ( key ) => repos [ key ] . isSelected ) ) ;
90
+ const selectedLanguages = new Set ( Object . keys ( languages ) . filter ( ( key ) => languages [ key ] . isSelected ) ) ;
103
91
104
92
const filteredMatches = matches . filter ( ( match ) =>
105
93
(
106
94
( selectedRepos . size === 0 ? true : selectedRepos . has ( match . Repository ) ) &&
107
95
( selectedLanguages . size === 0 ? true : selectedLanguages . has ( match . Language ) )
108
96
)
109
97
) ;
110
-
111
98
onFilterChanged ( filteredMatches ) ;
112
- } , [ matches , repos , languages , onFilterChanged ] ) ;
113
99
114
- const numRepos = Object . keys ( repos ) . length > 100 ? '100+' : Object . keys ( repos ) . length ;
115
- const numLanguages = Object . keys ( languages ) . length > 100 ? '100+' : Object . keys ( languages ) . length ;
100
+ } , [ matches , repos , languages , onFilterChanged , searchParams , router ] ) ;
101
+
102
+ const numRepos = useMemo ( ( ) => Object . keys ( repos ) . length > 100 ? '100+' : Object . keys ( repos ) . length , [ repos ] ) ;
103
+ const numLanguages = useMemo ( ( ) => Object . keys ( languages ) . length > 100 ? '100+' : Object . keys ( languages ) . length , [ languages ] ) ;
104
+
116
105
return (
117
106
< div className = "p-3 flex flex-col gap-3 h-full" >
118
107
< Filter
119
108
title = "Filter By Repository"
120
109
searchPlaceholder = { `Filter ${ numRepos } repositories` }
121
110
entries = { Object . values ( repos ) }
122
- onEntryClicked = { ( key ) => onEntryClicked ( key , setRepos ) }
111
+ onEntryClicked = { ( key ) => {
112
+ const newRepos = { ...repos } ;
113
+ newRepos [ key ] . isSelected = ! newRepos [ key ] . isSelected ;
114
+ const selectedRepos = Object . keys ( newRepos ) . filter ( ( key ) => newRepos [ key ] . isSelected ) ;
115
+ const newParams = new URLSearchParams ( searchParams . toString ( ) ) ;
116
+
117
+ if ( selectedRepos . length > 0 ) {
118
+ newParams . set ( REPOS_QUERY_PARAM , selectedRepos . join ( ',' ) ) ;
119
+ } else {
120
+ newParams . delete ( REPOS_QUERY_PARAM ) ;
121
+ }
122
+
123
+ if ( newParams . toString ( ) !== searchParams . toString ( ) ) {
124
+ router . replace ( `?${ newParams . toString ( ) } ` , { scroll : false } ) ;
125
+ }
126
+ } }
123
127
className = "max-h-[50%]"
124
128
/>
125
129
< Filter
126
130
title = "Filter By Language"
127
131
searchPlaceholder = { `Filter ${ numLanguages } languages` }
128
132
entries = { Object . values ( languages ) }
129
- onEntryClicked = { ( key ) => onEntryClicked ( key , setLanguages ) }
133
+ onEntryClicked = { ( key ) => {
134
+ const newLanguages = { ...languages } ;
135
+ newLanguages [ key ] . isSelected = ! newLanguages [ key ] . isSelected ;
136
+ const selectedLanguages = Object . keys ( newLanguages ) . filter ( ( key ) => newLanguages [ key ] . isSelected ) ;
137
+ const newParams = new URLSearchParams ( searchParams . toString ( ) ) ;
138
+
139
+ if ( selectedLanguages . length > 0 ) {
140
+ newParams . set ( LANGUAGES_QUERY_PARAM , selectedLanguages . join ( ',' ) ) ;
141
+ } else {
142
+ newParams . delete ( LANGUAGES_QUERY_PARAM ) ;
143
+ }
144
+
145
+ if ( newParams . toString ( ) !== searchParams . toString ( ) ) {
146
+ router . replace ( `?${ newParams . toString ( ) } ` , { scroll : false } ) ;
147
+ }
148
+ } }
130
149
className = "overflow-auto"
131
150
/>
132
151
</ div >
0 commit comments