@rpg-engine/long-bow 0.7.97 → 0.7.98

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 (53) hide show
  1. package/dist/components/DPad/JoystickDPad.d.ts +21 -0
  2. package/dist/components/InformationCenter/InformationCenter.d.ts +29 -0
  3. package/dist/components/InformationCenter/InformationCenterCell.d.ts +14 -0
  4. package/dist/components/InformationCenter/InformationCenterTabView.d.ts +19 -0
  5. package/dist/components/InformationCenter/InformationCenterTypes.d.ts +79 -0
  6. package/dist/components/InformationCenter/sections/bestiary/BestiarySection.d.ts +12 -0
  7. package/dist/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.d.ts +12 -0
  8. package/dist/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.d.ts +9 -0
  9. package/dist/components/InformationCenter/sections/faq/FaqSection.d.ts +8 -0
  10. package/dist/components/InformationCenter/sections/items/InformationCenterItemDetails.d.ts +11 -0
  11. package/dist/components/InformationCenter/sections/items/InformationCenterItemTooltip.d.ts +7 -0
  12. package/dist/components/InformationCenter/sections/items/ItemsSection.d.ts +11 -0
  13. package/dist/components/InformationCenter/sections/tutorials/TutorialsSection.d.ts +8 -0
  14. package/dist/components/InformationCenter/shared/BaseInformationDetails.d.ts +10 -0
  15. package/dist/components/shared/BaseTooltip.d.ts +12 -0
  16. package/dist/components/shared/Collapsible/Collapsible.d.ts +9 -0
  17. package/dist/components/shared/Portal/Portal.d.ts +6 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/long-bow.cjs.development.js +243 -5
  20. package/dist/long-bow.cjs.development.js.map +1 -1
  21. package/dist/long-bow.cjs.production.min.js +1 -1
  22. package/dist/long-bow.cjs.production.min.js.map +1 -1
  23. package/dist/long-bow.esm.js +244 -7
  24. package/dist/long-bow.esm.js.map +1 -1
  25. package/dist/mocks/informationCenter.mocks.d.ts +6 -0
  26. package/dist/stories/Features/craftbook/CraftBook.stories.d.ts +2 -0
  27. package/dist/stories/UI/info/InformationCenter.stories.d.ts +7 -0
  28. package/dist/stories/UI/joystick/JoystickDPad.stories.d.ts +6 -0
  29. package/package.json +1 -1
  30. package/src/components/CraftBook/CraftBook.tsx +70 -31
  31. package/src/components/DPad/JoystickDPad.tsx +318 -0
  32. package/src/components/InformationCenter/InformationCenter.tsx +155 -0
  33. package/src/components/InformationCenter/InformationCenterCell.tsx +96 -0
  34. package/src/components/InformationCenter/InformationCenterTabView.tsx +121 -0
  35. package/src/components/InformationCenter/InformationCenterTypes.ts +87 -0
  36. package/src/components/InformationCenter/sections/bestiary/BestiarySection.tsx +170 -0
  37. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.tsx +366 -0
  38. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.tsx +204 -0
  39. package/src/components/InformationCenter/sections/faq/FaqSection.tsx +71 -0
  40. package/src/components/InformationCenter/sections/items/InformationCenterItemDetails.tsx +323 -0
  41. package/src/components/InformationCenter/sections/items/InformationCenterItemTooltip.tsx +88 -0
  42. package/src/components/InformationCenter/sections/items/ItemsSection.tsx +180 -0
  43. package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +144 -0
  44. package/src/components/InformationCenter/shared/BaseInformationDetails.tsx +162 -0
  45. package/src/components/InternalTabs/InternalTabs.tsx +1 -3
  46. package/src/components/shared/BaseTooltip.tsx +60 -0
  47. package/src/components/shared/Collapsible/Collapsible.tsx +70 -0
  48. package/src/components/shared/Portal/Portal.tsx +19 -0
  49. package/src/index.tsx +1 -0
  50. package/src/mocks/informationCenter.mocks.ts +562 -0
  51. package/src/stories/Features/craftbook/CraftBook.stories.tsx +15 -1
  52. package/src/stories/UI/info/InformationCenter.stories.tsx +58 -0
  53. package/src/stories/UI/joystick/JoystickDPad.stories.tsx +52 -0
