11'use client' ;
22
3+ import { FileIcon } from "@/components/ui/fileIcon" ;
34import { Repository , SearchResultFile } from "@/lib/types" ;
45import { 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" ;
610import { Entry } from "./entry" ;
711import { Filter } from "./filter" ;
8- import Image from "next/image" ;
9- import { LaptopIcon } from "@radix-ui/react-icons" ;
10- import { FileIcon } from "@/components/ui/fileIcon" ;
1112
1213interface FilePanelProps {
1314 matches : SearchResultFile [ ] ;
1415 onFilterChanged : ( filteredMatches : SearchResultFile [ ] ) => void ,
1516 repoMetadata : Record < string , Repository > ;
1617}
1718
19+ const LANGUAGES_QUERY_PARAM = "langs" ;
20+ const REPOS_QUERY_PARAM = "repos" ;
21+
1822export const FilterPanel = ( {
1923 matches,
2024 onFilterChanged,
2125 repoMetadata,
2226} : 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 (
2839 "Repository" ,
2940 matches ,
3041 ( key ) => {
@@ -44,17 +55,16 @@ export const FilterPanel = ({
4455 key,
4556 displayName : info ?. displayName ?? key ,
4657 count : 0 ,
47- isSelected : false ,
58+ isSelected : selectedRepos . has ( key ) ,
4859 Icon,
4960 } ;
5061 }
51- ) ;
52-
53- setRepos ( _repos ) ;
54- } , [ matches , repoMetadata , setRepos ] ) ;
62+ )
63+ } , [ searchParams ] ) ;
5564
56- useEffect ( ( ) => {
57- const _languages = aggregateMatches (
65+ const languages = useMemo ( ( ) => {
66+ const selectedLanguages = getSelectedFromQuery ( LANGUAGES_QUERY_PARAM ) ;
67+ return aggregateMatches (
5868 "Language" ,
5969 matches ,
6070 ( key ) => {
@@ -66,67 +76,76 @@ export const FilterPanel = ({
6676 key,
6777 displayName : key ,
6878 count : 0 ,
69- isSelected : false ,
79+ isSelected : selectedLanguages . has ( key ) ,
7080 Icon : Icon ,
7181 } satisfies Entry ;
7282 }
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 )
9683 ) ;
84+ } , [ searchParams ] ) ;
9785
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 ) ) ;
10391
10492 const filteredMatches = matches . filter ( ( match ) =>
10593 (
10694 ( selectedRepos . size === 0 ? true : selectedRepos . has ( match . Repository ) ) &&
10795 ( selectedLanguages . size === 0 ? true : selectedLanguages . has ( match . Language ) )
10896 )
10997 ) ;
110-
11198 onFilterChanged ( filteredMatches ) ;
112- } , [ matches , repos , languages , onFilterChanged ] ) ;
11399
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+
116105 return (
117106 < div className = "p-3 flex flex-col gap-3 h-full" >
118107 < Filter
119108 title = "Filter By Repository"
120109 searchPlaceholder = { `Filter ${ numRepos } repositories` }
121110 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+ } }
123127 className = "max-h-[50%]"
124128 />
125129 < Filter
126130 title = "Filter By Language"
127131 searchPlaceholder = { `Filter ${ numLanguages } languages` }
128132 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+ } }
130149 className = "overflow-auto"
131150 />
132151 </ div >
0 commit comments