bloby-bot 0.39.1 → 0.41.0
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 +1 -1
- package/supervisor/channels/manager.ts +31 -0
- package/supervisor/channels/whatsapp.ts +43 -22
- package/workspace/client/index.html +0 -3
- package/workspace/client/src/App.tsx +9 -20
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +113 -117
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +34 -32
- package/workspace/client/src/components/Layout/MobileNav.tsx +6 -103
- package/workspace/client/src/components/Layout/Sidebar.tsx +10 -11
- package/workspace/client/src/styles/globals.css +4 -89
- package/workspace/client/public/.well-known/assetlinks.json +0 -8
- package/workspace/client/public/bloby-cyberpunk.png +0 -0
- package/workspace/client/public/brand/blackrock.svg +0 -8
- package/workspace/client/public/kid-breakfast.png +0 -0
- package/workspace/client/public/wallpapers/bg.jpg +0 -0
- package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
- package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
- package/workspace/client/src/components/Dashboard/AiChatPage.tsx +0 -145
- package/workspace/client/src/components/Dashboard/CryptoPage.tsx +0 -470
- package/workspace/client/src/components/Dashboard/WishlistPage.tsx +0 -464
- package/workspace/client/src/components/Layout/MiniSidebar.tsx +0 -64
- package/workspace/client/src/components/Lock/PinInput.tsx +0 -107
- package/workspace/client/src/components/Lock/WorkspaceLock.tsx +0 -484
- package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +0 -396
- package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +0 -427
- package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +0 -12
- package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +0 -160
- package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +0 -67
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import { X, Plus, StickyNote } from 'lucide-react';
|
|
3
|
-
|
|
4
|
-
// Adjust this if your workspace proxies API calls differently.
|
|
5
|
-
// Default Bloby workspaces: '/app/api'. Direct backend: '/api'.
|
|
6
|
-
const API_BASE = '/app/api';
|
|
7
|
-
|
|
8
|
-
interface StickyNote {
|
|
9
|
-
id: number;
|
|
10
|
-
content: string;
|
|
11
|
-
color: string;
|
|
12
|
-
x: number;
|
|
13
|
-
y: number;
|
|
14
|
-
width: number;
|
|
15
|
-
height: number;
|
|
16
|
-
visible: number;
|
|
17
|
-
created_at: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const PASTEL_COLORS = ['#E6C97A', '#D4A0B0', '#8BBD9F', '#8BAFC4', '#B0A0C8', '#D4AD8A'];
|
|
21
|
-
|
|
22
|
-
function randomRotation(id: number): number {
|
|
23
|
-
const seed = ((id * 9301 + 49297) % 233280) / 233280;
|
|
24
|
-
return (seed - 0.5) * 6; // -3 to +3 degrees
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default function StickyNotesOverlay() {
|
|
28
|
-
const [notes, setNotes] = useState<StickyNote[]>([]);
|
|
29
|
-
const [allVisible, setAllVisible] = useState(true);
|
|
30
|
-
const [focusedId, setFocusedId] = useState<number | null>(null);
|
|
31
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
-
|
|
33
|
-
const fetchNotes = useCallback(async () => {
|
|
34
|
-
try {
|
|
35
|
-
const res = await fetch(`${API_BASE}/sticky-notes`);
|
|
36
|
-
if (!res.ok) return;
|
|
37
|
-
const data: StickyNote[] = await res.json();
|
|
38
|
-
setNotes(data);
|
|
39
|
-
setAllVisible(data.length === 0 || data.some((n) => n.visible === 1));
|
|
40
|
-
} catch {
|
|
41
|
-
// silent
|
|
42
|
-
}
|
|
43
|
-
}, []);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
fetchNotes();
|
|
47
|
-
}, [fetchNotes]);
|
|
48
|
-
|
|
49
|
-
const createNote = useCallback(async () => {
|
|
50
|
-
// If notes are hidden, turn visibility on first
|
|
51
|
-
if (!allVisible) {
|
|
52
|
-
try {
|
|
53
|
-
await fetch(`${API_BASE}/sticky-notes/toggle-visibility`, { method: 'POST' });
|
|
54
|
-
} catch { /* silent */ }
|
|
55
|
-
}
|
|
56
|
-
const color = PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)];
|
|
57
|
-
const x = 60 + Math.floor(Math.random() * 300);
|
|
58
|
-
const y = 60 + Math.floor(Math.random() * 200);
|
|
59
|
-
try {
|
|
60
|
-
const res = await fetch(`${API_BASE}/sticky-notes`, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: { 'Content-Type': 'application/json' },
|
|
63
|
-
body: JSON.stringify({ content: '', color, x, y, width: 200, height: 180, visible: 1 }),
|
|
64
|
-
});
|
|
65
|
-
if (!res.ok) return;
|
|
66
|
-
const created = await res.json();
|
|
67
|
-
setAllVisible(true);
|
|
68
|
-
setFocusedId(created.id);
|
|
69
|
-
fetchNotes();
|
|
70
|
-
} catch {
|
|
71
|
-
// silent
|
|
72
|
-
}
|
|
73
|
-
}, [fetchNotes, allVisible]);
|
|
74
|
-
|
|
75
|
-
const toggleVisibility = useCallback(async () => {
|
|
76
|
-
try {
|
|
77
|
-
const res = await fetch(`${API_BASE}/sticky-notes/toggle-visibility`, { method: 'POST' });
|
|
78
|
-
if (!res.ok) return;
|
|
79
|
-
const data = await res.json();
|
|
80
|
-
setNotes(data.notes);
|
|
81
|
-
setAllVisible(data.visible === 1);
|
|
82
|
-
} catch {
|
|
83
|
-
// silent
|
|
84
|
-
}
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
const deleteNote = useCallback(async (id: number) => {
|
|
88
|
-
try {
|
|
89
|
-
await fetch(`${API_BASE}/sticky-notes/${id}`, { method: 'DELETE' });
|
|
90
|
-
setNotes((prev) => prev.filter((n) => n.id !== id));
|
|
91
|
-
} catch {
|
|
92
|
-
// silent
|
|
93
|
-
}
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
const updateNote = useCallback(async (id: number, updates: Partial<StickyNote>) => {
|
|
97
|
-
try {
|
|
98
|
-
await fetch(`${API_BASE}/sticky-notes/${id}`, {
|
|
99
|
-
method: 'PUT',
|
|
100
|
-
headers: { 'Content-Type': 'application/json' },
|
|
101
|
-
body: JSON.stringify(updates),
|
|
102
|
-
});
|
|
103
|
-
} catch {
|
|
104
|
-
// silent
|
|
105
|
-
}
|
|
106
|
-
}, []);
|
|
107
|
-
|
|
108
|
-
const visibleNotes = notes.filter((n) => n.visible === 1);
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<div ref={containerRef} className="absolute inset-0 pointer-events-none hidden md:block" style={{ zIndex: 30 }}>
|
|
112
|
-
{visibleNotes.map((note) => (
|
|
113
|
-
<DraggableNote
|
|
114
|
-
key={note.id}
|
|
115
|
-
note={note}
|
|
116
|
-
containerRef={containerRef}
|
|
117
|
-
onDelete={deleteNote}
|
|
118
|
-
onUpdate={updateNote}
|
|
119
|
-
isFocused={focusedId === note.id}
|
|
120
|
-
onFocus={() => setFocusedId(note.id)}
|
|
121
|
-
/>
|
|
122
|
-
))}
|
|
123
|
-
<FAB onCreate={createNote} onToggle={toggleVisibility} allVisible={allVisible} noteCount={notes.length} />
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function DraggableNote({
|
|
129
|
-
note,
|
|
130
|
-
containerRef,
|
|
131
|
-
onDelete,
|
|
132
|
-
onUpdate,
|
|
133
|
-
isFocused,
|
|
134
|
-
onFocus,
|
|
135
|
-
}: {
|
|
136
|
-
note: StickyNote;
|
|
137
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
138
|
-
onDelete: (id: number) => void;
|
|
139
|
-
onUpdate: (id: number, updates: Partial<StickyNote>) => void;
|
|
140
|
-
isFocused: boolean;
|
|
141
|
-
onFocus: () => void;
|
|
142
|
-
}) {
|
|
143
|
-
const [pos, setPos] = useState({ x: note.x, y: note.y });
|
|
144
|
-
const [dragging, setDragging] = useState(false);
|
|
145
|
-
const dragOffset = useRef({ x: 0, y: 0 });
|
|
146
|
-
const noteRef = useRef<HTMLDivElement>(null);
|
|
147
|
-
const rotation = randomRotation(note.id);
|
|
148
|
-
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
setPos({ x: note.x, y: note.y });
|
|
151
|
-
}, [note.x, note.y]);
|
|
152
|
-
|
|
153
|
-
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
|
154
|
-
onFocus();
|
|
155
|
-
if ((e.target as HTMLElement).closest('textarea') || (e.target as HTMLElement).closest('button')) return;
|
|
156
|
-
e.preventDefault();
|
|
157
|
-
const rect = noteRef.current?.getBoundingClientRect();
|
|
158
|
-
if (!rect) return;
|
|
159
|
-
dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
160
|
-
setDragging(true);
|
|
161
|
-
}, [onFocus]);
|
|
162
|
-
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
if (!dragging) return;
|
|
165
|
-
|
|
166
|
-
const onMove = (e: MouseEvent) => {
|
|
167
|
-
const container = containerRef.current;
|
|
168
|
-
if (!container) return;
|
|
169
|
-
const cr = container.getBoundingClientRect();
|
|
170
|
-
const newX = Math.max(0, Math.min(e.clientX - cr.left - dragOffset.current.x, cr.width - note.width));
|
|
171
|
-
const newY = Math.max(0, Math.min(e.clientY - cr.top - dragOffset.current.y, cr.height - note.height));
|
|
172
|
-
setPos({ x: newX, y: newY });
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const onUp = () => {
|
|
176
|
-
setDragging(false);
|
|
177
|
-
setPos((current) => {
|
|
178
|
-
onUpdate(note.id, { x: Math.round(current.x), y: Math.round(current.y) });
|
|
179
|
-
return current;
|
|
180
|
-
});
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
window.addEventListener('mousemove', onMove);
|
|
184
|
-
window.addEventListener('mouseup', onUp);
|
|
185
|
-
return () => {
|
|
186
|
-
window.removeEventListener('mousemove', onMove);
|
|
187
|
-
window.removeEventListener('mouseup', onUp);
|
|
188
|
-
};
|
|
189
|
-
}, [dragging, containerRef, note.id, note.width, note.height, onUpdate]);
|
|
190
|
-
|
|
191
|
-
// Touch support
|
|
192
|
-
const onTouchStart = useCallback((e: React.TouchEvent) => {
|
|
193
|
-
onFocus();
|
|
194
|
-
if ((e.target as HTMLElement).closest('textarea') || (e.target as HTMLElement).closest('button')) return;
|
|
195
|
-
const touch = e.touches[0];
|
|
196
|
-
const rect = noteRef.current?.getBoundingClientRect();
|
|
197
|
-
if (!rect) return;
|
|
198
|
-
dragOffset.current = { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
|
|
199
|
-
setDragging(true);
|
|
200
|
-
}, [onFocus]);
|
|
201
|
-
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
if (!dragging) return;
|
|
204
|
-
|
|
205
|
-
const onTouchMove = (e: TouchEvent) => {
|
|
206
|
-
const container = containerRef.current;
|
|
207
|
-
if (!container) return;
|
|
208
|
-
const touch = e.touches[0];
|
|
209
|
-
const cr = container.getBoundingClientRect();
|
|
210
|
-
const newX = Math.max(0, Math.min(touch.clientX - cr.left - dragOffset.current.x, cr.width - note.width));
|
|
211
|
-
const newY = Math.max(0, Math.min(touch.clientY - cr.top - dragOffset.current.y, cr.height - note.height));
|
|
212
|
-
setPos({ x: newX, y: newY });
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const onTouchEnd = () => {
|
|
216
|
-
setDragging(false);
|
|
217
|
-
setPos((current) => {
|
|
218
|
-
onUpdate(note.id, { x: Math.round(current.x), y: Math.round(current.y) });
|
|
219
|
-
return current;
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
window.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
224
|
-
window.addEventListener('touchend', onTouchEnd);
|
|
225
|
-
return () => {
|
|
226
|
-
window.removeEventListener('touchmove', onTouchMove);
|
|
227
|
-
window.removeEventListener('touchend', onTouchEnd);
|
|
228
|
-
};
|
|
229
|
-
}, [dragging, containerRef, note.id, note.width, note.height, onUpdate]);
|
|
230
|
-
|
|
231
|
-
const handleBlur = useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
232
|
-
const newContent = e.target.value;
|
|
233
|
-
if (newContent !== note.content) {
|
|
234
|
-
onUpdate(note.id, { content: newContent });
|
|
235
|
-
}
|
|
236
|
-
}, [note.id, note.content, onUpdate]);
|
|
237
|
-
|
|
238
|
-
// Darken the pastel for text readability
|
|
239
|
-
const textColor = '#1a1a1a';
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<div
|
|
243
|
-
ref={noteRef}
|
|
244
|
-
className="absolute pointer-events-auto group"
|
|
245
|
-
style={{
|
|
246
|
-
left: pos.x,
|
|
247
|
-
top: pos.y,
|
|
248
|
-
width: note.width,
|
|
249
|
-
height: note.height,
|
|
250
|
-
zIndex: dragging ? 50 : isFocused ? 45 : 35,
|
|
251
|
-
transform: `rotate(${rotation}deg)`,
|
|
252
|
-
transition: dragging ? 'none' : 'box-shadow 0.2s',
|
|
253
|
-
}}
|
|
254
|
-
onMouseDown={onMouseDown}
|
|
255
|
-
onTouchStart={onTouchStart}
|
|
256
|
-
>
|
|
257
|
-
<div
|
|
258
|
-
className="w-full h-full rounded-lg shadow-lg flex flex-col overflow-hidden"
|
|
259
|
-
style={{
|
|
260
|
-
backgroundColor: note.color,
|
|
261
|
-
boxShadow: dragging
|
|
262
|
-
? '0 12px 28px rgba(0,0,0,0.4), 0 4px 8px rgba(0,0,0,0.2)'
|
|
263
|
-
: '0 4px 12px rgba(0,0,0,0.25), 0 1px 3px rgba(0,0,0,0.15)',
|
|
264
|
-
cursor: dragging ? 'grabbing' : 'grab',
|
|
265
|
-
}}
|
|
266
|
-
>
|
|
267
|
-
{/* Top bar with drag handle and delete */}
|
|
268
|
-
<div className="flex items-center justify-between px-2.5 pt-2 pb-1" style={{ color: textColor }}>
|
|
269
|
-
<div className="flex gap-0.5 opacity-40">
|
|
270
|
-
<span className="block w-1 h-1 rounded-full bg-current" />
|
|
271
|
-
<span className="block w-1 h-1 rounded-full bg-current" />
|
|
272
|
-
<span className="block w-1 h-1 rounded-full bg-current" />
|
|
273
|
-
</div>
|
|
274
|
-
<button
|
|
275
|
-
onClick={() => onDelete(note.id)}
|
|
276
|
-
className="opacity-0 group-hover:opacity-70 hover:!opacity-100 p-0.5 rounded transition-opacity"
|
|
277
|
-
style={{ color: textColor }}
|
|
278
|
-
>
|
|
279
|
-
<X className="h-3.5 w-3.5" />
|
|
280
|
-
</button>
|
|
281
|
-
</div>
|
|
282
|
-
|
|
283
|
-
{/* Content area */}
|
|
284
|
-
<textarea
|
|
285
|
-
defaultValue={note.content}
|
|
286
|
-
onBlur={handleBlur}
|
|
287
|
-
placeholder="Write something..."
|
|
288
|
-
className="flex-1 w-full px-2.5 pb-2.5 text-xs leading-relaxed resize-none bg-transparent border-none outline-none placeholder:opacity-40"
|
|
289
|
-
style={{ color: textColor, fontFamily: "'Caveat', 'Segoe Print', 'Comic Sans MS', cursive" }}
|
|
290
|
-
/>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function FAB({
|
|
297
|
-
onCreate,
|
|
298
|
-
onToggle,
|
|
299
|
-
allVisible,
|
|
300
|
-
noteCount,
|
|
301
|
-
}: {
|
|
302
|
-
onCreate: () => void;
|
|
303
|
-
onToggle: () => void;
|
|
304
|
-
allVisible: boolean;
|
|
305
|
-
noteCount: number;
|
|
306
|
-
}) {
|
|
307
|
-
const [pressing, setPressing] = useState(false);
|
|
308
|
-
const [longPressed, setLongPressed] = useState(false);
|
|
309
|
-
const [hovered, setHovered] = useState(false);
|
|
310
|
-
const pressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
311
|
-
const didLongPress = useRef(false);
|
|
312
|
-
|
|
313
|
-
const startPress = useCallback(() => {
|
|
314
|
-
didLongPress.current = false;
|
|
315
|
-
setPressing(true);
|
|
316
|
-
pressTimer.current = setTimeout(() => {
|
|
317
|
-
didLongPress.current = true;
|
|
318
|
-
setLongPressed(true);
|
|
319
|
-
onToggle();
|
|
320
|
-
setPressing(false);
|
|
321
|
-
setTimeout(() => setLongPressed(false), 600);
|
|
322
|
-
}, 500);
|
|
323
|
-
}, [onToggle]);
|
|
324
|
-
|
|
325
|
-
const endPress = useCallback(() => {
|
|
326
|
-
if (pressTimer.current) {
|
|
327
|
-
clearTimeout(pressTimer.current);
|
|
328
|
-
pressTimer.current = null;
|
|
329
|
-
}
|
|
330
|
-
if (!didLongPress.current) {
|
|
331
|
-
onCreate();
|
|
332
|
-
}
|
|
333
|
-
setPressing(false);
|
|
334
|
-
}, [onCreate]);
|
|
335
|
-
|
|
336
|
-
const cancelPress = useCallback(() => {
|
|
337
|
-
if (pressTimer.current) {
|
|
338
|
-
clearTimeout(pressTimer.current);
|
|
339
|
-
pressTimer.current = null;
|
|
340
|
-
}
|
|
341
|
-
setPressing(false);
|
|
342
|
-
}, []);
|
|
343
|
-
|
|
344
|
-
return (
|
|
345
|
-
<div className="absolute bottom-5 left-5 pointer-events-auto flex flex-col items-center gap-2">
|
|
346
|
-
{/* Tooltip on hover */}
|
|
347
|
-
<div
|
|
348
|
-
className="text-[9px] text-muted-foreground/60 bg-[#1a1a1a]/90 px-2.5 py-1 rounded-lg backdrop-blur-sm select-none transition-all duration-200 whitespace-nowrap"
|
|
349
|
-
style={{
|
|
350
|
-
opacity: hovered ? 1 : 0,
|
|
351
|
-
transform: hovered ? 'translateY(0)' : 'translateY(4px)',
|
|
352
|
-
pointerEvents: 'none',
|
|
353
|
-
}}
|
|
354
|
-
>
|
|
355
|
-
hold to toggle visibility
|
|
356
|
-
</div>
|
|
357
|
-
{noteCount > 0 && !hovered && (
|
|
358
|
-
<div className="text-[9px] text-muted-foreground/50 bg-[#1a1a1a]/80 px-2 py-0.5 rounded-full backdrop-blur-sm select-none">
|
|
359
|
-
{allVisible ? `${noteCount} note${noteCount !== 1 ? 's' : ''}` : 'hidden'}
|
|
360
|
-
</div>
|
|
361
|
-
)}
|
|
362
|
-
<div className="relative">
|
|
363
|
-
<button
|
|
364
|
-
onMouseDown={startPress}
|
|
365
|
-
onMouseUp={endPress}
|
|
366
|
-
onMouseLeave={() => { cancelPress(); setHovered(false); }}
|
|
367
|
-
onMouseEnter={() => setHovered(true)}
|
|
368
|
-
onTouchStart={startPress}
|
|
369
|
-
onTouchEnd={(e) => { e.preventDefault(); endPress(); }}
|
|
370
|
-
onTouchCancel={cancelPress}
|
|
371
|
-
className="h-11 w-11 rounded-xl flex items-center justify-center shadow-lg select-none"
|
|
372
|
-
style={{
|
|
373
|
-
background: 'linear-gradient(135deg, #E6C97A, #D4A0B0)',
|
|
374
|
-
transform: pressing ? 'scale(0.9)' : longPressed ? 'scale(1.15)' : 'scale(1)',
|
|
375
|
-
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
376
|
-
boxShadow: longPressed
|
|
377
|
-
? '0 0 20px rgba(230, 201, 122, 0.4), 0 4px 16px rgba(0,0,0,0.3)'
|
|
378
|
-
: '0 4px 12px rgba(0,0,0,0.3)',
|
|
379
|
-
}}
|
|
380
|
-
>
|
|
381
|
-
<StickyNote className="h-5 w-5 text-white/90" />
|
|
382
|
-
</button>
|
|
383
|
-
{/* + badge on hover */}
|
|
384
|
-
<div
|
|
385
|
-
className="absolute -top-1.5 -right-1.5 h-4.5 w-4.5 rounded-full bg-white flex items-center justify-center shadow-md transition-all duration-200"
|
|
386
|
-
style={{
|
|
387
|
-
opacity: hovered && !pressing && !longPressed ? 1 : 0,
|
|
388
|
-
transform: hovered && !pressing && !longPressed ? 'scale(1)' : 'scale(0.5)',
|
|
389
|
-
}}
|
|
390
|
-
>
|
|
391
|
-
<Plus className="h-3 w-3 text-[#1a1a1a]" strokeWidth={3} />
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
);
|
|
396
|
-
}
|