@rpg-engine/long-bow 0.8.73 → 0.8.75

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.
@@ -2,14 +2,19 @@ import {
2
2
  ICharacterDailyTask,
3
3
  ICollectGlobalRewardRequest,
4
4
  ICollectRewardRequest,
5
+ TaskStatus,
5
6
  TaskType,
7
+ isMobileOrTablet,
6
8
  } from '@rpg-engine/shared';
7
- import React, { useState } from 'react';
9
+ import React, { useMemo, useState } from 'react';
10
+ import { FaClipboardList, FaThumbtack } from 'react-icons/fa';
8
11
  import styled from 'styled-components';
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';
15
+ import { Dropdown, IOptionsProps as DropdownOption } from '../Dropdown';
12
16
  import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
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,12 +45,17 @@ 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
  );
48
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', []);
58
+
49
59
  const handleClaimReward = (taskKey: string, taskType: TaskType) => {
50
60
  onClaimReward({
51
61
  taskKey,
@@ -63,7 +73,50 @@ export const DailyTasks: React.FC<IDailyTasksProps> = ({
63
73
  return claimedTasks.includes(taskKey);
64
74
  };
65
75
 
66
- if (!size) return null;
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..."
149
+ />
150
+ </SearchBarContainer>
151
+ <DropdownContainer>
152
+ <Dropdown
153
+ options={statusOptions}
154
+ onChange={setSelectedStatus}
155
+ width="100%"
98
156
  />
99
- ))}
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,135 @@ 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: 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
+ justify-content: center;
273
+ align-items: center;
274
+ margin-left: 12px;
275
+ margin-right: 12px;
276
+ padding: 0.25rem;
277
+ border-bottom: 1px solid ${uiColors.darkGray};
278
+ background: rgba(0, 0, 0, 0.3);
279
+ `;
138
280
 
139
- @media (min-width: 360px) {
140
- padding: 0.5rem;
141
- }
281
+ const SearchBarContainer = styled.div`
282
+ flex: 3;
283
+ `;
142
284
 
143
- @media (min-width: 480px) {
144
- padding: 0.75rem;
285
+ const DropdownContainer = styled.div`
286
+ flex: 1;
287
+ min-width: 120px;
288
+ `;
289
+
290
+ const TaskWrapper = styled.div`
291
+ position: relative;
292
+ width: 100%;
293
+ `;
294
+
295
+ const PinButton = styled.button<{ isPinned: boolean }>`
296
+ position: absolute;
297
+ top: 8px;
298
+ right: 32px;
299
+ background: rgba(0, 0, 0, 0.7);
300
+ color: ${props => props.isPinned ? uiColors.yellow : uiColors.lightGray};
301
+ border: 1px solid ${props => props.isPinned ? uiColors.yellow : 'transparent'};
302
+ cursor: pointer;
303
+ padding: 4px;
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: center;
307
+ border-radius: 3px;
308
+ transition: all 0.2s ease;
309
+ z-index: 10;
310
+ width: 20px;
311
+ height: 20px;
312
+
313
+ &:hover {
314
+ color: ${uiColors.yellow};
315
+ background: rgba(0, 0, 0, 0.9);
316
+ border-color: ${uiColors.yellow};
145
317
  }
318
+
319
+ ${props => props.isPinned && `
320
+ transform: rotate(45deg);
321
+ `}
146
322
  `;
147
323
 
148
- const TasksList = styled.div`
324
+ const EmptyState = styled.div`
149
325
  display: flex;
150
326
  flex-direction: column;
151
- gap: 12px;
152
- padding: 15px;
153
- max-height: 70vh;
327
+ align-items: center;
328
+ justify-content: center;
329
+ padding: 60px 20px;
330
+ text-align: center;
331
+ color: ${uiColors.lightGray};
332
+ opacity: 0.7;
333
+ gap: 16px;
334
+
335
+ p {
336
+ margin: 0;
337
+ font-size: 0.9rem;
338
+ line-height: 1.4;
339
+ }
154
340
  `;
155
341
 
156
- const TaskTitle = styled.h2`
342
+ const TaskTitle = styled.h2<{ isMobile: boolean }>`
157
343
  color: ${uiColors.yellow} !important;
158
344
  text-align: center;
159
345
  padding-right: 30px !important;
160
- font-size: 1.4rem !important;
346
+ font-size: ${props => props.isMobile ? '1rem' : '1.2rem'} !important;
161
347
  width: 100%;
348
+ margin: ${props => props.isMobile ? '4px 0' : '8px 0'} !important;
349
+ font-weight: 600;
162
350
  `;
163
351
 
164
352
 
@@ -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 }>`