Skip to content

Commit e515ffe

Browse files
committed
feat: search
1 parent 1dd1f15 commit e515ffe

3 files changed

Lines changed: 91 additions & 62 deletions

File tree

src/lib/i18n.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ const translations = {
4444
orderedList: 'Numbered List',
4545
quote: 'Quote',
4646
said: 'said',
47+
searchByAuthor: 'Search by author',
48+
searchByText: 'Search by text',
4749
welcomeTitle: 'Welcome to the Forum Template',
4850
welcomeMessage: 'This is a modern forum application built with React, TypeScript, and Tailwind CSS. You can use this as a template to create your own forum.',
4951
featuresTitle: 'Features',
@@ -91,6 +93,8 @@ const translations = {
9193
orderedList: 'Нумерованный список',
9294
quote: 'Цитата',
9395
said: 'сказал',
96+
searchByAuthor: 'Поиск по автору',
97+
searchByText: 'Поиск по тексту',
9498
welcomeTitle: 'Добро пожаловать в шаблон форума',
9599
welcomeMessage: 'Это современное форумное приложение, созданное с помощью React, TypeScript и Tailwind CSS. Вы можете использовать это как шаблон для создания своего собственного форума.',
96100
featuresTitle: 'Возможности',

src/pages/ForumPage.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export function ForumPage() {
2323
const [isCreating, setIsCreating] = useState(false);
2424
const [newTopicTitle, setNewTopicTitle] = useState('');
2525
const [newTopicBody, setNewTopicBody] = useState('');
26+
const [authorSearch, setAuthorSearch] = useState('');
27+
const [textSearch, setTextSearch] = useState('');
2628
const { t } = useTranslation();
2729
const { user } = useAuth();
2830
const loadData = async () => {
@@ -46,6 +48,10 @@ export function ForumPage() {
4648
useEffect(() => {
4749
loadData();
4850
}, [slug]);
51+
const filteredTopics = topics.filter(topic =>
52+
(authorSearch === '' || topic.author.toLowerCase().includes(authorSearch.toLowerCase())) &&
53+
(textSearch === '' || topic.body.toLowerCase().includes(textSearch.toLowerCase()))
54+
);
4955
const handleCreateTopic = async (e: React.FormEvent) => {
5056
e.preventDefault();
5157
if (!user || !slug) return;
@@ -116,6 +122,25 @@ export function ForumPage() {
116122
</div>
117123
</div>
118124

125+
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
126+
<div className="flex gap-4">
127+
<input
128+
type="text"
129+
placeholder={t('searchByAuthor')}
130+
value={authorSearch}
131+
onChange={(e) => setAuthorSearch(e.target.value)}
132+
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
133+
/>
134+
<input
135+
type="text"
136+
placeholder={t('searchByText')}
137+
value={textSearch}
138+
onChange={(e) => setTextSearch(e.target.value)}
139+
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
140+
/>
141+
</div>
142+
</div>
143+
119144
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
120145
{topics.length === 0 ?
121146
<div className="text-center py-12">
@@ -124,7 +149,7 @@ export function ForumPage() {
124149
</div> :
125150

126151
<div className="divide-y divide-gray-200 dark:divide-gray-700">
127-
{topics.map((topic) =>
152+
{filteredTopics.map((topic) =>
128153
<Link
129154
key={topic.id}
130155
to={`/topic/${topic.id}`}

src/pages/TopicPage.tsx

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export function TopicPage() {
1616
const [error, setError] = useState('');
1717
const [replyBody, setReplyBody] = useState('');
1818
const [isReplying, setIsReplying] = useState(false);
19+
const [authorSearch, setAuthorSearch] = useState('');
20+
const [textSearch, setTextSearch] = useState('');
1921
const { t } = useTranslation();
2022
const { user } = useAuth();
2123
const loadData = async () => {
@@ -39,6 +41,14 @@ export function TopicPage() {
3941
useEffect(() => {
4042
loadData();
4143
}, [id]);
44+
const allMessages = topic ? [
45+
{ id: 'original', author: topic.author, body: topic.body, createdAt: topic.createdAt },
46+
...posts
47+
] : [];
48+
const filteredMessages = allMessages.filter(message =>
49+
(authorSearch === '' || message.author.toLowerCase().includes(authorSearch.toLowerCase())) &&
50+
(textSearch === '' || message.body.toLowerCase().includes(textSearch.toLowerCase()))
51+
);
4252
const handleReply = async (e: React.FormEvent) => {
4353
e.preventDefault();
4454
if (!user || !id || !replyBody.trim()) return;
@@ -65,6 +75,39 @@ export function TopicPage() {
6575
const quote = `> ${author} ${t('said')}:\n> ${body.replace(/\n/g, '\n> ')}\n\n`;
6676
setReplyBody(prev => prev + quote);
6777
};
78+
const renderMessage = (message: any, isOriginal: boolean) => (
79+
<div
80+
key={message.id}
81+
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
82+
83+
<div className="flex flex-col sm:flex-row">
84+
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 sm:w-48 border-b sm:border-b-0 sm:border-r border-gray-200 dark:border-gray-700 flex flex-col items-center sm:items-start">
85+
<div className="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 mb-2">
86+
<UserIcon className="w-8 h-8" />
87+
</div>
88+
<span className="font-medium text-center sm:text-left w-full break-words">
89+
{message.author}
90+
</span>
91+
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-1">
92+
<Clock className="w-3 h-3" />
93+
{new Date(message.createdAt).toLocaleDateString()}
94+
</span>
95+
</div>
96+
<div className="p-6 flex-1">
97+
<MarkdownContent content={message.body} />
98+
{user && (
99+
<button
100+
onClick={() => handleQuote(message.author, message.body)}
101+
className="mt-2 px-3 py-1 text-sm bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded flex items-center gap-1"
102+
>
103+
<Quote className="w-3 h-3" />
104+
{t('quote')}
105+
</button>
106+
)}
107+
</div>
108+
</div>
109+
</div>
110+
);
68111
if (isLoading) {
69112
return (
70113
<div className="flex justify-center items-center py-20">
@@ -94,70 +137,27 @@ export function TopicPage() {
94137
<h1 className="text-2xl font-bold">{topic.title}</h1>
95138
</div>
96139

97-
{/* Original Topic Post */}
98-
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
99-
<div className="flex flex-col sm:flex-row">
100-
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 sm:w-48 border-b sm:border-b-0 sm:border-r border-gray-200 dark:border-gray-700 flex flex-col items-center sm:items-start">
101-
<div className="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 mb-2">
102-
<UserIcon className="w-8 h-8" />
103-
</div>
104-
<span className="font-medium text-center sm:text-left w-full break-words">
105-
{topic.author}
106-
</span>
107-
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-1">
108-
<Clock className="w-3 h-3" />
109-
{new Date(topic.createdAt).toLocaleDateString()}
110-
</span>
111-
</div>
112-
<div className="p-6 flex-1">
113-
<MarkdownContent content={topic.body} />
114-
{user && (
115-
<button
116-
onClick={() => handleQuote(topic.author, topic.body)}
117-
className="mt-2 px-3 py-1 text-sm bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded flex items-center gap-1"
118-
>
119-
<Quote className="w-3 h-3" />
120-
{t('quote')}
121-
</button>
122-
)}
123-
</div>
140+
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
141+
<div className="flex gap-4">
142+
<input
143+
type="text"
144+
placeholder={t('searchByAuthor')}
145+
value={authorSearch}
146+
onChange={(e) => setAuthorSearch(e.target.value)}
147+
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
148+
/>
149+
<input
150+
type="text"
151+
placeholder={t('searchByText')}
152+
value={textSearch}
153+
onChange={(e) => setTextSearch(e.target.value)}
154+
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
155+
/>
124156
</div>
125157
</div>
126158

127-
{/* Replies */}
128-
{posts.map((post) =>
129-
<div
130-
key={post.id}
131-
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
132-
133-
<div className="flex flex-col sm:flex-row">
134-
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 sm:w-48 border-b sm:border-b-0 sm:border-r border-gray-200 dark:border-gray-700 flex flex-col items-center sm:items-start">
135-
<div className="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 mb-2">
136-
<UserIcon className="w-8 h-8" />
137-
</div>
138-
<span className="font-medium text-center sm:text-left w-full break-words">
139-
{post.author}
140-
</span>
141-
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-1">
142-
<Clock className="w-3 h-3" />
143-
{new Date(post.createdAt).toLocaleDateString()}
144-
</span>
145-
</div>
146-
<div className="p-6 flex-1">
147-
<MarkdownContent content={post.body} />
148-
{user && (
149-
<button
150-
onClick={() => handleQuote(post.author, post.body)}
151-
className="mt-2 px-3 py-1 text-sm bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded flex items-center gap-1"
152-
>
153-
<Quote className="w-3 h-3" />
154-
{t('quote')}
155-
</button>
156-
)}
157-
</div>
158-
</div>
159-
</div>
160-
)}
159+
{/* Messages */}
160+
{filteredMessages.map((message) => renderMessage(message, message.id === 'original'))}
161161

162162
{/* Reply Form */}
163163
{user ?

0 commit comments

Comments
 (0)