@@ -0,0 +1,6 @@
1
+ import { IFaqItem, IVideoGuide } from '../components/InformationCenter/InformationCenter';
2
+ import { IInformationCenterItem, IInformationCenterNPC } from '../components/InformationCenter/InformationCenterTypes';
3
+ export declare const mockBestiaryItems: IInformationCenterNPC[];
4
+ export declare const mockItems: IInformationCenterItem[];
5
+ export declare const mockFaqItems: IFaqItem[];
6
+ export declare const mockTutorials: IVideoGuide[];
@@ -4,3 +4,5 @@ export default meta;
4
4
  export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
5
5
  export declare const WithSearch: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
6
6
  export declare const WithCategory: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
7
+ export declare const Empty: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
8
+ export declare const NoSearchResults: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
@@ -0,0 +1,7 @@
1
+ import type { Meta } from '@storybook/react';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
5
+ export declare const Loading: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
6
+ export declare const Error: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
7
+ export declare const Empty: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
@@ -0,0 +1,6 @@
1
+ declare const _default: import("@storybook/csf").ComponentAnnotations<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
2
+ export default _default;
3
+ export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
4
+ export declare const WithBackground: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
5
+ export declare const WithCustomOptions: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
6
+ export declare const Disabled: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.7.97",
3
+ "version": "0.7.98",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -7,6 +7,7 @@ import {
7
7
  } from '@rpg-engine/shared';
8
8
  import React, { useEffect, useState } from 'react';
