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 ( ) ;
1425const { 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
3227const dropdownRef = useRef < HTMLDivElement > ( null ) ;
33- const [ isOpen , setIsOpen ] = useState ( false ) ;
28+ const [ isOpen , setIsOpen ] = useState < boolean > ( false ) ;
3429const [ 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+ ) ;
3889setIsOpen ( false ) ;
3990setOpenedLanguages ( [ ] ) ;
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+
42119const { focusedIndex, handleKeyDown, resetFocus, focusFirst} =
43120useKeyboardNavigation ( {
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 ) ,
49125onClose :( ) => 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-
77139const toggleDropdown = ( ) => {
78140setIsOpen ( ( prev ) => {
79141if ( ! 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-
98153useEffect ( ( ) => {
99154if ( isOpen && focusedIndex >= 0 ) {
100155const 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
110170return (
111171< div
@@ -121,8 +181,8 @@ const LanguageSelector = () => {
121181onClick = { 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 ) =>
137197lang . subLanguages . length > 0 ?(
138198< SubLanguageSelector
139- key = { index }
140- mainLanguage = { lang }
141- afterSelect = { ( ) => {
142- setIsOpen ( false ) ;
143- } }
199+ key = { lang . name }
144200opened = { openedLanguages . includes ( lang ) }
145- onDropdownToggle = { handleToggleSublanguage }
201+ parentLanguage = { lang }
202+ onDropdownToggle = { handleToggleSubLanguage }
203+ handleParentSelect = { handleSelect }
204+ afterSelect = { afterSelect }
146205/>
147206) :(
148207< li