@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,15 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.HTMLAttributes<HTMLDivElement>) {
7
+ return (
8
+ <div
9
+ className={cn("animate-pulse rounded-md bg-primary/10", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ export { Skeleton }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import { Toaster as Sonner } from "sonner"
3
+
4
+ type ToasterProps = React.ComponentProps<typeof Sonner>
5
+
6
+ function useTheme() {
7
+ const [theme, setTheme] = React.useState<"light" | "dark" | "system">("system")
8
+ React.useEffect(() => {
9
+ const el = document.documentElement
10
+ const check = () =>
11
+ setTheme(el.classList.contains("dark") ? "dark" : "light")
12
+ check()
13
+ const observer = new MutationObserver(check)
14
+ observer.observe(el, {
15
+ attributes: true,
16
+ attributeFilter: ["class"],
17
+ })
18
+ return () => observer.disconnect()
19
+ }, [])
20
+ return { theme }
21
+ }
22
+
23
+ const Toaster = ({ ...props }: ToasterProps) => {
24
+ const { theme = "system" } = useTheme()
25
+
26
+ return (
27
+ <Sonner
28
+ theme={theme as ToasterProps["theme"]}
29
+ className="toaster group"
30
+ toastOptions={{
31
+ classNames: {
32
+ toast:
33
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
34
+ description: "group-[.toast]:text-muted-foreground",
35
+ actionButton:
36
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
37
+ cancelButton:
38
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
39
+ },
40
+ }}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Toaster }
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SwitchPrimitives from "@radix-ui/react-switch"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Switch = React.forwardRef<
8
+ React.ElementRef<typeof SwitchPrimitives.Root>,
9
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <SwitchPrimitives.Root
12
+ className={cn(
13
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
14
+ className
15
+ )}
16
+ {...props}
17
+ ref={ref}
18
+ >
19
+ <SwitchPrimitives.Thumb
20
+ className={cn(
21
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
22
+ )}
23
+ />
24
+ </SwitchPrimitives.Root>
25
+ ))
26
+ Switch.displayName = SwitchPrimitives.Root.displayName
27
+
28
+ export { Switch }
@@ -0,0 +1,120 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Table = React.forwardRef<
6
+ HTMLTableElement,
7
+ React.HTMLAttributes<HTMLTableElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div className="relative w-full overflow-auto">
10
+ <table
11
+ ref={ref}
12
+ className={cn("w-full caption-bottom text-sm", className)}
13
+ {...props}
14
+ />
15
+ </div>
16
+ ))
17
+ Table.displayName = "Table"
18
+
19
+ const TableHeader = React.forwardRef<
20
+ HTMLTableSectionElement,
21
+ React.HTMLAttributes<HTMLTableSectionElement>
22
+ >(({ className, ...props }, ref) => (
23
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
24
+ ))
25
+ TableHeader.displayName = "TableHeader"
26
+
27
+ const TableBody = React.forwardRef<
28
+ HTMLTableSectionElement,
29
+ React.HTMLAttributes<HTMLTableSectionElement>
30
+ >(({ className, ...props }, ref) => (
31
+ <tbody
32
+ ref={ref}
33
+ className={cn("[&_tr:last-child]:border-0", className)}
34
+ {...props}
35
+ />
36
+ ))
37
+ TableBody.displayName = "TableBody"
38
+
39
+ const TableFooter = React.forwardRef<
40
+ HTMLTableSectionElement,
41
+ React.HTMLAttributes<HTMLTableSectionElement>
42
+ >(({ className, ...props }, ref) => (
43
+ <tfoot
44
+ ref={ref}
45
+ className={cn(
46
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ ))
52
+ TableFooter.displayName = "TableFooter"
53
+
54
+ const TableRow = React.forwardRef<
55
+ HTMLTableRowElement,
56
+ React.HTMLAttributes<HTMLTableRowElement>
57
+ >(({ className, ...props }, ref) => (
58
+ <tr
59
+ ref={ref}
60
+ className={cn(
61
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ ))
67
+ TableRow.displayName = "TableRow"
68
+
69
+ const TableHead = React.forwardRef<
70
+ HTMLTableCellElement,
71
+ React.ThHTMLAttributes<HTMLTableCellElement>
72
+ >(({ className, ...props }, ref) => (
73
+ <th
74
+ ref={ref}
75
+ className={cn(
76
+ "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ ))
82
+ TableHead.displayName = "TableHead"
83
+
84
+ const TableCell = React.forwardRef<
85
+ HTMLTableCellElement,
86
+ React.TdHTMLAttributes<HTMLTableCellElement>
87
+ >(({ className, ...props }, ref) => (
88
+ <td
89
+ ref={ref}
90
+ className={cn(
91
+ "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ TableCell.displayName = "TableCell"
98
+
99
+ const TableCaption = React.forwardRef<
100
+ HTMLTableCaptionElement,
101
+ React.HTMLAttributes<HTMLTableCaptionElement>
102
+ >(({ className, ...props }, ref) => (
103
+ <caption
104
+ ref={ref}
105
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ TableCaption.displayName = "TableCaption"
110
+
111
+ export {
112
+ Table,
113
+ TableHeader,
114
+ TableBody,
115
+ TableFooter,
116
+ TableHead,
117
+ TableRow,
118
+ TableCell,
119
+ TableCaption,
120
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from "react"
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const TooltipProvider = TooltipPrimitive.Provider
7
+
8
+ const Tooltip = TooltipPrimitive.Root
9
+
10
+ const TooltipTrigger = TooltipPrimitive.Trigger
11
+
12
+ const TooltipContent = React.forwardRef<
13
+ React.ElementRef<typeof TooltipPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
15
+ >(({ className, sideOffset = 4, ...props }, ref) => (
16
+ <TooltipPrimitive.Portal>
17
+ <TooltipPrimitive.Content
18
+ ref={ref}
19
+ sideOffset={sideOffset}
20
+ className={cn(
21
+ "z-50 overflow-hidden rounded-md bg-tooltip px-3 py-1.5 text-xs text-tooltip-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ </TooltipPrimitive.Portal>
27
+ ))
28
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
29
+
30
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Self-hosted API config always uses relative paths by default.
3
+ */
4
+ export const apiBaseUrl: string = '';
5
+
6
+ /** Full URL to start OIDC login for a provider. Pass apiBaseUrl when using from context (e.g. useAppConfig().apiBaseUrl). */
7
+ export function getAuthProviderUrl(providerKey: string, baseUrl?: string): string {
8
+ const url = baseUrl ?? apiBaseUrl;
9
+ return url ? `${url.replace(/\/$/, '')}/api/auth/${providerKey}` : `/api/auth/${providerKey}`;
10
+ }
11
+
12
+ /** Base path for app routes. */
13
+ export const appBasePath: string = '';
14
+
15
+ /** Root path for the app (dashboard). */
16
+ export const appRootPath: string = '/';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * SlugBase frontend runtime mode is self-hosted only.
3
+ */
4
+ export const mode: 'selfhosted' = 'selfhosted';
5
+ export const isCloud = false;
6
+ export const isSelfhosted = true;
@@ -0,0 +1,39 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import { appBasePath as defaultAppBasePath, apiBaseUrl as defaultApiBaseUrl, appRootPath as defaultAppRootPath } from '../config/api';
3
+
4
+ export interface AppConfig {
5
+ appBasePath: string;
6
+ apiBaseUrl: string;
7
+ appRootPath: string;
8
+ }
9
+
10
+ const defaultConfig: AppConfig = {
11
+ appBasePath: defaultAppBasePath,
12
+ apiBaseUrl: defaultApiBaseUrl,
13
+ appRootPath: defaultAppRootPath,
14
+ };
15
+
16
+ const AppConfigContext = createContext<AppConfig>(defaultConfig);
17
+
18
+ export function AppConfigProvider({
19
+ children,
20
+ appBasePath,
21
+ apiBaseUrl,
22
+ appRootPath,
23
+ }: {
24
+ children: React.ReactNode;
25
+ appBasePath?: string;
26
+ apiBaseUrl?: string;
27
+ appRootPath?: string;
28
+ }) {
29
+ const value: AppConfig = {
30
+ appBasePath: appBasePath ?? defaultConfig.appBasePath,
31
+ apiBaseUrl: apiBaseUrl ?? defaultConfig.apiBaseUrl,
32
+ appRootPath: appRootPath ?? defaultConfig.appRootPath,
33
+ };
34
+ return <AppConfigContext.Provider value={value}>{children}</AppConfigContext.Provider>;
35
+ }
36
+
37
+ export function useAppConfig(): AppConfig {
38
+ return useContext(AppConfigContext);
39
+ }
@@ -0,0 +1,137 @@
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+ import api from '../api/client';
3
+ import { getAuthProviderUrl } from '../config/api';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useAppConfig } from './AppConfigContext';
6
+
7
+ export interface User {
8
+ id: string;
9
+ email: string;
10
+ name: string;
11
+ user_key: string;
12
+ is_admin: boolean;
13
+ language: string;
14
+ theme: string;
15
+ ai_suggestions_enabled?: boolean;
16
+ email_pending?: string | null;
17
+ oidc_provider?: string | null;
18
+ oidc_sub?: string | null;
19
+ }
20
+
21
+ interface AuthContextType {
22
+ user: User | null;
23
+ loading: boolean;
24
+ login: (provider: string) => void;
25
+ logout: () => Promise<void>;
26
+ updateUser: (updates: Partial<User>) => Promise<void>;
27
+ checkAuth: () => Promise<void>;
28
+ }
29
+
30
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
31
+
32
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
33
+ const [user, setUser] = useState<User | null>(null);
34
+ const [loading, setLoading] = useState(true);
35
+ const { i18n } = useTranslation();
36
+ const { apiBaseUrl } = useAppConfig();
37
+
38
+ useEffect(() => {
39
+ // Apply initial theme based on browser preference before checking auth
40
+ // This ensures dark mode works on login page and other public pages
41
+ // Only apply if user hasn't set a preference yet
42
+ const root = document.documentElement;
43
+ if (!root.dataset.userTheme) {
44
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
45
+ if (prefersDark) {
46
+ root.classList.add('dark');
47
+ } else {
48
+ root.classList.remove('dark');
49
+ }
50
+ }
51
+
52
+ checkAuth();
53
+ }, []);
54
+
55
+ useEffect(() => {
56
+ if (user) {
57
+ i18n.changeLanguage(user.language);
58
+ applyTheme(user.theme);
59
+ }
60
+ }, [user, i18n]);
61
+
62
+ async function checkAuth() {
63
+ try {
64
+ const response = await api.get('/auth/me');
65
+ setUser(response.data);
66
+ } catch {
67
+ setUser(null);
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ }
72
+
73
+ function login(provider: string) {
74
+ window.location.href = getAuthProviderUrl(provider, apiBaseUrl);
75
+ }
76
+
77
+ async function logout() {
78
+ try {
79
+ await api.post('/auth/logout');
80
+ setUser(null);
81
+ window.location.href = '/';
82
+ } catch (error) {
83
+ console.error('Logout error:', error);
84
+ }
85
+ }
86
+
87
+ async function updateUser(updates: Partial<User>): Promise<any> {
88
+ try {
89
+ const response = await api.put('/users/me', updates);
90
+ // Only update user if email wasn't changed (email verification required)
91
+ if (!response.data.emailVerificationRequired) {
92
+ setUser(response.data);
93
+ } else {
94
+ // Refresh user to get email_pending
95
+ await checkAuth();
96
+ }
97
+ if (updates.language) {
98
+ i18n.changeLanguage(updates.language);
99
+ }
100
+ if (updates.theme) {
101
+ applyTheme(updates.theme);
102
+ }
103
+ return response.data;
104
+ } catch (error) {
105
+ console.error('Update user error:', error);
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ function applyTheme(theme: string) {
111
+ const root = document.documentElement;
112
+ // Mark that user has set a theme preference
113
+ root.dataset.userTheme = 'true';
114
+
115
+ if (theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
116
+ root.classList.add('dark');
117
+ } else {
118
+ root.classList.remove('dark');
119
+ }
120
+ // Sync to localStorage for consistency when user logs out
121
+ localStorage.setItem('slugbase_theme', theme);
122
+ }
123
+
124
+ return (
125
+ <AuthContext.Provider value={{ user, loading, login, logout, updateUser, checkAuth }}>
126
+ {children}
127
+ </AuthContext.Provider>
128
+ );
129
+ }
130
+
131
+ export function useAuth() {
132
+ const context = useContext(AuthContext);
133
+ if (context === undefined) {
134
+ throw new Error('useAuth must be used within an AuthProvider');
135
+ }
136
+ return context;
137
+ }
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2
+
3
+ interface SearchCommandContextValue {
4
+ open: boolean;
5
+ setOpen: (open: boolean) => void;
6
+ openSearch: () => void;
7
+ }
8
+
9
+ const SearchCommandContext = createContext<SearchCommandContextValue | null>(null);
10
+
11
+ export function SearchCommandProvider({ children }: { children: ReactNode }) {
12
+ const [open, setOpen] = useState(false);
13
+ const openSearch = useCallback(() => setOpen(true), []);
14
+ const value: SearchCommandContextValue = { open, setOpen, openSearch };
15
+ return (
16
+ <SearchCommandContext.Provider value={value}>
17
+ {children}
18
+ </SearchCommandContext.Provider>
19
+ );
20
+ }
21
+
22
+ export function useSearchCommand(): SearchCommandContextValue {
23
+ const ctx = useContext(SearchCommandContext);
24
+ if (!ctx) {
25
+ throw new Error('useSearchCommand must be used within SearchCommandProvider');
26
+ }
27
+ return ctx;
28
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 1024 // lg - matches Layout mobile breakpoint
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,63 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ interface ConfirmDialogState {
4
+ isOpen: boolean;
5
+ title: string;
6
+ message: string;
7
+ itemName?: string;
8
+ confirmText?: string;
9
+ cancelText?: string;
10
+ variant?: 'danger' | 'warning' | 'default';
11
+ onConfirm: () => void;
12
+ }
13
+
14
+ export function useConfirmDialog() {
15
+ const [dialogState, setDialogState] = useState<ConfirmDialogState>({
16
+ isOpen: false,
17
+ title: '',
18
+ message: '',
19
+ onConfirm: () => {},
20
+ });
21
+
22
+ const showConfirm = useCallback((
23
+ title: string,
24
+ message: string,
25
+ onConfirm: () => void,
26
+ options?: {
27
+ itemName?: string;
28
+ confirmText?: string;
29
+ cancelText?: string;
30
+ variant?: 'danger' | 'warning' | 'default';
31
+ }
32
+ ) => {
33
+ setDialogState({
34
+ isOpen: true,
35
+ title,
36
+ message,
37
+ onConfirm,
38
+ itemName: options?.itemName,
39
+ confirmText: options?.confirmText,
40
+ cancelText: options?.cancelText,
41
+ variant: options?.variant || 'default',
42
+ });
43
+ }, []);
44
+
45
+ const hideConfirm = useCallback(() => {
46
+ setDialogState((prev) => ({ ...prev, isOpen: false }));
47
+ }, []);
48
+
49
+ const handleConfirm = useCallback(() => {
50
+ dialogState.onConfirm();
51
+ hideConfirm();
52
+ }, [dialogState, hideConfirm]);
53
+
54
+ return {
55
+ showConfirm,
56
+ hideConfirm,
57
+ dialogState: {
58
+ ...dialogState,
59
+ onConfirm: handleConfirm,
60
+ onCancel: hideConfirm,
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,47 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useAuth } from '../contexts/AuthContext';
3
+
4
+ const THEME_STORAGE_KEY = 'slugbase_theme';
5
+
6
+ function getIsDark(): boolean {
7
+ return document.documentElement.classList.contains('dark');
8
+ }
9
+
10
+ function applyThemeToDocument(theme: 'light' | 'dark') {
11
+ const root = document.documentElement;
12
+ root.dataset.userTheme = 'true';
13
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
14
+ if (theme === 'dark') {
15
+ root.classList.add('dark');
16
+ } else {
17
+ root.classList.remove('dark');
18
+ }
19
+ }
20
+
21
+ export function useMarketingTheme() {
22
+ const { user, updateUser } = useAuth();
23
+ const [isDark, setIsDark] = useState(getIsDark);
24
+
25
+ useEffect(() => {
26
+ const root = document.documentElement;
27
+ const observer = new MutationObserver(() => setIsDark(getIsDark()));
28
+ observer.observe(root, { attributes: true, attributeFilter: ['class'] });
29
+ return () => observer.disconnect();
30
+ }, []);
31
+
32
+ function setTheme(theme: 'light' | 'dark') {
33
+ if (user) {
34
+ updateUser({ theme });
35
+ } else {
36
+ applyThemeToDocument(theme);
37
+ setIsDark(theme === 'dark');
38
+ }
39
+ }
40
+
41
+ function toggleTheme() {
42
+ const nextTheme: 'light' | 'dark' = isDark ? 'light' : 'dark';
43
+ setTheme(nextTheme);
44
+ }
45
+
46
+ return { isDark, setTheme, toggleTheme };
47
+ }
@@ -0,0 +1,39 @@
1
+ import i18n from 'i18next';
2
+ import { initReactI18next } from 'react-i18next';
3
+ import LanguageDetector from 'i18next-browser-languagedetector';
4
+ import enTranslations from '@/locales/en.json';
5
+ import deTranslations from '@/locales/de.json';
6
+ import frTranslations from '@/locales/fr.json';
7
+ import esTranslations from '@/locales/es.json';
8
+ import itTranslations from '@/locales/it.json';
9
+ import ptTranslations from '@/locales/pt.json';
10
+ import nlTranslations from '@/locales/nl.json';
11
+ import ruTranslations from '@/locales/ru.json';
12
+ import jaTranslations from '@/locales/ja.json';
13
+ import zhTranslations from '@/locales/zh.json';
14
+ import plTranslations from '@/locales/pl.json';
15
+
16
+ i18n
17
+ .use(LanguageDetector)
18
+ .use(initReactI18next)
19
+ .init({
20
+ resources: {
21
+ en: { translation: enTranslations },
22
+ de: { translation: deTranslations },
23
+ fr: { translation: frTranslations },
24
+ es: { translation: esTranslations },
25
+ it: { translation: itTranslations },
26
+ pt: { translation: ptTranslations },
27
+ nl: { translation: nlTranslations },
28
+ ru: { translation: ruTranslations },
29
+ ja: { translation: jaTranslations },
30
+ zh: { translation: zhTranslations },
31
+ pl: { translation: plTranslations },
32
+ },
33
+ fallbackLng: 'en',
34
+ interpolation: {
35
+ escapeValue: false,
36
+ },
37
+ });
38
+
39
+ export default i18n;