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.
- package/CHANGELOG.md +43 -0
- package/MEILISEARCH_INTEGRATION.md +261 -0
- package/README.md +236 -0
- package/SEARCH_INTEGRATION_README.md +208 -0
- package/dist/App.d.ts +3 -0
- package/dist/components/ButtonComponent.scss +3 -0
- package/dist/components/Footer.scss +87 -0
- package/dist/components/MainHeader.scss +915 -0
- package/dist/components/Sidebar.scss +170 -0
- package/dist/components/UserMenu.scss +0 -0
- package/dist/components/common/Button/ButtonComponent.d.ts +4 -0
- package/dist/components/common/Footer/Footer.d.ts +4 -0
- package/dist/components/common/Header/MainHeader.d.ts +4 -0
- package/dist/components/common/Header/SimpleSearchContext.d.ts +36 -0
- package/dist/components/common/NavDropdownMenu/NavDropdownMenu.d.ts +11 -0
- package/dist/components/common/SearchHighlight.d.ts +11 -0
- package/dist/components/common/SearchResultItem.d.ts +12 -0
- package/dist/components/common/Sidebar/Sidebar.d.ts +12 -0
- package/dist/components/common/UserMenu/UserMenu.d.ts +15 -0
- package/dist/components/common/menu/MenuComponent.d.ts +4 -0
- package/dist/contexts/SidebarContext.d.ts +6 -0
- package/dist/examples/MeiliSearchUsageExample.d.ts +3 -0
- package/dist/hooks/useIsMobileSize.d.ts +2 -0
- package/dist/hooks/useSidebar.d.ts +1 -0
- package/dist/index.cjs +27 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1867 -0
- package/dist/interfaces/components.d.ts +181 -0
- package/dist/main.d.ts +0 -0
- package/dist/providers/SidebarProvider.d.ts +4 -0
- package/dist/services/meiliSearchService.d.ts +47 -0
- package/dist/utils/searchNavigation.d.ts +43 -0
- package/dist/vite.svg +1 -0
- package/eslint.config.js +28 -0
- package/index.html +13 -0
- package/next.config.ts +35 -0
- package/package.json +53 -0
- package/public/vite.svg +1 -0
- package/src/@edx-frontend-platform-i18n.d.ts +1 -0
- package/src/App.css +66 -0
- package/src/App.tsx +54 -0
- package/src/assets/react.svg +1 -0
- package/src/components/common/Button/ButtonComponent.scss +3 -0
- package/src/components/common/Button/ButtonComponent.tsx +34 -0
- package/src/components/common/Footer/Footer.scss +87 -0
- package/src/components/common/Footer/Footer.tsx +53 -0
- package/src/components/common/Header/MainHeader.scss +915 -0
- package/src/components/common/Header/MainHeader.tsx +701 -0
- package/src/components/common/Header/SimpleSearchContext.tsx +194 -0
- package/src/components/common/NavDropdownMenu/NavDropdownMenu.tsx +58 -0
- package/src/components/common/SearchHighlight.tsx +64 -0
- package/src/components/common/SearchResultItem.tsx +162 -0
- package/src/components/common/Sidebar/Sidebar.scss +170 -0
- package/src/components/common/Sidebar/Sidebar.tsx +137 -0
- package/src/components/common/UserMenu/UserMenu.scss +0 -0
- package/src/components/common/UserMenu/UserMenu.tsx +73 -0
- package/src/components/common/menu/MenuComponent.module.css +12 -0
- package/src/components/common/menu/MenuComponent.tsx +37 -0
- package/src/contexts/SidebarContext.tsx +9 -0
- package/src/declarations.d.ts +10 -0
- package/src/examples/MeiliSearchUsageExample.tsx +102 -0
- package/src/hooks/useIsMobileSize.ts +19 -0
- package/src/hooks/useSidebar.ts +10 -0
- package/src/index.css +68 -0
- package/src/index.ts +21 -0
- package/src/interfaces/components.ts +172 -0
- package/src/main.tsx +11 -0
- package/src/providers/SidebarProvider.tsx +20 -0
- package/src/services/meiliSearchService.ts +233 -0
- package/src/styles/global-overrides.scss +1131 -0
- package/src/utils/searchNavigation.ts +140 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +17 -0
- package/tsconfig.node.json +24 -0
- 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
|
+
×
|
|
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;
|