@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
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Link } from 'react-router-dom';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import Button from '../components/ui/Button';
|
|
4
|
+
import { Share2, X, ChevronDown, ChevronRight, CheckCircle } from 'lucide-react';
|
|
6
5
|
import { Card, CardContent } from '../components/ui/card';
|
|
7
|
-
import { Badge } from '../components/ui/badge';
|
|
8
6
|
import { StatCard } from '../components/StatCard';
|
|
9
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DashboardHeader,
|
|
9
|
+
StatsCardsRow,
|
|
10
|
+
PinnedSection,
|
|
11
|
+
QuickAccessSection,
|
|
12
|
+
MostUsedTagsSection,
|
|
13
|
+
} from '../components/dashboard';
|
|
10
14
|
import api from '../api/client';
|
|
11
15
|
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
12
16
|
|
|
@@ -39,25 +43,9 @@ interface DashboardStats {
|
|
|
39
43
|
pinnedBookmarks?: QuickAccessBookmark[];
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
function getDomain(url: string): string {
|
|
43
|
-
try {
|
|
44
|
-
return new URL(url).hostname.replace(/^www\./, '');
|
|
45
|
-
} catch {
|
|
46
|
-
return '';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getFaviconUrl(url: string): string {
|
|
51
|
-
try {
|
|
52
|
-
const host = new URL(url).hostname;
|
|
53
|
-
return `https://www.google.com/s2/favicons?domain=${host}&sz=32`;
|
|
54
|
-
} catch {
|
|
55
|
-
return '';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
46
|
const PRO_TIP_DISMISSED_KEY = 'slugbase_dashboard_protip_dismissed';
|
|
60
47
|
const ONBOARDING_DISMISSED_KEY = 'slugbase_dashboard_onboarding_dismissed';
|
|
48
|
+
const PINNED_QUICK_ACCESS_MAX_ITEMS = 6;
|
|
61
49
|
|
|
62
50
|
function ProTipBanner({
|
|
63
51
|
onDismiss,
|
|
@@ -72,7 +60,10 @@ function ProTipBanner({
|
|
|
72
60
|
<div className="flex items-start gap-3 rounded-xl border border-border bg-card shadow-sm px-4 py-3">
|
|
73
61
|
<p className="text-sm text-muted-foreground flex-1 min-w-0">
|
|
74
62
|
{t('dashboard.proTipBody')}{' '}
|
|
75
|
-
<Link
|
|
63
|
+
<Link
|
|
64
|
+
to={`${pathPrefix}/search-engine-guide`.replace(/\/+/g, '/') || '/search-engine-guide'}
|
|
65
|
+
className="text-primary font-medium hover:underline"
|
|
66
|
+
>
|
|
76
67
|
{t('dashboard.proTipLink')}
|
|
77
68
|
</Link>
|
|
78
69
|
</p>
|
|
@@ -102,17 +93,35 @@ function OnboardingChecklist({
|
|
|
102
93
|
t: (key: string) => string;
|
|
103
94
|
}) {
|
|
104
95
|
const [collapsed, setCollapsed] = useState(true);
|
|
105
|
-
const [dismissed, setDismissed] = useState(
|
|
96
|
+
const [dismissed, setDismissed] = useState(
|
|
97
|
+
() => typeof window !== 'undefined' && !!localStorage.getItem(ONBOARDING_DISMISSED_KEY)
|
|
98
|
+
);
|
|
106
99
|
const allDone = totalBookmarks > 0 && totalFolders > 0 && topTagsCount > 0;
|
|
107
100
|
const show = !dismissed && !allDone;
|
|
108
101
|
|
|
109
102
|
if (!show) return null;
|
|
110
103
|
|
|
111
104
|
const steps = [
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
{
|
|
106
|
+
done: totalBookmarks > 0,
|
|
107
|
+
label: t('dashboard.onboardingImport'),
|
|
108
|
+
to: `${pathPrefix}/bookmarks?import=true`.replace(/\/+/g, '/') || '/bookmarks?import=true',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
done: false,
|
|
112
|
+
label: t('dashboard.onboardingSearchEngine'),
|
|
113
|
+
to: `${pathPrefix}/search-engine-guide`.replace(/\/+/g, '/') || '/search-engine-guide',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
done: totalFolders > 0,
|
|
117
|
+
label: t('dashboard.onboardingFolder'),
|
|
118
|
+
to: `${pathPrefix}/folders`.replace(/\/+/g, '/') || '/folders',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
done: topTagsCount > 0,
|
|
122
|
+
label: t('dashboard.onboardingTag'),
|
|
123
|
+
to: `${pathPrefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks',
|
|
124
|
+
},
|
|
116
125
|
];
|
|
117
126
|
|
|
118
127
|
function handleDismiss() {
|
|
@@ -128,7 +137,11 @@ function OnboardingChecklist({
|
|
|
128
137
|
onClick={() => setCollapsed(!collapsed)}
|
|
129
138
|
className="w-full flex items-center gap-2 p-4 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset rounded-t-xl"
|
|
130
139
|
>
|
|
131
|
-
{collapsed ?
|
|
140
|
+
{collapsed ? (
|
|
141
|
+
<ChevronRight className="h-5 w-5 text-muted-foreground" />
|
|
142
|
+
) : (
|
|
143
|
+
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
|
144
|
+
)}
|
|
132
145
|
<span className="font-medium text-sm">{t('dashboard.onboardingTitle')}</span>
|
|
133
146
|
</button>
|
|
134
147
|
{!collapsed && (
|
|
@@ -140,7 +153,11 @@ function OnboardingChecklist({
|
|
|
140
153
|
to={step.to}
|
|
141
154
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
142
155
|
>
|
|
143
|
-
{step.done ?
|
|
156
|
+
{step.done ? (
|
|
157
|
+
<CheckCircle className="h-4 w-4 text-primary shrink-0" />
|
|
158
|
+
) : (
|
|
159
|
+
<span className="w-4 h-4 rounded-full border border-muted-foreground shrink-0" />
|
|
160
|
+
)}
|
|
144
161
|
<span>{step.label}</span>
|
|
145
162
|
</Link>
|
|
146
163
|
</li>
|
|
@@ -164,7 +181,9 @@ export default function Dashboard() {
|
|
|
164
181
|
const { pathPrefixForLinks } = useAppConfig();
|
|
165
182
|
const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
|
|
166
183
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
167
|
-
const [proTipDismissed, setProTipDismissed] = useState(
|
|
184
|
+
const [proTipDismissed, setProTipDismissed] = useState(
|
|
185
|
+
() => typeof window !== 'undefined' && !!localStorage.getItem(PRO_TIP_DISMISSED_KEY)
|
|
186
|
+
);
|
|
168
187
|
|
|
169
188
|
useEffect(() => {
|
|
170
189
|
loadStats();
|
|
@@ -182,9 +201,17 @@ export default function Dashboard() {
|
|
|
182
201
|
const quickAccess = stats?.quickAccessBookmarks ?? [];
|
|
183
202
|
const pinnedBookmarks = stats?.pinnedBookmarks ?? [];
|
|
184
203
|
|
|
204
|
+
function handleBookmarkOpen(id: string, url: string) {
|
|
205
|
+
api.post(`/bookmarks/${id}/track-access`).catch(() => {});
|
|
206
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handleCopyUrl(url: string) {
|
|
210
|
+
navigator.clipboard.writeText(url).catch(() => {});
|
|
211
|
+
}
|
|
212
|
+
|
|
185
213
|
return (
|
|
186
|
-
<div className="space-y-
|
|
187
|
-
{/* Feature discovery banner — dismissable, localStorage */}
|
|
214
|
+
<div className="space-y-6">
|
|
188
215
|
{!proTipDismissed && (
|
|
189
216
|
<ProTipBanner
|
|
190
217
|
onDismiss={() => {
|
|
@@ -196,172 +223,50 @@ export default function Dashboard() {
|
|
|
196
223
|
/>
|
|
197
224
|
)}
|
|
198
225
|
|
|
199
|
-
|
|
226
|
+
<DashboardHeader
|
|
227
|
+
title={t('dashboard.overview')}
|
|
228
|
+
subtitle={t('dashboard.overviewSubtitle')}
|
|
229
|
+
/>
|
|
230
|
+
|
|
200
231
|
{stats && (
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
label
|
|
204
|
-
value
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
href
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
iconColorClassName="text-primary"
|
|
219
|
-
/>
|
|
220
|
-
<StatCard
|
|
221
|
-
label={t('dashboard.statsTags')}
|
|
222
|
-
value={stats.totalTags}
|
|
223
|
-
icon={Tag}
|
|
224
|
-
href={prefix + '/tags'}
|
|
225
|
-
dense
|
|
226
|
-
iconContainerClassName="bg-primary/20"
|
|
227
|
-
iconColorClassName="text-primary"
|
|
228
|
-
/>
|
|
229
|
-
</div>
|
|
232
|
+
<StatsCardsRow
|
|
233
|
+
bookmarks={{
|
|
234
|
+
label: t('dashboard.statsBookmarks'),
|
|
235
|
+
value: stats.totalBookmarks,
|
|
236
|
+
href: prefix + '/bookmarks',
|
|
237
|
+
}}
|
|
238
|
+
folders={{
|
|
239
|
+
label: t('dashboard.statsFolders'),
|
|
240
|
+
value: stats.totalFolders,
|
|
241
|
+
href: prefix + '/folders',
|
|
242
|
+
}}
|
|
243
|
+
tags={{
|
|
244
|
+
label: t('dashboard.statsTags'),
|
|
245
|
+
value: stats.totalTags,
|
|
246
|
+
href: prefix + '/tags',
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
230
249
|
)}
|
|
231
250
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
className="text-sm font-medium text-primary hover:underline"
|
|
241
|
-
>
|
|
242
|
-
{t('dashboard.viewAll')}
|
|
243
|
-
<ArrowRight className="inline-block ml-1 h-4 w-4" />
|
|
244
|
-
</Link>
|
|
245
|
-
</div>
|
|
246
|
-
{pinnedBookmarks.length > 0 ? (
|
|
247
|
-
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 gap-3 overflow-hidden min-w-0">
|
|
248
|
-
{pinnedBookmarks.map((b) => (
|
|
249
|
-
<button
|
|
250
|
-
key={b.id}
|
|
251
|
-
type="button"
|
|
252
|
-
onClick={() => {
|
|
253
|
-
api.post(`/bookmarks/${b.id}/track-access`).catch(() => {});
|
|
254
|
-
window.open(b.url, '_blank', 'noopener,noreferrer');
|
|
255
|
-
}}
|
|
256
|
-
className="group flex flex-col rounded-xl border border-border bg-card p-3 hover:border-primary/50 hover:shadow-md transition-all text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
257
|
-
>
|
|
258
|
-
<div className="flex items-start gap-2.5 mb-1.5">
|
|
259
|
-
<div className="relative h-7 w-7 shrink-0 rounded bg-muted overflow-hidden">
|
|
260
|
-
<img
|
|
261
|
-
src={getFaviconUrl(b.url)}
|
|
262
|
-
alt=""
|
|
263
|
-
className="h-7 w-7 w-full object-cover"
|
|
264
|
-
onError={(e) => {
|
|
265
|
-
(e.target as HTMLImageElement).style.display = 'none';
|
|
266
|
-
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden');
|
|
267
|
-
}}
|
|
268
|
-
/>
|
|
269
|
-
<span className="hidden absolute inset-0 flex items-center justify-center bg-primary/10 rounded">
|
|
270
|
-
<Bookmark className="h-3.5 w-3.5 text-primary" />
|
|
271
|
-
</span>
|
|
272
|
-
</div>
|
|
273
|
-
<p className="font-medium text-foreground line-clamp-2 text-xs flex-1 min-w-0">{b.title}</p>
|
|
274
|
-
</div>
|
|
275
|
-
<p className="text-[11px] text-primary font-mono truncate" title={b.slug ? `go/${b.slug}` : undefined}>{b.slug ? `go/${b.slug}` : getDomain(b.url)}</p>
|
|
276
|
-
<p className="text-[11px] text-muted-foreground truncate mt-0.5">{getDomain(b.url)}</p>
|
|
277
|
-
</button>
|
|
278
|
-
))}
|
|
279
|
-
</div>
|
|
280
|
-
) : (
|
|
281
|
-
<Card className="border border-border bg-card shadow-sm">
|
|
282
|
-
<CardContent className="p-6">
|
|
283
|
-
<EmptyState
|
|
284
|
-
icon={Bookmark}
|
|
285
|
-
title={t('dashboard.noPinnedBookmarks')}
|
|
286
|
-
description={t('dashboard.pinFromBookmarks')}
|
|
287
|
-
action={
|
|
288
|
-
<Link to={prefix + '/bookmarks'}>
|
|
289
|
-
<Button variant="secondary">{t('dashboard.pinFromBookmarksLink')}</Button>
|
|
290
|
-
</Link>
|
|
291
|
-
}
|
|
292
|
-
/>
|
|
293
|
-
</CardContent>
|
|
294
|
-
</Card>
|
|
295
|
-
)}
|
|
296
|
-
</section>
|
|
251
|
+
<PinnedSection
|
|
252
|
+
items={pinnedBookmarks}
|
|
253
|
+
pathPrefix={prefix}
|
|
254
|
+
maxItems={PINNED_QUICK_ACCESS_MAX_ITEMS}
|
|
255
|
+
t={t}
|
|
256
|
+
onOpen={handleBookmarkOpen}
|
|
257
|
+
onCopyUrl={handleCopyUrl}
|
|
258
|
+
/>
|
|
297
259
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
>
|
|
308
|
-
{t('dashboard.viewAll')}
|
|
309
|
-
<ArrowRight className="inline-block ml-1 h-4 w-4" />
|
|
310
|
-
</Link>
|
|
311
|
-
</div>
|
|
312
|
-
{quickAccess.length > 0 ? (
|
|
313
|
-
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 gap-3 overflow-hidden min-w-0">
|
|
314
|
-
{quickAccess.map((b) => (
|
|
315
|
-
<button
|
|
316
|
-
key={b.id}
|
|
317
|
-
type="button"
|
|
318
|
-
onClick={() => {
|
|
319
|
-
api.post(`/bookmarks/${b.id}/track-access`).catch(() => {});
|
|
320
|
-
window.open(b.url, '_blank', 'noopener,noreferrer');
|
|
321
|
-
}}
|
|
322
|
-
className="group flex flex-col rounded-xl border border-border bg-card p-3 hover:border-primary/50 hover:shadow-md transition-all text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
323
|
-
>
|
|
324
|
-
<div className="flex items-start gap-2.5 mb-1.5">
|
|
325
|
-
<div className="relative h-7 w-7 shrink-0 rounded bg-muted overflow-hidden">
|
|
326
|
-
<img
|
|
327
|
-
src={getFaviconUrl(b.url)}
|
|
328
|
-
alt=""
|
|
329
|
-
className="h-7 w-7 w-full object-cover"
|
|
330
|
-
onError={(e) => {
|
|
331
|
-
(e.target as HTMLImageElement).style.display = 'none';
|
|
332
|
-
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden');
|
|
333
|
-
}}
|
|
334
|
-
/>
|
|
335
|
-
<span className="hidden absolute inset-0 flex items-center justify-center bg-primary/10 rounded">
|
|
336
|
-
<Bookmark className="h-3.5 w-3.5 text-primary" />
|
|
337
|
-
</span>
|
|
338
|
-
</div>
|
|
339
|
-
<p className="font-medium text-foreground line-clamp-2 text-xs flex-1 min-w-0">{b.title}</p>
|
|
340
|
-
</div>
|
|
341
|
-
<p className="text-[11px] text-primary font-mono truncate" title={`go/${b.slug}`}>go/{b.slug}</p>
|
|
342
|
-
<p className="text-[11px] text-muted-foreground truncate mt-0.5">{getDomain(b.url)}</p>
|
|
343
|
-
</button>
|
|
344
|
-
))}
|
|
345
|
-
</div>
|
|
346
|
-
) : (
|
|
347
|
-
<Card className="border border-border bg-card shadow-sm">
|
|
348
|
-
<CardContent className="p-6">
|
|
349
|
-
<EmptyState
|
|
350
|
-
icon={Bookmark}
|
|
351
|
-
title={t('dashboard.noQuickAccessBookmarks')}
|
|
352
|
-
description={t('dashboard.noQuickAccessBookmarksHint')}
|
|
353
|
-
action={
|
|
354
|
-
<Link to={`${prefix}/bookmarks?create=true`}>
|
|
355
|
-
<Button variant="primary" icon={Plus}>{t('bookmarks.create')}</Button>
|
|
356
|
-
</Link>
|
|
357
|
-
}
|
|
358
|
-
/>
|
|
359
|
-
</CardContent>
|
|
360
|
-
</Card>
|
|
361
|
-
)}
|
|
362
|
-
</section>
|
|
260
|
+
<QuickAccessSection
|
|
261
|
+
items={quickAccess}
|
|
262
|
+
pathPrefix={prefix}
|
|
263
|
+
maxItems={PINNED_QUICK_ACCESS_MAX_ITEMS}
|
|
264
|
+
subtitle={t('dashboard.quickAccessSubtitle')}
|
|
265
|
+
t={t}
|
|
266
|
+
onOpen={handleBookmarkOpen}
|
|
267
|
+
onCopyUrl={handleCopyUrl}
|
|
268
|
+
/>
|
|
363
269
|
|
|
364
|
-
{/* Shared With You — only when has shared content */}
|
|
365
270
|
{stats && (stats.sharedBookmarks > 0 || stats.sharedFolders > 0) && (
|
|
366
271
|
<section className="space-y-3">
|
|
367
272
|
<h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
|
|
@@ -388,32 +293,10 @@ export default function Dashboard() {
|
|
|
388
293
|
</section>
|
|
389
294
|
)}
|
|
390
295
|
|
|
391
|
-
{/* Most used tags — small row; click goes to bookmarks filtered by tag */}
|
|
392
296
|
{stats && stats.topTags.length > 0 && (
|
|
393
|
-
<
|
|
394
|
-
<h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-2">
|
|
395
|
-
<TrendingUp className="h-4 w-4" />
|
|
396
|
-
{t('dashboard.topTags')}
|
|
397
|
-
</h2>
|
|
398
|
-
<div className="flex flex-wrap gap-2">
|
|
399
|
-
{stats.topTags.map((tag) => (
|
|
400
|
-
<Link
|
|
401
|
-
key={tag.id}
|
|
402
|
-
to={`${prefix}/bookmarks?tag_id=${tag.id}`}
|
|
403
|
-
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-foreground hover:bg-accent hover:border-primary/50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
404
|
-
title={t('dashboard.filterByTagHint')}
|
|
405
|
-
>
|
|
406
|
-
<span>{tag.name}</span>
|
|
407
|
-
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 font-normal">
|
|
408
|
-
{tag.bookmark_count}
|
|
409
|
-
</Badge>
|
|
410
|
-
</Link>
|
|
411
|
-
))}
|
|
412
|
-
</div>
|
|
413
|
-
</section>
|
|
297
|
+
<MostUsedTagsSection tags={stats.topTags} pathPrefix={prefix} t={t} />
|
|
414
298
|
)}
|
|
415
299
|
|
|
416
|
-
{/* Onboarding checklist — collapsible, show when incomplete and not dismissed */}
|
|
417
300
|
{stats && (
|
|
418
301
|
<OnboardingChecklist
|
|
419
302
|
totalBookmarks={stats.totalBookmarks}
|