@mdguggenbichler/slugbase-core 0.0.13 → 0.0.15

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.
@@ -30,25 +30,27 @@ const GoPreferences = lazy(() => import('./pages/GoPreferences'));
30
30
  function PrivateRoute({ children }: { children: React.ReactNode }) {
31
31
  const { user, loading } = useAuth();
32
32
  const { t } = useTranslation();
33
- const { appBasePath } = useAppConfig();
33
+ const { pathPrefixForLinks } = useAppConfig();
34
+ const loginPath = `${pathPrefixForLinks || ''}/login`.replace(/\/+/g, '/') || '/login';
34
35
  if (loading) return <div className="min-h-screen flex items-center justify-center"><div className="text-lg">{t('common.loading')}</div></div>;
35
- if (!user) return <Navigate to={`${appBasePath}/login`} replace />;
36
+ if (!user) return <Navigate to={loginPath} replace />;
36
37
  return <>{children}</>;
37
38
  }
38
39
 
39
40
  function AdminRoute({ children }: { children: React.ReactNode }) {
40
41
  const { user, loading } = useAuth();
41
42
  const { t } = useTranslation();
42
- const { appBasePath, appRootPath } = useAppConfig();
43
+ const { pathPrefixForLinks, appRootPath } = useAppConfig();
44
+ const loginPath = `${pathPrefixForLinks || ''}/login`.replace(/\/+/g, '/') || '/login';
43
45
  if (loading) return <div className="min-h-screen flex items-center justify-center"><div className="text-lg">{t('common.loading')}</div></div>;
44
- if (!user) return <Navigate to={`${appBasePath}/login`} replace />;
46
+ if (!user) return <Navigate to={loginPath} replace />;
45
47
  if (!user.is_admin) return <Navigate to={appRootPath} replace />;
46
48
  return <>{children}</>;
47
49
  }
48
50
 
49
51
  function SharedRedirect() {
50
- const { appBasePath } = useAppConfig();
51
- const to = `${appBasePath || ''}/bookmarks?scope=shared_with_me`;
52
+ const { pathPrefixForLinks } = useAppConfig();
53
+ const to = `${pathPrefixForLinks || ''}/bookmarks?scope=shared_with_me`.replace(/\/+/g, '/') || '/bookmarks?scope=shared_with_me';
52
54
  return <Navigate to={to} replace />;
53
55
  }
54
56
 
@@ -1,6 +1,6 @@
1
+ import React, { useState, useEffect } from 'react';
1
2
  import { Link, useLocation } from 'react-router-dom';
2
3
  import { useTranslation } from 'react-i18next';
3
- import { useState, useEffect } from 'react';
4
4
  import {
5
5
  Bookmark,
6
6
  Folder,
@@ -30,7 +30,7 @@ import {
30
30
  } from './ui/sidebar';
31
31
  import { useAppConfig } from '../contexts/AppConfigContext';
32
32
  import type { User } from '../contexts/AuthContext';
33
- import { cn } from '@/lib/utils';
33
+ import { cn } from '../lib/utils';
34
34
 
35
35
  const SIDEBAR_ADMIN_OPEN_KEY = 'slugbase_sidebar_admin_open';
36
36
 
@@ -43,16 +43,18 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
43
43
  const { t } = useTranslation();
44
44
  const location = useLocation();
45
45
  const pathname = location.pathname;
46
- const { appBasePath } = useAppConfig();
46
+ const { appBasePath, pathPrefixForLinks } = useAppConfig();
47
47
  const { setOpenMobile, toggleSidebar, isMobile, state } = useSidebar();
48
- const adminBase = `${appBasePath || ''}/admin`;
48
+ const prefix = pathPrefixForLinks || '';
49
+ const adminBaseFull = `${appBasePath || ''}/admin`;
50
+ const adminBaseLink = `${prefix}/admin`.replace(/\/+/g, '/') || '/admin';
49
51
 
50
52
  const adminNavItems = [
51
- { path: `${adminBase}/members`, label: t('admin.users'), icon: Users },
52
- { path: `${adminBase}/teams`, label: t('admin.teams'), icon: UserCog },
53
- { path: `${adminBase}/oidc`, label: t('admin.oidcProviders'), icon: Key },
54
- { path: `${adminBase}/settings`, label: t('admin.settings'), icon: Settings },
55
- { path: `${adminBase}/ai`, label: t('admin.ai.nav'), icon: Sparkles },
53
+ { pathForLink: `${adminBaseLink}/members`, pathForActive: `${adminBaseFull}/members`, label: t('admin.users'), icon: Users },
54
+ { pathForLink: `${adminBaseLink}/teams`, pathForActive: `${adminBaseFull}/teams`, label: t('admin.teams'), icon: UserCog },
55
+ { pathForLink: `${adminBaseLink}/oidc`, pathForActive: `${adminBaseFull}/oidc`, label: t('admin.oidcProviders'), icon: Key },
56
+ { pathForLink: `${adminBaseLink}/settings`, pathForActive: `${adminBaseFull}/settings`, label: t('admin.settings'), icon: Settings },
57
+ { pathForLink: `${adminBaseLink}/ai`, pathForActive: `${adminBaseFull}/ai`, label: t('admin.ai.nav'), icon: Sparkles },
56
58
  ];
57
59
 
58
60
  const isOverviewActive =
@@ -71,11 +73,13 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
71
73
  localStorage.setItem(SIDEBAR_ADMIN_OPEN_KEY, String(adminOpen));
72
74
  }, [adminOpen]);
