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,20 @@
|
|
|
1
|
+
import React, { useState, ReactNode } from 'react';
|
|
2
|
+
import { SidebarContext } from '../contexts/SidebarContext';
|
|
3
|
+
|
|
4
|
+
export const SidebarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
5
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
6
|
+
|
|
7
|
+
const toggleSidebar = () => {
|
|
8
|
+
setIsCollapsed(prev => !prev);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const setSidebarCollapsed = (collapsed: boolean) => {
|
|
12
|
+
setIsCollapsed(collapsed);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<SidebarContext.Provider value={{ isCollapsed, toggleSidebar, setSidebarCollapsed }}>
|
|
17
|
+
{children}
|
|
18
|
+
</SidebarContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { MeiliSearchConfig, MeiliSearchResponse, MeiliSearchResult } from '../interfaces/components';
|
|
2
|
+
|
|
3
|
+
// Interface for MeiliSearch client headers
|
|
4
|
+
export interface MeiliSearchClientHeaders {
|
|
5
|
+
'Content-Type': string;
|
|
6
|
+
'Authorization': string;
|
|
7
|
+
'X-Meilisearch-Client': string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SearchOptions {
|
|
11
|
+
limit?: number;
|
|
12
|
+
offset?: number;
|
|
13
|
+
filter?: string[];
|
|
14
|
+
attributesToHighlight?: string[];
|
|
15
|
+
attributesToCrop?: string[];
|
|
16
|
+
sort?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SearchResult {
|
|
20
|
+
hits: MeiliSearchResult[];
|
|
21
|
+
totalHits: number;
|
|
22
|
+
facets?: {
|
|
23
|
+
block_type?: Record<string, number>;
|
|
24
|
+
'content.problem_types'?: Record<string, number>;
|
|
25
|
+
};
|
|
26
|
+
processingTimeMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class MeiliSearchService {
|
|
30
|
+
private config: MeiliSearchConfig;
|
|
31
|
+
private headers: MeiliSearchClientHeaders;
|
|
32
|
+
|
|
33
|
+
constructor(config: MeiliSearchConfig) {
|
|
34
|
+
// Validate required configuration
|
|
35
|
+
if (!config.host) {
|
|
36
|
+
throw new Error('MeiliSearch host is required');
|
|
37
|
+
}
|
|
38
|
+
if (!config.apiKey) {
|
|
39
|
+
throw new Error('MeiliSearch apiKey is required');
|
|
40
|
+
}
|
|
41
|
+
if (!config.indexName) {
|
|
42
|
+
throw new Error('MeiliSearch indexName is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.headers = {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
49
|
+
'X-Meilisearch-Client': 'Meilisearch JavaScript (v0.41.0)'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update headers from MeiliSearch client object
|
|
56
|
+
* This allows you to use the exact headers from your console logs
|
|
57
|
+
*/
|
|
58
|
+
updateHeadersFromClient(client: any) {
|
|
59
|
+
if (client && client.httpRequest && client.httpRequest.headers) {
|
|
60
|
+
this.headers = {
|
|
61
|
+
'Content-Type': client.httpRequest.headers['Content-Type'] || 'application/json',
|
|
62
|
+
'Authorization': client.httpRequest.headers['Authorization'] || `Bearer ${this.config.apiKey}`,
|
|
63
|
+
'X-Meilisearch-Client': client.httpRequest.headers['X-Meilisearch-Client'] || 'Meilisearch JavaScript (v0.41.0)'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Performs a search using MeiliSearch multi-search API
|
|
70
|
+
* This matches the exact payload structure from your console logs
|
|
71
|
+
*/
|
|
72
|
+
async search(
|
|
73
|
+
query: string,
|
|
74
|
+
options: SearchOptions = {}
|
|
75
|
+
): Promise<SearchResult> {
|
|
76
|
+
const {
|
|
77
|
+
limit = 20,
|
|
78
|
+
offset = 0,
|
|
79
|
+
filter = ['type = "course_block"'],
|
|
80
|
+
attributesToHighlight = ['display_name', 'description', 'published'],
|
|
81
|
+
attributesToCrop = ['description', 'published'],
|
|
82
|
+
sort = []
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
// Validate configuration
|
|
86
|
+
if (!this.config.indexName) {
|
|
87
|
+
throw new Error('MeiliSearch indexName is required');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Construct the exact payload structure from your console logs
|
|
91
|
+
const payload = {
|
|
92
|
+
queries: [
|
|
93
|
+
{
|
|
94
|
+
indexUid: this.config.indexName,
|
|
95
|
+
q: query,
|
|
96
|
+
filter: [[], ...filter],
|
|
97
|
+
attributesToHighlight,
|
|
98
|
+
highlightPreTag: '__meili-highlight__',
|
|
99
|
+
highlightPostTag: '__/meili-highlight__',
|
|
100
|
+
attributesToCrop,
|
|
101
|
+
sort,
|
|
102
|
+
offset,
|
|
103
|
+
limit
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
indexUid: this.config.indexName,
|
|
107
|
+
facets: ['block_type', 'content.problem_types'],
|
|
108
|
+
filter: filter,
|
|
109
|
+
limit: 0
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// Use the configured headers
|
|
116
|
+
const headers = { ...this.headers };
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`${this.config.host}/multi-search`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers,
|
|
122
|
+
body: JSON.stringify(payload)
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new Error(`MeiliSearch API error: ${response.status} ${response.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data: MeiliSearchResponse = await response.json();
|
|
130
|
+
|
|
131
|
+
// Extract search results from the first query
|
|
132
|
+
const searchResult = data.results[0];
|
|
133
|
+
const facetResult = data.results[1];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
hits: searchResult.hits || [],
|
|
137
|
+
totalHits: searchResult.estimatedTotalHits || 0,
|
|
138
|
+
facets: facetResult.facetDistribution,
|
|
139
|
+
processingTimeMs: searchResult.processingTimeMs
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('MeiliSearch service error:', error);
|
|
144
|
+
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Performs a simple search without facets (for basic search functionality)
|
|
150
|
+
*/
|
|
151
|
+
async simpleSearch(
|
|
152
|
+
query: string,
|
|
153
|
+
options: SearchOptions = {}
|
|
154
|
+
): Promise<SearchResult> {
|
|
155
|
+
const {
|
|
156
|
+
limit = 20,
|
|
157
|
+
offset = 0,
|
|
158
|
+
filter = ['type = "course_block"'],
|
|
159
|
+
attributesToHighlight = ['display_name', 'description', 'published'],
|
|
160
|
+
attributesToCrop = ['description', 'published'],
|
|
161
|
+
sort = []
|
|
162
|
+
} = options;
|
|
163
|
+
|
|
164
|
+
const payload = {
|
|
165
|
+
queries: [
|
|
166
|
+
{
|
|
167
|
+
indexUid: this.config.indexName,
|
|
168
|
+
q: query,
|
|
169
|
+
filter: [[], ...filter],
|
|
170
|
+
attributesToHighlight,
|
|
171
|
+
highlightPreTag: '__meili-highlight__',
|
|
172
|
+
highlightPostTag: '__/meili-highlight__',
|
|
173
|
+
attributesToCrop,
|
|
174
|
+
sort,
|
|
175
|
+
offset,
|
|
176
|
+
limit
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Use the configured headers
|
|
182
|
+
const headers = { ...this.headers };
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(`${this.config.host}/multi-search`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers,
|
|
188
|
+
body: JSON.stringify(payload)
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error(`MeiliSearch API error: ${response.status} ${response.statusText}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const data: MeiliSearchResponse = await response.json();
|
|
196
|
+
const searchResult = data.results[0];
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
hits: searchResult.hits || [],
|
|
200
|
+
totalHits: searchResult.estimatedTotalHits || 0,
|
|
201
|
+
processingTimeMs: searchResult.processingTimeMs
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('MeiliSearch service error:', error);
|
|
206
|
+
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if the MeiliSearch service is available
|
|
212
|
+
*/
|
|
213
|
+
async checkConnection(): Promise<boolean> {
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch(`${this.config.host}/health`, {
|
|
216
|
+
method: 'GET',
|
|
217
|
+
headers: {
|
|
218
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
219
|
+
'X-Meilisearch-Client': 'Meilisearch JavaScript (v0.41.0)'
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return response.ok;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('MeiliSearch connection check failed:', error);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Factory function to create a MeiliSearch service instance
|
|
231
|
+
export const createMeiliSearchService = (config: MeiliSearchConfig): MeiliSearchService => {
|
|
232
|
+
return new MeiliSearchService(config);
|
|
233
|
+
};
|