@mdguggenbichler/slugbase-core 0.0.4 → 0.0.6

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.
Files changed (120) hide show
  1. package/frontend/index.tsx +3 -3
  2. package/frontend/public/favicon.svg +1 -0
  3. package/frontend/public/slugbase_icon_blue.svg +1 -0
  4. package/frontend/public/slugbase_icon_white.png +0 -0
  5. package/frontend/public/slugbase_icon_white.svg +1 -0
  6. package/frontend/src/App.tsx +179 -0
  7. package/frontend/src/api/client.ts +134 -0
  8. package/frontend/src/components/AppSidebar.tsx +214 -0
  9. package/frontend/src/components/EmptyState.tsx +33 -0
  10. package/frontend/src/components/Favicon.tsx +76 -0
  11. package/frontend/src/components/FilterChips.tsx +60 -0
  12. package/frontend/src/components/FolderIcon.tsx +207 -0
  13. package/frontend/src/components/GlobalSearch.tsx +275 -0
  14. package/frontend/src/components/Layout.tsx +60 -0
  15. package/frontend/src/components/PageHeader.tsx +31 -0
  16. package/frontend/src/components/ScopeSegmentedControl.tsx +42 -0
  17. package/frontend/src/components/SentryDebug.tsx +32 -0
  18. package/frontend/src/components/StatCard.tsx +66 -0
  19. package/frontend/src/components/TopBar.tsx +63 -0
  20. package/frontend/src/components/UserDropdown.tsx +86 -0
  21. package/frontend/src/components/admin/AdminAI.tsx +207 -0
  22. package/frontend/src/components/admin/AdminOIDCProviders.tsx +183 -0
  23. package/frontend/src/components/admin/AdminSettings.tsx +413 -0
  24. package/frontend/src/components/admin/AdminTeams.tsx +177 -0
  25. package/frontend/src/components/admin/AdminUsers.tsx +225 -0
  26. package/frontend/src/components/bookmarks/BookmarkCard.tsx +312 -0
  27. package/frontend/src/components/bookmarks/BookmarkListItem.tsx +159 -0
  28. package/frontend/src/components/bookmarks/BookmarkTableView.tsx +419 -0
  29. package/frontend/src/components/bookmarks/BulkActionModals.tsx +162 -0
  30. package/frontend/src/components/bookmarks/FilterChips.tsx +5 -0
  31. package/frontend/src/components/modals/BookmarkModal.tsx +493 -0
  32. package/frontend/src/components/modals/FolderModal.tsx +306 -0
  33. package/frontend/src/components/modals/ImportModal.tsx +232 -0
  34. package/frontend/src/components/modals/OIDCProviderModal.tsx +284 -0
  35. package/frontend/src/components/modals/SharingModal.tsx +96 -0
  36. package/frontend/src/components/modals/TagModal.tsx +101 -0
  37. package/frontend/src/components/modals/TeamAssignmentModal.tsx +354 -0
  38. package/frontend/src/components/modals/TeamModal.tsx +117 -0
  39. package/frontend/src/components/modals/UserModal.tsx +225 -0
  40. package/frontend/src/components/profile/CreateTokenModal.tsx +172 -0
  41. package/frontend/src/components/sharing/ShareResourceDialog.tsx +422 -0
  42. package/frontend/src/components/ui/Autocomplete.tsx +155 -0
  43. package/frontend/src/components/ui/Button.tsx +68 -0
  44. package/frontend/src/components/ui/ConfirmDialog.tsx +79 -0
  45. package/frontend/src/components/ui/FormFieldWrapper.tsx +36 -0
  46. package/frontend/src/components/ui/ModalFooterActions.tsx +49 -0
  47. package/frontend/src/components/ui/ModalSection.tsx +34 -0
  48. package/frontend/src/components/ui/PageLoadingSkeleton.tsx +24 -0
  49. package/frontend/src/components/ui/Select.tsx +61 -0
  50. package/frontend/src/components/ui/SharingField.tsx +298 -0
  51. package/frontend/src/components/ui/Toast.tsx +47 -0
  52. package/frontend/src/components/ui/Tooltip.tsx +21 -0
  53. package/frontend/src/components/ui/alert-dialog.tsx +139 -0
  54. package/frontend/src/components/ui/badge.tsx +36 -0
  55. package/frontend/src/components/ui/button-base.tsx +57 -0
  56. package/frontend/src/components/ui/card.tsx +76 -0
  57. package/frontend/src/components/ui/command.tsx +161 -0
  58. package/frontend/src/components/ui/dialog.tsx +120 -0
  59. package/frontend/src/components/ui/dropdown-menu.tsx +199 -0
  60. package/frontend/src/components/ui/input.tsx +22 -0
  61. package/frontend/src/components/ui/label.tsx +24 -0
  62. package/frontend/src/components/ui/popover.tsx +33 -0
  63. package/frontend/src/components/ui/progress.tsx +26 -0
  64. package/frontend/src/components/ui/scroll-area.tsx +48 -0
  65. package/frontend/src/components/ui/select-base.tsx +159 -0
  66. package/frontend/src/components/ui/separator.tsx +29 -0
  67. package/frontend/src/components/ui/sheet.tsx +140 -0
  68. package/frontend/src/components/ui/sidebar.tsx +783 -0
  69. package/frontend/src/components/ui/skeleton.tsx +15 -0
  70. package/frontend/src/components/ui/sonner.tsx +46 -0
  71. package/frontend/src/components/ui/switch.tsx +28 -0
  72. package/frontend/src/components/ui/table.tsx +120 -0
  73. package/frontend/src/components/ui/tooltip-base.tsx +30 -0
  74. package/frontend/src/config/api.ts +16 -0
  75. package/frontend/src/config/mode.ts +6 -0
  76. package/frontend/src/contexts/AppConfigContext.tsx +39 -0
  77. package/frontend/src/contexts/AuthContext.tsx +137 -0
  78. package/frontend/src/contexts/SearchCommandContext.tsx +28 -0
  79. package/frontend/src/hooks/use-mobile.tsx +19 -0
  80. package/frontend/src/hooks/useConfirmDialog.ts +63 -0
  81. package/frontend/src/hooks/useMarketingTheme.ts +47 -0
  82. package/frontend/src/i18n.ts +39 -0
  83. package/frontend/src/index.css +117 -0
  84. package/frontend/src/instrument.ts +20 -0
  85. package/frontend/src/lib/utils.ts +6 -0
  86. package/frontend/src/locales/de.json +899 -0
  87. package/frontend/src/locales/en.json +937 -0
  88. package/frontend/src/locales/es.json +884 -0
  89. package/frontend/src/locales/fr.json +550 -0
  90. package/frontend/src/locales/it.json +535 -0
  91. package/frontend/src/locales/ja.json +535 -0
  92. package/frontend/src/locales/nl.json +550 -0
  93. package/frontend/src/locales/pl.json +535 -0
  94. package/frontend/src/locales/pt.json +535 -0
  95. package/frontend/src/locales/ru.json +535 -0
  96. package/frontend/src/locales/zh.json +535 -0
  97. package/frontend/src/main.tsx +44 -0
  98. package/frontend/src/pages/Bookmarks.tsx +1004 -0
  99. package/frontend/src/pages/Dashboard.tsx +427 -0
  100. package/frontend/src/pages/Folders.tsx +578 -0
  101. package/frontend/src/pages/GoPreferences.tsx +134 -0
  102. package/frontend/src/pages/Login.tsx +196 -0
  103. package/frontend/src/pages/PasswordReset.tsx +242 -0
  104. package/frontend/src/pages/Profile.tsx +593 -0
  105. package/frontend/src/pages/SearchEngineGuide.tsx +135 -0
  106. package/frontend/src/pages/Setup.tsx +210 -0
  107. package/frontend/src/pages/Signup.tsx +199 -0
  108. package/frontend/src/pages/Tags.tsx +421 -0
  109. package/frontend/src/pages/VerifyEmail.tsx +254 -0
  110. package/frontend/src/pages/admin/AdminAIPage.tsx +5 -0
  111. package/frontend/src/pages/admin/AdminLayout.tsx +40 -0
  112. package/frontend/src/pages/admin/AdminMembersPage.tsx +5 -0
  113. package/frontend/src/pages/admin/AdminOIDCPage.tsx +5 -0
  114. package/frontend/src/pages/admin/AdminSettingsPage.tsx +5 -0
  115. package/frontend/src/pages/admin/AdminTeamsPage.tsx +5 -0
  116. package/frontend/src/utils/favicon.ts +36 -0
  117. package/frontend/src/utils/formatRelativeTime.ts +37 -0
  118. package/frontend/src/utils/safeHref.ts +31 -0
  119. package/frontend/src/vite-env.d.ts +10 -0
  120. package/package.json +9 -1