73
75
 
76
+ const rootLink = prefix || '/';
77
+ const rootActive = appBasePath || '/';
74
78
  const primaryNavItems = [
75
- { path: appBasePath || '/', label: t('dashboard.overview'), icon: LayoutDashboard },
76
- { path: `${appBasePath}/bookmarks`, label: t('bookmarks.title'), icon: Bookmark },
77
- { path: `${appBasePath}/folders`, label: t('folders.title'), icon: Folder },
78
- { path: `${appBasePath}/tags`, label: t('tags.title'), icon: Tag },
79
+ { pathForLink: rootLink, pathForActive: rootActive, label: t('dashboard.overview'), icon: LayoutDashboard },
80
+ { pathForLink: `${prefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks', pathForActive: `${appBasePath || ''}/bookmarks`, label: t('bookmarks.title'), icon: Bookmark },
81
+ { pathForLink: `${prefix}/folders`.replace(/\/+/g, '/') || '/folders', pathForActive: `${appBasePath || ''}/folders`, label: t('folders.title'), icon: Folder },
82
+ { pathForLink: `${prefix}/tags`.replace(/\/+/g, '/') || '/tags', pathForActive: `${appBasePath || ''}/tags`, label: t('tags.title'), icon: Tag },
79
83
  ];
80
84
 
81
85
  const handleNavClick = () => {
@@ -85,21 +89,22 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
85
89
  };
86
90
 
87
91
  return (
92
+ <React.Fragment>
88
93
  <Sidebar collapsible="icon" side="left">
89
94
  <SidebarContent>
90
95
  <SidebarGroup>
91
96
  <SidebarGroupContent>
92
97
  <SidebarMenu>
93
98
  {primaryNavItems.map((item) => (
94
- <SidebarMenuItem key={item.path}>
99
+ <SidebarMenuItem key={item.pathForLink}>
95
100
  <SidebarMenuButton
96
101
  asChild
97
102
  isActive={
98
- item.path === (appBasePath || '/') ? isOverviewActive : pathname === item.path
103
+ item.pathForActive === (appBasePath || '/') ? isOverviewActive : pathname === item.pathForActive
99
104
  }
100
105
  tooltip={item.label}
101
106
  >
102
- <Link to={item.path} onClick={handleNavClick} aria-current={pathname === item.path ? 'page' : undefined}>
107
+ <Link to={item.pathForLink} onClick={handleNavClick} aria-current={pathname === item.pathForActive ? 'page' : undefined}>
103
108
  <item.icon className="h-5 w-5" />
104
109
  <span>{item.label}</span>
105
110
  </Link>
@@ -138,16 +143,16 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
138
143
  {adminNavItems.map((item) => {
139
144
  const Icon = item.icon;
140
145
  return (
141
- <SidebarMenuItem key={item.path}>
146
+ <SidebarMenuItem key={item.pathForLink}>
142
147
  <SidebarMenuButton
143
148
  asChild
144
- isActive={pathname === item.path}
149
+ isActive={pathname === item.pathForActive}
145
150
  tooltip={item.label}
146
151
  >
147
152
  <Link
148
- to={item.path}
153
+ to={item.pathForLink}
149
154
  onClick={handleNavClick}
150
- aria-current={pathname === item.path ? 'page' : undefined}
155
+ aria-current={pathname === item.pathForActive ? 'page' : undefined}
151
156
  >
152
157
  <Icon className="h-5 w-5" />
153
158
  <span>{item.label}</span>
@@ -210,5 +215,6 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
210
215
  </SidebarGroup>
211
216
  </SidebarFooter>
212
217
  </Sidebar>
218
+ </React.Fragment>
213
219
  );
214
220
  }
@@ -37,7 +37,8 @@ interface SearchResult {
37
37
  export default function GlobalSearch() {
38
38
  const { t } = useTranslation();
39
39
  const navigate = useNavigate();
40
- const { appBasePath } = useAppConfig();
40
+ const { pathPrefixForLinks } = useAppConfig();
41
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
41
42
  const { user } = useAuth();
42
43
  const { open, setOpen, openSearch } = useSearchCommand();
43
44
  const [query, setQuery] = useState('');
@@ -47,19 +48,19 @@ export default function GlobalSearch() {
47
48
  const showAdmin = user?.is_admin;
48
49
 
49
50
  const navigationItems: SearchResult[] = useMemo(() => [
50
- { type: 'navigation', title: t('bookmarks.title'), path: `${appBasePath}/bookmarks`, id: 'nav-bookmarks' },
51
- { type: 'navigation', title: t('folders.title'), path: `${appBasePath}/folders`, id: 'nav-folders' },
52
- { type: 'navigation', title: t('tags.title'), path: `${appBasePath}/tags`, id: 'nav-tags' },
53
- { type: 'navigation', title: t('shared.title'), path: `${appBasePath}/shared`, id: 'nav-shared' },
54
- ...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${appBasePath}/admin/members`, id: 'nav-admin' }] : []),
55
- ], [showAdmin, t, appBasePath]);
51
+ { type: 'navigation', title: t('bookmarks.title'), path: `${prefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks', id: 'nav-bookmarks' },
52
+ { type: 'navigation', title: t('folders.title'), path: `${prefix}/folders`.replace(/\/+/g, '/') || '/folders', id: 'nav-folders' },
53
+ { type: 'navigation', title: t('tags.title'), path: `${prefix}/tags`.replace(/\/+/g, '/') || '/tags', id: 'nav-tags' },
54
+ { type: 'navigation', title: t('shared.title'), path: `${prefix}/shared`.replace(/\/+/g, '/') || '/shared', id: 'nav-shared' },
55
+ ...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${prefix}/admin/members`.replace(/\/+/g, '/') || '/admin/members', id: 'nav-admin' }] : []),
56
+ ], [showAdmin, t, prefix]);
56
57
 
