@rpg-engine/long-bow 0.8.122 → 0.8.124
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 +237 -247
- 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 +237 -247
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Friends/FriendList.tsx +261 -70
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,197 @@ 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
|
+
friends.length > 0 ? (
|
|
84
|
+
<ScrollableArea>
|
|
85
|
+
<Table>
|
|
86
|
+
<thead>
|
|
87
|
+
<TableRow>
|
|
88
|
+
<TableHeader>Online</TableHeader>
|
|
89
|
+
<TableHeader>Name</TableHeader>
|
|
90
|
+
<TableHeader>Actions</TableHeader>
|
|
91
|
+
</TableRow>
|
|
92
|
+
</thead>
|
|
93
|
+
<tbody>
|
|
94
|
+
{friends.map(friend => (
|
|
95
|
+
<TableRow key={friend._id}>
|
|
96
|
+
<TableCell>
|
|
97
|
+
<IsOnlineBadge $isOnline={friend.isOnline} />
|
|
98
|
+
</TableCell>
|
|
99
|
+
<TableCell>{friend.name}</TableCell>
|
|
100
|
+
<TableCell>
|
|
101
|
+
<ActionButtons>
|
|
102
|
+
<UserActionLink
|
|
103
|
+
color={uiColors.white}
|
|
104
|
+
onClick={() => onOpenPrivateMessage(friend)}
|
|
105
|
+
>
|
|
106
|
+
Chat
|
|
107
|
+
</UserActionLink>
|
|
108
|
+
<UserActionLink
|
|
109
|
+
color={uiColors.red}
|
|
110
|
+
onClick={() => onRemoveFriend(friend)}
|
|
111
|
+
>
|
|
112
|
+
Remove
|
|
113
|
+
</UserActionLink>
|
|
114
|
+
</ActionButtons>
|
|
115
|
+
</TableCell>
|
|
116
|
+
</TableRow>
|
|
117
|
+
))}
|
|
118
|
+
</tbody>
|
|
119
|
+
</Table>
|
|
120
|
+
</ScrollableArea>
|
|
121
|
+
) : (
|
|
122
|
+
<EmptyStateContainer>
|
|
123
|
+
<EmptyStateText>You don't have any friends yet</EmptyStateText>
|
|
124
|
+
<AddFriendCTA onClick={() => setActiveTab('search')}>
|
|
125
|
+
Add your first friend
|
|
126
|
+
</AddFriendCTA>
|
|
127
|
+
</EmptyStateContainer>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const searchTabContent = (
|
|
131
|
+
<ListContainer>
|
|
132
|
+
<SearchForm onSubmit={handleSubmit}>
|
|
133
|
+
<SearchInput
|
|
134
|
+
value={characterName}
|
|
135
|
+
id="characterName"
|
|
136
|
+
name="characterName"
|
|
137
|
+
onChange={handleInputChange}
|
|
138
|
+
type="text"
|
|
139
|
+
autoComplete="off"
|
|
71
140
|
onFocus={onFocus}
|
|
141
|
+
onBlur={onBlur}
|
|
142
|
+
onPointerDown={onFocus}
|
|
143
|
+
placeholder="Search for a character..."
|
|
72
144
|
/>
|
|
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>
|
|
145
|
+
</SearchForm>
|
|
146
|
+
<CharacterList
|
|
147
|
+
characters={searchedCharacters}
|
|
148
|
+
onAction={onSendFriendRequest}
|
|
149
|
+
actionLabel="Add Friend"
|
|
150
|
+
/>
|
|
151
|
+
</ListContainer>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const requestsTabContent = (
|
|
155
|
+
<ListContainer>
|
|
156
|
+
<FriendRequestSection
|
|
157
|
+
friendRequests={friendRequests}
|
|
158
|
+
onAccept={onAcceptRequest}
|
|
159
|
+
onReject={onRejectRequest}
|
|
160
|
+
/>
|
|
161
|
+
</ListContainer>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const tabs = [
|
|
165
|
+
{ id: 'friends', title: `Friends (${friends.length})`, content: friendsTabContent },
|
|
166
|
+
{ id: 'search', title: 'Search', content: searchTabContent },
|
|
167
|
+
{
|
|
168
|
+
id: 'requests',
|
|
169
|
+
title: `Requests (${friendRequests.length})`,
|
|
170
|
+
content: requestsTabContent,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<ListWrapper>
|
|
176
|
+
<InternalTabs
|
|
177
|
+
tabs={tabs}
|
|
178
|
+
activeTab={activeTab}
|
|
179
|
+
onTabChange={setActiveTab}
|
|
180
|
+
activeTextColor="#000"
|
|
181
|
+
inactiveColor="#777"
|
|
182
|
+
borderColor="#f59e0b"
|
|
183
|
+
/>
|
|
123
184
|
</ListWrapper>
|
|
124
185
|
);
|
|
125
186
|
};
|
|
126
187
|
|
|
188
|
+
const CharacterList: React.FC<{
|
|
189
|
+
characters: IFriend[];
|
|
190
|
+
onAction: (character: IFriend) => void;
|
|
191
|
+
actionLabel: string;
|
|
192
|
+
}> = ({ characters, onAction, actionLabel }) => (
|
|
193
|
+
<ListContainer>
|
|
194
|
+
{characters.map(character => (
|
|
195
|
+
<ListItem key={character._id}>
|
|
196
|
+
<CharacterName>{character.name}</CharacterName>
|
|
197
|
+
<UserActionLink
|
|
198
|
+
color={uiColors.lightGreen}
|
|
199
|
+
onClick={() => onAction(character)}
|
|
200
|
+
>
|
|
201
|
+
{actionLabel}
|
|
202
|
+
</UserActionLink>
|
|
203
|
+
</ListItem>
|
|
204
|
+
))}
|
|
205
|
+
</ListContainer>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const FriendRequestSection: React.FC<{
|
|
209
|
+
friendRequests: IFriend[];
|
|
210
|
+
onAccept: (character: IFriend) => void;
|
|
211
|
+
onReject: (character: IFriend) => void;
|
|
212
|
+
}> = ({ friendRequests, onAccept, onReject }) => (
|
|
213
|
+
<>
|
|
214
|
+
{friendRequests.length > 0 ? (
|
|
215
|
+
<ListContainer>
|
|
216
|
+
{friendRequests.map(character => (
|
|
217
|
+
<ListItem key={character._id}>
|
|
218
|
+
<CharacterName>{character.name}</CharacterName>
|
|
219
|
+
<AcceptRejectActions>
|
|
220
|
+
<UserActionLink
|
|
221
|
+
color={uiColors.lightGreen}
|
|
222
|
+
onClick={() => onAccept(character)}
|
|
223
|
+
>
|
|
224
|
+
Accept
|
|
225
|
+
</UserActionLink>
|
|
226
|
+
<UserActionLink
|
|
227
|
+
color={uiColors.red}
|
|
228
|
+
onClick={() => onReject(character)}
|
|
229
|
+
>
|
|
230
|
+
Reject
|
|
231
|
+
</UserActionLink>
|
|
232
|
+
</AcceptRejectActions>
|
|
233
|
+
</ListItem>
|
|
234
|
+
))}
|
|
235
|
+
</ListContainer>
|
|
236
|
+
) : (
|
|
237
|
+
<EmptyMessage>No pending friend requests</EmptyMessage>
|
|
238
|
+
)}
|
|
239
|
+
</>
|
|
240
|
+
);
|
|
241
|
+
|
|
127
242
|
// Styled components for FriendList UI
|
|
128
243
|
const ListWrapper = styled.div`
|
|
129
244
|
margin-top: 1rem;
|
|
@@ -134,7 +249,7 @@ const ListWrapper = styled.div`
|
|
|
134
249
|
|
|
135
250
|
const ScrollableArea = styled.div`
|
|
136
251
|
overflow-y: auto;
|
|
137
|
-
max-height: 300px;
|
|
252
|
+
max-height: 300px;
|
|
138
253
|
`;
|
|
139
254
|
|
|
140
255
|
const IsOnlineBadge = styled.div<{ $isOnline: boolean }>`
|
|
@@ -146,10 +261,86 @@ const IsOnlineBadge = styled.div<{ $isOnline: boolean }>`
|
|
|
146
261
|
margin-right: 1rem;
|
|
147
262
|
`;
|
|
148
263
|
|
|
149
|
-
const
|
|
264
|
+
const SearchForm = styled.form`
|
|
150
265
|
display: flex;
|
|
151
|
-
justify-content: center;
|
|
152
266
|
align-items: center;
|
|
153
267
|
width: 100%;
|
|
154
268
|
margin-top: 1rem;
|
|
155
269
|
`;
|
|
270
|
+
|
|
271
|
+
const SearchInput = styled.input`
|
|
272
|
+
width: 100%;
|
|
273
|
+
background-color: rgba(0, 0, 0, 0.25);
|
|
274
|
+
border: none;
|
|
275
|
+
padding: 0.5rem;
|
|
276
|
+
font-size: ${uiFonts.size.small};
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
const ListContainer = styled.ul`
|
|
280
|
+
list-style: none;
|
|
281
|
+
padding: 0;
|
|
282
|
+
margin: 0;
|
|
283
|
+
width: 100%;
|
|
284
|
+
max-height: 50vh;
|
|
285
|
+
overflow-y: auto;
|
|
286
|
+
|
|
287
|
+
@media (max-width: 768px) {
|
|
288
|
+
max-height: 90vh;
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
const ListItem = styled.li`
|
|
293
|
+
display: flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
justify-content: space-between;
|
|
296
|
+
padding: 0.5rem;
|
|
297
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
298
|
+
`;
|
|
299
|
+
|
|
300
|
+
const CharacterName = styled.p`
|
|
301
|
+
font-size: ${uiFonts.size.small};
|
|
302
|
+
margin: 0;
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
const AcceptRejectActions = styled.div`
|
|
306
|
+
display: flex;
|
|
307
|
+
gap: 0.5rem;
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
const EmptyMessage = styled.p`
|
|
311
|
+
text-align: center;
|
|
312
|
+
color: #888;
|
|
313
|
+
padding: 1rem;
|
|
314
|
+
font-size: ${uiFonts.size.small};
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
const EmptyStateContainer = styled.div`
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
align-items: center;
|
|
321
|
+
justify-content: center;
|
|
322
|
+
padding: 2rem;
|
|
323
|
+
gap: 1rem;
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
const EmptyStateText = styled.p`
|
|
327
|
+
text-align: center;
|
|
328
|
+
color: #888;
|
|
329
|
+
font-size: ${uiFonts.size.small};
|
|
330
|
+
margin: 0;
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const AddFriendCTA = styled.button`
|
|
334
|
+
background-color: ${uiColors.lightGreen};
|
|
335
|
+
color: #000;
|
|
336
|
+
border: none;
|
|
337
|
+
padding: 0.5rem 1rem;
|
|
338
|
+
font-size: ${uiFonts.size.small};
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
border-radius: 4px;
|
|
341
|
+
transition: opacity 0.2s;
|
|
342
|
+
|
|
343
|
+
&:hover {
|
|
344
|
+
opacity: 0.8;
|
|
345
|
+
}
|
|
346
|
+
`;
|