@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.
- package/frontend/src/App.tsx +8 -6
- package/frontend/src/components/AppSidebar.tsx +26 -20
- package/frontend/src/components/GlobalSearch.tsx +15 -14
- package/frontend/src/components/TopBar.tsx +4 -3
- package/frontend/src/components/UserDropdown.tsx +4 -3
- package/frontend/src/pages/Bookmarks.tsx +4 -3
- package/frontend/src/pages/Dashboard.tsx +23 -22
- package/frontend/src/pages/Folders.tsx +4 -3
- package/frontend/src/pages/GoPreferences.tsx +3 -2
- package/frontend/src/pages/PasswordReset.tsx +5 -4
- package/frontend/src/pages/Profile.tsx +3 -2
- package/frontend/src/pages/SearchEngineGuide.tsx +3 -2
- package/frontend/src/pages/Tags.tsx +4 -3
- package/package.json +1 -1
package/frontend/src/App.tsx
CHANGED
|
@@ -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 {
|
|
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={
|
|
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 {
|
|
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={
|
|
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 {
|
|
51
|
-
const to = `${
|
|
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 '
|
|
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
|
|
48
|
+
const prefix = pathPrefixForLinks || '';
|
|
49
|
+
const adminBaseFull = `${appBasePath || ''}/admin`;
|
|
50
|
+
const adminBaseLink = `${prefix}/admin`.replace(/\/+/g, '/') || '/admin';
|
|
49
51
|
|
|
50
52
|
const adminNavItems = [
|
|
51
|
-
{
|
|
52
|
-
{
|
|
53
|
-
{
|
|
54
|
-
{
|
|
55
|
-
{
|
|
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
|
-
{
|
|
76
|
-
{
|
|
77
|
-
{
|
|
78
|
-
{
|
|
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.
|
|
99
|
+
<SidebarMenuItem key={item.pathForLink}>
|
|
95
100
|
<SidebarMenuButton
|
|
96
101
|
asChild
|
|
97
102
|
isActive={
|
|
98
|
-
item.
|
|
103
|
+
item.pathForActive === (appBasePath || '/') ? isOverviewActive : pathname === item.pathForActive
|
|
99
104
|
}
|
|
100
105
|
tooltip={item.label}
|
|
101
106
|
>
|
|
102
|
-
<Link to={item.
|
|
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.
|
|
146
|
+
<SidebarMenuItem key={item.pathForLink}>
|
|
142
147
|
<SidebarMenuButton
|
|
143
148
|
asChild
|
|
144
|
-
isActive={pathname === item.
|
|
149
|
+
isActive={pathname === item.pathForActive}
|
|
145
150
|
tooltip={item.label}
|
|
146
151
|
>
|
|
147
152
|
<Link
|
|
148
|
-
to={item.
|
|
153
|
+
to={item.pathForLink}
|
|
149
154
|
onClick={handleNavClick}
|
|
150
|
-
aria-current={pathname === item.
|
|
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 {
|
|
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: `${
|
|
51
|
-
{ type: 'navigation', title: t('folders.title'), path: `${
|
|
52
|
-
{ type: 'navigation', title: t('tags.title'), path: `${
|
|
53
|
-
{ type: 'navigation', title: t('shared.title'), path: `${
|
|
54
|
-
...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${
|
|
55
|
-
], [showAdmin, t,
|
|
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: `${
|
|
59
|
-
{ type: 'action', title: t('folders.create'), path: `${
|
|
60
|
-
{ type: 'action', title: t('bookmarks.import'), path: `${
|
|
61
|
-
{ type: 'action', title: t('bookmarks.export'), path: `${
|
|
62
|
-
], [t, navigate,
|
|
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(`${
|
|
177
|
+
navigate(`${prefix}/bookmarks?folder_id=${result.id}`.replace(/\/+/g, '/') || `/bookmarks?folder_id=${result.id}`);
|
|
177
178
|
} else if (result.type === 'tag') {
|
|
178
|
-
navigate(`${
|
|
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 {
|
|
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={
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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={`${
|
|
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
|
-
|
|
64
|
+
pathPrefix,
|
|
65
65
|
t,
|
|
66
66
|
}: {
|
|
67
67
|
onDismiss: () => void;
|
|
68
|
-
|
|
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={`${
|
|
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
|
-
|
|
95
|
+
pathPrefix,
|
|
96
96
|
t,
|
|
97
97
|
}: {
|
|
98
98
|
totalBookmarks: number;
|
|
99
99
|
totalFolders: number;
|
|
100
100
|
topTagsCount: number;
|
|
101
|
-
|
|
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: `${
|
|
113
|
-
{ done: false, label: t('dashboard.onboardingSearchEngine'), to: `${
|
|
114
|
-
{ done: totalFolders > 0, label: t('dashboard.onboardingFolder'), to: `${
|
|
115
|
-
{ done: topTagsCount > 0, label: t('dashboard.onboardingTag'), to: `${
|
|
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 {
|
|
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
|
-
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={`${
|
|
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={
|
|
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={
|
|
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={`${
|
|
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
|
-
|
|
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 {
|
|
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={`${
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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={`${
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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 {
|
|
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={`${
|
|
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={`${
|
|
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`}>
|