57
58
  const actionItems: SearchResult[] = useMemo(() => [
58
- { type: 'action', title: t('bookmarks.create'), path: `${appBasePath}/bookmarks`, id: 'action-create-bookmark', action: () => navigate(`${appBasePath}/bookmarks?create=true`) },
59
- { type: 'action', title: t('folders.create'), path: `${appBasePath}/folders`, id: 'action-create-folder', action: () => navigate(`${appBasePath}/folders?create=true`) },
60
- { type: 'action', title: t('bookmarks.import'), path: `${appBasePath}/bookmarks`, id: 'action-import', action: () => navigate(`${appBasePath}/bookmarks?import=true`) },
61
- { type: 'action', title: t('bookmarks.export'), path: `${appBasePath}/bookmarks`, id: 'action-export', action: () => navigate(`${appBasePath}/bookmarks?export=true`) },
62
- ], [t, navigate, appBasePath]);
59
+ { type: 'action', title: t('bookmarks.create'), path: `${prefix}/bookmarks`, id: 'action-create-bookmark', action: () => navigate(`${prefix}/bookmarks?create=true`.replace(/\/+/g, '/') || '/bookmarks?create=true') },
60
+ { type: 'action', title: t('folders.create'), path: `${prefix}/folders`, id: 'action-create-folder', action: () => navigate(`${prefix}/folders?create=true`.replace(/\/+/g, '/') || '/folders?create=true') },
61
+ { type: 'action', title: t('bookmarks.import'), path: `${prefix}/bookmarks`, id: 'action-import', action: () => navigate(`${prefix}/bookmarks?import=true`.replace(/\/+/g, '/') || '/bookmarks?import=true') },
62
+ { type: 'action', title: t('bookmarks.export'), path: `${prefix}/bookmarks`, id: 'action-export', action: () => navigate(`${prefix}/bookmarks?export=true`.replace(/\/+/g, '/') || '/bookmarks?export=true') },
63
+ ], [t, navigate, prefix]);
63
64
 
