@rpg-engine/long-bow 0.8.72 → 0.8.74

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.
@@ -3,13 +3,18 @@ import {
3
3
  ICollectGlobalRewardRequest,
4
4
  ICollectRewardRequest,
5
5
  TaskType,
6
+ TaskStatus,
7
+ isMobileOrTablet,
6
8
  } from '@rpg-engine/shared';
7
- import React, { useState } from 'react';
9
+ import React, { useState, useMemo } from 'react';
8
10
  import styled from 'styled-components';
11
+ import { FaThumbtack, FaClipboardList } from 'react-icons/fa';
9
12
  import { uiColors } from '../../constants/uiColors';
10
- import { useResponsiveSize } from '../CraftBook/hooks/useResponsiveSize';
13
+ import { useLocalStorage } from '../../hooks/useLocalStorage';
11
14
  import { DraggableContainer } from '../DraggableContainer';
12
15
  import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
16
+ import { Dropdown, IOptionsProps as DropdownOption } from '../Dropdown';
17
+ import { SearchBar } from '../shared/SearchBar/SearchBar';
13
18
  import { DailyTaskItem } from './DailyTaskItem';
14
19
  import { GlobalDailyProgress } from './GlobalDailyProgress';
15
20
  import { getTaskIcon } from './utils/dailyTasks.utils';
