1
- import React , { useCallback , useId , useMemo , useState } from 'react' ;
1
+ import React , { useCallback , useEffect , useId , useMemo , useState } from 'react' ;
2
+ import { useRouter } from 'next/router' ;
2
3
import Switcher from '@rescui/switcher' ;
3
4
import Checkbox from '@rescui/checkbox' ;
4
5
import { createTextCn } from '@rescui/typography' ;
5
6
import '@jetbrains/kotlin-web-site-ui/out/components/layout' ;
6
7
import styles from './case-studies-filter.module.css' ;
8
+ import { CasePlatform , CaseStudyType , CaseTypeSwitch , Platforms } from '../case-studies' ;
9
+
10
+
11
+ function parseType ( maybeCaseType : unknown ) : CaseTypeSwitch {
12
+ const maybeCaseTypeString = String ( maybeCaseType || 'all' ) ;
13
+ return ( maybeCaseTypeString === 'multiplatform' || maybeCaseTypeString === 'server-side' ) ? maybeCaseTypeString : 'all' ;
14
+ }
15
+
16
+ function parsePlatforms ( maybePlatforms : unknown ) : CasePlatform [ ] {
17
+ if ( ! maybePlatforms ) {
18
+ return [ ...Platforms ] ;
19
+ }
20
+ const list = String ( maybePlatforms ) . split ( ',' ) . map ( ( x ) => x . trim ( ) ) . filter ( Boolean ) ;
21
+ const set = new Set < CasePlatform > ( ) ;
22
+ for ( const i of list ) {
23
+ if ( ( Platforms as readonly string [ ] ) . includes ( i ) ) {
24
+ set . add ( i as CasePlatform ) ;
25
+ }
26
+ }
27
+ return set . size === 0 ? [ ...Platforms ] : Array . from ( set ) ;
28
+ }
29
+
30
+ function parseCompose ( v : unknown ) : boolean {
31
+ if ( v === true ) return true ;
32
+ const s = String ( v || 'true' ) . toLowerCase ( ) ;
33
+ return s === 'true' || s === '1' || s === 'yes' ;
34
+ }
35
+
36
+ function buildQuery ( type : CaseTypeSwitch , platforms : CasePlatform [ ] , compose : boolean ) {
37
+ const q : Record < string , any > = { } ;
38
+ if ( type && type !== 'all' ) q . type = type ;
39
+ // only keep platforms if not all selected
40
+ const allSelected = platforms . length === Platforms . length && Platforms . every ( ( p ) => platforms . includes ( p ) ) ;
41
+ if ( ! allSelected && ( type === 'multiplatform' || type === 'all' ) ) {
42
+ q . platforms = platforms . join ( ',' ) ;
43
+ }
44
+ if ( type === 'multiplatform' || type === 'all' ) q . compose = compose ? 'true' : 'false' ;
45
+ return q ;
46
+ }
7
47
8
48
export const CaseStudiesFilter : React . FC = ( ) => {
49
+ const router = useRouter ( ) ;
9
50
const darkTextCn = createTextCn ( 'dark' ) ;
51
+
10
52
// Case study type switcher
11
- const typeOptions = useMemo (
53
+ const typeOptions : Array < { value : CaseTypeSwitch , label : string } > = useMemo (
12
54
( ) => [
13
55
{ value : 'all' , label : 'All' } ,
14
- { value : 'kotlin- multiplatform' , label : 'Kotlin Multiplatform' } ,
15
- { value : 'server-side' , label : 'Server-side' } ,
56
+ { value : 'multiplatform' , label : 'Kotlin Multiplatform' } ,
57
+ { value : 'server-side' , label : 'Server-side' }
16
58
] , [ ]
17
59
) ;
18
- const [ type , setType ] = useState < string > ( 'all' ) ;
19
- const onTypeChange = useCallback ( ( value : string ) => setType ( value ) , [ ] ) ;
20
60
21
- // Code shared across (checkboxes)
22
- const codeSharedOptions = useMemo (
23
- ( ) => [
24
- { id : 'android' , label : 'Android' } ,
25
- { id : 'ios' , label : 'iOS' } ,
26
- { id : 'desktop' , label : 'Desktop' } ,
27
- { id : 'frontend' , label : 'Frontend' } ,
28
- { id : 'backend' , label : 'Backend' } ,
29
- ] , [ ]
30
- ) ;
31
- const [ codeShared , setCodeShared ] = useState < string [ ] > ( [ ] ) ;
32
- const toggleCodeShared = useCallback ( ( id : string ) => {
33
- setCodeShared ( ( prev ) => ( prev . includes ( id ) ? prev . filter ( ( x ) => x !== id ) : [ ...prev , id ] ) ) ;
34
- } , [ ] ) ;
61
+ // State synchronized with URL
62
+ const [ type , setType ] = useState < CaseTypeSwitch > ( 'all' ) ;
63
+ const [ platforms , setPlatforms ] = useState < CasePlatform [ ] > ( [ ...Platforms ] ) ;
64
+ const [ compose , setCompose ] = useState < boolean > ( true ) ;
35
65
36
- // UI Technology (checkboxes)
37
- const uiTechOptions = useMemo (
38
- ( ) => [
39
- { id : 'built-with-compose-multiplatform' , label : 'Built with Compose Multiplatform' } ,
40
- ] , [ ]
41
- ) ;
42
- const [ uiTech , setUiTech ] = useState < string [ ] > ( [ ] ) ;
43
- const toggleUiTech = useCallback ( ( id : string ) => {
44
- setUiTech ( ( prev ) => ( prev . includes ( id ) ? prev . filter ( ( x ) => x !== id ) : [ ...prev , id ] ) ) ;
45
- } , [ ] ) ;
66
+ // Initialize/Sync from URL
67
+ useEffect ( ( ) => {
68
+ const q = router . query ;
69
+ const t = parseType ( q . type ) ;
70
+ const p = parsePlatforms ( q . platforms ) ;
71
+ const c = parseCompose ( q . compose ) ;
72
+ setType ( t ) ;
73
+ setPlatforms ( p ) ;
74
+ setCompose ( c ) ;
75
+ // eslint-disable-next-line react-hooks/exhaustive-deps
76
+ } , [ router . query . type , router . query . platforms , router . query . compose ] ) ;
77
+
78
+ // Handlers update both state and URL (shallow)
79
+ const pushQuery = useCallback ( ( nextType : CaseTypeSwitch , nextPlatforms : CasePlatform [ ] , nextCompose : boolean ) => {
80
+ const q = buildQuery ( nextType , nextPlatforms , nextCompose ) ;
81
+ router . replace ( { pathname : router . pathname , query : q } , undefined , { shallow : true } ) ;
82
+ } , [ router ] ) ;
83
+
84
+ const onTypeChange = useCallback ( ( value : string ) => {
85
+ const nextType = parseType ( value ) ;
86
+ // When switching to server-side, we can keep platforms/compose but they won't be shown
87
+ pushQuery ( nextType , platforms , compose ) ;
88
+ } , [ platforms , compose , pushQuery ] ) ;
89
+
90
+ const togglePlatform = useCallback ( ( id : CasePlatform ) => {
91
+ let next = platforms . includes ( id ) ? platforms . filter ( ( x ) => x !== id ) : [ ...platforms , id ] ;
92
+ // If user unchecks all, reset to all selected
93
+ if ( next . length === 0 ) next = [ ...Platforms ] ;
94
+ pushQuery ( type , next , compose ) ;
95
+ } , [ platforms , type , compose , pushQuery ] ) ;
96
+
97
+ const onComposeChange = useCallback ( ( ) => {
98
+ const next = ! compose ;
99
+ pushQuery ( type , platforms , next ) ;
100
+ } , [ compose , type , platforms , pushQuery ] ) ;
46
101
47
102
// for accessibility ids
48
103
const typeTitleId = useId ( ) ;
49
104
const codeSharedTitleId = useId ( ) ;
50
105
const uiTechTitleId = useId ( ) ;
51
106
107
+ const showKmpFilters = type === 'multiplatform' || type === 'all' ;
108
+
52
109
return (
53
110
< section data-testid = "case-studies-filter" aria-label = "Case Studies Filter" className = { styles . wrapper } >
54
111
< div className = { 'ktl-layout ktl-layout--center' } >
@@ -57,56 +114,57 @@ export const CaseStudiesFilter: React.FC = () => {
57
114
</ h2 >
58
115
< div className = { styles . inner } >
59
116
{ /* Case study type */ }
60
- < div className = { `${ styles . group } ${ styles . groupType } ` } role = "group" aria-labelledby = { typeTitleId } data-test = "filter-type" >
61
- < h3 id = { typeTitleId } className = { styles . groupTitle } > < span className = { darkTextCn ( 'rs-h4' ) } > Case study type</ span > </ h3 >
117
+ < div className = { `${ styles . group } ${ styles . groupType } ` } role = "group" aria-labelledby = { typeTitleId }
118
+ data-test = "filter-type" >
119
+ < h3 id = { typeTitleId } className = { styles . groupTitle } > < span className = { darkTextCn ( 'rs-h4' ) } > Case study type</ span >
120
+ </ h3 >
62
121
< div className = { styles . switcherSmall } >
63
122
< Switcher mode = { 'rock' } value = { type } onChange = { onTypeChange } options = { typeOptions } />
64
123
</ div >
65
124
</ div >
66
125
67
- { /* Code shared across */ }
68
- < div className = { styles . group } role = "group" aria-labelledby = { codeSharedTitleId } data-test = "filter-code-shared" >
69
- < h3 id = { codeSharedTitleId } className = { styles . groupTitle } > < span className = { darkTextCn ( 'rs-h4' ) } > Code shared across</ span > </ h3 >
70
- < div className = { styles . checkboxes } >
71
- { codeSharedOptions . map ( ( opt ) => {
72
- const id = `code-shared-${ opt . id } ` ;
73
- const checked = codeShared . includes ( opt . id ) ;
74
- return (
75
- < Checkbox
76
- key = { opt . id }
77
- checked = { checked }
78
- onChange = { ( ) => toggleCodeShared ( opt . id ) }
79
- mode = "classic"
80
- size = "m"
81
- >
82
- { opt . label }
83
- </ Checkbox >
84
- ) ;
85
- } ) }
126
+ { showKmpFilters && (
127
+ < div className = { styles . group } role = "group" aria-labelledby = { codeSharedTitleId }
128
+ data-test = "filter-code-shared" >
129
+ < h3 id = { codeSharedTitleId } className = { styles . groupTitle } > < span
130
+ className = { darkTextCn ( 'rs-h4' ) } > Code shared across</ span > </ h3 >
131
+ < div className = { styles . checkboxes } >
132
+ { Platforms . map ( ( pid ) => {
133
+ const checked = platforms . includes ( pid ) ;
134
+ const label = pid === 'ios' ? 'iOS' : pid . charAt ( 0 ) . toUpperCase ( ) + pid . slice ( 1 ) ;
135
+ return (
136
+ < Checkbox
137
+ key = { pid }
138
+ checked = { checked }
139
+ onChange = { ( ) => togglePlatform ( pid ) }
140
+ mode = "classic"
141
+ size = "m"
142
+ >
143
+ { label }
144
+ </ Checkbox >
145
+ ) ;
146
+ } ) }
147
+ </ div >
86
148
</ div >
87
- </ div >
149
+ ) }
88
150
89
- { /* UI technology */ }
90
- < div className = { styles . group } role = "group" aria-labelledby = { uiTechTitleId } data-test = "filter-ui-technology" >
91
- < h3 id = { uiTechTitleId } className = { styles . groupTitle } > < span className = { darkTextCn ( 'rs-h4' ) } > UI technology</ span > </ h3 >
92
- < div className = { styles . checkboxes } >
93
- { uiTechOptions . map ( ( opt ) => {
94
- const id = `ui-tech-${ opt . id } ` ;
95
- const checked = uiTech . includes ( opt . id ) ;
96
- return (
97
- < Checkbox
98
- key = { opt . id }
99
- checked = { checked }
100
- onChange = { ( ) => toggleUiTech ( opt . id ) }
101
- mode = "classic"
102
- size = "m"
103
- >
104
- { opt . label }
105
- </ Checkbox >
106
- ) ;
107
- } ) }
151
+ { showKmpFilters && (
152
+ < div className = { styles . group } role = "group" aria-labelledby = { uiTechTitleId }
153
+ data-test = "filter-ui-technology" >
154
+ < h3 id = { uiTechTitleId } className = { styles . groupTitle } > < span className = { darkTextCn ( 'rs-h4' ) } > UI technology</ span >
155
+ </ h3 >
156
+ < div className = { styles . checkboxes } >
157
+ < Checkbox
158
+ checked = { compose }
159
+ onChange = { onComposeChange }
160
+ mode = "classic"
161
+ size = "m"
162
+ >
163
+ Built with Compose Multiplatform
164
+ </ Checkbox >
165
+ </ div >
108
166
</ div >
109
- </ div >
167
+ ) }
110
168
</ div >
111
169
</ div >
112
170
</ section >
0 commit comments