@mdguggenbichler/slugbase-core 0.0.30 → 0.0.32
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/frontend/src/components/FilterChips.tsx +5 -3
- package/frontend/src/components/StatCard.tsx +82 -5
- package/frontend/src/components/bookmarks/BookmarkCard.tsx +317 -210
- package/frontend/src/components/bookmarks/BookmarkTableView.tsx +47 -23
- package/frontend/src/components/collections/CollectionToolbar.tsx +294 -0
- package/frontend/src/components/collections/README.md +44 -0
- package/frontend/src/components/collections/index.ts +2 -0
- package/frontend/src/components/dashboard/DashboardHeader.tsx +16 -0
- package/frontend/src/components/dashboard/MostUsedTagsSection.tsx +49 -0
- package/frontend/src/components/dashboard/PinnedSection.tsx +110 -0
- package/frontend/src/components/dashboard/QuickAccessSection.tsx +120 -0
- package/frontend/src/components/dashboard/README.md +35 -0
- package/frontend/src/components/dashboard/StatsCardsRow.tsx +78 -0
- package/frontend/src/components/dashboard/index.ts +17 -0
- package/frontend/src/config/docs.ts +19 -0
- package/frontend/src/config/mode.ts +6 -4
- package/frontend/src/locales/de.json +2 -0
- package/frontend/src/locales/en.json +1 -0
- package/frontend/src/locales/es.json +2 -0
- package/frontend/src/locales/fr.json +2 -0
- package/frontend/src/locales/it.json +2 -0
- package/frontend/src/locales/ja.json +2 -0
- package/frontend/src/locales/nl.json +2 -0
- package/frontend/src/locales/pl.json +2 -0
- package/frontend/src/locales/pt.json +2 -0
- package/frontend/src/locales/ru.json +2 -0
- package/frontend/src/locales/zh.json +2 -0
- package/frontend/src/pages/Bookmarks.tsx +97 -214
- package/frontend/src/pages/Dashboard.tsx +99 -216
- package/frontend/src/pages/Folders.tsx +181 -251
- package/frontend/src/pages/Profile.tsx +3 -2
- package/frontend/src/pages/Signup.tsx +3 -2
- package/frontend/src/pages/Tags.tsx +87 -145
- package/frontend/src/pages/admin/AdminLayout.tsx +2 -1
- package/package.json +1 -1
|
@@ -4,15 +4,14 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import api from '../api/client';
|
|
5
5
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
6
6
|
import { useConfirmDialog } from '../hooks/useConfirmDialog';
|
|
7
|
-
import { Plus, Edit, Trash2, Tag as TagIcon,
|
|
7
|
+
import { Plus, Edit, Trash2, Tag as TagIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
8
8
|
import TagModal from '../components/modals/TagModal';
|
|
9
9
|
import Button from '../components/ui/Button';
|
|
10
|
-
import
|
|
11
|
-
import { PageHeader } from '../components/PageHeader';
|
|
10
|
+
import { CollectionToolbar } from '../components/collections';
|
|
12
11
|
import { EmptyState } from '../components/EmptyState';
|
|
13
12
|
import { PageLoadingSkeleton } from '../components/ui/PageLoadingSkeleton';
|
|
14
13
|
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
15
|
-
import {
|
|
14
|
+
import { Card } from '../components/ui/card';
|
|
16
15
|
|
|
17
16
|
interface Tag {
|
|
18
17
|
id: string;
|
|
@@ -39,9 +38,6 @@ export default function Tags() {
|
|
|
39
38
|
const saved = localStorage.getItem('tags-view-mode');
|
|
40
39
|
return (saved === 'list' || saved === 'card') ? saved : 'card';
|
|
41
40
|
});
|
|
42
|
-
const [compactMode, setCompactMode] = useState(() => {
|
|
43
|
-
return localStorage.getItem('tags-compact-mode') === 'true';
|
|
44
|
-
});
|
|
45
41
|
|
|
46
42
|
const sortParam = searchParams.get('sort');
|
|
47
43
|
const sortBy = (sortParam === 'recently_added' || sortParam === 'alphabetical') ? sortParam : DEFAULT_SORT;
|
|
@@ -57,10 +53,6 @@ export default function Tags() {
|
|
|
57
53
|
localStorage.setItem('tags-view-mode', viewMode);
|
|
58
54
|
}, [viewMode]);
|
|
59
55
|
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
localStorage.setItem('tags-compact-mode', compactMode.toString());
|
|
62
|
-
}, [compactMode]);
|
|
63
|
-
|
|
64
56
|
useEffect(() => {
|
|
65
57
|
loadTags();
|
|
66
58
|
}, [sortBy, page, pageSize]);
|
|
@@ -171,83 +163,43 @@ export default function Tags() {
|
|
|
171
163
|
|
|
172
164
|
return (
|
|
173
165
|
<div className="space-y-6 pb-24">
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
value={String(pageSize)}
|
|
212
|
-
onChange={(value) => updateParams({ limit: value })}
|
|
213
|
-
options={PAGE_SIZE_OPTIONS.map((n) => ({ value: String(n), label: String(n) }))}
|
|
214
|
-
className="min-w-[80px]"
|
|
215
|
-
/>
|
|
216
|
-
<span className="text-sm text-muted-foreground whitespace-nowrap">{t('bookmarks.perPage')}</span>
|
|
217
|
-
</div>
|
|
218
|
-
<div className="flex items-center gap-2 border-l border-border pl-3 ml-auto">
|
|
219
|
-
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-1 border border-border">
|
|
220
|
-
<button
|
|
221
|
-
onClick={() => setViewMode('card')}
|
|
222
|
-
className={`p-1.5 rounded transition-colors ${
|
|
223
|
-
viewMode === 'card' ? 'bg-background text-primary shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
224
|
-
}`}
|
|
225
|
-
title={t('tags.viewCard')}
|
|
226
|
-
>
|
|
227
|
-
<LayoutGrid className="h-4 w-4" />
|
|
228
|
-
</button>
|
|
229
|
-
<button
|
|
230
|
-
onClick={() => setViewMode('list')}
|
|
231
|
-
className={`p-1.5 rounded transition-colors ${
|
|
232
|
-
viewMode === 'list' ? 'bg-background text-primary shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
233
|
-
}`}
|
|
234
|
-
title={t('tags.viewList')}
|
|
235
|
-
>
|
|
236
|
-
<List className="h-4 w-4" />
|
|
237
|
-
</button>
|
|
238
|
-
</div>
|
|
239
|
-
<button
|
|
240
|
-
onClick={() => setCompactMode(!compactMode)}
|
|
241
|
-
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
|
|
242
|
-
compactMode ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground hover:bg-accent'
|
|
243
|
-
}`}
|
|
244
|
-
title={t('tags.compactMode')}
|
|
245
|
-
>
|
|
246
|
-
{t('tags.compactMode')}
|
|
247
|
-
</button>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
166
|
+
<CollectionToolbar
|
|
167
|
+
title={t('tags.title')}
|
|
168
|
+
count={totalTags}
|
|
169
|
+
subtitle={
|
|
170
|
+
hasActiveFilters || totalTags > pageSize
|
|
171
|
+
? t('bookmarks.showingXOfY', { x: sortedTags.length, y: totalTags })
|
|
172
|
+
: undefined
|
|
173
|
+
}
|
|
174
|
+
createButton={{ label: t('tags.create'), onClick: handleCreate }}
|
|
175
|
+
filterChips={{
|
|
176
|
+
chips: filterChips,
|
|
177
|
+
onRemove: handleRemoveFilter,
|
|
178
|
+
onClearAll: handleResetFilters,
|
|
179
|
+
clearAllLabel: t('bookmarks.clearAllFilters'),
|
|
180
|
+
clearAllAriaLabel: t('bookmarks.clearAllFilters'),
|
|
181
|
+
}}
|
|
182
|
+
sort={{
|
|
183
|
+
value: sortBy,
|
|
184
|
+
onChange: (value) => updateParams({ sort: value as SortOption }),
|
|
185
|
+
options: sortOptions,
|
|
186
|
+
className: 'min-w-[160px]',
|
|
187
|
+
}}
|
|
188
|
+
perPage={{
|
|
189
|
+
value: pageSize,
|
|
190
|
+
onChange: (value) => {
|
|
191
|
+
updateParams({ limit: String(value) });
|
|
192
|
+
},
|
|
193
|
+
options: [...PAGE_SIZE_OPTIONS],
|
|
194
|
+
label: t('bookmarks.perPage'),
|
|
195
|
+
}}
|
|
196
|
+
viewMode={{
|
|
197
|
+
value: viewMode,
|
|
198
|
+
onChange: setViewMode,
|
|
199
|
+
cardLabel: t('tags.viewCard'),
|
|
200
|
+
listLabel: t('tags.viewList'),
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
251
203
|
|
|
252
204
|
{/* Tags Display */}
|
|
253
205
|
{sortedTags.length === 0 ? (
|
|
@@ -262,55 +214,48 @@ export default function Tags() {
|
|
|
262
214
|
}
|
|
263
215
|
/>
|
|
264
216
|
) : viewMode === 'card' ? (
|
|
265
|
-
<div className=
|
|
266
|
-
compactMode
|
|
267
|
-
? 'sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
|
|
268
|
-
: 'sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6'
|
|
269
|
-
}`}>
|
|
217
|
+
<div className="grid gap-3 items-start [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]">
|
|
270
218
|
{sortedTags.map((tag) => (
|
|
271
|
-
<
|
|
219
|
+
<Card
|
|
272
220
|
key={tag.id}
|
|
273
|
-
className=
|
|
221
|
+
className="group relative flex flex-col cursor-pointer rounded-lg border bg-card/95 dark:bg-card/90 transition-[border-color,box-shadow] duration-150 border-border/80 hover:border-primary/80 hover:shadow-[0_2px_6px_rgba(0,0,0,0.06)] dark:border-border/70 dark:hover:border-primary/80 dark:hover:shadow-[0_2px_6px_rgba(0,0,0,0.25)] px-3 pt-0 pb-1.5 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
|
|
274
222
|
>
|
|
275
223
|
<Link
|
|
276
224
|
to={`${prefix}/bookmarks?tag_id=${tag.id}`}
|
|
277
|
-
className="
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
</div>
|
|
284
|
-
<div className="flex-1 min-w-0 pt-0.5">
|
|
285
|
-
<h3 className={`${compactMode ? 'text-xs' : 'text-sm'} font-medium text-foreground truncate`}>
|
|
286
|
-
{tag.name}
|
|
287
|
-
</h3>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
225
|
+
className="absolute inset-0 rounded-lg z-0 focus:outline-none"
|
|
226
|
+
aria-label={tag.name}
|
|
227
|
+
/>
|
|
228
|
+
<header className="flex-shrink-0 flex items-center gap-1.5 min-w-0 pt-3 relative z-10">
|
|
229
|
+
<div className="flex-shrink-0 w-7 h-7 rounded-md bg-background/90 dark:bg-muted/20 flex items-center justify-center border border-border/50 overflow-hidden">
|
|
230
|
+
<TagIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
290
231
|
</div>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
size="sm"
|
|
296
|
-
icon={Edit}
|
|
297
|
-
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
298
|
-
onClick={() => handleEdit(tag)}
|
|
299
|
-
className="flex-1 h-8 min-w-0 text-xs"
|
|
300
|
-
>
|
|
301
|
-
{t('common.edit')}
|
|
302
|
-
</Button>
|
|
303
|
-
<Button
|
|
304
|
-
variant="ghost"
|
|
305
|
-
size="sm"
|
|
306
|
-
icon={Trash2}
|
|
307
|
-
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
308
|
-
onClick={() => handleDelete(tag.id)}
|
|
309
|
-
title={t('common.delete')}
|
|
310
|
-
className="h-8 w-8 p-0 flex-shrink-0 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
311
|
-
/>
|
|
232
|
+
<div className="flex-1 min-w-0 min-h-0 overflow-hidden">
|
|
233
|
+
<h3 className="text-sm font-semibold text-foreground line-clamp-2 break-words leading-snug tracking-tight min-h-0">
|
|
234
|
+
{tag.name}
|
|
235
|
+
</h3>
|
|
312
236
|
</div>
|
|
313
|
-
|
|
237
|
+
</header>
|
|
238
|
+
<footer className="flex-shrink-0 flex items-center justify-end gap-0.5 h-6 min-h-[24px] pt-2.5 relative z-10 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150 w-[52px] ml-auto">
|
|
239
|
+
<Button
|
|
240
|
+
variant="ghost"
|
|
241
|
+
size="sm"
|
|
242
|
+
icon={Edit}
|
|
243
|
+
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
244
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleEdit(tag); }}
|
|
245
|
+
className="h-6 w-6 p-0 min-w-6 text-muted-foreground hover:text-foreground"
|
|
246
|
+
title={t('common.edit')}
|
|
247
|
+
/>
|
|
248
|
+
<Button
|
|
249
|
+
variant="ghost"
|
|
250
|
+
size="sm"
|
|
251
|
+
icon={Trash2}
|
|
252
|
+
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
253
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleDelete(tag.id); }}
|
|
254
|
+
className="h-6 w-6 p-0 min-w-6 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
255
|
+
title={t('common.delete')}
|
|
256
|
+
/>
|
|
257
|
+
</footer>
|
|
258
|
+
</Card>
|
|
314
259
|
))}
|
|
315
260
|
</div>
|
|
316
261
|
) : (
|
|
@@ -318,10 +263,10 @@ export default function Tags() {
|
|
|
318
263
|
<table className="w-full">
|
|
319
264
|
<thead className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-700">
|
|
320
265
|
<tr>
|
|
321
|
-
<th className=
|
|
266
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
|
|
322
267
|
{t('tags.name')}
|
|
323
268
|
</th>
|
|
324
|
-
<th className=
|
|
269
|
+
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
|
|
325
270
|
{t('common.actions')}
|
|
326
271
|
</th>
|
|
327
272
|
</tr>
|
|
@@ -330,32 +275,29 @@ export default function Tags() {
|
|
|
330
275
|
{sortedTags.map((tag) => (
|
|
331
276
|
<tr
|
|
332
277
|
key={tag.id}
|
|
333
|
-
className=
|
|
278
|
+
className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
|
334
279
|
>
|
|
335
|
-
<td className=
|
|
280
|
+
<td className="px-4 py-3">
|
|
336
281
|
<Link
|
|
337
282
|
to={`${prefix}/bookmarks?tag_id=${tag.id}`}
|
|
338
|
-
className=
|
|
283
|
+
className="flex items-center gap-3 hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
|
|
339
284
|
>
|
|
340
|
-
<div className=
|
|
341
|
-
<TagIcon className=
|
|
285
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/20 flex items-center justify-center border border-purple-100 dark:border-purple-800/50">
|
|
286
|
+
<TagIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
342
287
|
</div>
|
|
343
|
-
<div className=
|
|
288
|
+
<div className="font-medium text-gray-900 dark:text-white text-[15px]">
|
|
344
289
|
{tag.name}
|
|
345
290
|
</div>
|
|
346
291
|
</Link>
|
|
347
292
|
</td>
|
|
348
|
-
|
|
349
|
-
<
|
|
350
|
-
)}
|
|
351
|
-
<td className={`${compactMode ? 'px-2 py-1.5' : 'px-4 py-3'}`}>
|
|
352
|
-
<div className={`flex items-center justify-end ${compactMode ? 'gap-1' : 'gap-2'}`}>
|
|
293
|
+
<td className="px-4 py-3">
|
|
294
|
+
<div className="flex items-center justify-end gap-2">
|
|
353
295
|
<Button
|
|
354
296
|
variant="ghost"
|
|
355
297
|
size="sm"
|
|
356
298
|
icon={Edit}
|
|
357
299
|
onClick={() => handleEdit(tag)}
|
|
358
|
-
className=
|
|
300
|
+
className="px-2"
|
|
359
301
|
/>
|
|
360
302
|
<Button
|
|
361
303
|
variant="ghost"
|
|
@@ -363,7 +305,7 @@ export default function Tags() {
|
|
|
363
305
|
icon={Trash2}
|
|
364
306
|
onClick={() => handleDelete(tag.id)}
|
|
365
307
|
title={t('common.delete')}
|
|
366
|
-
className=
|
|
308
|
+
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 px-2"
|
|
367
309
|
/>
|
|
368
310
|
</div>
|
|
369
311
|
</td>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next';
|
|
2
2
|
import { Outlet } from 'react-router-dom';
|
|
3
3
|
import { ExternalLink } from 'lucide-react';
|
|
4
|
+
import { getDocsApiReferenceUrl } from '../../config/docs';
|
|
4
5
|
|
|
5
6
|
export default function AdminLayout() {
|
|
6
7
|
const { t } = useTranslation();
|
|
@@ -25,7 +26,7 @@ export default function AdminLayout() {
|
|
|
25
26
|
</p>
|
|
26
27
|
</div>
|
|
27
28
|
<a
|
|
28
|
-
href=
|
|
29
|
+
href={getDocsApiReferenceUrl()}
|
|
29
30
|
target="_blank"
|
|
30
31
|
rel="noopener noreferrer"
|
|
31
32
|
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary hover:text-primary/90 transition-colors"
|