9
9
  import {
10
+ FaBoxOpen,
10
11
  FaChevronLeft,
11
12
  FaChevronRight,
12
13
  FaSearch,
@@ -183,35 +184,42 @@ export const CraftBook: React.FC<IItemCraftSelectorProps> = ({
183
184
  )}
184
185
 
185
186
  <ContentContainer>
186
- <RadioInputScroller className="inputRadioCraftBook">
187
- {paginatedItems?.map(item => (
188
- <CraftingRecipeWrapper
189
- key={item.key}
190
- isSelected={pinnedItems.includes(item.key)}
191
- >
192
- <PinButton
193
- onClick={e => {
194
- e.stopPropagation();
195
- togglePinItem(item.key);
196
- }}
197
- isPinned={pinnedItems.includes(item.key)}
187
+ {paginatedItems.length > 0 ? (
188
+ <RadioInputScroller className="inputRadioCraftBook">
189
+ {paginatedItems?.map(item => (
190
+ <CraftingRecipeWrapper
191
+ key={item.key}
192
+ isSelected={pinnedItems.includes(item.key)}
198
193
  >
199
- <FaThumbtack size={14} />
200
- </PinButton>
201
- <CraftingRecipe
202
- atlasIMG={atlasIMG}
203
- atlasJSON={atlasJSON}
204
- equipmentSet={equipmentSet}
205
- recipe={item}
206
- scale={scale}
207
- handleRecipeSelect={setCraftItemKey.bind(null, item.key)}
208
- selectedCraftItemKey={craftItemKey}
209
- inventory={inventory}
210
- skills={skills}
211
- />
212
- </CraftingRecipeWrapper>
213
- ))}
214
- </RadioInputScroller>
194
+ <PinButton
195
+ onClick={e => {
196
+ e.stopPropagation();
197
+ togglePinItem(item.key);
198
+ }}
199
+ isPinned={pinnedItems.includes(item.key)}
200
+ >
201
+ <FaThumbtack size={14} />
202
+ </PinButton>
203
+ <CraftingRecipe
204
+ atlasIMG={atlasIMG}
205
+ atlasJSON={atlasJSON}
206
+ equipmentSet={equipmentSet}
207
+ recipe={item}
208
+ scale={scale}
209
+ handleRecipeSelect={setCraftItemKey.bind(null, item.key)}
210
+ selectedCraftItemKey={craftItemKey}
211
+ inventory={inventory}
212
+ skills={skills}
213
+ />
214
+ </CraftingRecipeWrapper>
215
+ ))}
216
+ </RadioInputScroller>
217
+ ) : (
218
+ <EmptyState>
219
+ <FaBoxOpen size={48} />
220
+ <p>No craftable items found</p>
221
+ </EmptyState>
222
+ )}
215
223
  </ContentContainer>
216
224
 
217
225
  {totalPages > 1 && (
@@ -353,17 +361,21 @@ const SearchContainer = styled.div`
353
361
 
354
362
  const ContentContainer = styled.div`
355
363
  flex: 1;
356
- min-height: 0;
364
+ display: flex;
365
+ flex-direction: column;
357
366
  padding: 16px;
358
367
  padding-right: 0;
359
368
  padding-bottom: 0;
360
- overflow: hidden;
361
369
  width: 100%;
370
+ position: relative;
371
+ min-height: 300px;
372
+ overflow: hidden;
362
373
  `;
363
374
 
364
375
  const RadioInputScroller = styled.div`
365
376
  height: 100%;
366
- overflow-y: scroll;
377
+ min-height: 300px;
378
+ overflow-y: auto;
367
379
  overflow-x: hidden;
368
380
  padding: 8px 16px;
369
381
  padding-right: 24px;
@@ -464,3 +476,30 @@ const PageInfo = styled.div`
464
476
  font-size: 0.8rem;
465
477
  font-family: 'Press Start 2P', cursive;
466
478
  `;
479
+
480
+ const EmptyState = styled.div`
481
+ position: absolute;
482
+ top: 50%;
483
+ left: 50%;
484
+ transform: translate(-50%, -50%);
485
+ display: flex;
486
+ flex-direction: column;
487
+ align-items: center;
488
+ justify-content: center;
489
+ text-align: center;
490
+ color: ${uiColors.lightGray};
491
+ width: 100%;
492
+ padding: 2rem;
493
+
494
+ svg {
495
+ font-size: 3rem;
496
+ margin-bottom: 1rem;
497
+ opacity: 0.7;
498
+ }
499
+
500
+ p {
501
+ font-family: 'Press Start 2P', cursive;
502
+ font-size: 0.9rem;
503
+ margin: 0;
504
+ }
505
+ `;
@@ -0,0 +1,318 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ interface IDPadContainerProps {
5
+ opacity?: number;
6
+ showBackground?: boolean;
7
+ size?: number;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ interface IDPadOptions {
12
+ /** Opacity of the entire component (0-1) */
13
+ opacity?: number;
14
+ /** Show the silver background behind the controller (default: false) */
15
+ showBackground?: boolean;
16
+ /** Size in pixels (default: 100) */
17
+ size?: number;
18
+ /** Interval in ms for continuous press events (default: 500) */
19
+ pressInterval?: number;
20
+ }
21
+
22
+ interface IDPadProps {
23
+ /** Callback fired when a direction is pressed */
24
+ onDirectionPress?: (direction: 'up' | 'down' | 'left' | 'right') => void;
25
+ /** Whether the component is disabled */
26
+ disabled?: boolean;
27
+ /** Additional options for customizing the D-pad */
28
+ options?: IDPadOptions;
29
+ }
30
+
31
+ export const JoystickDPad = ({
32
+ onDirectionPress,
33
+ disabled = false,
34
+ options = {},
35
+ }: IDPadProps): JSX.Element => {
36
+ const {
37
+ opacity = 1,
38
+ showBackground = false,
39
+ size = 100,
40
+ pressInterval = 500,
41
+ } = options;
42
+
43
+ const [pressedButtons, setPressedButtons] = useState<Set<string>>(new Set());
44
+ const intervalRef = useRef<number | null>(null);
45
+ const activeDirectionRef = useRef<'up' | 'down' | 'left' | 'right' | null>(
46
+ null
47
+ );
48
+
49
+ const clearPressInterval = useCallback(() => {
50
+ if (intervalRef.current) {
51
+ window.clearInterval(intervalRef.current);
52
+ intervalRef.current = null;
53
+ }
54
+ activeDirectionRef.current = null;
55
+ }, []);
56
+
57
+ const handleDirectionPress = useCallback(
58
+ (direction: 'up' | 'down' | 'left' | 'right') => {
59
+ if (disabled) return;
60
+
61
+ // Clear any existing interval
62
+ clearPressInterval();
63
+
64
+ // Set the active direction
65
+ activeDirectionRef.current = direction;
66
+ setPressedButtons(prev => new Set(prev).add(direction));
67
+
68
+ // Trigger first press immediately
69
+ onDirectionPress?.(direction);
70
+
71
+ // Set up the interval for continuous press
72
+ intervalRef.current = window.setInterval(() => {
73
+ if (activeDirectionRef.current === direction) {
74
+ onDirectionPress?.(direction);
75
+ }
76
+ }, pressInterval);
77
+ },
78
+ [disabled, onDirectionPress, pressInterval, clearPressInterval]
79
+ );
80
+
81
+ const handleDirectionRelease = useCallback(
82
+ (direction: 'up' | 'down' | 'left' | 'right') => {
83
+ setPressedButtons(prev => {
84
+ const next = new Set(prev);
85
+ next.delete(direction);
86
+ return next;
87
+ });
88
+
89
+ if (activeDirectionRef.current === direction) {
90
+ clearPressInterval();
91
+ }
92
+ },
93
+ [clearPressInterval]
94
+ );
95
+
96
+ // Cleanup on unmount
97
+ useEffect(() => {
98
+ return () => {
99
+ clearPressInterval();
100
+ };
101
+ }, [clearPressInterval]);
102
+
103
+ const preventDefault = (e: React.MouseEvent | React.TouchEvent) => {
104
+ e.preventDefault();
105
+ e.stopPropagation();
106
+ };
107
+
108
+ return (
109
+ <DPadContainer
110
+ opacity={opacity}
111
+ showBackground={showBackground}
112
+ size={size}
113
+ disabled={disabled}
114
+ onContextMenu={preventDefault}
115
+ >
116
+ <DPadButton
117
+ className="up"
118
+ onMouseDown={() => handleDirectionPress('up')}
119
+ onMouseUp={() => handleDirectionRelease('up')}
120
+ onMouseLeave={() => handleDirectionRelease('up')}
121
+ onTouchStart={() => handleDirectionPress('up')}
122
+ onTouchEnd={() => handleDirectionRelease('up')}
123
+ onContextMenu={preventDefault}
124
+ size={size}
125
+ isPressed={pressedButtons.has('up')}
126
+ disabled={disabled}
127
+ />
128
+ <DPadButton
129
+ className="right"
130
+ onMouseDown={() => handleDirectionPress('right')}
131
+ onMouseUp={() => handleDirectionRelease('right')}
132
+ onMouseLeave={() => handleDirectionRelease('right')}
133
+ onTouchStart={() => handleDirectionPress('right')}
134
+ onTouchEnd={() => handleDirectionRelease('right')}
135
+ onContextMenu={preventDefault}
136
+ size={size}
137
+ isPressed={pressedButtons.has('right')}
138
+ disabled={disabled}
139
+ />
140
+ <DPadButton
141
+ className="down"
142
+ onMouseDown={() => handleDirectionPress('down')}
143
+ onMouseUp={() => handleDirectionRelease('down')}
144
+ onMouseLeave={() => handleDirectionRelease('down')}
145
+ onTouchStart={() => handleDirectionPress('down')}
146
+ onTouchEnd={() => handleDirectionRelease('down')}
147
+ onContextMenu={preventDefault}
148
+ size={size}
149
+ isPressed={pressedButtons.has('down')}
150
+ disabled={disabled}
151
+ />
152
+ <DPadButton
153
+ className="left"
154
+ onMouseDown={() => handleDirectionPress('left')}
155
+ onMouseUp={() => handleDirectionRelease('left')}
156
+ onMouseLeave={() => handleDirectionRelease('left')}
157
+ onTouchStart={() => handleDirectionPress('left')}
158
+ onTouchEnd={() => handleDirectionRelease('left')}
159
+ onContextMenu={preventDefault}
160
+ size={size}
161
+ isPressed={pressedButtons.has('left')}
162
+ disabled={disabled}
163
+ />
164
+ <DPadCenter
165
+ size={size}
166
+ disabled={disabled}
167
+ onContextMenu={preventDefault}
168
+ />
169
+ </DPadContainer>
170
+ );
171
+ };
172
+
173
+ const DPadContainer = styled.div<IDPadContainerProps>`
174
+ width: ${props => props.size ?? 100}px;
175
+ height: ${props => props.size ?? 100}px;
176
+ position: relative;
177
+ background: ${props => (props.showBackground ? '#b8b8b8' : 'transparent')};
178
+ border-radius: 50%;
179
+ box-shadow: ${props =>
180
+ props.showBackground
181
+ ? 'inset 0 0 10px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2)'
182
+ : 'none'};
183
+ opacity: ${props => (props.disabled ? 0.5 : props.opacity ?? 1)};
184
+ user-select: none;
185
+ cursor: ${props => (props.disabled ? 'not-allowed' : 'default')};
186
+ transition: opacity 0.2s ease;
187
+ touch-action: none;
188
+ -webkit-tap-highlight-color: transparent;
189
+ `;
190
+
191
+ interface IDPadButtonProps {
192
+ size?: number;
193
+ isPressed?: boolean;
194
+ disabled?: boolean;
195
+ }
196
+
197
+ const DPadButton = styled.div<IDPadButtonProps>`
198
+ position: absolute;
199
+ background: ${props => (props.isPressed ? '#363636' : '#424242')};
200
+ box-shadow: ${props =>
201
+ props.isPressed
202
+ ? 'inset 0 0 12px rgba(0, 0, 0, 0.8), 0 0 2px rgba(0, 0, 0, 0.3)'
203
+ : 'inset 0 0 5px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.2)'};
204
+ cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
205
+ user-select: none;
206
+ transition: all 0.08s cubic-bezier(0.4, 0, 0.2, 1);
207
+ touch-action: none;
208
+ -webkit-tap-highlight-color: transparent;
209
+ transform-origin: center center;
210
+
211
+ &:hover:not(:active) {
212
+ @media (hover: hover) {
213
+ filter: ${props =>
214
+ !props.disabled && !props.isPressed ? 'brightness(1.1)' : 'none'};
215
+ }
216
+ }
217
+
218
+ &::after {
219
+ content: '';
220
+ position: absolute;
221
+ width: 0;
222
+ height: 0;
223
+ border: ${props => (props.size ?? 100) * 0.05}px solid transparent;
224
+ pointer-events: none;
225
+ opacity: ${props => (props.isPressed ? 0.7 : 1)};
226
+ transition: all 0.08s cubic-bezier(0.4, 0, 0.2, 1);
227
+ }
228
+
229
+ &.up,
230
+ &.down {
231
+ width: ${props => (props.size ?? 100) * 0.3}px;
232
+ height: ${props => (props.size ?? 100) * 0.4}px;
233
+ left: 50%;
234
+ transform: translateX(-50%) scale(${props => (props.isPressed ? 0.95 : 1)});
235
+ }
236
+
237
+ &.left,
238
+ &.right {
239
+ width: ${props => (props.size ?? 100) * 0.4}px;
240
+ height: ${props => (props.size ?? 100) * 0.3}px;
241
+ top: 50%;
242
+ transform: translateY(-50%) scale(${props => (props.isPressed ? 0.95 : 1)});
243
+ }
244
+
245
+ &.up {
246
+ top: 0;
247
+ border-radius: 5px 5px 0 0;
248
+ &::after {
249
+ border-bottom-color: #2a2a2a;
250
+ top: 45%;
251
+ left: 50%;
252
+ transform: translate(-50%, -50%);
253
+ }
254
+ }
255
+
256
+ &.down {
257
+ bottom: 0;
258
+ border-radius: 0 0 5px 5px;
259
+ &::after {
260
+ border-top-color: #2a2a2a;
261
+ bottom: 45%;
262
+ left: 50%;
263
+ transform: translate(-50%, 50%);
264
+ }
265
+ }
266
+
267
+ &.left {
268
+ left: 0;
269
+ border-radius: 5px 0 0 5px;
270
+ &::after {
271
+ border-right-color: #2a2a2a;
272
+ left: 45%;
273
+ top: 50%;
274
+ transform: translate(-50%, -50%);
275
+ }
276
+ }
277
+
278
+ &.right {
279
+ right: 0;
280
+ border-radius: 0 5px 5px 0;
281
+ &::after {
282
+ border-left-color: #2a2a2a;
283
+ right: 45%;
284
+ top: 50%;
285
+ transform: translate(50%, -50%);
286
+ }
287
+ }
288
+ `;
289
+
290
+ const DPadCenter = styled.div<IDPadButtonProps>`
291
+ position: absolute;
292
+ width: ${props => (props.size ?? 100) * 0.3}px;
293
+ height: ${props => (props.size ?? 100) * 0.3}px;
294
+ background: #424242;
295
+ top: 50%;
296
+ left: 50%;
297
+ transform: translate(-50%, -50%);
298
+ box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.6);
299
+ border-radius: 50%;
300
+ user-select: none;
301
+ cursor: ${props => (props.disabled ? 'not-allowed' : 'default')};
302
+ touch-action: none;
303
+ -webkit-tap-highlight-color: transparent;
304
+
305
+ &::after {
306
+ content: '';
307
+ position: absolute;
308
+ width: ${props => (props.size ?? 100) * 0.08}px;
309
+ height: ${props => (props.size ?? 100) * 0.08}px;
310
+ background: #2a2a2a;
311
+ border-radius: 50%;
312
+ top: 50%;
313
+ left: 50%;
314
+ transform: translate(-50%, -50%);
315
+ pointer-events: none;
316
+ box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8);
317
+ }
318
+ `;
@@ -0,0 +1,155 @@
1
+ import React, { useState } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { InternalTabs } from '../InternalTabs/InternalTabs';
5
+ import {
6
+ IInformationCenterItem,
7
+ IInformationCenterNPC,
8
+ } from './InformationCenterTypes';
9
+ import { BestiarySection } from './sections/bestiary/BestiarySection';
10
+ import { FaqSection } from './sections/faq/FaqSection';
11
+ import { InformationCenterItemDetails } from './sections/items/InformationCenterItemDetails';
12
+ import { ItemsSection } from './sections/items/ItemsSection';
13
+ import { TutorialsSection } from './sections/tutorials/TutorialsSection';
14
+
15
+ export interface IFaqItem {
16
+ id: string;
17
+ question: string;
18
+ answer: string;
19
+ }
20
+
21
+ export interface IVideoGuide {
22
+ id: string;
23
+ title: string;
24
+ description: string;
25
+ thumbnailUrl: string;
26
+ videoUrl: string;
27
+ category: 'Combat' | 'Crafting' | 'Exploration' | 'General';
28
+ }
29
+
30
+ export interface IInformationCenterProps {
31
+ itemsAtlasJSON: Record<string, any>;
32
+ itemsAtlasIMG: string;
33
+ entitiesAtlasJSON: Record<string, any>;
34
+ entitiesAtlasIMG: string;
35
+ faqItems?: IFaqItem[];
36
+ bestiaryItems?: IInformationCenterNPC[];
37
+ videoGuides?: IVideoGuide[];
38
+ items?: IInformationCenterItem[];
39
+ loading?: boolean;
40
+ error?: string;
41
+ initialSearchQuery?: string;
42
+ }
43
+
44
+ export const InformationCenter: React.FC<IInformationCenterProps> = ({
45
+ itemsAtlasJSON,
46
+ itemsAtlasIMG,
47
+ entitiesAtlasJSON,
48
+ entitiesAtlasIMG,
49
+ faqItems = [],
50
+ bestiaryItems = [],
51
+ videoGuides = [],
52
+ items = [],
53
+ loading = false,
54
+ error,
55
+ initialSearchQuery = '',
56
+ }) => {
57
+ const [
58
+ selectedItem,
59
+ setSelectedItem,
60
+ ] = useState<IInformationCenterItem | null>(null);
61
+
62
+ if (loading) {
63
+ return <LoadingMessage>Loading...</LoadingMessage>;
64
+ }
65
+
66
+ if (error) {
67
+ return <ErrorMessage>{error}</ErrorMessage>;
68
+ }
69
+
70
+ const tabs = [
71
+ {
72
+ id: 'bestiary',
73
+ title: 'Bestiary',
74
+ content: (
75
+ <BestiarySection
76
+ bestiaryItems={bestiaryItems}
77
+ itemsAtlasJSON={itemsAtlasJSON}
78
+ itemsAtlasIMG={itemsAtlasIMG}
79
+ entitiesAtlasJSON={entitiesAtlasJSON}
80
+ entitiesAtlasIMG={entitiesAtlasIMG}
81
+ initialSearchQuery={initialSearchQuery}
82
+ />
83
+ ),
84
+ },
85
+ {
86
+ id: 'items',
87
+ title: 'Items',
88
+ content: (
89
+ <ItemsSection
90
+ items={items}
91
+ bestiaryItems={bestiaryItems}
92
+ itemsAtlasJSON={itemsAtlasJSON}
93
+ itemsAtlasIMG={itemsAtlasIMG}
94
+ initialSearchQuery={initialSearchQuery}
95
+ />
96
+ ),
97
+ },
98
+ {
99
+ id: 'faq',
100
+ title: 'FAQ',
101
+ content: (
102
+ <FaqSection
103
+ faqItems={faqItems}
104
+ initialSearchQuery={initialSearchQuery}
105
+ />
106
+ ),
107
+ },
108
+ {
109
+ id: 'tutorials',
110
+ title: 'Tutorials',
111
+ content: (
112
+ <TutorialsSection
113
+ videoGuides={videoGuides}
114
+ initialSearchQuery={initialSearchQuery}
115
+ />
116
+ ),
117
+ },
118
+ ];
119
+
120
+ return (
121
+ <Container>
122
+ <InternalTabs tabs={tabs} activeTextColor="#000000" />
123
+ {selectedItem && (
124
+ <InformationCenterItemDetails
125
+ item={selectedItem}
126
+ itemsAtlasJSON={itemsAtlasJSON}
127
+ itemsAtlasIMG={itemsAtlasIMG}
128
+ droppedBy={bestiaryItems.filter(npc =>
129
+ npc.loots?.some(loot => loot.itemBlueprintKey === selectedItem.key)
130
+ )}
131
+ onBack={() => setSelectedItem(null)}
132
+ />
133
+ )}
134
+ </Container>
135
+ );
136
+ };
137
+
138
+ const Container = styled.div`
139
+ width: 100%;
140
+ max-width: 800px;
141
+ margin: 0 auto;
142
+ padding: 1rem;
143
+ `;
144
+
145
+ const LoadingMessage = styled.div`
146
+ text-align: center;
147
+ color: #ffffff;
148
+ padding: 2rem;
149
+ `;
150
+
151
+ const ErrorMessage = styled.div`
152
+ text-align: center;
153
+ color: #ef4444;
154
+ padding: 2rem;
155
+ `;