titaned-frontend-library 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/MEILISEARCH_INTEGRATION.md +261 -0
  3. package/README.md +236 -0
  4. package/SEARCH_INTEGRATION_README.md +208 -0
  5. package/dist/App.d.ts +3 -0
  6. package/dist/components/ButtonComponent.scss +3 -0
  7. package/dist/components/Footer.scss +87 -0
  8. package/dist/components/MainHeader.scss +915 -0
  9. package/dist/components/Sidebar.scss +170 -0
  10. package/dist/components/UserMenu.scss +0 -0
  11. package/dist/components/common/Button/ButtonComponent.d.ts +4 -0
  12. package/dist/components/common/Footer/Footer.d.ts +4 -0
  13. package/dist/components/common/Header/MainHeader.d.ts +4 -0
  14. package/dist/components/common/Header/SimpleSearchContext.d.ts +36 -0
  15. package/dist/components/common/NavDropdownMenu/NavDropdownMenu.d.ts +11 -0
  16. package/dist/components/common/SearchHighlight.d.ts +11 -0
  17. package/dist/components/common/SearchResultItem.d.ts +12 -0
  18. package/dist/components/common/Sidebar/Sidebar.d.ts +12 -0
  19. package/dist/components/common/UserMenu/UserMenu.d.ts +15 -0
  20. package/dist/components/common/menu/MenuComponent.d.ts +4 -0
  21. package/dist/contexts/SidebarContext.d.ts +6 -0
  22. package/dist/examples/MeiliSearchUsageExample.d.ts +3 -0
  23. package/dist/hooks/useIsMobileSize.d.ts +2 -0
  24. package/dist/hooks/useSidebar.d.ts +1 -0
  25. package/dist/index.cjs +27 -0
  26. package/dist/index.css +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +1867 -0
  29. package/dist/interfaces/components.d.ts +181 -0
  30. package/dist/main.d.ts +0 -0
  31. package/dist/providers/SidebarProvider.d.ts +4 -0
  32. package/dist/services/meiliSearchService.d.ts +47 -0
  33. package/dist/utils/searchNavigation.d.ts +43 -0
  34. package/dist/vite.svg +1 -0
  35. package/eslint.config.js +28 -0
  36. package/index.html +13 -0
  37. package/next.config.ts +35 -0
  38. package/package.json +53 -0
  39. package/public/vite.svg +1 -0
  40. package/src/@edx-frontend-platform-i18n.d.ts +1 -0
  41. package/src/App.css +66 -0
  42. package/src/App.tsx +54 -0
  43. package/src/assets/react.svg +1 -0
  44. package/src/components/common/Button/ButtonComponent.scss +3 -0
  45. package/src/components/common/Button/ButtonComponent.tsx +34 -0
  46. package/src/components/common/Footer/Footer.scss +87 -0
  47. package/src/components/common/Footer/Footer.tsx +53 -0
  48. package/src/components/common/Header/MainHeader.scss +915 -0
  49. package/src/components/common/Header/MainHeader.tsx +701 -0
  50. package/src/components/common/Header/SimpleSearchContext.tsx +194 -0
  51. package/src/components/common/NavDropdownMenu/NavDropdownMenu.tsx +58 -0
  52. package/src/components/common/SearchHighlight.tsx +64 -0
  53. package/src/components/common/SearchResultItem.tsx +162 -0
  54. package/src/components/common/Sidebar/Sidebar.scss +170 -0
  55. package/src/components/common/Sidebar/Sidebar.tsx +137 -0
  56. package/src/components/common/UserMenu/UserMenu.scss +0 -0
  57. package/src/components/common/UserMenu/UserMenu.tsx +73 -0
  58. package/src/components/common/menu/MenuComponent.module.css +12 -0
  59. package/src/components/common/menu/MenuComponent.tsx +37 -0
  60. package/src/contexts/SidebarContext.tsx +9 -0
  61. package/src/declarations.d.ts +10 -0
  62. package/src/examples/MeiliSearchUsageExample.tsx +102 -0
  63. package/src/hooks/useIsMobileSize.ts +19 -0
  64. package/src/hooks/useSidebar.ts +10 -0
  65. package/src/index.css +68 -0
  66. package/src/index.ts +21 -0
  67. package/src/interfaces/components.ts +172 -0
  68. package/src/main.tsx +11 -0
  69. package/src/providers/SidebarProvider.tsx +20 -0
  70. package/src/services/meiliSearchService.ts +233 -0
  71. package/src/styles/global-overrides.scss +1131 -0
  72. package/src/utils/searchNavigation.ts +140 -0
  73. package/src/vite-env.d.ts +1 -0
  74. package/tsconfig.app.json +26 -0
  75. package/tsconfig.json +17 -0
  76. package/tsconfig.node.json +24 -0
  77. package/vite.config.ts +34 -0