@@ -0,0 +1,427 @@
1
+ import { Link } from 'react-router-dom';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useEffect, useState } from 'react';
4
+ import { Bookmark, Share2, TrendingUp, Plus, ArrowRight, X, ChevronDown, ChevronRight, CheckCircle, Folder, Tag } from 'lucide-react';
5
+ import Button from '../components/ui/Button';
6
+ import { Card, CardContent } from '../components/ui/card';
7
+ import { Badge } from '../components/ui/badge';
8
+ import { StatCard } from '../components/StatCard';
9
+ import { EmptyState } from '../components/EmptyState';
10
+ import api from '../api/client';
11
+ import { useAppConfig } from '../contexts/AppConfigContext';
12
+
13
+ interface RecentBookmark {
14
+ id: string;
15
+ title: string;
16
+ url: string;
17
+ created_at: string;
18
+ last_accessed_at?: string | null;
19
+ folder_names?: string[];
20
+ tag_names?: string[];
21
+ }
22
+
23
+ interface QuickAccessBookmark {
24
+ id: string;
25
+ title: string;
26
+ url: string;
27
+ slug: string;
28
+ }
29
+
30
+ interface DashboardStats {
31
+ totalBookmarks: number;
32
+ totalFolders: number;
33
+ totalTags: number;
34
+ sharedBookmarks: number;
35
+ sharedFolders: number;
36
+ recentBookmarks: RecentBookmark[];
37
+ topTags: Array<{ id: string; name: string; bookmark_count: number }>;
38
+ quickAccessBookmarks?: QuickAccessBookmark[];
39
+ pinnedBookmarks?: QuickAccessBookmark[];
40
+ }
41
+
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
+ const PRO_TIP_DISMISSED_KEY = 'slugbase_dashboard_protip_dismissed';
60
+ const ONBOARDING_DISMISSED_KEY = 'slugbase_dashboard_onboarding_dismissed';
61
+
62
+ function ProTipBanner({
63
+ onDismiss,
64
+ appBasePath,
65
+ t,
66
+ }: {
67
+ onDismiss: () => void;
68
+ appBasePath: string;
69
+ t: (key: string) => string;
70
+ }) {
71
+ return (
72
+ <div className="flex items-start gap-3 rounded-xl border border-border bg-card shadow-sm px-4 py-3">
73
+ <p className="text-sm text-muted-foreground flex-1 min-w-0">
74
+ {t('dashboard.proTipBody')}{' '}
75
+ <Link to={`${appBasePath}/search-engine-guide`} className="text-primary font-medium hover:underline">
76
+ {t('dashboard.proTipLink')}
77
+ </Link>
78
+ </p>
79
+ <button
80
+ type="button"
81
+ onClick={onDismiss}
82
+ className="shrink-0 p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
83
+ aria-label={t('dashboard.dismiss')}
84
+ >
85
+ <X className="h-4 w-4" />
86
+ </button>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function OnboardingChecklist({
92
+ totalBookmarks,
93
+ totalFolders,
94
+ topTagsCount,
95
+ appBasePath,
96
+ t,
97
+ }: {
98
+ totalBookmarks: number;
99
+ totalFolders: number;
100
+ topTagsCount: number;
101
+ appBasePath: string;
102
+ t: (key: string) => string;
103
+ }) {
104
+ const [collapsed, setCollapsed] = useState(true);
105
+ const [dismissed, setDismissed] = useState(() => typeof window !== 'undefined' && !!localStorage.getItem(ONBOARDING_DISMISSED_KEY));
106
+ const allDone = totalBookmarks > 0 && totalFolders > 0 && topTagsCount > 0;
107
+ const show = !dismissed && !allDone;
108
+
109
+ if (!show) return null;
110
+
111
+ const steps = [
112
+ { done: totalBookmarks > 0, label: t('dashboard.onboardingImport'), to: `${appBasePath}/bookmarks?import=true` },
113
+ { done: false, label: t('dashboard.onboardingSearchEngine'), to: `${appBasePath}/search-engine-guide` },
114
+ { done: totalFolders > 0, label: t('dashboard.onboardingFolder'), to: `${appBasePath}/folders` },
115
+ { done: topTagsCount > 0, label: t('dashboard.onboardingTag'), to: `${appBasePath}/bookmarks` },
116
+ ];
117
+
118
+ function handleDismiss() {
119
+ localStorage.setItem(ONBOARDING_DISMISSED_KEY, '1');
120
+ setDismissed(true);
121
+ setCollapsed(true);
122
+ }
123
+
124
+ return (
125
+ <Card className="border border-border bg-card shadow-sm">
126
+ <button
127
+ type="button"
128
+ onClick={() => setCollapsed(!collapsed)}
129
+ 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
+ >
131
+ {collapsed ? <ChevronRight className="h-5 w-5 text-muted-foreground" /> : <ChevronDown className="h-5 w-5 text-muted-foreground" />}
132
+ <span className="font-medium text-sm">{t('dashboard.onboardingTitle')}</span>
133
+ </button>
134
+ {!collapsed && (
135
+ <CardContent className="pt-0 pb-4 px-4">
136
+ <ul className="space-y-2">
137
+ {steps.map((step, i) => (
138
+ <li key={i}>
139
+ <Link
140
+ to={step.to}
141
+ className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
142
+ >
143
+ {step.done ? <CheckCircle className="h-4 w-4 text-primary shrink-0" /> : <span className="w-4 h-4 rounded-full border border-muted-foreground shrink-0" />}
144
+ <span>{step.label}</span>
145
+ </Link>
146
+ </li>
147
+ ))}
148
+ </ul>
149
+ <button
150
+ type="button"
151
+ onClick={handleDismiss}
152
+ className="mt-3 text-xs text-muted-foreground hover:text-foreground"
153
+ >
154
+ {t('dashboard.onboardingDismiss')}
155
+ </button>
156
+ </CardContent>
157
+ )}
158
+ </Card>
159
+ );
160
+ }
161
+
162
+ export default function Dashboard() {
163
+ const { t } = useTranslation();
164
+ const { appBasePath } = useAppConfig();
165
+ const [stats, setStats] = useState<DashboardStats | null>(null);
166
+ const [proTipDismissed, setProTipDismissed] = useState(() => typeof window !== 'undefined' && !!localStorage.getItem(PRO_TIP_DISMISSED_KEY));
167
+
168
+ useEffect(() => {
169
+ loadStats();
170
+ }, []);
171
+
172
+ async function loadStats() {
173
+ try {
174
+ const res = await api.get('/dashboard/stats');
175
+ setStats(res.data);
176
+ } catch (error) {
177
+ console.error('Failed to load dashboard stats:', error);
178
+ }
179
+ }
180
+
181
+ const quickAccess = stats?.quickAccessBookmarks ?? [];
182
+ const pinnedBookmarks = stats?.pinnedBookmarks ?? [];
183
+
184
+ return (
185
+ <div className="space-y-8">
186
+ {/* Feature discovery banner — dismissable, localStorage */}
187
+ {!proTipDismissed && (
188
+ <ProTipBanner
189
+ onDismiss={() => {
190
+ localStorage.setItem(PRO_TIP_DISMISSED_KEY, '1');
191
+ setProTipDismissed(true);
192
+ }}
193
+ appBasePath={appBasePath}
194
+ t={t}
195
+ />
196
+ )}
197
+
198
+ {/* Top stats: bookmarks / folders / tags */}
199
+ {stats && (
200
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
201
+ <StatCard
202
+ label={t('dashboard.statsBookmarks')}
203
+ value={stats.totalBookmarks}
204
+ icon={Bookmark}
205
+ href={appBasePath + '/bookmarks'}
206
+ dense
207
+ iconContainerClassName="bg-primary/20"
208
+ iconColorClassName="text-primary"
209
+ />
210
+ <StatCard
211
+ label={t('dashboard.statsFolders')}
212
+ value={stats.totalFolders}
213
+ icon={Folder}
214
+ href={appBasePath + '/folders'}
215
+ dense
216
+ iconContainerClassName="bg-primary/20"
217
+ iconColorClassName="text-primary"
218
+ />
219
+ <StatCard
220
+ label={t('dashboard.statsTags')}
221
+ value={stats.totalTags}
222
+ icon={Tag}
223
+ href={appBasePath + '/tags'}
224
+ dense
225
+ iconContainerClassName="bg-primary/20"
226
+ iconColorClassName="text-primary"
227
+ />
228
+ </div>
229
+ )}
230
+
231
+ {/* Pinned bookmarks — same style as Quick Access */}
232
+ <section className="space-y-3">
233
+ <div className="flex items-center justify-between flex-wrap gap-2">
234
+ <h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
235
+ {t('dashboard.pinned')}
236
+ </h2>
237
+ <Link
238
+ to={appBasePath + '/bookmarks?pinned=true'}
239
+ className="text-sm font-medium text-primary hover:underline"
240
+ >
241
+ {t('dashboard.viewAll')}
242
+ <ArrowRight className="inline-block ml-1 h-4 w-4" />
243
+ </Link>
244
+ </div>
245
+ {pinnedBookmarks.length > 0 ? (
246
+ <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">
247
+ {pinnedBookmarks.map((b) => (
248
+ <button
249
+ key={b.id}
250
+ type="button"
251
+ onClick={() => {
252
+ api.post(`/bookmarks/${b.id}/track-access`).catch(() => {});
253
+ window.open(b.url, '_blank', 'noopener,noreferrer');
254
+ }}
255
+ 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"
256
+ >
257
+ <div className="flex items-start gap-2.5 mb-1.5">
258
+ <div className="relative h-7 w-7 shrink-0 rounded bg-muted overflow-hidden">
259
+ <img
260
+ src={getFaviconUrl(b.url)}
261
+ alt=""
262
+ className="h-7 w-7 w-full object-cover"
263
+ onError={(e) => {
264
+ (e.target as HTMLImageElement).style.display = 'none';
265
+ (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden');
266
+ }}
267
+ />
268
+ <span className="hidden absolute inset-0 flex items-center justify-center bg-primary/10 rounded">
269
+ <Bookmark className="h-3.5 w-3.5 text-primary" />
270
+ </span>
271
+ </div>
272
+ <p className="font-medium text-foreground line-clamp-2 text-xs flex-1 min-w-0">{b.title}</p>
273
+ </div>
274
+ <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>
275
+ <p className="text-[11px] text-muted-foreground truncate mt-0.5">{getDomain(b.url)}</p>
276
+ </button>
277
+ ))}
278
+ </div>
279
+ ) : (
280
+ <Card className="border border-border bg-card shadow-sm">
281
+ <CardContent className="p-6">
282
+ <EmptyState
283
+ icon={Bookmark}
284
+ title={t('dashboard.noPinnedBookmarks')}
285
+ description={t('dashboard.pinFromBookmarks')}
286
+ action={
287
+ <Link to={appBasePath + '/bookmarks'}>
288
+ <Button variant="secondary">{t('dashboard.pinFromBookmarksLink')}</Button>
289
+ </Link>
290
+ }
291
+ />
292
+ </CardContent>
293
+ </Card>
294
+ )}
295
+ </section>
296
+
297
+ {/* Quick Access bookmarks — horizontal card grid; responsive columns and scroll on narrow */}
298
+ <section className="space-y-3">
299
+ <div className="flex items-center justify-between flex-wrap gap-2">
300
+ <h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
301
+ {t('dashboard.quickAccess')}
302
+ </h2>
303
+ <Link
304
+ to={appBasePath + '/bookmarks'}
305
+ className="text-sm font-medium text-primary hover:underline"
306
+ >
307
+ {t('dashboard.viewAll')}
308
+ <ArrowRight className="inline-block ml-1 h-4 w-4" />
309
+ </Link>
310
+ </div>
311
+ {quickAccess.length > 0 ? (
312
+ <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">
313
+ {quickAccess.map((b) => (
314
+ <button
315
+ key={b.id}
316
+ type="button"
317
+ onClick={() => {
318
+ api.post(`/bookmarks/${b.id}/track-access`).catch(() => {});
319
+ window.open(b.url, '_blank', 'noopener,noreferrer');
320
+ }}
321
+ 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"
322
+ >
323
+ <div className="flex items-start gap-2.5 mb-1.5">
324
+ <div className="relative h-7 w-7 shrink-0 rounded bg-muted overflow-hidden">
325
+ <img
326
+ src={getFaviconUrl(b.url)}
327
+ alt=""
328
+ className="h-7 w-7 w-full object-cover"
329
+ onError={(e) => {
330
+ (e.target as HTMLImageElement).style.display = 'none';
331
+ (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden');
332
+ }}
333
+ />
334
+ <span className="hidden absolute inset-0 flex items-center justify-center bg-primary/10 rounded">
335
+ <Bookmark className="h-3.5 w-3.5 text-primary" />
336
+ </span>
337
+ </div>
338
+ <p className="font-medium text-foreground line-clamp-2 text-xs flex-1 min-w-0">{b.title}</p>
339
+ </div>
340
+ <p className="text-[11px] text-primary font-mono truncate" title={`go/${b.slug}`}>go/{b.slug}</p>
341
+ <p className="text-[11px] text-muted-foreground truncate mt-0.5">{getDomain(b.url)}</p>
342
+ </button>
343
+ ))}
344
+ </div>
345
+ ) : (
346
+ <Card className="border border-border bg-card shadow-sm">
347
+ <CardContent className="p-6">
348
+ <EmptyState
349
+ icon={Bookmark}
350
+ title={t('dashboard.noQuickAccessBookmarks')}
351
+ description={t('dashboard.noQuickAccessBookmarksHint')}
352
+ action={
353
+ <Link to={`${appBasePath}/bookmarks?create=true`}>
354
+ <Button variant="primary" icon={Plus}>{t('bookmarks.create')}</Button>
355
+ </Link>
356
+ }
357
+ />
358
+ </CardContent>
359
+ </Card>
360
+ )}
361
+ </section>
362
+
363
+ {/* Shared With You — only when has shared content */}
364
+ {stats && (stats.sharedBookmarks > 0 || stats.sharedFolders > 0) && (
365
+ <section className="space-y-3">
366
+ <h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
367
+ {t('dashboard.sharedWithYou')}
368
+ </h2>
369
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
370
+ <StatCard
371
+ label={t('dashboard.sharedBookmarks')}
372
+ value={stats.sharedBookmarks}
373
+ icon={Share2}
374
+ href={appBasePath + '/shared'}
375
+ iconContainerClassName="bg-primary/20"
376
+ iconColorClassName="text-primary"
377
+ />
378
+ <StatCard
379
+ label={t('dashboard.sharedFolders')}
380
+ value={stats.sharedFolders}
381
+ icon={Share2}
382
+ href={appBasePath + '/shared'}
383
+ iconContainerClassName="bg-primary/20"
384
+ iconColorClassName="text-primary"
385
+ />
386
+ </div>
387
+ </section>
388
+ )}
389
+
390
+ {/* Most used tags — small row; click goes to bookmarks filtered by tag */}
391
+ {stats && stats.topTags.length > 0 && (
392
+ <section className="space-y-2">
393
+ <h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-2">
394
+ <TrendingUp className="h-4 w-4" />
395
+ {t('dashboard.topTags')}
396
+ </h2>
397
+ <div className="flex flex-wrap gap-2">
398
+ {stats.topTags.map((tag) => (
399
+ <Link
400
+ key={tag.id}
401
+ to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
402
+ 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"
403
+ title={t('dashboard.filterByTagHint')}
404
+ >
405
+ <span>{tag.name}</span>
406
+ <Badge variant="secondary" className="text-[10px] px-1.5 py-0 font-normal">
407
+ {tag.bookmark_count}
408
+ </Badge>
409
+ </Link>
410
+ ))}
411
+ </div>
412
+ </section>
413
+ )}
414
+
415
+ {/* Onboarding checklist — collapsible, show when incomplete and not dismissed */}
416
+ {stats && (
417
+ <OnboardingChecklist
418
+ totalBookmarks={stats.totalBookmarks}
419
+ totalFolders={stats.totalFolders}
420
+ topTagsCount={stats.topTags.length}
421
+ appBasePath={appBasePath}
422
+ t={t}
423
+ />
424
+ )}
425
+ </div>
426
+ );
427
+ }