64
65
  useEffect(() => {
65
66
  function handleKeyDown(e: KeyboardEvent) {
@@ -173,9 +174,9 @@ export default function GlobalSearch() {
173
174
  api.post(`/bookmarks/${result.id}/track-access`).catch(() => {});
174
175
  window.open(result.url, '_blank', 'noopener,noreferrer');
175
176
  } else if (result.type === 'folder') {
176
- navigate(`${appBasePath}/bookmarks?folder_id=${result.id}`);
177
+ navigate(`${prefix}/bookmarks?folder_id=${result.id}`.replace(/\/+/g, '/') || `/bookmarks?folder_id=${result.id}`);
177
178
  } else if (result.type === 'tag') {
178
- navigate(`${appBasePath}/bookmarks?tag_id=${result.id}`);
179
+ navigate(`${prefix}/bookmarks?tag_id=${result.id}`.replace(/\/+/g, '/') || `/bookmarks?tag_id=${result.id}`);
179
180
  }
180
181
  }
181
182
 
@@ -14,7 +14,8 @@ interface TopBarProps {
14
14
 
15
15
  export default function TopBar({ user }: TopBarProps) {
16
16
  const { t } = useTranslation();
17
- const { appBasePath } = useAppConfig();
17
+ const { pathPrefixForLinks } = useAppConfig();
18
+ const prefix = pathPrefixForLinks || '';
18
19
  const { isMobile } = useSidebar();
19
20
 
20
21
  return (
@@ -25,7 +26,7 @@ export default function TopBar({ user }: TopBarProps) {
25
26
  <SidebarTrigger className="-ml-2" aria-label={t('common.expandSidebar')} />
26
27
  )}
27
28
  <Link
28
- to={appBasePath || '/'}
29
+ to={prefix || '/'}
29
30
  className="flex items-center gap-2 text-xl font-bold text-foreground hover:text-primary transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg"
30
31
  >
31
32
  <img
@@ -51,7 +52,7 @@ export default function TopBar({ user }: TopBarProps) {
51
52
 
52
53
  {/* Right: Create bookmark + Profile — right-aligned */}
53
54
  <div className="flex items-center gap-2 sm:gap-4 shrink-0 ml-auto">
54
- <Link to={`${appBasePath}/bookmarks?create=true`}>
55
+ <Link to={`${prefix}/bookmarks?create=true`.replace(/\/+/g, '/') || '/bookmarks?create=true'}>
55
56
  <Button variant="primary" size="sm" icon={Plus}>
56
57
  <span className="hidden sm:inline">{t('bookmarks.create')}</span>
57
58
  </Button>
@@ -27,7 +27,8 @@ function getInitials(name: string): string {
27
27
 
28
28
  export default function UserDropdown({ user }: UserDropdownProps) {
29
29
  const { t } = useTranslation();
30
- const { appBasePath } = useAppConfig();
30
+ const { pathPrefixForLinks } = useAppConfig();
31
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
31
32
  const { logout } = useAuth();
32
33
 
33
34
  const showAdmin = user?.is_admin;
@@ -59,14 +60,14 @@ export default function UserDropdown({ user }: UserDropdownProps) {
59
60
  </DropdownMenuLabel>
60
61
  <DropdownMenuSeparator />
61
62
  <DropdownMenuItem asChild>
62
- <Link to={`${appBasePath}/profile`} className="flex items-center gap-2 cursor-pointer">
63
+ <Link to={`${prefix}/profile`} className="flex items-center gap-2 cursor-pointer">
63
64
  <UserIcon className="h-4 w-4" />
64
65
  {t('profile.title')}
65
66
  </Link>
66
67
  </DropdownMenuItem>
67
68
  {showAdmin && (
68
69
  <DropdownMenuItem asChild>
69
- <Link to={`${appBasePath}/admin/members`} className="flex items-center gap-2 cursor-pointer">
70
+ <Link to={`${prefix}/admin/members`} className="flex items-center gap-2 cursor-pointer">
70
71
  <Settings className="h-4 w-4" />
71
72
  {t('admin.title')}
72
73
  </Link>
@@ -47,7 +47,8 @@ type SortOption = 'recently_added' | 'alphabetical' | 'most_used' | 'recently_ac
47
47
 
48
48
  export default function Bookmarks() {
49
49
  const { t } = useTranslation();
50
- const { appBasePath } = useAppConfig();
50
+ const { pathPrefixForLinks } = useAppConfig();
51
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
51
52
  const { user } = useAuth();
52
53
  const { isMobile, state: sidebarState } = useSidebar();
53
54
  const [searchParams, setSearchParams] = useSearchParams();
@@ -823,7 +824,7 @@ export default function Bookmarks() {
823
824
  >
824
825
  {t('bookmarks.emptyImport')}
825
826
  </Button>
826
- <Link to={`${appBasePath}/search-engine-guide`}>
827
+ <Link to={`${prefix}/search-engine-guide`}>
827
828
  <Button variant="ghost" icon={ExternalLink}>
828
829
  {t('bookmarks.emptyLearnForwarding')}
829
830
  </Button>
@@ -921,7 +922,7 @@ export default function Bookmarks() {
921
922
  <p className="text-sm text-gray-700 dark:text-gray-300">
922
923
  {t('bookmarks.searchEngineNote')}{' '}
923
924
  <Link
924
- to={`${appBasePath}/search-engine-guide`}
925
+ to={`${prefix}/search-engine-guide`}
925
926
  className="text-primary hover:text-primary/90 font-medium underline"
926
927
  >
927
928
  {t('bookmarks.searchEngineGuideLink')}
@@ -61,18 +61,18 @@ const ONBOARDING_DISMISSED_KEY = 'slugbase_dashboard_onboarding_dismissed';
61
61
 
62
62
  function ProTipBanner({
63
63
  onDismiss,
64
- appBasePath,
64
+ pathPrefix,
65
65
  t,
66
66
  }: {
67
67
  onDismiss: () => void;
68
- appBasePath: string;
68
+ pathPrefix: string;
69
69
  t: (key: string) => string;
70
70
  }) {
71
71
  return (
72
72
  <div className="flex items-start gap-3 rounded-xl border border-border bg-card shadow-sm px-4 py-3">
73
73
  <p className="text-sm text-muted-foreground flex-1 min-w-0">
74
74
  {t('dashboard.proTipBody')}{' '}
75
- <Link to={`${appBasePath}/search-engine-guide`} className="text-primary font-medium hover:underline">
75
+ <Link to={`${pathPrefix}/search-engine-guide`.replace(/\/+/g, '/') || '/search-engine-guide'} className="text-primary font-medium hover:underline">
76
76
  {t('dashboard.proTipLink')}
77
77
  </Link>
78
78
  </p>
@@ -92,13 +92,13 @@ function OnboardingChecklist({
92
92
  totalBookmarks,
93
93
  totalFolders,
94
94
  topTagsCount,
95
- appBasePath,
95
+ pathPrefix,
96
96
  t,
97
97
  }: {
98
98
  totalBookmarks: number;
99
99
  totalFolders: number;
100
100
  topTagsCount: number;
101
- appBasePath: string;
101
+ pathPrefix: string;
102
102
  t: (key: string) => string;
103
103
  }) {
104
104
  const [collapsed, setCollapsed] = useState(true);
@@ -109,10 +109,10 @@ function OnboardingChecklist({
109
109
  if (!show) return null;
110
110
 
111
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` },
112
+ { done: totalBookmarks > 0, label: t('dashboard.onboardingImport'), to: `${pathPrefix}/bookmarks?import=true`.replace(/\/+/g, '/') || '/bookmarks?import=true' },
113
+ { done: false, label: t('dashboard.onboardingSearchEngine'), to: `${pathPrefix}/search-engine-guide`.replace(/\/+/g, '/') || '/search-engine-guide' },
114
+ { done: totalFolders > 0, label: t('dashboard.onboardingFolder'), to: `${pathPrefix}/folders`.replace(/\/+/g, '/') || '/folders' },
115
+ { done: topTagsCount > 0, label: t('dashboard.onboardingTag'), to: `${pathPrefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks' },
116
116
  ];
117
117
 
118
118
  function handleDismiss() {
@@ -161,7 +161,8 @@ function OnboardingChecklist({
161
161
 
162
162
  export default function Dashboard() {
163
163
  const { t } = useTranslation();
164
- const { appBasePath } = useAppConfig();
164
+ const { pathPrefixForLinks } = useAppConfig();
165
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
165
166
  const [stats, setStats] = useState<DashboardStats | null>(null);
166
167
  const [proTipDismissed, setProTipDismissed] = useState(() => typeof window !== 'undefined' && !!localStorage.getItem(PRO_TIP_DISMISSED_KEY));
167
168
 
@@ -190,7 +191,7 @@ export default function Dashboard() {
190
191
  localStorage.setItem(PRO_TIP_DISMISSED_KEY, '1');
191
192
  setProTipDismissed(true);
192
193
  }}
193
- appBasePath={appBasePath}
194
+ pathPrefix={prefix}
194
195
  t={t}
195
196
  />
196
197
  )}
@@ -202,7 +203,7 @@ export default function Dashboard() {
202
203
  label={t('dashboard.statsBookmarks')}
203
204
  value={stats.totalBookmarks}
204
205
  icon={Bookmark}
205
- href={appBasePath + '/bookmarks'}
206
+ href={prefix + '/bookmarks'}
206
207
  dense
207
208
  iconContainerClassName="bg-primary/20"
208
209
  iconColorClassName="text-primary"
@@ -211,7 +212,7 @@ export default function Dashboard() {
211
212
  label={t('dashboard.statsFolders')}
212
213
  value={stats.totalFolders}
213
214
  icon={Folder}
214
- href={appBasePath + '/folders'}
215
+ href={prefix + '/folders'}
215
216
  dense
216
217
  iconContainerClassName="bg-primary/20"
217
218
  iconColorClassName="text-primary"
@@ -220,7 +221,7 @@ export default function Dashboard() {
220
221
  label={t('dashboard.statsTags')}
221
222
  value={stats.totalTags}
222
223
  icon={Tag}
223
- href={appBasePath + '/tags'}
224
+ href={prefix + '/tags'}
224
225
  dense
225
226
  iconContainerClassName="bg-primary/20"
226
227
  iconColorClassName="text-primary"
@@ -235,7 +236,7 @@ export default function Dashboard() {
235
236
  {t('dashboard.pinned')}
236
237
  </h2>
237
238
  <Link
238
- to={appBasePath + '/bookmarks?pinned=true'}
239
+ to={prefix + '/bookmarks?pinned=true'}
239
240
  className="text-sm font-medium text-primary hover:underline"
240
241
  >
241
242
  {t('dashboard.viewAll')}
@@ -284,7 +285,7 @@ export default function Dashboard() {
284
285
  title={t('dashboard.noPinnedBookmarks')}
285
286
  description={t('dashboard.pinFromBookmarks')}
286
287
  action={
287
- <Link to={appBasePath + '/bookmarks'}>
288
+ <Link to={prefix + '/bookmarks'}>
288
289
  <Button variant="secondary">{t('dashboard.pinFromBookmarksLink')}</Button>
289
290
  </Link>
290
291
  }
@@ -301,7 +302,7 @@ export default function Dashboard() {
301
302
  {t('dashboard.quickAccess')}
302
303
  </h2>
303
304
  <Link
304
- to={appBasePath + '/bookmarks'}
305
+ to={prefix + '/bookmarks'}
305
306
  className="text-sm font-medium text-primary hover:underline"
306
307
  >
307
308
  {t('dashboard.viewAll')}
@@ -350,7 +351,7 @@ export default function Dashboard() {
350
351
  title={t('dashboard.noQuickAccessBookmarks')}
351
352
  description={t('dashboard.noQuickAccessBookmarksHint')}
352
353
  action={
353
- <Link to={`${appBasePath}/bookmarks?create=true`}>
354
+ <Link to={`${prefix}/bookmarks?create=true`}>
354
355
  <Button variant="primary" icon={Plus}>{t('bookmarks.create')}</Button>
355
356
  </Link>
356
357
  }
@@ -371,7 +372,7 @@ export default function Dashboard() {
371
372
  label={t('dashboard.sharedBookmarks')}
372
373
  value={stats.sharedBookmarks}
373
374
  icon={Share2}
374
- href={appBasePath + '/shared'}
375
+ href={prefix + '/shared'}
375
376
  iconContainerClassName="bg-primary/20"
376
377
  iconColorClassName="text-primary"
377
378
  />
@@ -379,7 +380,7 @@ export default function Dashboard() {
379
380
  label={t('dashboard.sharedFolders')}
380
381
  value={stats.sharedFolders}
381
382
  icon={Share2}
382
- href={appBasePath + '/shared'}
383
+ href={prefix + '/shared'}
383
384
  iconContainerClassName="bg-primary/20"
384
385
  iconColorClassName="text-primary"
385
386
  />
@@ -398,7 +399,7 @@ export default function Dashboard() {
398
399
  {stats.topTags.map((tag) => (
399
400
  <Link
400
401
  key={tag.id}
401
- to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
402
+ to={`${prefix}/bookmarks?tag_id=${tag.id}`}
402
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"
403
404
  title={t('dashboard.filterByTagHint')}
404
405
  >
@@ -418,7 +419,7 @@ export default function Dashboard() {
418
419
  totalBookmarks={stats.totalBookmarks}
419
420
  totalFolders={stats.totalFolders}
420
421
  topTagsCount={stats.topTags.length}
421
- appBasePath={appBasePath}
422
+ pathPrefix={prefix}
422
423
  t={t}
423
424
  />
424
425
  )}
@@ -35,7 +35,8 @@ const DEFAULT_SORT: SortOption = 'alphabetical';
35
35
 
36
36
  export default function Folders() {
37
37
  const { t } = useTranslation();
38
- const { appBasePath } = useAppConfig();
38
+ const { pathPrefixForLinks } = useAppConfig();
39
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
39
40
  const { showConfirm, dialogState } = useConfirmDialog();
40
41
  const [searchParams, setSearchParams] = useSearchParams();
41
42
  const [folders, setFolders] = useState<Folder[]>([]);
@@ -331,7 +332,7 @@ export default function Folders() {
331
332
  className={`group bg-card rounded-lg border border-border hover:border-primary/70 hover:bg-muted/50 hover:shadow-md transition-all duration-200 flex flex-col h-full min-h-0 ${compactMode ? 'p-2.5 min-h-[160px]' : 'p-2.5 min-h-[140px]'}`}
332
333
  >
333
334
  <Link
334
- to={`${appBasePath}/bookmarks?folder_id=${folder.id}`}
335
+ to={`${prefix}/bookmarks?folder_id=${folder.id}`}
335
336
  className="flex-1 flex flex-col min-w-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
336
337
  >
337
338
  <div className="space-y-3 flex-1 flex flex-col">
@@ -437,7 +438,7 @@ export default function Folders() {
437
438
  >
438
439
  <td className={`${compactMode ? 'px-2 py-1.5' : 'px-4 py-3'}`}>
439
440
  <Link
440
- to={`${appBasePath}/bookmarks?folder_id=${folder.id}`}
441
+ to={`${prefix}/bookmarks?folder_id=${folder.id}`}
441
442
  className={`flex items-center ${compactMode ? 'gap-2' : 'gap-3'} hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded`}
442
443
  >
443
444
  <div className={`flex-shrink-0 ${compactMode ? 'w-6 h-6' : 'w-8 h-8'} rounded-lg bg-primary/20 flex items-center justify-center border border-primary/30`}>
@@ -21,7 +21,8 @@ interface SlugPreference {
21
21
 
22
22
  export default function GoPreferences() {
23
23
  const { t } = useTranslation();
24
- const { appBasePath } = useAppConfig();
24
+ const { pathPrefixForLinks } = useAppConfig();
25
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
25
26
  const { showToast } = useToast();
26
27
  const [preferences, setPreferences] = useState<SlugPreference[]>([]);
27
28
  const [loading, setLoading] = useState(true);
@@ -64,7 +65,7 @@ export default function GoPreferences() {
64
65
  return (
65
66
  <div className="space-y-6 max-w-3xl">
66
67
  <div className="flex items-center gap-4">
67
- <Link to={`${appBasePath}/profile`}>
68
+ <Link to={`${prefix}/profile`}>
68
69
  <Button variant="ghost" size="sm" icon={ArrowLeft}>
69
70
  {t('common.back')}
70
71
  </Button>
@@ -8,7 +8,8 @@ import Button from '../components/ui/Button';
8
8
 
9
9
  export default function PasswordReset() {
10
10
  const { t } = useTranslation();
11
- const { appBasePath } = useAppConfig();
11
+ const { pathPrefixForLinks } = useAppConfig();
12
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
12
13
  const [searchParams] = useSearchParams();
13
14
  const navigate = useNavigate();
14
15
  const token = searchParams.get('token');
@@ -143,7 +144,7 @@ export default function PasswordReset() {
143
144
 
144
145
  <div className="text-center">
145
146
  <Link
146
- to={`${appBasePath}/login`}
147
+ to={`${prefix}/login`}
147
148
  className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
148
149
  >
149
150
  <ArrowLeft className="h-4 w-4" />
@@ -163,7 +164,7 @@ export default function PasswordReset() {
163
164
  {t('passwordReset.invalidToken')}
164
165
  </p>
165
166
  <Link
166
- to={`${appBasePath}/password-reset`}
167
+ to={`${prefix}/password-reset`}
167
168
  className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
168
169
  >
169
170
  <ArrowLeft className="h-4 w-4" />
@@ -225,7 +226,7 @@ export default function PasswordReset() {
225
226
 
226
227
  <div className="text-center">
227
228
  <Link
228
- to={`${appBasePath}/login`}
229
+ to={`${prefix}/login`}
229
230
  className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
230
231
  >
231
232
  <ArrowLeft className="h-4 w-4" />
@@ -58,7 +58,8 @@ function SettingsRow({
58
58
 
59
59
  export default function Profile() {
60
60
  const { t } = useTranslation();
61
- const { appBasePath, apiBaseUrl } = useAppConfig();
61
+ const { pathPrefixForLinks, apiBaseUrl } = useAppConfig();
62
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
62
63
  const { user, updateUser, checkAuth } = useAuth();
63
64
  const { showToast } = useToast();
64
65
  const [formData, setFormData] = useState({
@@ -406,7 +407,7 @@ export default function Profile() {
406
407
  <>
407
408
  {t('profile.quickAccessDescription')}{' '}
408
409
  <Link
409
- to={`${appBasePath}/go-preferences`}
410
+ to={`${prefix}/go-preferences`}
410
411
  className="text-primary hover:text-primary/90 font-medium"
411
412
  >
412
413
  {t('profile.manageQuickAccess')} →
@@ -6,7 +6,8 @@ import { useAppConfig } from '../contexts/AppConfigContext';
6
6
 
7
7
  export default function SearchEngineGuide() {
8
8
  const { t } = useTranslation();
9
- const { appBasePath } = useAppConfig();
9
+ const { pathPrefixForLinks } = useAppConfig();
10
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
10
11
 
11
12
  const baseUrl = window.location.origin;
12
13
  const goPath = '/go/%s';
@@ -16,7 +17,7 @@ export default function SearchEngineGuide() {
16
17
  <div className="space-y-6 max-w-4xl mx-auto">
17
18
  {/* Header */}
18
19
  <div className="flex items-center gap-4">
19
- <Link to={`${appBasePath}/bookmarks`}>
20
+ <Link to={`${prefix}/bookmarks`}>
20
21
  <Button variant="ghost" size="sm" icon={ArrowLeft}>
21
22
  {t('common.back')}
22
23
  </Button>
@@ -27,7 +27,8 @@ const DEFAULT_SORT: SortOption = 'alphabetical';
27
27
 
28
28
  export default function Tags() {
29
29
  const { t } = useTranslation();
30
- const { appBasePath } = useAppConfig();
30
+ const { pathPrefixForLinks } = useAppConfig();
31
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
31
32
  const { showConfirm, dialogState } = useConfirmDialog();
32
33
  const [searchParams, setSearchParams] = useSearchParams();
33
34
  const [tags, setTags] = useState<Tag[]>([]);
@@ -272,7 +273,7 @@ export default function Tags() {
272
273
  className={`group bg-card rounded-lg border border-border hover:border-primary/70 hover:bg-muted/50 hover:shadow-md transition-all duration-200 flex flex-col h-full min-h-0 ${compactMode ? 'p-2.5 min-h-[160px]' : 'p-2.5 min-h-[140px]'}`}
273
274
  >
274
275
  <Link
275
- to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
276
+ to={`${prefix}/bookmarks?tag_id=${tag.id}`}
276
277
  className="flex-1 flex flex-col min-w-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
277
278
  >
278
279
  <div className="space-y-3 flex-1 flex flex-col">
@@ -335,7 +336,7 @@ export default function Tags() {
335
336
  >
336
337
  <td className={`${compactMode ? 'px-2 py-1.5' : 'px-4 py-3'}`}>
337
338
  <Link
338
- to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
339
+ to={`${prefix}/bookmarks?tag_id=${tag.id}`}
339
340
  className={`flex items-center ${compactMode ? 'gap-2' : 'gap-3'} hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded`}
340
341
  >
341
342
  <div className={`flex-shrink-0 ${compactMode ? 'w-6 h-6' : '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`}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdguggenbichler/slugbase-core",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "SlugBase core: backend and frontend entrypoints for self-hosted and cloud apps",
5
5
  "type": "module",
6
6
  "exports": {