@@ -40,11 +45,16 @@ export const DailyTasks: React.FC<IDailyTasksProps> = ({
40
45
  globalRewardClaimed = false,
41
46
  }): JSX.Element | null => {
42
47
  const [localTasks] = React.useState(tasks);
43
- const size = useResponsiveSize(scale);
48
+ const isMobile = isMobileOrTablet();
44
49
  const [claimedTasks, setClaimedTasks] = useState<string[]>([]);
45
50
  const [globalRewardClaimedLocal, setGlobalRewardClaimedLocal] = useState(
46
51
  false
47
52
  );
53
+
54
+ // Search and filter state
55
+ const [searchQuery, setSearchQuery] = useState('');
56
+ const [selectedStatus, setSelectedStatus] = useState('all');
57
+ const [pinnedTasks, setPinnedTasks] = useLocalStorage<string[]>('dailyTasks.pinned', []);
48
58
 
49
59
  const handleClaimReward = (taskKey: string, taskType: TaskType) => {
50
60
  onClaimReward({
@@ -62,8 +72,51 @@ export const DailyTasks: React.FC<IDailyTasksProps> = ({
62
72
  const isTaskRewardClaimed = (taskKey: string): boolean => {
63
73
  return claimedTasks.includes(taskKey);
64
74
  };
65
-
66
- if (!size) return null;
75
+
76
+ const togglePinTask = (taskKey: string) => {
77
+ setPinnedTasks(prev =>
78
+ prev.includes(taskKey)
79
+ ? prev.filter(key => key !== taskKey)
80
+ : [...prev, taskKey]
81
+ );
82
+ };
83
+
84
+ // Filter options using Store pattern
85
+ const statusOptions: DropdownOption[] = [
86
+ { id: 0, value: 'all', option: 'All' },
87
+ { id: 1, value: 'pinned', option: 'Pinned' },
88
+ { id: 2, value: 'completed', option: 'Completed' },
89
+ { id: 3, value: 'inprogress', option: 'In Progress' },
90
+ { id: 4, value: 'notstarted', option: 'Not Started' },
91
+ ];
92
+
93
+ // Filtered tasks using InformationCenter pattern
94
+ const filteredTasks = useMemo(() => {
95
+ let filtered = localTasks.filter(task => {
96
+ const matchesSearch =
97
+ task.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
98
+ task.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
99
+ task.key.toLowerCase().includes(searchQuery.toLowerCase());
100
+
101
+ const matchesStatus =
102
+ selectedStatus === 'all' ||
103
+ (selectedStatus === 'pinned' && pinnedTasks.includes(task.key)) ||
104
+ (selectedStatus === 'completed' && task.status === TaskStatus.Completed) ||
105
+ (selectedStatus === 'inprogress' && task.status === TaskStatus.InProgress) ||
106
+ (selectedStatus === 'notstarted' && task.status === TaskStatus.NotStarted);
107
+
108
+ return matchesSearch && matchesStatus;
109
+ });
110
+
111
+ // Sort with pinned tasks first
112
+ return filtered.sort((a, b) => {
113
+ const aIsPinned = pinnedTasks.includes(a.key);
114
+ const bIsPinned = pinnedTasks.includes(b.key);
115
+ if (aIsPinned && !bIsPinned) return -1;
116
+ if (!aIsPinned && bIsPinned) return 1;
117
+ return 0;
118
+ });
119
+ }, [localTasks, searchQuery, selectedStatus, pinnedTasks]);
67
120
 
68
121
  return (
69
122
  <TasksContainer
@@ -71,12 +124,13 @@ export const DailyTasks: React.FC<IDailyTasksProps> = ({
71
124
  onCloseButton={onClose}
72
125
  cancelDrag=".tasks-container"
73
126
  scale={scale}
74
- width={size.width}
75
- height={size.height}
127
+ width={isMobile ? '100vw' : 'max(50vw, 900px)'}
128
+ height={isMobile ? '100vh' : 'min(85vh, 800px)'}
129
+ isMobile={isMobile}
76
130
  >
77
- <TaskTitle>Daily Tasks</TaskTitle>
78
- <Container>
79
- <TasksList className="tasks-container">
131
+ <TaskTitle isMobile={isMobile}>Daily Tasks</TaskTitle>
132
+ <Container isMobile={isMobile}>
133
+ <GlobalProgressFixed>
80
134
  <GlobalDailyProgress
81
135
  tasks={localTasks}
82
136
  onClaimAllRewards={handleClaimGlobalRewards}
@@ -84,29 +138,79 @@ export const DailyTasks: React.FC<IDailyTasksProps> = ({
84
138
  globalRewardClaimed || globalRewardClaimedLocal
85
139
  }
86
140
  />
87
- {localTasks.map(task => (
88
- <DailyTaskItem
89
- key={task.key}
90
- task={task}
91
- spriteKey={getTaskIcon(task.type, task.difficulty)}
92
- onClaimReward={handleClaimReward}
93
- itemsAtlasJSON={itemsAtlasJSON}
94
- itemsAtlasIMG={itemsAtlasIMG}
95
- iconAtlasJSON={iconAtlasJSON}
96
- iconAtlasIMG={iconAtlasIMG}
97
- isRewardClaimed={task.claimed || isTaskRewardClaimed(task.key)}
141
+ </GlobalProgressFixed>
142
+
143
+ <SearchHeader>
144
+ <SearchBarContainer>
145
+ <SearchBar
146
+ value={searchQuery}
147
+ onChange={setSearchQuery}
148
+ placeholder="Search tasks..."
98
149
  />
99
- ))}
150
+ </SearchBarContainer>
151
+ <DropdownContainer>
152
+ <Dropdown
153
+ options={statusOptions}
154
+ onChange={setSelectedStatus}
155
+ width="100%"
156
+ />
157
+ </DropdownContainer>
158
+ </SearchHeader>
159
+
160
+ <TasksList className="tasks-container" isMobile={isMobile}>
161
+ {filteredTasks.length > 0 ? (
162
+ filteredTasks.map(task => (
163
+ <TaskWrapper key={task.key}>
164
+ <DailyTaskItem
165
+ task={task}
166
+ spriteKey={getTaskIcon(task.type, task.difficulty)}
167
+ onClaimReward={handleClaimReward}
168
+ itemsAtlasJSON={itemsAtlasJSON}
169
+ itemsAtlasIMG={itemsAtlasIMG}
170
+ iconAtlasJSON={iconAtlasJSON}
171
+ iconAtlasIMG={iconAtlasIMG}
172
+ isRewardClaimed={task.claimed || isTaskRewardClaimed(task.key)}
173
+ isPinned={pinnedTasks.includes(task.key)}
174
+ />
175
+ <PinButton
176
+ onClick={() => togglePinTask(task.key)}
177
+ isPinned={pinnedTasks.includes(task.key)}
178
+ >
179
+ <FaThumbtack size={10} />
180
+ </PinButton>
181
+ </TaskWrapper>
182
+ ))
183
+ ) : (
184
+ <EmptyState>
185
+ <FaClipboardList size={48} />
186
+ <p>
187
+ {searchQuery || selectedStatus !== 'all'
188
+ ? 'No tasks match your criteria.'
189
+ : 'No daily tasks available.'}
190
+ </p>
191
+ </EmptyState>
192
+ )}
100
193
  </TasksList>
101
194
  </Container>
102
195
  </TasksContainer>
103
196
  );
104
197
  };
105
198
 
106
- const TasksContainer = styled(DraggableContainer)`
107
- min-width: 450px;
108
- max-width: 550px;
109
- margin: 0 auto;
199
+ const TasksContainer = styled(DraggableContainer) <{ isMobile: boolean }>`
200
+ ${props => props.isMobile ? `
201
+ position: fixed !important;
202
+ top: 0 !important;
203
+ left: 0 !important;
204
+ right: 0 !important;
205
+ bottom: 0 !important;
206
+ width: 100vw !important;
207
+ height: 100vh !important;
208
+ margin: 0 !important;
209
+ z-index: 1000 !important;
210
+ ` : `
211
+ margin: 0 auto;
212
+ min-width: 50vw;
213
+ `}
110
214
 
111
215
  .rpgui-container-title {
112
216
  width: 100%;
@@ -114,51 +218,131 @@ const TasksContainer = styled(DraggableContainer)`
114
218
  justify-content: center;
115
219
  align-items: center;
116
220
  text-align: center;
221
+ ${props => props.isMobile ? 'padding: 8px 0;' : ''}
117
222
  }
118
223
 
119
224
  .rpgui-container {
120
225
  padding: 0 !important;
121
226
  overflow: hidden !important;
122
- background-color: rgba(30, 30, 30, 0.9) !important;
227
+ background-color: rgba(30, 30, 30, 0.98) !important;
228
+ ${props => props.isMobile ? `
229
+ border-radius: 0 !important;
230
+ height: 100vh !important;
231
+ width: 100vw !important;
232
+ ` : ''}
123
233
  }
124
234
  `;
125
235
 
126
- const Container = styled.div`
236
+ const Container = styled.div<{ isMobile: boolean }>`
127
237
  width: 100%;
128
- max-width: 100%;
129
- margin: 0 auto;
130
- padding: 0.125rem;
238
+ height: 100%;
239
+ margin: 0;
240
+ padding: 0;
241
+ overflow: hidden;
242
+ box-sizing: border-box;
243
+ background-color: transparent;
244
+ display: flex;
245
+ flex-direction: column;
246
+ `;
247
+
248
+ const TasksList = styled.div<{ isMobile: boolean }>`
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: ${props => props.isMobile ? '8px' : '12px'};
252
+ padding: ${props => props.isMobile ? '8px' : '12px'};
131
253
  overflow-y: auto;
254
+ flex: 1;
255
+ background-color: transparent;
256
+
257
+ ${props => props.isMobile ? `
258
+ -webkit-overflow-scrolling: touch;
259
+ scrollbar-width: thin;
260
+ ` : ''}
261
+ `;
132
262
 
133
- box-sizing: border-box;
263
+ const GlobalProgressFixed = styled.div`
264
+ flex-shrink: 0;
265
+ padding: 8px 12px;
266
+ background-color: transparent;
267
+ `;
134
268
 
135
- @media (min-width: 320px) {
136
- padding: 0.25rem;
137
- }
269
+ const SearchHeader = styled.div`
270
+ display: flex;
271
+ gap: 12px;
272
+ padding: 12px 12px 8px 12px;
273
+ border-bottom: 1px solid ${uiColors.darkGray};
274
+ background: rgba(0, 0, 0, 0.3);
275
+ `;
138
276
 
139
- @media (min-width: 360px) {
140
- padding: 0.5rem;
141
- }
277
+ const SearchBarContainer = styled.div`
278
+ flex: 3;
279
+ `;
280
+
281
+ const DropdownContainer = styled.div`
282
+ flex: 1;
283
+ min-width: 120px;
284
+ `;
142
285
 
143
- @media (min-width: 480px) {
144
- padding: 0.75rem;
286
+ const TaskWrapper = styled.div`
287
+ position: relative;
288
+ width: 100%;
289
+ `;
290
+
291
+ const PinButton = styled.button<{ isPinned: boolean }>`
292
+ position: absolute;
293
+ top: 8px;
294
+ right: 8px;
295
+ background: rgba(0, 0, 0, 0.7);
296
+ color: ${props => props.isPinned ? uiColors.yellow : uiColors.lightGray};
297
+ border: 1px solid ${props => props.isPinned ? uiColors.yellow : 'transparent'};
298
+ cursor: pointer;
299
+ padding: 4px;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ border-radius: 3px;
304
+ transition: all 0.2s ease;
305
+ z-index: 10;
306
+ width: 20px;
307
+ height: 20px;
308
+
309
+ &:hover {
310
+ color: ${uiColors.yellow};
311
+ background: rgba(0, 0, 0, 0.9);
312
+ border-color: ${uiColors.yellow};
145
313
  }
314
+
315
+ ${props => props.isPinned && `
316
+ transform: rotate(45deg);
317
+ `}
146
318
  `;
147
319
 
148
- const TasksList = styled.div`
320
+ const EmptyState = styled.div`
149
321
  display: flex;
150
322
  flex-direction: column;
151
- gap: 12px;
152
- padding: 15px;
153
- max-height: 70vh;
323
+ align-items: center;
324
+ justify-content: center;
325
+ padding: 60px 20px;
326
+ text-align: center;
327
+ color: ${uiColors.lightGray};
328
+ opacity: 0.7;
329
+ gap: 16px;
330
+
331
+ p {
332
+ margin: 0;
333
+ font-size: 0.9rem;
334
+ line-height: 1.4;
335
+ }
154
336
  `;
155
337
 
156
- const TaskTitle = styled.h2`
338
+ const TaskTitle = styled.h2<{ isMobile: boolean }>`
157
339
  color: ${uiColors.yellow} !important;
158
340
  text-align: center;
159
341
  padding-right: 30px !important;
160
- font-size: 1.4rem !important;
342
+ font-size: ${props => props.isMobile ? '1rem' : '1.2rem'} !important;
161
343
  width: 100%;
344
+ margin: ${props => props.isMobile ? '4px 0' : '8px 0'} !important;
345
+ font-weight: 600;
162
346
  `;
163
347
 
164
348
 
@@ -2,6 +2,7 @@ import {
2
2
  ICharacterDailyTask,
3
3
  ICollectGlobalRewardRequest,
4
4
  TaskStatus,
5
+ isMobileOrTablet,
5
6
  } from '@rpg-engine/shared';
6
7
  import React from 'react';
7
8
  import styled from 'styled-components';
@@ -19,6 +20,7 @@ export const GlobalDailyProgress: React.FC<IGlobalTaskProgressProps> = ({
19
20
  onClaimAllRewards,
20
21
  globalRewardClaimed,
21
22
  }) => {
23
+ const isMobile = isMobileOrTablet();
22
24
  const totalTasks = tasks.length;
23
25
  const completedTasks = tasks.filter(
24
26
  task => task.status === TaskStatus.Completed
@@ -45,11 +47,11 @@ export const GlobalDailyProgress: React.FC<IGlobalTaskProgressProps> = ({
45
47
  };
46
48
 
47
49
  return (
48
- <GlobalProgressContainer>
50
+ <GlobalProgressContainer isMobile={isMobile}>
49
51
  <HeaderContainer>
50
52
  <GlobeIcon>🌍</GlobeIcon>
51
- <ProgressText>
52
- Global Tasks Completed: {completedTasks}/{totalTasks}
53
+ <ProgressText isMobile={isMobile}>
54
+ {isMobile ? `${completedTasks}/${totalTasks} Complete` : `Global Tasks: ${completedTasks}/${totalTasks}`}
53
55
  </ProgressText>
54
56
  </HeaderContainer>
55
57
  <ProgressBar>
@@ -63,12 +65,12 @@ export const GlobalDailyProgress: React.FC<IGlobalTaskProgressProps> = ({
63
65
  buttonType={ButtonTypes.RPGUIButton}
64
66
  onPointerDown={handleClaimAll}
65
67
  >
66
- Collect Global Rewards
68
+ {isMobile ? 'Global Rewards' : 'Collect Global Rewards'}
67
69
  </Button>
68
70
  </CollectWrapper>
69
71
  )}
70
72
  {shouldShowClaimedMessage && (
71
- <ClaimedText>Global Rewards Claimed</ClaimedText>
73
+ <ClaimedText isMobile={isMobile}>✓ Global Rewards Claimed</ClaimedText>
72
74
  )}
73
75
  </>
74
76
  )}
@@ -76,26 +78,27 @@ export const GlobalDailyProgress: React.FC<IGlobalTaskProgressProps> = ({
76
78
  );
77
79
  };
78
80
 
79
- const GlobalProgressContainer = styled.div`
80
- background: rgba(0, 0, 0, 0.5) !important;
81
- border: 2px solid ${uiColors.darkGray} !important;
82
- border-radius: 8px;
83
- padding: 12px;
81
+ const GlobalProgressContainer = styled.div<{ isMobile: boolean }>`
82
+ background: rgba(0, 0, 0, 0.6) !important;
83
+ border: 1px solid ${uiColors.blue} !important;
84
+ border-radius: ${props => props.isMobile ? '4px' : '6px'};
85
+ padding: ${props => props.isMobile ? '6px' : '8px'};
84
86
  display: flex;
85
87
  flex-direction: column;
86
- gap: 12px;
87
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
88
+ gap: ${props => props.isMobile ? '6px' : '8px'};
89
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
90
+ margin-bottom: ${props => props.isMobile ? '4px' : '6px'};
88
91
  `;
89
92
 
90
93
  const HeaderContainer = styled.div`
91
94
  display: flex;
92
95
  align-items: center;
93
- gap: 8px;
94
- margin-bottom: 8px;
96
+ gap: 6px;
97
+ margin-bottom: 2px;
95
98
  `;
96
99
 
97
100
  const GlobeIcon = styled.span`
98
- font-size: 1.5rem !important;
101
+ font-size: 1rem !important;
99
102
  line-height: 1;
100
103
  color: ${uiColors.blue};
101
104
  display: flex;
@@ -103,18 +106,19 @@ const GlobeIcon = styled.span`
103
106
  justify-content: center;
104
107
  `;
105
108
 
106
- const ProgressText = styled.div`
109
+ const ProgressText = styled.div<{ isMobile: boolean }>`
107
110
  color: ${uiColors.white};
108
- text-align: center !important;
109
- margin-top: 8px;
111
+ font-size: ${props => props.isMobile ? '0.75rem' : '0.8rem'};
112
+ font-weight: 500;
113
+ flex: 1;
110
114
  line-height: 1.2;
111
115
  `;
112
116
 
113
117
  const ProgressBar = styled.div`
114
118
  width: 100%;
115
- height: 8px;
119
+ height: 4px;
116
120
  background: ${uiColors.darkGray};
117
- border-radius: 4px;
121
+ border-radius: 2px;
118
122
  overflow: hidden;
119
123
  `;
120
124
 
@@ -125,12 +129,15 @@ const ProgressFill = styled.div<{ percentage: number }>`
125
129
  transition: width 0.3s ease;
126
130
  `;
127
131
 
128
- const ClaimedText = styled.span`
132
+ const ClaimedText = styled.span<{ isMobile: boolean }>`
129
133
  color: ${uiColors.green};
130
- font-size: 0.9rem;
134
+ font-size: ${props => props.isMobile ? '0.8rem' : '0.9rem'};
131
135
  text-align: center;
132
- margin-top: 8px;
133
136
  font-weight: bold;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ gap: 4px;
134
141
  `;
135
142
 
136
143
  const CollectWrapper = styled.div`
@@ -139,6 +146,6 @@ const CollectWrapper = styled.div`
139
146
  display: flex !important;
140
147
  justify-content: center !important;
141
148
  align-items: center !important;
142
- margin: 5px 0 !important;
149
+ margin: 4px 0 !important;
143
150
  }
144
151
  `;
@@ -1,4 +1,4 @@
1
- import { ICharacterDailyTask } from '@rpg-engine/shared';
1
+ import { ICharacterDailyTask, isMobileOrTablet } from '@rpg-engine/shared';
2
2
  import React from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { uiColors } from '../../constants/uiColors';
@@ -22,23 +22,25 @@ export const TaskProgress: React.FC<TaskProgressProps> = ({
22
22
  iconAtlasIMG,
23
23
  }) => {
24
24
  const { difficulty } = task;
25
+ const isMobile = isMobileOrTablet();
25
26
 
26
27
  return (
27
- <ProgressContainer>
28
- <ProgressList>
29
- <ProgressItem>
30
- <ProgressLabel>Difficulty:</ProgressLabel>
31
- <TaskDifficulty difficulty={difficulty}>
32
- {formatDifficulty(difficulty)}
33
- </TaskDifficulty>
34
- </ProgressItem>
35
-
36
- <ProgressItem>
37
- <ProgressLabel>Status:</ProgressLabel>
38
- <StatusText color={getStatusInfo(task).color}>
39
- {getStatusInfo(task).text}
40
- </StatusText>
41
- </ProgressItem>
28
+ <ProgressContainer isMobile={isMobile}>
29
+ <ProgressList isMobile={isMobile}>
30
+ <ProgressRow>
31
+ <ProgressItem>
32
+ <ProgressLabel>Difficulty:</ProgressLabel>
33
+ <TaskDifficulty difficulty={difficulty}>
34
+ {formatDifficulty(difficulty)}
35
+ </TaskDifficulty>
36
+ </ProgressItem>
37
+ <ProgressItem>
38
+ <ProgressLabel>Status:</ProgressLabel>
39
+ <StatusText color={getStatusInfo(task).color}>
40
+ {getStatusInfo(task).text}
41
+ </StatusText>
42
+ </ProgressItem>
43
+ </ProgressRow>
42
44
 
43
45
  <TaskProgressDetails task={task} />
44
46
 
@@ -58,25 +60,34 @@ export const TaskProgress: React.FC<TaskProgressProps> = ({
58
60
  );
59
61
  };
60
62
 
61
- const ProgressContainer = styled.div`
63
+ const ProgressContainer = styled.div<{ isMobile: boolean }>`
62
64
  width: 100%;
63
65
  position: relative;
64
66
  `;
65
67
 
66
- const ProgressList = styled.div`
68
+ const ProgressList = styled.div<{ isMobile: boolean }>`
67
69
  display: flex;
68
70
  flex-direction: column;
69
- gap: 6px;
71
+ gap: ${props => props.isMobile ? '3px' : '4px'};
72
+ `;
73
+
74
+ const ProgressRow = styled.div`
75
+ display: flex;
76
+ gap: 12px;
77
+ flex-wrap: wrap;
70
78
  `;
71
79
 
72
80
  const ProgressItem = styled.div`
73
81
  display: flex;
74
82
  justify-content: space-between;
75
83
  align-items: center;
84
+ flex: 1;
85
+ min-width: 100px;
76
86
  `;
77
87
 
78
88
  const ProgressLabel = styled.span`
79
- color: ${uiColors.white} !important;
89
+ color: ${uiColors.lightGray} !important;
90
+ font-size: 0.7rem;
80
91
  `;
81
92
 
82
93
  const TaskDifficulty = styled.span<{ difficulty: string }>`