@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.121",
3
+ "version": "0.8.123",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -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 { Button, ButtonTypes } from '../Button';
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 [isAddFriendUI, setIsAddFriendUI] = useState(false);
51
+ const [characterName, setCharacterName] = useState('');
52
+ const [activeTab, setActiveTab] = useState('friends');
51
53
 
52
54
  useEffect(() => {
53
- if (isAddFriendUI) {
55
+ if (activeTab === 'search') {
54
56
  onAddFriend?.();
55
57
  } else {
56
58
  onBackFriendList?.();
57
59
  }
58
- }, [isAddFriendUI]);
60
+ }, [activeTab]);
59
61
 
60
- return (
61
- <ListWrapper>
62
- {isAddFriendUI ? (
63
- <SearchFriend
64
- searchedCharacters={searchedCharacters}
65
- onSendFriendRequest={onSendFriendRequest}
66
- onAcceptRequest={onAcceptRequest}
67
- onRejectRequest={onRejectRequest}
68
- friendRequests={friendRequests}
69
- onSearch={onSearch}
70
- onBlur={onBlur}
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
- <ScrollableArea>
76
- <Table>
77
- <thead>
78
- <TableRow>
79
- <TableHeader>Online</TableHeader>
80
- <TableHeader>Name</TableHeader>
81
- <TableHeader>Actions</TableHeader>
82
- </TableRow>
83
- </thead>
84
- <tbody>
85
- {friends.map(friend => (
86
- <TableRow key={friend._id}>
87
- <TableCell>
88
- <IsOnlineBadge $isOnline={friend.isOnline} />
89
- </TableCell>
90
- <TableCell>{friend.name}</TableCell>
91
- <TableCell>
92
- <ActionButtons>
93
- <UserActionLink
94
- color={uiColors.white}
95
- onClick={() => onOpenPrivateMessage(friend)}
96
- >
97
- Chat
98
- </UserActionLink>
99
- <UserActionLink
100
- color={uiColors.red}
101
- onClick={() => onRemoveFriend(friend)}
102
- >
103
- Remove
104
- </UserActionLink>
105
- </ActionButtons>
106
- </TableCell>
107
- </TableRow>
108
- ))}
109
- </tbody>
110
- </Table>
111
- </ScrollableArea>
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; /* Adjust this value based on your layout */
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 ButtonContainer = styled.div`
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 activeTabId = externalActiveTab ?? tabs[0].id;
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={() => onTabChange?.(tab.id)}
52
+ onClick={() => handleTabClick(tab.id)}
47
53
  >
48
54
  {tab.title}
49
55
  </TabButton>