Skip to content

Commit d0e6ecb

Browse files
author
Goodspoken
committed
Implement board filtering and support copying both image blob and plain text URL
1 parent 4947c88 commit d0e6ecb

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

frontend/src/i18n.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ export const translations = {
148148
readOnlyToggleDesc: 'Гости смогут просматривать доску, но не смогут добавлять или удалять записи.',
149149
readOnlyError: 'Доска в режиме «только чтение»',
150150
loading: 'Загрузка...',
151+
filterAll: 'Все',
152+
filterImages: 'Картинки',
153+
filterOther: 'Не картинки',
151154
},
152155
EN: {
153156
// Header
@@ -296,5 +299,8 @@ export const translations = {
296299
readOnlyToggleDesc: 'Guests can view the board but cannot add or delete items.',
297300
readOnlyError: 'Board is in read-only mode',
298301
loading: 'Loading...',
302+
filterAll: 'All',
303+
filterImages: 'Images',
304+
filterOther: 'Not Images',
299305
}
300306
};

frontend/src/pages/Board.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useCallback, useMemo } from 'react';
1+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { useBoardStore } from '../stores/useBoardStore';
44
import { useWebSocket } from '../hooks/useWebSocket';
@@ -28,6 +28,19 @@ export default function Board() {
2828
// --- Store ---
2929
const store = useBoardStore();
3030

31+
// --- Filter and Sort ---
32+
const [filter, setFilter] = useState<'all' | 'image' | 'other'>('all');
33+
34+
const filteredItems = useMemo(() => {
35+
if (filter === 'image') {
36+
return store.items.filter(item => item.type === 'image');
37+
}
38+
if (filter === 'other') {
39+
return store.items.filter(item => item.type !== 'image');
40+
}
41+
return store.items;
42+
}, [store.items, filter]);
43+
3144
// --- Sync roomId into store ---
3245
useEffect(() => {
3346
store.setRoomId(roomId || null);
@@ -174,8 +187,48 @@ export default function Board() {
174187
{/* Main Content Area */}
175188
<div className="flex-1 pt-[120px] flex overflow-hidden">
176189
<main className="flex-1 overflow-auto p-4 sm:p-6 custom-scrollbar relative z-10 w-full h-full cursor-default">
190+
{/* Filter Bar */}
191+
<div className="flex items-center justify-between mb-6 max-w-[1920px] mx-auto px-1">
192+
<div className="flex bg-white/80 dark:bg-slate-800/80 backdrop-blur-md p-1 rounded-xl border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
193+
<button
194+
onClick={() => setFilter('all')}
195+
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 cursor-pointer ${
196+
filter === 'all'
197+
? 'bg-indigo-600 text-white shadow-sm'
198+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'
199+
}`}
200+
>
201+
{t('filterAll')}
202+
</button>
203+
<button
204+
onClick={() => setFilter('image')}
205+
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 cursor-pointer ${
206+
filter === 'image'
207+
? 'bg-indigo-600 text-white shadow-sm'
208+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'
209+
}`}
210+
>
211+
{t('filterImages')}
212+
</button>
213+
<button
214+
onClick={() => setFilter('other')}
215+
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 cursor-pointer ${
216+
filter === 'other'
217+
? 'bg-indigo-600 text-white shadow-sm'
218+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'
219+
}`}
220+
>
221+
{t('filterOther')}
222+
</button>
223+
</div>
224+
225+
<div className="text-xs font-medium text-slate-400 dark:text-slate-500 bg-white/40 dark:bg-slate-800/40 px-3 py-1.5 rounded-lg border border-slate-200/30 dark:border-slate-700/30 select-none">
226+
{filteredItems.length}
227+
</div>
228+
</div>
229+
177230
<CardGrid
178-
items={store.items}
231+
items={filteredItems}
179232
copiedId={store.copiedId}
180233
isReadOnly={isReadOnly}
181234
onCopy={(content, id) => store.handleCopy(content, showToast, t, id)}
@@ -189,7 +242,13 @@ export default function Board() {
189242
const res = await fetch(url);
190243
const blob = await res.blob();
191244
const mimeType = blob.type.startsWith('image/') ? blob.type : 'image/png';
192-
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })]);
245+
const textBlob = new Blob([absoluteUrl], { type: 'text/plain' });
246+
await navigator.clipboard.write([
247+
new ClipboardItem({
248+
[mimeType]: blob,
249+
'text/plain': textBlob
250+
})
251+
]);
193252
showToast(t('copied'), 'success');
194253
} catch (err) {
195254
console.warn('Failed to copy image blob, copying URL instead:', err);

0 commit comments

Comments
 (0)