Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit 5afda8c

Browse files
feat: add search by name (#319)
* feat: add search by name * feat: manage mention profile better * feat: new UI hover tag post (#316) * feat: new UI hover tag post * fix: loading icon and empty image --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> * fix: add link profile/menu (post-profile) (#317) * fix: add link profile/menu (post-profile) * fix: change bgcolor to pages/component * fix: bg dropdown * fix: change icon link in profile * fix: add See All button to active friends * fix: width-height social icons * fix: loading contact info * fix: pass loading in creatorPubky profile page * fix: change gap creatorPubky page profile * fix: sidebar key prop issue * feat: new UI hover tag post (#316) * feat: new UI hover tag post * fix: loading icon and empty image --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> * feat: add `settings` page (#318) * feat: add settings-page * feat: new UI hover tag post (#316) * feat: new UI hover tag post * fix: loading icon and empty image --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> * fix: add link profile/menu (post-profile) (#317) * fix: add link profile/menu (post-profile) * fix: change bgcolor to pages/component * fix: bg dropdown * fix: change icon link in profile * fix: add See All button to active friends * fix: width-height social icons * fix: loading contact info * fix: pass loading in creatorPubky profile page * fix: change gap creatorPubky page profile * fix: sidebar key prop issue * feat: new UI hover tag post (#316) * feat: new UI hover tag post * fix: loading icon and empty image --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> * fix: sidebar border --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br> * fix: add rebounce * fix: cannot access content before init * fix: ui post connector --------- Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br>
1 parent 249f0de commit 5afda8c

File tree

14 files changed

+723
-211
lines changed

14 files changed

+723
-211
lines changed

apps/web/app/onboarding/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ export default function Index() {
6767
</Typography.H2>
6868
<div className="relative flex gap-3">
6969
<Link id="onboarding-sign-in-link" href="/onboarding/sign-in">
70-
<Button.Large className="sm:w-[162px] w-full mt-12 relative z-20">
70+
<Button.Large className="mt-12 relative z-20">
7171
Let&apos;s get started
7272
</Button.Large>
7373
</Link>
7474
<Button.Large
7575
onClick={!loading ? () => handleSubmit() : undefined}
7676
variant="secondary"
77-
className="w-[12%] mt-12 relative z-20"
77+
className="w-auto mt-12 relative z-20"
7878
loading={loading}
7979
>
8080
Explore first

apps/web/app/post/[pubky]/[postId]/components/_ReplyForm.tsx

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { useClientContext } from '@/contexts';
1818
import Modal from '@/components/Modal';
1919
import { Utils } from '@social/utils-shared';
20-
import { IPost } from '@/types';
20+
import { IPost, IUserProfile } from '@/types';
2121
import LinkPreviewer from '@/components/LinkPreview';
2222
import Partecipants from './_Partecipants';
2323
import { IReply } from '@/types';
@@ -36,7 +36,8 @@ export default function ReplyForm({
3636
replies: IReply;
3737
}) {
3838
const router = useRouter();
39-
const { getProfile, pubky, createReply, createTag } = useClientContext();
39+
const { getProfile, pubky, createReply, createTag, searchUsers } =
40+
useClientContext();
4041
const [image, setImage] = useState('/images/Userpic.png');
4142
const [name, setName] = useState('');
4243
const [arrayTags, setArrayTags] = useState<string[]>([]);
@@ -49,6 +50,69 @@ export default function ReplyForm({
4950
const [contentReply, setContentReply] = useState('');
5051
const [sendingReply, setSendingReply] = useState(false);
5152
const [cursorPosition, setCursorPosition] = useState<number>(0);
53+
const [searchedUsers, setSearchedUsers] = useState<IUserProfile[]>([]);
54+
const [debounceTimeout, setDebounceTimeout] = useState<NodeJS.Timeout | null>(
55+
null
56+
);
57+
58+
const handleUserClick = (userId: string) => {
59+
const regex = /@\w+/;
60+
const newContent = contentReply.replace(regex, `pk:${userId}`);
61+
62+
setContentReply(newContent);
63+
setSearchedUsers([]);
64+
};
65+
66+
const searchProfiles = async (text: string) => {
67+
try {
68+
const result = await searchUsers(text);
69+
return result || [];
70+
} catch (error) {
71+
console.error('Error searching profiles:', error);
72+
return [];
73+
}
74+
};
75+
76+
const searchUsername = async (content: string) => {
77+
const pkMatches = content.match(/(pk:[^\s]+)/g);
78+
const atMatches = content.match(/(@[^\s]+)/g);
79+
80+
const searchQueries = [...(pkMatches || []), ...(atMatches || [])];
81+
82+
if (searchQueries.length === 0) {
83+
setSearchedUsers([]);
84+
return;
85+
}
86+
87+
let results: IUserProfile[] = [];
88+
89+
for (const query of searchQueries) {
90+
if (query.startsWith('@')) {
91+
const username = query.slice(1);
92+
const searchResult = await searchUsers(username);
93+
results = [...results, ...(searchResult || [])];
94+
} else if (query.startsWith('pk:')) {
95+
const searchResult = await searchProfiles(query);
96+
results = [...results, ...(searchResult || [])];
97+
}
98+
}
99+
setSearchedUsers(results.length > 0 ? results : []);
100+
};
101+
102+
useEffect(() => {
103+
if (debounceTimeout) {
104+
clearTimeout(debounceTimeout);
105+
}
106+
107+
const timeout = setTimeout(() => {
108+
searchUsername(contentReply);
109+
}, 500);
110+
111+
setDebounceTimeout(timeout);
112+
113+
return () => clearTimeout(timeout);
114+
// eslint-disable-next-line react-hooks/exhaustive-deps
115+
}, [contentReply]);
52116

53117
const handleReply = async (content: string) => {
54118
setSendingReply(true);
@@ -181,23 +245,31 @@ export default function ReplyForm({
181245
)}
182246
</div>
183247
<Post.Content text="">
184-
<Input.CursorArea
185-
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
186-
setContentReply(e.target.value);
187-
setCursorPosition(e.target.selectionStart);
188-
setIsValidContent(Utils.isValidContent(e.target.value));
189-
}}
190-
onSelect={(
191-
e: React.SyntheticEvent<HTMLTextAreaElement>
192-
) => {
193-
setCursorPosition(e.currentTarget.selectionStart);
194-
}}
195-
onClick={() => setTextArea(true)}
196-
value={contentReply}
197-
maxLength={300}
198-
className="h-[25px] max-h-[300px] w-[250px] md:w-[500px] lg:w-[650px]"
199-
placeholder="What are your thoughts on this?"
200-
/>
248+
<div className="w-full relative">
249+
<Input.CursorArea
250+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
251+
setContentReply(e.target.value);
252+
setCursorPosition(e.target.selectionStart);
253+
setIsValidContent(Utils.isValidContent(e.target.value));
254+
}}
255+
onSelect={(
256+
e: React.SyntheticEvent<HTMLTextAreaElement>
257+
) => {
258+
setCursorPosition(e.currentTarget.selectionStart);
259+
}}
260+
onClick={() => setTextArea(true)}
261+
value={contentReply}
262+
maxLength={300}
263+
className="h-[25px] max-h-[300px] w-[250px] md:w-[500px] lg:w-[650px]"
264+
placeholder="What are your thoughts on this?"
265+
/>
266+
{searchedUsers.length > 0 && (
267+
<Modal.SearchedUsersCard
268+
handleUserClick={handleUserClick}
269+
searchedUsers={searchedUsers}
270+
/>
271+
)}
272+
</div>
201273
<LinkPreviewer content={contentReply} />
202274
</Post.Content>
203275
</div>

apps/web/components/CreateQuickPost/index.tsx

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import EmojiPicker, {
1616
} from 'emoji-picker-react';
1717
import { useClientContext, useAlertContext } from '@/contexts';
1818
import Image from 'next/image';
19-
import { INewPost } from '@/types';
19+
import { INewPost, IUserProfile } from '@/types';
2020
import Modal from '../Modal';
2121
import { Utils } from '@social/utils-shared';
2222
import LinkPreviewer from '../LinkPreview';
@@ -29,7 +29,7 @@ interface CreateQuickPostProps extends React.HTMLAttributes<HTMLDivElement> {
2929
export default function CreateQuickPost({
3030
largeView = false,
3131
}: CreateQuickPostProps) {
32-
const { pubky, getProfile, createPost, setPosts, createTag } =
32+
const { pubky, getProfile, createPost, setPosts, createTag, searchUsers } =
3333
useClientContext();
3434
const router = useRouter();
3535
const { setContent, setShow } = useAlertContext();
@@ -45,6 +45,69 @@ export default function CreateQuickPost({
4545
const wrapperRef = useRef<HTMLDivElement>(null);
4646
const wrapperRefEmojis = useRef<HTMLDivElement>(null);
4747
const [cursorPosition, setCursorPosition] = useState<number>(0);
48+
const [searchedUsers, setSearchedUsers] = useState<IUserProfile[]>([]);
49+
const [debounceTimeout, setDebounceTimeout] = useState<NodeJS.Timeout | null>(
50+
null
51+
);
52+
53+
const handleUserClick = (userId: string) => {
54+
const regex = /@\w+/;
55+
const newContent = contentPost.replace(regex, `pk:${userId}`);
56+
57+
setContentPost(newContent);
58+
setSearchedUsers([]);
59+
};
60+
61+
const searchProfiles = async (text: string) => {
62+
try {
63+
const result = await searchUsers(text);
64+
return result || [];
65+
} catch (error) {
66+
console.error('Error searching profiles:', error);
67+
return [];
68+
}
69+
};
70+
71+
const searchUsername = async (content: string) => {
72+
const pkMatches = content.match(/(pk:[^\s]+)/g);
73+
const atMatches = content.match(/(@[^\s]+)/g);
74+
75+
const searchQueries = [...(pkMatches || []), ...(atMatches || [])];
76+
77+
if (searchQueries.length === 0) {
78+
setSearchedUsers([]);
79+
return;
80+
}
81+
82+
let results: IUserProfile[] = [];
83+
84+
for (const query of searchQueries) {
85+
if (query.startsWith('@')) {
86+
const username = query.slice(1);
87+
const searchResult = await searchUsers(username);
88+
results = [...results, ...(searchResult || [])];
89+
} else if (query.startsWith('pk:')) {
90+
const searchResult = await searchProfiles(query);
91+
results = [...results, ...(searchResult || [])];
92+
}
93+
}
94+
setSearchedUsers(results.length > 0 ? results : []);
95+
};
96+
97+
useEffect(() => {
98+
if (debounceTimeout) {
99+
clearTimeout(debounceTimeout);
100+
}
101+
102+
const timeout = setTimeout(() => {
103+
searchUsername(contentPost);
104+
}, 500);
105+
106+
setDebounceTimeout(timeout);
107+
108+
return () => clearTimeout(timeout);
109+
// eslint-disable-next-line react-hooks/exhaustive-deps
110+
}, [contentPost]);
48111

49112
async function fetchProfile() {
50113
try {
@@ -227,23 +290,31 @@ export default function CreateQuickPost({
227290
ref={wrapperRef}
228291
className="w-full flex justify-between gap-6 items-start flex-col"
229292
>
230-
<Input.CursorArea
231-
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
232-
setContentPost(e.target.value);
233-
setCursorPosition(e.target.selectionStart);
234-
setIsValidContent(Utils.isValidContent(e.target.value));
235-
}}
236-
onSelect={(e: React.SyntheticEvent<HTMLTextAreaElement>) => {
237-
setCursorPosition(e.currentTarget.selectionStart);
238-
}}
239-
value={contentPost}
240-
maxLength={300}
241-
onClick={() => setTextArea(true)}
242-
className={`w-full max-h-[300px] h-auto mt-4 ${
243-
largeView && 'text-2xl min-h-[50px]'
244-
}`}
245-
placeholder="What's on your mind?"
246-
/>
293+
<div className="w-full relative">
294+
<Input.CursorArea
295+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
296+
setContentPost(e.target.value);
297+
setCursorPosition(e.target.selectionStart);
298+
setIsValidContent(Utils.isValidContent(e.target.value));
299+
}}
300+
onSelect={(e: React.SyntheticEvent<HTMLTextAreaElement>) => {
301+
setCursorPosition(e.currentTarget.selectionStart);
302+
}}
303+
value={contentPost}
304+
maxLength={300}
305+
onClick={() => setTextArea(true)}
306+
className={`w-full max-h-[300px] h-auto mt-4 ${
307+
largeView && 'text-2xl min-h-[50px]'
308+
}`}
309+
placeholder="What's on your mind?"
310+
/>
311+
{searchedUsers.length > 0 && (
312+
<Modal.SearchedUsersCard
313+
handleUserClick={handleUserClick}
314+
searchedUsers={searchedUsers}
315+
/>
316+
)}
317+
</div>
247318
<LinkPreviewer content={contentPost} />
248319
{(textArea || contentPost || showModalTag || arrayTags.length > 0) && (
249320
<Post.Actions className="w-full">

apps/web/components/Header/index.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,27 @@ export default function Header({ title, className }: HeaderProps) {
9898
};
9999

100100
const handleSearchTag = () => {
101-
const trimmedValue = inputValue.toLowerCase().trim();
102-
//if (trimmedValue.startsWith('#')) {
103-
if (searchTags.includes(trimmedValue.slice(0))) return;
104-
105-
if (searchTags.length < 3) {
106-
setSearchTags([...searchTags, trimmedValue.slice(0)]);
101+
if (
102+
(inputValue.startsWith('pk:') && inputValue.length === 55) ||
103+
inputValue.length === 52
104+
) {
105+
const profileId = inputValue.replace(/^pk:/, '');
106+
router.push(`/profile/${profileId}`);
107107
} else {
108-
const newSearchTags = [...searchTags.slice(0), trimmedValue.slice(0)];
109-
setSearchTags(newSearchTags);
108+
const trimmedValue = inputValue.trim();
109+
//if (trimmedValue.startsWith('#')) {
110+
if (searchTags.includes(trimmedValue.slice(0))) return;
111+
112+
if (searchTags.length < 3) {
113+
setSearchTags([...searchTags, trimmedValue.slice(0)]);
114+
} else {
115+
const newSearchTags = [...searchTags.slice(0), trimmedValue.slice(0)];
116+
setSearchTags(newSearchTags);
117+
}
118+
setInputValue('');
119+
router.push('/search');
120+
// }
110121
}
111-
setInputValue('');
112-
router.push('/search');
113-
// }
114122
};
115123

116124
const handleRemoveTag = (indexToRemove: number) => {
@@ -156,6 +164,7 @@ export default function Header({ title, className }: HeaderProps) {
156164
<Modal.SearchInputCard
157165
className={searchInputCard ? 'hidden xl:block' : 'hidden'}
158166
refCard={refSearchInputCard}
167+
inputValue={inputValue}
159168
/>
160169
<Input.SearchActions className="hidden sm:flex">
161170
<div

0 commit comments

Comments
 (0)