@rpg-engine/long-bow 0.8.121 → 0.8.123
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/dist/long-bow.cjs.development.js +221 -240
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +221 -240
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Friends/FriendList.tsx +222 -70
- package/src/components/InternalTabs/InternalTabs.tsx +9 -3
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { uiColors } from '../../constants/uiColors';
|
|
4
|
-
import {
|
|
4
|
+
import { uiFonts } from '../../constants/uiFonts';
|
|
5
|
+
import { InternalTabs } from '../InternalTabs/InternalTabs';
|
|
5
6
|
|
|
6
7
|
import { ICharacter } from '@rpg-engine/shared';
|
|
8
|
+
import { debounce } from 'lodash';
|
|
7
9
|
import {
|
|
8
10
|
ActionButtons,
|
|
9
11
|
Table,
|
|
@@ -12,7 +14,6 @@ import {
|
|
|
12
14
|
TableRow,
|
|
13
15
|
UserActionLink,
|
|
14
16
|
} from '../Table/Table';
|
|
15
|
-
import { SearchFriend } from './SearchFriend';
|
|
16
17
|
export type IFriend = Pick<ICharacter, 'name' | '_id' | 'isOnline'>;
|
|
17
18
|
interface IFriendListProps {
|
|
18
19
|
scale?: number;
|
|
@@ -47,83 +48,189 @@ export const FriendList: React.FC<IFriendListProps> = ({
|
|
|
47
48
|
onAddFriend,
|
|
48
49
|
onBackFriendList,
|
|
49
50
|
}) => {
|
|
50
|
-
const [
|
|
51
|
+
const [characterName, setCharacterName] = useState('');
|
|
52
|
+
const [activeTab, setActiveTab] = useState('friends');
|
|
51
53
|
|
|
52
54
|
useEffect(() => {
|
|
53
|
-
if (
|
|
55
|
+
if (activeTab === 'search') {
|
|
54
56
|
onAddFriend?.();
|
|
55
57
|
} else {
|
|
56
58
|
onBackFriendList?.();
|
|
57
59
|
}
|
|
58
|
-
}, [
|
|
60
|
+
}, [activeTab]);
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
const debouncedSearch = useCallback(
|
|
63
|
+
debounce((name: string) => {
|
|
64
|
+
onSearch(name);
|
|
65
|
+
}, 300),
|
|
66
|
+
[]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
70
|
+
const name = e.target.value;
|
|
71
|
+
setCharacterName(name);
|
|
72
|
+
debouncedSearch(name);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
if (characterName.trim()) {
|
|
78
|
+
onSearch(characterName);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const friendsTabContent = (
|
|
83
|
+
<ScrollableArea>
|
|
84
|
+
<Table>
|
|
85
|
+
<thead>
|
|
86
|
+
<TableRow>
|
|
87
|
+
<TableHeader>Online</TableHeader>
|
|
88
|
+
<TableHeader>Name</TableHeader>
|
|
89
|
+
<TableHeader>Actions</TableHeader>
|
|
90
|
+
</TableRow>
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody>
|
|
93
|
+
{friends.map(friend => (
|
|
94
|
+
<TableRow key={friend._id}>
|
|
95
|
+
<TableCell>
|
|
96
|
+
<IsOnlineBadge $isOnline={friend.isOnline} />
|
|
97
|
+
</TableCell>
|
|
98
|
+
<TableCell>{friend.name}</TableCell>
|
|
99
|
+
<TableCell>
|
|
100
|
+
<ActionButtons>
|
|
101
|
+
<UserActionLink
|
|
102
|
+
color={uiColors.white}
|
|
103
|
+
onClick={() => onOpenPrivateMessage(friend)}
|
|
104
|
+
>
|
|
105
|
+
Chat
|
|
106
|
+
</UserActionLink>
|
|
107
|
+
<UserActionLink
|
|
108
|
+
color={uiColors.red}
|
|
109
|
+
onClick={() => onRemoveFriend(friend)}
|
|
110
|
+
>
|
|
111
|
+
Remove
|
|
112
|
+
</UserActionLink>
|
|
113
|
+
</ActionButtons>
|
|
114
|
+
</TableCell>
|
|
115
|
+
</TableRow>
|
|
116
|
+
))}
|
|
117
|
+
</tbody>
|
|
118
|
+
</Table>
|
|
119
|
+
</ScrollableArea>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const searchTabContent = (
|
|
123
|
+
<ListContainer>
|
|
124
|
+
<SearchForm onSubmit={handleSubmit}>
|
|
125
|
+
<SearchInput
|
|
126
|
+
value={characterName}
|
|
127
|
+
id="characterName"
|
|
128
|
+
name="characterName"
|
|
129
|
+
onChange={handleInputChange}
|
|
130
|
+
type="text"
|
|
131
|
+
autoComplete="off"
|
|
71
132
|
onFocus={onFocus}
|
|
133
|
+
onBlur={onBlur}
|
|
134
|
+
onPointerDown={onFocus}
|
|
135
|
+
placeholder="Search for a character..."
|
|
72
136
|
/>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
</>
|
|
113
|
-
)}
|
|
114
|
-
|
|
115
|
-
<ButtonContainer>
|
|
116
|
-
<Button
|
|
117
|
-
buttonType={ButtonTypes.RPGUIButton}
|
|
118
|
-
onClick={() => setIsAddFriendUI(prev => !prev)}
|
|
119
|
-
>
|
|
120
|
-
{isAddFriendUI ? 'Back' : 'Add New Friend'}
|
|
121
|
-
</Button>
|
|
122
|
-
</ButtonContainer>
|
|
137
|
+
</SearchForm>
|
|
138
|
+
<CharacterList
|
|
139
|
+
characters={searchedCharacters}
|
|
140
|
+
onAction={onSendFriendRequest}
|
|
141
|
+
actionLabel="Add Friend"
|
|
142
|
+
/>
|
|
143
|
+
</ListContainer>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const requestsTabContent = (
|
|
147
|
+
<ListContainer>
|
|
148
|
+
<FriendRequestSection
|
|
149
|
+
friendRequests={friendRequests}
|
|
150
|
+
onAccept={onAcceptRequest}
|
|
151
|
+
onReject={onRejectRequest}
|
|
152
|
+
/>
|
|
153
|
+
</ListContainer>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const tabs = [
|
|
157
|
+
{ id: 'friends', title: `Friends (${friends.length})`, content: friendsTabContent },
|
|
158
|
+
{ id: 'search', title: 'Search', content: searchTabContent },
|
|
159
|
+
{
|
|
160
|
+
id: 'requests',
|
|
161
|
+
title: `Requests (${friendRequests.length})`,
|
|
162
|
+
content: requestsTabContent,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<ListWrapper>
|
|
168
|
+
<InternalTabs
|
|
169
|
+
tabs={tabs}
|
|
170
|
+
activeTab={activeTab}
|
|
171
|
+
onTabChange={setActiveTab}
|
|
172
|
+
activeTextColor="#000"
|
|
173
|
+
inactiveColor="#777"
|
|
174
|
+
borderColor="#f59e0b"
|
|
175
|
+
/>
|
|
123
176
|
</ListWrapper>
|
|
124
177
|
);
|
|
125
178
|
};
|
|
126
179
|
|
|
180
|
+
const CharacterList: React.FC<{
|
|
181
|
+
characters: IFriend[];
|
|
182
|
+
onAction: (character: IFriend) => void;
|
|
183
|
+
actionLabel: string;
|
|
184
|
+
}> = ({ characters, onAction, actionLabel }) => (
|
|
185
|
+
<ListContainer>
|
|
186
|
+
{characters.map(character => (
|
|
187
|
+
<ListItem key={character._id}>
|
|
188
|
+
<CharacterName>{character.name}</CharacterName>
|
|
189
|
+
<UserActionLink
|
|
190
|
+
color={uiColors.lightGreen}
|
|
191
|
+
onClick={() => onAction(character)}
|
|
192
|
+
>
|
|
193
|
+
{actionLabel}
|
|
194
|
+
</UserActionLink>
|
|
195
|
+
</ListItem>
|
|
196
|
+
))}
|
|
197
|
+
</ListContainer>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const FriendRequestSection: React.FC<{
|
|
201
|
+
friendRequests: IFriend[];
|
|
202
|
+
onAccept: (character: IFriend) => void;
|
|
203
|
+
onReject: (character: IFriend) => void;
|
|
204
|
+
}> = ({ friendRequests, onAccept, onReject }) => (
|
|
205
|
+
<>
|
|
206
|
+
{friendRequests.length > 0 ? (
|
|
207
|
+
<ListContainer>
|
|
208
|
+
{friendRequests.map(character => (
|
|
209
|
+
<ListItem key={character._id}>
|
|
210
|
+
<CharacterName>{character.name}</CharacterName>
|
|
211
|
+
<AcceptRejectActions>
|
|
212
|
+
<UserActionLink
|
|
213
|
+
color={uiColors.lightGreen}
|
|
214
|
+
onClick={() => onAccept(character)}
|
|
215
|
+
>
|
|
216
|
+
Accept
|
|
217
|
+
</UserActionLink>
|
|
218
|
+
<UserActionLink
|
|
219
|
+
color={uiColors.red}
|
|
220
|
+
onClick={() => onReject(character)}
|
|
221
|
+
>
|
|
222
|
+
Reject
|
|
223
|
+
</UserActionLink>
|
|
224
|
+
</AcceptRejectActions>
|
|
225
|
+
</ListItem>
|
|
226
|
+
))}
|
|
227
|
+
</ListContainer>
|
|
228
|
+
) : (
|
|
229
|
+
<EmptyMessage>No pending friend requests</EmptyMessage>
|
|
230
|
+
)}
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
|
|
127
234
|
// Styled components for FriendList UI
|
|
128
235
|
const ListWrapper = styled.div`
|
|
129
236
|
margin-top: 1rem;
|
|
@@ -134,7 +241,7 @@ const ListWrapper = styled.div`
|
|
|
134
241
|
|
|
135
242
|
const ScrollableArea = styled.div`
|
|
136
243
|
overflow-y: auto;
|
|
137
|
-
max-height: 300px;
|
|
244
|
+
max-height: 300px;
|
|
138
245
|
`;
|
|
139
246
|
|
|
140
247
|
const IsOnlineBadge = styled.div<{ $isOnline: boolean }>`
|
|
@@ -146,10 +253,55 @@ const IsOnlineBadge = styled.div<{ $isOnline: boolean }>`
|
|
|
146
253
|
margin-right: 1rem;
|
|
147
254
|
`;
|
|
148
255
|
|
|
149
|
-
const
|
|
256
|
+
const SearchForm = styled.form`
|
|
150
257
|
display: flex;
|
|
151
|
-
justify-content: center;
|
|
152
258
|
align-items: center;
|
|
153
259
|
width: 100%;
|
|
154
260
|
margin-top: 1rem;
|
|
155
261
|
`;
|
|
262
|
+
|
|
263
|
+
const SearchInput = styled.input`
|
|
264
|
+
width: 100%;
|
|
265
|
+
background-color: rgba(0, 0, 0, 0.25);
|
|
266
|
+
border: none;
|
|
267
|
+
padding: 0.5rem;
|
|
268
|
+
font-size: ${uiFonts.size.small};
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
const ListContainer = styled.ul`
|
|
272
|
+
list-style: none;
|
|
273
|
+
padding: 0;
|
|
274
|
+
margin: 0;
|
|
275
|
+
width: 100%;
|
|
276
|
+
max-height: 50vh;
|
|
277
|
+
overflow-y: auto;
|
|
278
|
+
|
|
279
|
+
@media (max-width: 768px) {
|
|
280
|
+
max-height: 90vh;
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
const ListItem = styled.li`
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
justify-content: space-between;
|
|
288
|
+
padding: 0.5rem;
|
|
289
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
const CharacterName = styled.p`
|
|
293
|
+
font-size: ${uiFonts.size.small};
|
|
294
|
+
margin: 0;
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
const AcceptRejectActions = styled.div`
|
|
298
|
+
display: flex;
|
|
299
|
+
gap: 0.5rem;
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
const EmptyMessage = styled.p`
|
|
303
|
+
text-align: center;
|
|
304
|
+
color: #888;
|
|
305
|
+
padding: 1rem;
|
|
306
|
+
font-size: ${uiFonts.size.small};
|
|
307
|
+
`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { UI_BREAKPOINT_MOBILE } from '../../constants/uiBreakpoints';
|
|
4
4
|
|
|
@@ -29,7 +29,13 @@ export const InternalTabs: React.FC<TableTabProps> = ({
|
|
|
29
29
|
onTabChange,
|
|
30
30
|
activeTab: externalActiveTab,
|
|
31
31
|
}) => {
|
|
32
|
-
const
|
|
32
|
+
const [internalActiveTab, setInternalActiveTab] = useState(tabs[0]?.id);
|
|
33
|
+
const activeTabId = externalActiveTab ?? internalActiveTab;
|
|
34
|
+
|
|
35
|
+
const handleTabClick = (tabId: string): void => {
|
|
36
|
+
setInternalActiveTab(tabId);
|
|
37
|
+
onTabChange?.(tabId);
|
|
38
|
+
};
|
|
33
39
|
|
|
34
40
|
return (
|
|
35
41
|
<TableWrapper>
|
|
@@ -43,7 +49,7 @@ export const InternalTabs: React.FC<TableTabProps> = ({
|
|
|
43
49
|
inactiveColor={inactiveColor}
|
|
44
50
|
borderColor={borderColor}
|
|
45
51
|
hoverColor={hoverColor}
|
|
46
|
-
onClick={() =>
|
|
52
|
+
onClick={() => handleTabClick(tab.id)}
|
|
47
53
|
>
|
|
48
54
|
{tab.title}
|
|
49
55
|
</TabButton>
|