1+ /**
2+ * Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
3+ */
4+
15import { useRef , useEffect , useState , useMemo } from "react" ;
6+ import { useNavigate } from "react-router-dom" ;
27
38import { useAppContext } from "@contexts/AppContext" ;
49import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation" ;
510import { useLanguages } from "@hooks/useLanguages" ;
611import { LanguageType } from "@types" ;
12+ import { configureUserSelection } from "@utils/configureUserSelection" ;
13+ import {
14+ getLanguageDisplayLogo ,
15+ getLanguageDisplayName ,
16+ } from "@utils/languageUtils" ;
17+ import { slugify } from "@utils/slugify" ;
718
819import SubLanguageSelector from "./SubLanguageSelector" ;
920
10- // Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
11-
1221const LanguageSelector = ( ) => {
13- const { language, setLanguage } = useAppContext ( ) ;
22+ const navigate = useNavigate ( ) ;
23+
24+ const { language, subLanguage, setSearchText } = useAppContext ( ) ;
1425 const { fetchedLanguages, loading, error } = useLanguages ( ) ;
15- const allLanguages = useMemo (
16- ( ) =>
17- fetchedLanguages . flatMap ( ( lang ) =>
18- lang . subLanguages . length > 0
19- ? [
20- lang ,
21- ...lang . subLanguages . map ( ( subLang ) => ( {
22- ...subLang ,
23- mainLanguage : lang ,
24- subLanguages : [ ] ,
25- } ) ) ,
26- ]
27- : [ lang ]
28- ) ,
29- [ fetchedLanguages ]
30- ) ;
3126
3227 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
33- const [ isOpen , setIsOpen ] = useState ( false ) ;
28+ const [ isOpen , setIsOpen ] = useState < boolean > ( false ) ;
3429 const [ openedLanguages , setOpenedLanguages ] = useState < LanguageType [ ] > ( [ ] ) ;
3530
36- const handleSelect = ( selected : LanguageType ) => {
37- setLanguage ( selected ) ;
31+ const keyboardItems = useMemo ( ( ) => {
32+ return fetchedLanguages . flatMap ( ( lang ) =>
33+ openedLanguages . map ( ( ol ) => ol . name ) . includes ( lang . name )
34+ ? [
35+ { languageName : lang . name } ,
36+ ...lang . subLanguages . map ( ( sl ) => ( {
37+ languageName : lang . name ,
38+ subLanguageName : sl . name ,
39+ } ) ) ,
40+ ]
41+ : [ { languageName : lang . name } ]
42+ ) ;
43+ } , [ fetchedLanguages , openedLanguages ] ) ;
44+
45+ const displayName = useMemo (
46+ ( ) => getLanguageDisplayName ( language . name , subLanguage ) ,
47+ [ language . name , subLanguage ]
48+ ) ;
49+
50+ const displayLogo = useMemo (
51+ ( ) => getLanguageDisplayLogo ( language . name , subLanguage ) ,
52+ [ language . name , subLanguage ]
53+ ) ;
54+
55+ const handleToggleSubLanguage = ( name : LanguageType [ "name" ] ) => {
56+ const isAlreadyOpened = openedLanguages . some ( ( lang ) => lang . name === name ) ;
57+ const openedLang = fetchedLanguages . find ( ( lang ) => lang . name === name ) ;
58+ if ( openedLang === undefined || openedLang . subLanguages . length === 0 ) {
59+ return ;
60+ }
61+
62+ if ( ! isAlreadyOpened ) {
63+ setOpenedLanguages ( ( prev ) => [ ...prev , openedLang ] ) ;
64+ } else {
65+ setOpenedLanguages ( ( prev ) =>
66+ prev . filter ( ( lang ) => lang . name !== openedLang . name )
67+ ) ;
68+ }
69+ } ;
70+
71+ /**
72+ * When setting a new language we need to ensure that a category
73+ * has been set given this new language.
74+ * Ensure that the search text is cleared.
75+ */
76+ const handleSelect = async ( selected : LanguageType ) => {
77+ const {
78+ language : newLanguage ,
79+ subLanguage : newSubLanguage ,
80+ category : newCategory ,
81+ } = await configureUserSelection ( {
82+ languageName : selected . name ,
83+ } ) ;
84+
85+ setSearchText ( "" ) ;
86+ navigate (
87+ `/${ slugify ( newLanguage . name ) } /${ slugify ( newSubLanguage ) } /${ slugify ( newCategory ) } `
88+ ) ;
3889 setIsOpen ( false ) ;
3990 setOpenedLanguages ( [ ] ) ;
4091 } ;
4192
93+ const afterSelect = ( ) => {
94+ setIsOpen ( false ) ;
95+ } ;
96+
97+ const handleSubLanguageSelect = async (
98+ selectedLanguageName : LanguageType [ "name" ] ,
99+ selectedSubLanguageName :
100+ | LanguageType [ "subLanguages" ] [ number ] [ "name" ]
101+ | undefined
102+ ) => {
103+ const {
104+ language : newLanguage ,
105+ subLanguage : newSubLanguage ,
106+ category : newCategory ,
107+ } = await configureUserSelection ( {
108+ languageName : selectedLanguageName ,
109+ subLanguageName : selectedSubLanguageName ,
110+ } ) ;
111+
112+ setSearchText ( "" ) ;
113+ navigate (
114+ `/${ slugify ( newLanguage . name ) } /${ slugify ( newSubLanguage ) } /${ slugify ( newCategory ) } `
115+ ) ;
116+ afterSelect ( ) ;
117+ } ;
118+
42119 const { focusedIndex, handleKeyDown, resetFocus, focusFirst } =
43120 useKeyboardNavigation ( {
44- items : allLanguages ,
121+ items : keyboardItems ,
45122 isOpen,
46- openedLanguages,
47- toggleDropdown : ( openedLang ) => handleToggleSublanguage ( openedLang ) ,
48- onSelect : handleSelect ,
123+ toggleDropdown : ( l ) => handleToggleSubLanguage ( l ) ,
124+ onSelect : ( l , sl ) => handleSubLanguageSelect ( l , sl ) ,
49125 onClose : ( ) => setIsOpen ( false ) ,
50126 } ) ;
51127
@@ -60,20 +136,6 @@ const LanguageSelector = () => {
60136 } , 0 ) ;
61137 } ;
62138
63- const handleToggleSublanguage = ( openedLang : LanguageType ) => {
64- const isAlreadyOpened = openedLanguages . some (
65- ( lang ) => lang . name === openedLang . name
66- ) ;
67-
68- if ( ! isAlreadyOpened ) {
69- setOpenedLanguages ( ( prev ) => [ ...prev , openedLang ] ) ;
70- } else {
71- setOpenedLanguages ( ( prev ) =>
72- prev . filter ( ( lang ) => lang . name !== openedLang . name )
73- ) ;
74- }
75- } ;
76-
77139 const toggleDropdown = ( ) => {
78140 setIsOpen ( ( prev ) => {
79141 if ( ! prev ) setTimeout ( focusFirst , 0 ) ;
@@ -88,13 +150,6 @@ const LanguageSelector = () => {
88150 // eslint-disable-next-line react-hooks/exhaustive-deps
89151 } , [ isOpen ] ) ;
90152
91- useEffect ( ( ) => {
92- if ( language . mainLanguage ) {
93- handleToggleSublanguage ( language . mainLanguage ) ;
94- }
95- // eslint-disable-next-line react-hooks/exhaustive-deps
96- } , [ language ] ) ;
97-
98153 useEffect ( ( ) => {
99154 if ( isOpen && focusedIndex >= 0 ) {
100155 const element = document . querySelector (
@@ -104,8 +159,13 @@ const LanguageSelector = () => {
104159 }
105160 } , [ isOpen , focusedIndex ] ) ;
106161
107- if ( loading ) return < p > Loading languages...</ p > ;
108- if ( error ) return < p > Error fetching languages: { error } </ p > ;
162+ if ( loading ) {
163+ return < p > Loading languages...</ p > ;
164+ }
165+
166+ if ( error ) {
167+ return < p > Error fetching languages: { error } </ p > ;
168+ }
109169
110170 return (
111171 < div
@@ -121,8 +181,8 @@ const LanguageSelector = () => {
121181 onClick = { toggleDropdown }
122182 >
123183 < div className = "selector__value" >
124- < img src = { language . icon } alt = "" />
125- < span > { language . name || "Select a language" } </ span >
184+ < img src = { displayLogo } alt = "" />
185+ < span > { displayName } </ span >
126186 </ div >
127187 < span className = "selector__arrow" />
128188 </ button >
@@ -136,13 +196,12 @@ const LanguageSelector = () => {
136196 { fetchedLanguages . map ( ( lang , index ) =>
137197 lang . subLanguages . length > 0 ? (
138198 < SubLanguageSelector
139- key = { index }
140- mainLanguage = { lang }
141- afterSelect = { ( ) => {
142- setIsOpen ( false ) ;
143- } }
199+ key = { lang . name }
144200 opened = { openedLanguages . includes ( lang ) }
145- onDropdownToggle = { handleToggleSublanguage }
201+ parentLanguage = { lang }
202+ onDropdownToggle = { handleToggleSubLanguage }
203+ handleParentSelect = { handleSelect }
204+ afterSelect = { afterSelect }
146205 />
147206 ) : (
148207 < li
0 commit comments