@@ -0,0 +1,701 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import {
3
+ Navbar,
4
+ Nav,
5
+ NavDropdown,
6
+ Button,
7
+ SearchField,
8
+ Icon,
9
+ IconButton,
10
+ Dropdown,
11
+ IconButtonWithTooltip,
12
+ } from '@openedx/paragon';
13
+ import useIsMobileSize from '../../../hooks/useIsMobileSize';
14
+ import { MainHeaderProps, MeiliSearchResult } from '../../../interfaces/components';
15
+ import { useSidebar } from '../../../hooks/useSidebar';
16
+ import {
17
+ ExitToApp, HelpCenter, Notifications, Sync, MenuIcon, Search, MoreVert,
18
+ } from '@openedx/paragon/icons';
19
+ import UserMenu from '../UserMenu/UserMenu';
20
+ import Cookies from 'universal-cookie';
21
+ import { createMeiliSearchService } from '../../../services/meiliSearchService';
22
+ import SearchResultItem from '../SearchResultItem';
23
+ import './MainHeader.scss';
24
+
25
+ const cookies = new Cookies();
26
+
27
+ const MainHeader: React.FC<MainHeaderProps> = ({
28
+ logoUrl,
29
+ menuList,
30
+ loginSignupButtons,
31
+ authenticatedUser = null,
32
+ userMenuItems,
33
+ menuAlignment,
34
+ onLanguageChange,
35
+ getBaseUrl,
36
+ meiliSearchConfig,
37
+ headerButtons,
38
+ languageSelectorList,
39
+ }) => {
40
+ const isMobile = useIsMobileSize();
41
+ const { toggleSidebar, setSidebarCollapsed } = useSidebar();
42
+ const [selectedLanguage, setSelectedLanguage] = useState(() => {
43
+ // Try to get from cookie first, fallback to localStorage, then default to 'en'
44
+ return cookies.get('openedx-language-preference') || 'en';
45
+ });
46
+ const [showSearchModal, setShowSearchModal] = useState(false);
47
+ const [showMoreMenu, setShowMoreMenu] = useState(false);
48
+ const [showNoResults, setShowNoResults] = useState(false);
49
+ const [searchKeywords, setSearchKeywords] = useState('');
50
+ const [searchResults, setSearchResults] = useState<MeiliSearchResult[]>([]);
51
+ const [isSearching, setIsSearching] = useState(false);
52
+ const [searchError, setSearchError] = useState<string | null>(null);
53
+ const [totalHits, setTotalHits] = useState(0);
54
+ const [showSearchResults, setShowSearchResults] = useState(false);
55
+ const [visibleResultsCount, setVisibleResultsCount] = useState(5);
56
+ const moreMenuRef = useRef<HTMLDivElement>(null);
57
+ const searchTimeoutRef = useRef<number | null>(null);
58
+ const searchService = useRef(meiliSearchConfig ? createMeiliSearchService(meiliSearchConfig) : null);
59
+
60
+ console.log('languageSelectorList', languageSelectorList);
61
+
62
+
63
+ // Update headers from MeiliSearch client if provided
64
+ useEffect(() => {
65
+ if (meiliSearchConfig?.client && searchService.current) {
66
+ searchService.current.updateHeadersFromClient(meiliSearchConfig.client);
67
+ }
68
+ }, [meiliSearchConfig?.client]);
69
+
70
+ // Cleanup timeout on unmount
71
+ useEffect(() => {
72
+ return () => {
73
+ if (searchTimeoutRef.current) {
74
+ clearTimeout(searchTimeoutRef.current);
75
+ }
76
+ };
77
+ }, []);
78
+
79
+ useEffect(() => {
80
+ document.body.dir = selectedLanguage === 'ar' ? 'rtl' : 'ltr';
81
+ }, [selectedLanguage]);
82
+
83
+ useEffect(() => {
84
+ if (isMobile) {
85
+ setSidebarCollapsed(true);
86
+ }
87
+ }, [isMobile, setSidebarCollapsed]);
88
+
89
+ useEffect(() => {
90
+ if (!showMoreMenu) return;
91
+
92
+ function handleClickOutside(event: MouseEvent) {
93
+ if (moreMenuRef.current && !moreMenuRef.current.contains(event.target as Node)) {
94
+ setShowMoreMenu(false);
95
+ }
96
+ }
97
+
98
+ document.addEventListener('mousedown', handleClickOutside);
99
+ return () => {
100
+ document.removeEventListener('mousedown', handleClickOutside);
101
+ };
102
+ }, [showMoreMenu]);
103
+
104
+ const handleLanguageChange = (lang: string) => {
105
+ setSelectedLanguage(lang);
106
+ // Update cookie
107
+ cookies.set('openedx-language-preference', lang, {
108
+ path: getBaseUrl ? getBaseUrl() : '/',
109
+ // secure: process.env.NODE_ENV === 'production',
110
+ // sameSite: 'strict'
111
+ });
112
+ // window.location.reload();
113
+ // Also update localStorage for backward compatibility
114
+ // localStorage.setItem('selectedLanguage', lang);
115
+ // Notify parent app if callback is provided
116
+ if (onLanguageChange) {
117
+ onLanguageChange();
118
+ }
119
+ };
120
+
121
+ const alignmentMap: { [key: string]: string } = {
122
+ left: 'justify-content-start',
123
+ center: 'justify-content-center',
124
+ right: 'justify-content-end',
125
+ };
126
+
127
+ const alignmentClass = menuAlignment
128
+ ? alignmentMap[menuAlignment.toLowerCase()] || 'justify-content-start'
129
+ : 'justify-content-start';
130
+
131
+ const performSearch = async (query: string) => {
132
+
133
+ if (!query.trim() || !meiliSearchConfig || !searchService.current) {
134
+ setSearchResults([]);
135
+ setTotalHits(0);
136
+ setShowNoResults(false);
137
+ setShowSearchResults(false);
138
+ return;
139
+ }
140
+
141
+ setIsSearching(true);
142
+ setSearchError(null);
143
+ setShowNoResults(false);
144
+ setVisibleResultsCount(5);
145
+
146
+ try {
147
+ const result = await searchService.current.simpleSearch(query, {
148
+ limit: 20,
149
+ offset: 0
150
+ });
151
+
152
+
153
+ setSearchResults(result.hits);
154
+ setTotalHits(result.totalHits);
155
+ setShowNoResults(result.totalHits === 0);
156
+ setShowSearchResults(true);
157
+
158
+ } catch (error) {
159
+ setSearchError(error instanceof Error ? error.message : 'Search failed');
160
+ setShowNoResults(true);
161
+ setSearchResults([]);
162
+ setTotalHits(0);
163
+ } finally {
164
+ setIsSearching(false);
165
+ }
166
+ };
167
+
168
+ const handleSearch = (value: string) => {
169
+ setSearchKeywords(value);
170
+ performSearch(value);
171
+
172
+ // Close search modal on mobile after search
173
+ if (isMobile && showSearchModal) {
174
+ setShowSearchModal(false);
175
+ }
176
+ };
177
+
178
+ const handleSearchChange = (value: string) => {
179
+
180
+ // Update the search keywords immediately
181
+ setSearchKeywords(value);
182
+
183
+ // Clear no results message when user starts typing
184
+ if (showNoResults && value.trim() === '') {
185
+ setShowNoResults(false);
186
+ setShowSearchResults(false);
187
+ }
188
+
189
+ // Clear previous timeout
190
+ if (searchTimeoutRef.current) {
191
+ clearTimeout(searchTimeoutRef.current);
192
+ }
193
+
194
+ // Perform search as user types (with debounce) - only if there's content
195
+ if (value.trim().length > 0) {
196
+ searchTimeoutRef.current = setTimeout(() => {
197
+ performSearch(value);
198
+ }, 500); // Increased to 500ms for better UX
199
+ } else {
200
+ // Clear results if input is empty
201
+ setSearchResults([]);
202
+ setTotalHits(0);
203
+ setShowNoResults(false);
204
+ setShowSearchResults(false);
205
+ }
206
+ };
207
+
208
+ const handleSearchClear = () => {
209
+
210
+ // Clear timeout if exists
211
+ if (searchTimeoutRef.current) {
212
+ clearTimeout(searchTimeoutRef.current);
213
+ searchTimeoutRef.current = null;
214
+ }
215
+
216
+ setSearchKeywords('');
217
+ setSearchResults([]);
218
+ setTotalHits(0);
219
+ setShowNoResults(false);
220
+ setShowSearchResults(false);
221
+ setSearchError(null);
222
+ setIsSearching(false);
223
+ };
224
+
225
+ const resetSearchState = () => {
226
+ setSearchKeywords('');
227
+ setSearchResults([]);
228
+ setTotalHits(0);
229
+ setShowNoResults(false);
230
+ setShowSearchResults(false);
231
+ setSearchError(null);
232
+ setIsSearching(false);
233
+ setVisibleResultsCount(5);
234
+
235
+ // Clear timeout if exists
236
+ if (searchTimeoutRef.current) {
237
+ clearTimeout(searchTimeoutRef.current);
238
+ searchTimeoutRef.current = null;
239
+ }
240
+ };
241
+
242
+ const showMoreResults = () => {
243
+ setVisibleResultsCount(prev => Math.min(prev + 10, searchResults.length));
244
+ };
245
+
246
+ // Helper function to get language name from language code
247
+ const getLanguageName = (langCode: string) => {
248
+ if (languageSelectorList && languageSelectorList.length > 0) {
249
+ const langObj = languageSelectorList.find(item => item[langCode]);
250
+ if (langObj) {
251
+ return langObj[langCode];
252
+ }
253
+ }
254
+
255
+ // Fallback to default language names
256
+ const defaultLanguages: Record<string, string> = {
257
+ 'en': 'English',
258
+ 'ar': 'Arabic',
259
+ 'hi': 'Hindi'
260
+ };
261
+
262
+ return defaultLanguages[langCode] || langCode.toUpperCase();
263
+ };
264
+
265
+ // Helper function to render language options
266
+ const renderLanguageOptions = () => {
267
+ console.log('renderLanguageOptions - languageSelectorList:', languageSelectorList);
268
+ console.log('renderLanguageOptions - length:', languageSelectorList?.length);
269
+
270
+ // Only use custom languageSelectorList if it's provided and has items
271
+ if (languageSelectorList && languageSelectorList.length > 0) {
272
+ console.log('Using custom languageSelectorList');
273
+ return languageSelectorList.map((langObj, index) => {
274
+ const [langCode, langName] = Object.entries(langObj)[0];
275
+ return (
276
+ <Dropdown.Item key={`lang-${langCode}-${index}`} onClick={() => handleLanguageChange(langCode)}>
277
+ {langName}
278
+ </Dropdown.Item>
279
+ );
280
+ });
281
+ }
282
+
283
+ // Fallback to default languages ONLY if no languageSelectorList provided
284
+ console.log('Using fallback languages');
285
+ return (
286
+ <>
287
+ <Dropdown.Item onClick={() => handleLanguageChange('en')}>English</Dropdown.Item>
288
+ <Dropdown.Item onClick={() => handleLanguageChange('ar')}>Arabic</Dropdown.Item>
289
+ <Dropdown.Item onClick={() => handleLanguageChange('hi')}>Hindi</Dropdown.Item>
290
+ </>
291
+ );
292
+ };
293
+
294
+ return (
295
+ <Navbar expand="lg" className="navbarContainer py-1">
296
+ <div className="d-flex align-items-center">
297
+ <div className="toggle-sidebar-icon">
298
+ <IconButton
299
+ src={MenuIcon}
300
+ iconAs={Icon}
301
+ alt="Toggle Sidebar"
302
+ onClick={toggleSidebar}
303
+ className="ms-2 ml-2"
304
+ onMouseDown={(e) => e.stopPropagation()}
305
+ />
306
+ </div>
307
+ <Navbar.Brand href="/" className="navbar-brand">
308
+ <img
309
+ src={logoUrl}
310
+ alt="Logo"
311
+ />
312
+ </Navbar.Brand>
313
+ </div>
314
+
315
+ {authenticatedUser !== null && (
316
+ <>
317
+ {isMobile ? (
318
+ <div className="d-flex justify-content-between align-items-center ml-auto">
319
+ <div className="d-flex align-items-center navbar-buttons">
320
+ {meiliSearchConfig && (
321
+ <IconButton
322
+ src={Search}
323
+ iconAs={Icon}
324
+ alt="Search"
325
+ onClick={() => setShowSearchModal(true)}
326
+ />
327
+ )}
328
+ <div className="ml-2 usermenu-dropdown-mobile">
329
+ <UserMenu
330
+ username={authenticatedUser?.username}
331
+ authenticatedUserAvatar={authenticatedUser?.avatar}
332
+ isMobile={isMobile}
333
+ isAdmin={authenticatedUser?.administrator}
334
+ menuItems={userMenuItems}
335
+ />
336
+ </div>
337
+ <IconButton
338
+ src={MoreVert}
339
+ iconAs={Icon}
340
+ alt="More"
341
+ onMouseDown={e => e.stopPropagation()}
342
+ onClick={() => setShowMoreMenu(prev => !prev)}
343
+ />
344
+ {showMoreMenu && (
345
+ <div className="more-menu-dropdown" ref={moreMenuRef}>
346
+ <div className="more-menu-icons-row">
347
+ {headerButtons?.reSync && (
348
+ <div className="icon-curved-square">
349
+ <IconButtonWithTooltip src={Sync} iconAs={Icon} alt="Sync" tooltipPlacement="bottom" tooltipContent="Re-Sync" />
350
+ </div>
351
+ )}
352
+ {headerButtons?.contextSwitcher && (
353
+ <div className="icon-curved-square">
354
+ <IconButtonWithTooltip src={ExitToApp} iconAs={Icon} alt="Sync" tooltipPlacement="bottom" tooltipContent="Switch to User Mode" />
355
+ </div>
356
+ )}
357
+ {headerButtons?.help && (
358
+ <div className="icon-curved-square">
359
+ <IconButtonWithTooltip src={HelpCenter} iconAs={Icon} alt="Help" tooltipPlacement="bottom" tooltipContent="Help" />
360
+ </div>
361
+ )}
362
+ {headerButtons?.notification && (
363
+ <div className="icon-curved-square">
364
+ <IconButtonWithTooltip src={Notifications} iconAs={Icon} alt="Notifications" tooltipPlacement="bottom" tooltipContent="Notifications" />
365
+ </div>
366
+ )}
367
+ </div>
368
+ {
369
+ headerButtons?.translation && (
370
+ <Dropdown className="language-dropdown">
371
+ <Dropdown.Toggle variant="tertiary" id="language-dropdown">
372
+ {getLanguageName(selectedLanguage)}
373
+ </Dropdown.Toggle>
374
+ <Dropdown.Menu>
375
+ {renderLanguageOptions()}
376
+ </Dropdown.Menu>
377
+ </Dropdown>
378
+ )
379
+ }
380
+
381
+ </div>
382
+ )}
383
+ </div>
384
+ </div>
385
+ ) : (
386
+ <>
387
+ {meiliSearchConfig && (
388
+ <div className="d-flex align-items-center justify-content-start flex-grow-1 ml-4">
389
+ <div style={{ width: '30rem', position: 'relative' }}>
390
+ {/* Temporary native input replacement for debugging */}
391
+ <div style={{
392
+ display: 'flex',
393
+ alignItems: 'center',
394
+ border: '2px solid #ccc',
395
+ borderRadius: '25px',
396
+ padding: '8px 16px',
397
+ background: '#fff',
398
+ position: 'relative'
399
+ }}>
400
+ <input
401
+ type="text"
402
+ value={searchKeywords}
403
+ onChange={(e) => {
404
+ handleSearchChange(e.target.value);
405
+ }}
406
+ onKeyDown={(e) => {
407
+ if (e.key === 'Enter') {
408
+ handleSearch(searchKeywords);
409
+ }
410
+ }}
411
+ placeholder="Search"
412
+ style={{
413
+ flex: 1,
414
+ border: 'none',
415
+ outline: 'none',
416
+ fontSize: '16px',
417
+ background: 'transparent',
418
+ padding: '4px'
419
+ }}
420
+ autoComplete="off"
421
+ />
422
+ {searchKeywords && (
423
+ <button
424
+ type="button"
425
+ onClick={() => {
426
+ handleSearchClear();
427
+ }}
428
+ style={{
429
+ background: 'none',
430
+ border: 'none',
431
+ cursor: 'pointer',
432
+ padding: '4px',
433
+ marginRight: '8px'
434
+ }}
435
+ >
436
+
437
+ </button>
438
+ )}
439
+ </div>
440
+ {isSearching && (
441
+ <div className="search-loading-spinner-container">
442
+ <div className="search-loading-spinner"></div>
443
+ <span>Searching...</span>
444
+ </div>
445
+ )}
446
+ {showNoResults && !isSearching && (
447
+ <div className="no-results-message-desktop">
448
+ <p>We didn't find anything matching your search</p>
449
+ </div>
450
+ )}
451
+ {searchError && (
452
+ <div className="search-error-message-desktop">
453
+ <p>Search error: {searchError}</p>
454
+ </div>
455
+ )}
456
+ {showSearchResults && searchResults.length > 0 && (
457
+ <div className="search-results-dropdown">
458
+ <div className="search-results-header">
459
+ <span>{totalHits} result{totalHits !== 1 ? 's' : ''} found</span>
460
+ </div>
461
+ <div className="search-results-list">
462
+ {searchResults.slice(0, visibleResultsCount).map((result, index) => (
463
+ <SearchResultItem
464
+ key={result.id || index}
465
+ result={result}
466
+ onNavigate={(url) => {
467
+ resetSearchState(); // Reset search state after navigation
468
+ window.location.href = url;
469
+ }}
470
+ baseUrl={getBaseUrl ? getBaseUrl() : undefined}
471
+ />
472
+ ))}
473
+ {visibleResultsCount < searchResults.length && (
474
+ <div className="search-results-more">
475
+ <button
476
+ onClick={showMoreResults}
477
+ style={{
478
+ background: 'none',
479
+ border: 'none',
480
+ color: '#007bff',
481
+ cursor: 'pointer',
482
+ padding: '8px 16px',
483
+ width: '100%',
484
+ textAlign: 'center',
485
+ fontSize: '14px'
486
+ }}
487
+ >
488
+ Show {Math.min(10, searchResults.length - visibleResultsCount)} more results
489
+ </button>
490
+ </div>
491
+ )}
492
+ </div>
493
+ </div>
494
+ )}
495
+ </div>
496
+ </div>
497
+ )}
498
+ <div className="d-flex align-items-center gap-2 ms-auto order-lg-3">
499
+ {headerButtons?.reSync && (
500
+ <div className="icon-curved-square">
501
+ <IconButtonWithTooltip src={Sync} iconAs={Icon} alt="Sync" tooltipPlacement="bottom" tooltipContent="Re-Sync" />
502
+ </div>
503
+ )}
504
+ {headerButtons?.contextSwitcher && (
505
+ <div className="icon-curved-square">
506
+ <IconButtonWithTooltip src={ExitToApp} iconAs={Icon} alt="Sync" tooltipPlacement="bottom" tooltipContent="Switch to User Mode" />
507
+ </div>
508
+ )}
509
+ {headerButtons?.help && (
510
+ <div className="icon-curved-square">
511
+ <IconButtonWithTooltip src={HelpCenter} iconAs={Icon} alt="Help" tooltipPlacement="bottom" tooltipContent="Help" />
512
+ </div>
513
+ )}
514
+ {headerButtons?.notification && (
515
+ <div className="icon-curved-square">
516
+ <IconButtonWithTooltip src={Notifications} iconAs={Icon} alt="Notifications" tooltipPlacement="bottom" tooltipContent="Notifications" />
517
+ </div>
518
+ )}
519
+ {
520
+ headerButtons?.translation && (
521
+ <Dropdown className="language-dropdown">
522
+ <Dropdown.Toggle variant="tertiary" id="language-dropdown">
523
+ {getLanguageName(selectedLanguage)}
524
+ </Dropdown.Toggle>
525
+ <Dropdown.Menu>
526
+ {renderLanguageOptions()}
527
+ </Dropdown.Menu>
528
+ </Dropdown>
529
+ )
530
+ }
531
+ <div className="usermenu-dropdown">
532
+ <UserMenu
533
+ username={authenticatedUser?.username}
534
+ authenticatedUserAvatar={authenticatedUser?.avatar}
535
+ isMobile={isMobile}
536
+ isAdmin={authenticatedUser?.administrator}
537
+ menuItems={userMenuItems}
538
+ />
539
+ </div>
540
+ </div>
541
+ </>
542
+ )}
543
+ </>
544
+ )}
545
+
546
+ {authenticatedUser === null && (
547
+ <>
548
+ <Navbar.Toggle aria-controls="main-navbar-nav" />
549
+ <div className="d-flex align-items-center gap-2 ms-auto order-lg-3">
550
+ {loginSignupButtons && (
551
+ <>
552
+ <Button variant="primary">Log In</Button>
553
+ <Button variant="outline-primary">Sign Up</Button>
554
+ </>
555
+ )}
556
+ </div>
557
+ </>
558
+ )}
559
+
560
+ <Navbar.Collapse id="main-navbar-nav" className="justify-content-between">
561
+ <Nav className={`nav-gap gap-3 flex-grow-1 ${alignmentClass}`}>
562
+ {authenticatedUser === null
563
+ && menuList.map((menu) => (menu.subMenu ? (
564
+ <NavDropdown title={menu.label} key={`menu-${menu.label}`} id={`nav-dropdown-${menu.label}`}>
565
+ {menu.subMenu.map((sub) => (
566
+ <NavDropdown.Item key={`sub-${sub.label}`} onClick={() => console.log('Selected submenu:', sub.label)}>
567
+ {sub.label}
568
+ </NavDropdown.Item>
569
+ ))}
570
+ </NavDropdown>
571
+ ) : (
572
+ <Nav.Link key={`menu-${menu.label}`} as="span" className="text-uppercase fw-semibold">
573
+ {menu.label}
574
+ </Nav.Link>
575
+ )))}
576
+ </Nav>
577
+ </Navbar.Collapse>
578
+
579
+ {/* Fixed search bar overlay */}
580
+ {meiliSearchConfig && showSearchModal && (
581
+ <div className="fixed-search-bar">
582
+ <div className="fixed-search-bar-inner">
583
+ {/* Temporary native input replacement for mobile debugging */}
584
+ <div style={{
585
+ display: 'flex',
586
+ alignItems: 'center',
587
+ border: '2px solid #ccc',
588
+ borderRadius: '25px',
589
+ padding: '12px 16px',
590
+ background: '#fff',
591
+ position: 'relative',
592
+ width: '100%'
593
+ }}>
594
+ <input
595
+ type="text"
596
+ value={searchKeywords}
597
+ onChange={(e) => {
598
+ handleSearchChange(e.target.value);
599
+ }}
600
+ onKeyDown={(e) => {
601
+ if (e.key === 'Enter') {
602
+ handleSearch(searchKeywords);
603
+ }
604
+ }}
605
+ placeholder="Search"
606
+ style={{
607
+ flex: 1,
608
+ border: 'none',
609
+ outline: 'none',
610
+ fontSize: '18px',
611
+ background: 'transparent',
612
+ padding: '4px'
613
+ }}
614
+ autoComplete="off"
615
+ autoFocus
616
+ />
617
+ {searchKeywords && (
618
+ <button
619
+ type="button"
620
+ onClick={() => {
621
+ handleSearchClear();
622
+ }}
623
+ style={{
624
+ background: 'none',
625
+ border: 'none',
626
+ cursor: 'pointer',
627
+ padding: '4px',
628
+ marginRight: '8px',
629
+ fontSize: '18px'
630
+ }}
631
+ >
632
+
633
+ </button>
634
+ )}
635
+ </div>
636
+ <button className="close-search-bar" type="button" onClick={() => setShowSearchModal(false)} aria-label="Close Search">
637
+ &times;
638
+ </button>
639
+ </div>
640
+ {isSearching && (
641
+ <div className="search-loading-spinner-container">
642
+ <div className="search-loading-spinner"></div>
643
+ <span>Searching...</span>
644
+ </div>
645
+ )}
646
+ {showNoResults && !isSearching && (
647
+ <div className="no-results-message">
648
+ <p>We didn't find anything matching your search</p>
649
+ </div>
650
+ )}
651
+ {searchError && (
652
+ <div className="search-error-message">
653
+ <p>Search error: {searchError}</p>
654
+ </div>
655
+ )}
656
+ {showSearchResults && searchResults.length > 0 && (
657
+ <div className="search-results-mobile">
658
+ <div className="search-results-header">
659
+ <span>{totalHits} result{totalHits !== 1 ? 's' : ''} found</span>
660
+ </div>
661
+ <div className="search-results-list">
662
+ {searchResults.slice(0, visibleResultsCount).map((result, index) => (
663
+ <SearchResultItem
664
+ key={result.id || index}
665
+ result={result}
666
+ onNavigate={(url) => {
667
+ resetSearchState(); // Reset search state after navigation
668
+ window.location.href = url;
669
+ }}
670
+ baseUrl={getBaseUrl ? getBaseUrl() : undefined}
671
+ />
672
+ ))}
673
+ {visibleResultsCount < searchResults.length && (
674
+ <div className="search-results-more">
675
+ <button
676
+ onClick={showMoreResults}
677
+ style={{
678
+ background: 'none',
679
+ border: 'none',
680
+ color: '#007bff',
681
+ cursor: 'pointer',
682
+ padding: '12px 16px',
683
+ width: '100%',
684
+ textAlign: 'center',
685
+ fontSize: '16px'
686
+ }}
687
+ >
688
+ Show {Math.min(10, searchResults.length - visibleResultsCount)} more results
689
+ </button>
690
+ </div>
691
+ )}
692
+ </div>
693
+ </div>
694
+ )}
695
+ </div>
696
+ )}
697
+ </Navbar>
698
+ );
699
+ };
700
+
701
+ export default MainHeader;