@stevederico/skateboard-ui 1.3.0 → 1.3.2

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 (71) hide show
  1. package/App.jsx +15 -10
  2. package/AppSidebar.jsx +97 -34
  3. package/CHANGELOG.md +21 -0
  4. package/Context.jsx +3 -1
  5. package/LandingView.jsx +6 -14
  6. package/Layout.jsx +1 -1
  7. package/PaymentView.jsx +3 -1
  8. package/README.md +50 -0
  9. package/SettingsView.jsx +58 -12
  10. package/Sheet.jsx +14 -14
  11. package/SignInView.jsx +3 -1
  12. package/SignUpView.jsx +44 -1
  13. package/SkeletonLoader.jsx +41 -0
  14. package/TabBar.jsx +3 -1
  15. package/TextView.jsx +3 -1
  16. package/ThemeToggle.jsx +17 -35
  17. package/Toast.jsx +11 -0
  18. package/USAGE.md +266 -0
  19. package/UpgradeSheet.jsx +10 -10
  20. package/package.json +10 -1
  21. package/shadcn/ui/accordion.jsx +1 -1
  22. package/shadcn/ui/alert-dialog.jsx +2 -2
  23. package/shadcn/ui/alert.jsx +1 -1
  24. package/shadcn/ui/aspect-ratio.jsx +1 -1
  25. package/shadcn/ui/avatar.jsx +1 -1
  26. package/shadcn/ui/badge.jsx +1 -1
  27. package/shadcn/ui/breadcrumb.jsx +1 -1
  28. package/shadcn/ui/button-group.jsx +2 -2
  29. package/shadcn/ui/button.jsx +1 -1
  30. package/shadcn/ui/calendar.jsx +2 -2
  31. package/shadcn/ui/card.jsx +1 -1
  32. package/shadcn/ui/carousel.jsx +2 -2
  33. package/shadcn/ui/chart.jsx +1 -1
  34. package/shadcn/ui/checkbox.jsx +1 -1
  35. package/shadcn/ui/command.jsx +3 -3
  36. package/shadcn/ui/context-menu.jsx +1 -1
  37. package/shadcn/ui/dialog.jsx +2 -2
  38. package/shadcn/ui/drawer.jsx +1 -1
  39. package/shadcn/ui/dropdown-menu.jsx +1 -1
  40. package/shadcn/ui/empty.jsx +1 -1
  41. package/shadcn/ui/field.jsx +3 -3
  42. package/shadcn/ui/hover-card.jsx +1 -1
  43. package/shadcn/ui/input-group.jsx +4 -4
  44. package/shadcn/ui/input.jsx +1 -1
  45. package/shadcn/ui/item.jsx +2 -2
  46. package/shadcn/ui/kbd.jsx +1 -1
  47. package/shadcn/ui/label.jsx +1 -1
  48. package/shadcn/ui/menubar.jsx +2 -2
  49. package/shadcn/ui/navigation-menu.jsx +1 -1
  50. package/shadcn/ui/pagination.jsx +2 -2
  51. package/shadcn/ui/popover.jsx +1 -1
  52. package/shadcn/ui/progress.jsx +1 -1
  53. package/shadcn/ui/radio-group.jsx +1 -1
  54. package/shadcn/ui/resizable.jsx +1 -1
  55. package/shadcn/ui/scroll-area.jsx +1 -1
  56. package/shadcn/ui/select.jsx +1 -1
  57. package/shadcn/ui/separator.jsx +1 -1
  58. package/shadcn/ui/sheet.jsx +2 -2
  59. package/shadcn/ui/sidebar.jsx +8 -8
  60. package/shadcn/ui/skeleton.jsx +1 -1
  61. package/shadcn/ui/slider.jsx +1 -1
  62. package/shadcn/ui/spinner.jsx +1 -1
  63. package/shadcn/ui/switch.jsx +1 -1
  64. package/shadcn/ui/table.jsx +1 -1
  65. package/shadcn/ui/tabs.jsx +1 -1
  66. package/shadcn/ui/textarea.jsx +1 -1
  67. package/shadcn/ui/toggle-group.jsx +2 -2
  68. package/shadcn/ui/toggle.jsx +1 -1
  69. package/shadcn/ui/tooltip.jsx +1 -1
  70. package/styles.css +4 -1
  71. package/LandingViewSimple.jsx +0 -35
package/App.jsx CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  useLocation,
8
8
  } from 'react-router-dom';
9
9
  import { useEffect } from 'react';
10
+ import { ThemeProvider } from 'next-themes';
10
11
  import Layout from './Layout.jsx';
11
12
  import LandingView from './LandingView.jsx';
12
13
  import TextView from './TextView.jsx';
@@ -20,6 +21,7 @@ import ProtectedRoute from './ProtectedRoute.jsx';
20
21
  import ErrorBoundary from './ErrorBoundary.jsx';
21
22
  import { useAppSetup, initializeUtilities, validateConstants } from './Utilities.js';
22
23
  import { ContextProvider } from './Context.jsx';
24
+ import Toast from './Toast.jsx';
23
25
 
24
26
  function App({ constants, appRoutes, defaultRoute }) {
25
27
  const location = useLocation();
@@ -68,19 +70,22 @@ export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRo
68
70
 
69
71
  root.render(
70
72
  <ErrorBoundary>
71
- <ContextProvider constants={constants}>
72
- {Wrapper ? (
73
- <Wrapper>
73
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
74
+ <Toast />
75
+ <ContextProvider constants={constants}>
76
+ {Wrapper ? (
77
+ <Wrapper>
78
+ <Router>
79
+ <App constants={constants} appRoutes={appRoutes} defaultRoute={defaultRoute} />
80
+ </Router>
81
+ </Wrapper>
82
+ ) : (
74
83
  <Router>
75
84
  <App constants={constants} appRoutes={appRoutes} defaultRoute={defaultRoute} />
76
85
  </Router>
77
- </Wrapper>
78
- ) : (
79
- <Router>
80
- <App constants={constants} appRoutes={appRoutes} defaultRoute={defaultRoute} />
81
- </Router>
82
- )}
83
- </ContextProvider>
86
+ )}
87
+ </ContextProvider>
88
+ </ThemeProvider>
84
89
  </ErrorBoundary>
85
90
  );
86
91
  }
package/AppSidebar.jsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import React from "react";
2
2
  import { useNavigate, useLocation } from "react-router-dom";
3
- import constants from "@/constants.json";
4
3
  import DynamicIcon from "./DynamicIcon.jsx";
4
+ import { getState } from "./Context.jsx";
5
+ import { Avatar, AvatarFallback } from "./shadcn/ui/avatar.jsx";
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./shadcn/ui/tooltip.jsx";
5
7
  import {
6
8
  Sidebar,
7
9
  SidebarContent,
@@ -21,6 +23,8 @@ export default function AppSidebar() {
21
23
  const navigate = useNavigate();
22
24
  const location = useLocation();
23
25
  const currentPage = (location.pathname.split("/")[2] || "").toLowerCase();
26
+ const { state } = getState();
27
+ const constants = state.constants;
24
28
 
25
29
  const handleNavigation = (url) => {
26
30
  navigate(url);
@@ -57,24 +61,39 @@ export default function AppSidebar() {
57
61
  >
58
62
  {constants.pages.map((item) => {
59
63
  const isActive = currentPage === item.url.toLowerCase();
64
+ const buttonElement = (
65
+ <button
66
+ type="button"
67
+ className={`cursor-pointer items-center flex w-full px-4 py-3 rounded-lg ${open ? "h-14" : "h-12 w-12"} ${isActive ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
68
+ onClick={() => handleNavigation(`/app/${item.url.toLowerCase()}`)}
69
+ aria-label={item.title}
70
+ aria-current={isActive ? 'page' : undefined}
71
+ >
72
+ <span className="flex w-full items-center">
73
+ <DynamicIconComponent
74
+ name={item.icon}
75
+ size={20}
76
+ strokeWidth={1.5}
77
+ />
78
+ {open && <span className="ml-3">{item.title}</span>}
79
+ </span>
80
+ </button>
81
+ );
82
+
60
83
  return (
61
84
  <li key={item.title}>
62
- <button
63
- type="button"
64
- className={`cursor-pointer items-center flex w-full px-4 py-3 rounded-lg ${open ? "h-14" : "h-12 w-12"} ${isActive ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
65
- onClick={() => handleNavigation(`/app/${item.url.toLowerCase()}`)}
66
- aria-label={item.title}
67
- aria-current={isActive ? 'page' : undefined}
68
- >
69
- <span className="flex w-full items-center">
70
- <DynamicIconComponent
71
- name={item.icon}
72
- size={20}
73
- strokeWidth={1.5}
74
- />
75
- {open && <span className="ml-3">{item.title}</span>}
76
- </span>
77
- </button>
85
+ {!open ? (
86
+ <Tooltip>
87
+ <TooltipTrigger asChild>
88
+ {buttonElement}
89
+ </TooltipTrigger>
90
+ <TooltipContent side="right">
91
+ {item.title}
92
+ </TooltipContent>
93
+ </Tooltip>
94
+ ) : (
95
+ buttonElement
96
+ )}
78
97
  </li>
79
98
  );
80
99
  })}
@@ -82,24 +101,68 @@ export default function AppSidebar() {
82
101
  </SidebarContent>
83
102
  <SidebarFooter>
84
103
  <ul className={`flex flex-col gap-1 ${open ? "" : "items-center"}`}>
104
+ {state.user && (constants.noLogin === false || typeof constants.noLogin === 'undefined') && (
105
+ <li className={`px-2 pb-2 ${!open ? "flex justify-center" : ""}`}>
106
+ <div className={`flex items-center gap-3 ${open ? "w-full" : ""}`}>
107
+ <Avatar size="default">
108
+ <AvatarFallback className="bg-app dark:text-black text-white uppercase text-xs font-medium">
109
+ {state.user?.name?.split(' ').map(word => word[0]).join('') || "NA"}
110
+ </AvatarFallback>
111
+ </Avatar>
112
+ {open && (
113
+ <div className="flex-1 min-w-0">
114
+ <div className="text-sm font-medium truncate capitalize">{state.user?.name || "User"}</div>
115
+ <div className="text-xs text-muted-foreground truncate">{state.user?.email}</div>
116
+ </div>
117
+ )}
118
+ </div>
119
+ </li>
120
+ )}
85
121
  <li>
86
- <button
87
- type="button"
88
- className={`cursor-pointer items-center rounded-lg flex w-full px-4 py-3 ${open ? "h-14" : "h-12 w-12"}
89
- ${location.pathname.toLowerCase().includes("settings") ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
90
- onClick={() => handleNavigation("/app/settings")}
91
- aria-label="Settings"
92
- aria-current={location.pathname.toLowerCase().includes("settings") ? 'page' : undefined}
93
- >
94
- <span className="flex w-full items-center">
95
- <DynamicIconComponent
96
- name="settings"
97
- size={20}
98
- strokeWidth={1.5}
99
- />
100
- {open && <span className="ml-3">Settings</span>}
101
- </span>
102
- </button>
122
+ {!open ? (
123
+ <Tooltip>
124
+ <TooltipTrigger asChild>
125
+ <button
126
+ type="button"
127
+ className={`cursor-pointer items-center rounded-lg flex w-full px-4 py-3 ${open ? "h-14" : "h-12 w-12"}
128
+ ${location.pathname.toLowerCase().includes("settings") ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
129
+ onClick={() => handleNavigation("/app/settings")}
130
+ aria-label="Settings"
131
+ aria-current={location.pathname.toLowerCase().includes("settings") ? 'page' : undefined}
132
+ >
133
+ <span className="flex w-full items-center">
134
+ <DynamicIconComponent
135
+ name="settings"
136
+ size={20}
137
+ strokeWidth={1.5}
138
+ />
139
+ {open && <span className="ml-3">Settings</span>}
140
+ </span>
141
+ </button>
142
+ </TooltipTrigger>
143
+ <TooltipContent side="right">
144
+ Settings
145
+ </TooltipContent>
146
+ </Tooltip>
147
+ ) : (
148
+ <button
149
+ type="button"
150
+ className={`cursor-pointer items-center rounded-lg flex w-full px-4 py-3 ${open ? "h-14" : "h-12 w-12"}
151
+ ${location.pathname.toLowerCase().includes("settings") ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
152
+ onClick={() => handleNavigation("/app/settings")}
153
+ aria-label="Settings"
154
+ aria-current={location.pathname.toLowerCase().includes("settings") ? 'page' : undefined}
155
+ >
156
+ <span className="flex w-full items-center">
157
+ <DynamicIconComponent
158
+ name="settings"
159
+ size={20}
160
+ strokeWidth={1.5}
161
+ />
162
+ {open && <span className="ml-3">Settings</span>}
163
+ </span>
164
+ </button>
165
+ )}
103
166
  </li>
104
167
  </ul>
105
168
  </SidebarFooter>
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # CHANGELOG
2
2
 
3
+ 1.3.2
4
+
5
+ Remove Geist font imports
6
+ Replace path aliases with relative
7
+ Add constants to Context state
8
+
9
+ 1.3.1
10
+
11
+ Add Geist font
12
+ Add Toast notifications
13
+ Add SkeletonLoader component
14
+ Add Avatar components
15
+ Add Badge components
16
+ Add Tooltip components
17
+ Add Checkbox components
18
+ Add AlertDialog components
19
+ Add user profile
20
+ Add USAGE.md documentation
21
+ Update README documentation
22
+ Export new components
23
+
3
24
  1.3.0
4
25
 
5
26
  Migrate to Base UI
package/Context.jsx CHANGED
@@ -115,7 +115,8 @@ export function ContextProvider({ children, constants }) {
115
115
  ui: {
116
116
  sidebarVisible: true,
117
117
  tabBarVisible: true
118
- }
118
+ },
119
+ constants
119
120
  };
120
121
 
121
122
  function reducer(state, action) {
@@ -173,6 +174,7 @@ export function ContextProvider({ children, constants }) {
173
174
  * Returns { state, dispatch } where state contains:
174
175
  * - user: Current user object or null
175
176
  * - ui: { sidebarVisible, tabBarVisible }
177
+ * - constants: App configuration constants
176
178
  *
177
179
  * @returns {{ state: Object, dispatch: Function }}
178
180
  *
package/LandingView.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
- import constants from "@/constants.json";
3
+ import { useTheme } from 'next-themes';
4
+ import { getState } from "./Context.jsx";
4
5
  import * as LucideIcons from "lucide-react";
5
6
  import ThemeToggle from './ThemeToggle.jsx';
6
7
 
@@ -13,20 +14,11 @@ const DynamicIcon = ({ name, size = 24, color = 'currentColor', strokeWidth = 2,
13
14
  };
14
15
 
15
16
  export default function LandingView() {
17
+ const { state } = getState();
18
+ const constants = state.constants;
16
19
  const navigate = useNavigate();
17
- const [isDarkMode, setIsDarkMode] = useState(false);
18
-
19
- // Initialize dark mode from localStorage or system preference
20
- useEffect(() => {
21
- const savedTheme = localStorage.getItem('theme');
22
- const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
23
-
24
- if (savedTheme) {
25
- setIsDarkMode(savedTheme === 'dark');
26
- } else {
27
- setIsDarkMode(systemPrefersDark);
28
- }
29
- }, []);
20
+ const { theme } = useTheme();
21
+ const isDarkMode = theme === 'dark';
30
22
 
31
23
  const renderHeroContent = () => {
32
24
  return (
package/Layout.jsx CHANGED
@@ -3,12 +3,12 @@ import TabBar from './TabBar.jsx'
3
3
  import { SidebarProvider, SidebarTrigger } from "./shadcn/ui/sidebar"
4
4
  import AppSidebar from "./AppSidebar"
5
5
  import { useEffect } from 'react';
6
- import constants from "@/constants.json";
7
6
  import { getState } from './Context.jsx';
8
7
 
9
8
  export default function Layout({ children }) {
10
9
  const { state } = getState();
11
10
  const { sidebarVisible, tabBarVisible } = state.ui;
11
+ const constants = state.constants;
12
12
 
13
13
  // Combine constants (static config) with context state (programmatic control)
14
14
  const showSidebar = !constants.hideSidebar && sidebarVisible;
package/PaymentView.jsx CHANGED
@@ -2,7 +2,7 @@ import React, { useEffect, useCallback } from 'react';
2
2
  import { useNavigate, useSearchParams } from 'react-router-dom';
3
3
  import { getState } from './Context.jsx';
4
4
  import { getCurrentUser } from './Utilities.js'
5
- import constants from "@/constants.json";
5
+ import { getState } from "./Context.jsx";
6
6
 
7
7
  // Whitelist of allowed redirect paths to prevent open redirect vulnerabilities
8
8
  const ALLOWED_REDIRECT_PREFIXES = ['/app/', '/'];
@@ -15,6 +15,8 @@ function isAllowedRedirect(path) {
15
15
  }
16
16
 
17
17
  export default function PaymentView() {
18
+ const { state } = getState();
19
+ const constants = state.constants;
18
20
  const navigate = useNavigate();
19
21
  const { dispatch } = getState();
20
22
  const [searchParams] = useSearchParams();
package/README.md CHANGED
@@ -29,6 +29,39 @@ createSkateboardApp({ constants, appRoutes });
29
29
 
30
30
  That's it! You get routing, auth, layout, landing page, settings, and payments.
31
31
 
32
+ ## Dark Mode Setup
33
+
34
+ To prevent flash of unstyled content (FOUC) when using dark mode, add this script to your `index.html` **before** your app loads:
35
+
36
+ ```html
37
+ <!DOCTYPE html>
38
+ <html lang="en">
39
+ <head>
40
+ <meta charset="UTF-8" />
41
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
42
+
43
+ <!-- Prevent dark mode FOUC -->
44
+ <script>
45
+ try {
46
+ const theme = localStorage.getItem('theme');
47
+ const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
48
+ if (theme === 'dark' || (!theme && systemDark)) {
49
+ document.documentElement.classList.add('dark');
50
+ }
51
+ } catch (e) {}
52
+ </script>
53
+
54
+ <title>Your App</title>
55
+ </head>
56
+ <body>
57
+ <div id="root"></div>
58
+ <script type="module" src="/src/main.jsx"></script>
59
+ </body>
60
+ </html>
61
+ ```
62
+
63
+ This ensures the correct theme is applied before React renders, eliminating any flash between light and dark modes.
64
+
32
65
  ## Configuration
33
66
 
34
67
  skateboard-ui requires a `constants` object that configures your application:
@@ -144,6 +177,17 @@ Quick overview:
144
177
  | TextView | `@stevederico/skateboard-ui/TextView` | Legal pages |
145
178
  | NotFound | `@stevederico/skateboard-ui/NotFound` | 404 page |
146
179
 
180
+ ### Enhanced Components (New in v1.3.0)
181
+
182
+ | Component | Import | Description |
183
+ |-----------|--------|-------------|
184
+ | Toast | `@stevederico/skateboard-ui/Toast` | Toast notifications (Sonner) |
185
+ | SkeletonLoader | `@stevederico/skateboard-ui/SkeletonLoader` | Loading state patterns |
186
+
187
+ **Font System:** Geist font family loaded automatically for improved typography.
188
+
189
+ **See [USAGE.md](./USAGE.md) for detailed examples of Toast, Skeleton, Avatar, Badge, Tooltip, AlertDialog, Checkbox, Switch, and more.**
190
+
147
191
  ### shadcn/ui Components
148
192
 
149
193
  51 components available at `@stevederico/skateboard-ui/shadcn/ui/*`:
@@ -153,6 +197,10 @@ import { Button } from '@stevederico/skateboard-ui/shadcn/ui/button'
153
197
  import { Card } from '@stevederico/skateboard-ui/shadcn/ui/card'
154
198
  import { Input } from '@stevederico/skateboard-ui/shadcn/ui/input'
155
199
  import { Dialog } from '@stevederico/skateboard-ui/shadcn/ui/dialog'
200
+ import { Avatar } from '@stevederico/skateboard-ui/shadcn/ui/avatar'
201
+ import { Badge } from '@stevederico/skateboard-ui/shadcn/ui/badge'
202
+ import { Tooltip } from '@stevederico/skateboard-ui/shadcn/ui/tooltip'
203
+ import { AlertDialog } from '@stevederico/skateboard-ui/shadcn/ui/alert-dialog'
156
204
  ```
157
205
 
158
206
  ## Usage Examples
@@ -357,9 +405,11 @@ Used internally by createSkateboardApp. Redirects to /signin if not authenticate
357
405
  ### Core Dependencies
358
406
  - @base-ui/react - Accessible UI primitives
359
407
  - TailwindCSS 4.0+ - Utility-first CSS framework
408
+ - geist - Vercel's Geist font family (sans & mono)
360
409
  - lucide-react - Icon library
361
410
  - class-variance-authority - Type-safe variant styling
362
411
  - clsx & tailwind-merge - className utilities
412
+ - sonner - Toast notifications
363
413
 
364
414
  ## Repository
365
415
 
package/SettingsView.jsx CHANGED
@@ -2,11 +2,26 @@ import React from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import { getState } from './Context.jsx';
4
4
  import ThemeToggle from './ThemeToggle.jsx';
5
- import constants from "@/constants.json";
5
+ import { Avatar, AvatarFallback } from './shadcn/ui/avatar.jsx';
6
+ import { Badge } from './shadcn/ui/badge.jsx';
7
+ import {
8
+ AlertDialog,
9
+ AlertDialogAction,
10
+ AlertDialogCancel,
11
+ AlertDialogContent,
12
+ AlertDialogDescription,
13
+ AlertDialogFooter,
14
+ AlertDialogHeader,
15
+ AlertDialogTitle,
16
+ AlertDialogTrigger,
17
+ } from './shadcn/ui/alert-dialog.jsx';
18
+ import { getState } from "./Context.jsx";
6
19
  import pkg from '@package';
7
20
  import { showCheckout, showManage } from './Utilities';
8
21
 
9
22
  export default function SettingsView() {
23
+ const { state } = getState();
24
+ const constants = state.constants;
10
25
  const navigate = useNavigate();
11
26
  const { state, dispatch } = getState();
12
27
 
@@ -31,19 +46,41 @@ export default function SettingsView() {
31
46
  {(constants.noLogin === false || typeof constants.noLogin === 'undefined') && (
32
47
  <div className="w-full max-w-lg bg-accent rounded-2xl p-5">
33
48
  <div className="flex items-center gap-4">
34
- <div className="w-12 h-12 bg-app dark:text-black text-white flex justify-center items-center rounded-full font-medium">
35
- <span className="uppercase">{state.user?.name?.split(' ').map(word => word[0]).join('') || "NA"}</span>
36
- </div>
49
+ <Avatar size="lg">
50
+ <AvatarFallback className="bg-app dark:text-black text-white uppercase font-medium">
51
+ {state.user?.name?.split(' ').map(word => word[0]).join('') || "NA"}
52
+ </AvatarFallback>
53
+ </Avatar>
37
54
  <div className="flex-1 min-w-0">
38
- <div className="font-medium truncate capitalize">{state.user?.name || "No User"}</div>
55
+ <div className="font-medium truncate capitalize flex items-center gap-2">
56
+ {state.user?.name || "No User"}
57
+ {state.user?.subscription?.status === "active" && (
58
+ <Badge variant="default">Pro</Badge>
59
+ )}
60
+ </div>
39
61
  <div className="text-sm text-muted-foreground">{state.user?.email || "no@user.com"}</div>
40
62
  </div>
41
- <button
42
- onClick={signOutClicked}
43
- className="px-4 py-2 rounded-full text-sm bg-sidebar-background border border-foreground/30 hover:border-foreground transition-all cursor-pointer"
44
- >
45
- Sign Out
46
- </button>
63
+ <AlertDialog>
64
+ <AlertDialogTrigger asChild>
65
+ <button className="px-4 py-2 rounded-full text-sm bg-sidebar-background border border-foreground/30 hover:border-foreground transition-all cursor-pointer">
66
+ Sign Out
67
+ </button>
68
+ </AlertDialogTrigger>
69
+ <AlertDialogContent>
70
+ <AlertDialogHeader>
71
+ <AlertDialogTitle>Sign Out</AlertDialogTitle>
72
+ <AlertDialogDescription>
73
+ Are you sure you want to sign out? You'll need to sign in again to access your account.
74
+ </AlertDialogDescription>
75
+ </AlertDialogHeader>
76
+ <AlertDialogFooter>
77
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
78
+ <AlertDialogAction onClick={signOutClicked}>
79
+ Sign Out
80
+ </AlertDialogAction>
81
+ </AlertDialogFooter>
82
+ </AlertDialogContent>
83
+ </AlertDialog>
47
84
  </div>
48
85
  </div>
49
86
  )}
@@ -69,7 +106,16 @@ export default function SettingsView() {
69
106
  <div className="w-full max-w-lg bg-accent rounded-2xl p-5">
70
107
  <div className="flex items-center justify-between">
71
108
  <div>
72
- <div className="mb-1 font-medium">Billing</div>
109
+ <div className="mb-1 font-medium flex items-center gap-2">
110
+ Billing
111
+ {state.user?.subscription?.status === "active" ? (
112
+ <Badge variant="default">Active</Badge>
113
+ ) : state.user?.subscription?.status === "canceled" ? (
114
+ <Badge variant="destructive">Canceled</Badge>
115
+ ) : (
116
+ <Badge variant="secondary">Free</Badge>
117
+ )}
118
+ </div>
73
119
  <div className="text-sm text-muted-foreground">
74
120
  {state.user?.subscription?.status === null || typeof state.user?.subscription?.status === 'undefined'
75
121
  ? "Free plan"
package/Sheet.jsx CHANGED
@@ -1,10 +1,10 @@
1
1
  import { useState, useRef, useImperativeHandle, forwardRef } from 'react';
2
2
  import {
3
- Sheet,
4
- SheetContent,
5
- SheetHeader,
6
- SheetTitle,
7
- } from "./shadcn/ui/sheet"
3
+ Drawer,
4
+ DrawerContent,
5
+ DrawerHeader,
6
+ DrawerTitle,
7
+ } from "./shadcn/ui/drawer"
8
8
 
9
9
  const MySheet = forwardRef(function MySheet(props, ref) {
10
10
  const { title = "", minHeight = "auto", children } = props;
@@ -19,16 +19,16 @@ const MySheet = forwardRef(function MySheet(props, ref) {
19
19
  }));
20
20
 
21
21
  return (
22
- <Sheet open={isOpen} onOpenChange={setIsOpen}>
23
- <SheetContent className="bg-background w-full overflow-y-auto [&_button]:cursor-pointer [&_[role=button]]:cursor-pointer" side="bottom" style={{ minHeight }}>
24
- <SheetHeader className={"mb-0"}>
25
- <SheetTitle>{title}</SheetTitle>
26
- </SheetHeader>
27
- <span className="mx-4 mb-4">{children}</span>
22
+ <Drawer open={isOpen} onOpenChange={setIsOpen}>
23
+ <DrawerContent className="bg-background w-full overflow-y-auto [&_button]:cursor-pointer [&_[role=button]]:cursor-pointer" style={{ minHeight }}>
24
+ <DrawerHeader className={"mb-0"}>
25
+ <DrawerTitle>{title}</DrawerTitle>
26
+ </DrawerHeader>
27
+ <div className="px-4 pb-4">{children}</div>
28
28
 
29
-
30
- </SheetContent>
31
- </Sheet>
29
+
30
+ </DrawerContent>
31
+ </Drawer>
32
32
  );
33
33
  });
34
34
 
package/SignInView.jsx CHANGED
@@ -14,10 +14,12 @@ import DynamicIcon from './DynamicIcon';
14
14
  import { useState, useEffect, useRef } from 'react';
15
15
  import { useNavigate } from 'react-router-dom';
16
16
  import { getState } from './Context.jsx';
17
- import constants from "@/constants.json";
17
+ import { getState } from "./Context.jsx";
18
18
  import { getBackendURL } from './Utilities'
19
19
 
20
20
  export default function LoginForm({
21
+ const { state } = getState();
22
+ const constants = state.constants;
21
23
  className,
22
24
  ...props
23
25
  }) {
package/SignUpView.jsx CHANGED
@@ -8,20 +8,24 @@ import {
8
8
  } from "./shadcn/ui/card"
9
9
  import { Input } from "./shadcn/ui/input"
10
10
  import { Label } from "./shadcn/ui/label"
11
+ import { Checkbox } from "./shadcn/ui/checkbox.jsx"
11
12
  import DynamicIcon from './DynamicIcon';
12
13
  import { useState, useEffect, useRef } from 'react';
13
14
  import { useNavigate } from 'react-router-dom';
14
- import constants from "@/constants.json";
15
+ import { getState } from "./Context.jsx";
15
16
  import { getState } from './Context.jsx';
16
17
  import { getBackendURL } from './Utilities'
17
18
 
18
19
  export default function LoginForm({
20
+ const { state } = getState();
21
+ const constants = state.constants;
19
22
  className,
20
23
  ...props
21
24
  }) {
22
25
  const [email, setEmail] = useState('');
23
26
  const [password, setPassword] = useState('');
24
27
  const [name, setName] = useState('');
28
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
25
29
  const navigate = useNavigate();
26
30
  const { state, dispatch } = getState();
27
31
  const [errorMessage, setErrorMessage] = useState('')
@@ -45,6 +49,10 @@ export default function LoginForm({
45
49
  setErrorMessage('Password must be 72 characters or less');
46
50
  return;
47
51
  }
52
+ if (!acceptedTerms) {
53
+ setErrorMessage('You must accept the Terms of Service to continue');
54
+ return;
55
+ }
48
56
  try {
49
57
  const response = await fetch(`${getBackendURL()}/signup`, {
50
58
  method: 'POST',
@@ -138,6 +146,41 @@ export default function LoginForm({
138
146
  <p className="text-xs text-gray-500 dark:text-gray-400 ml-1">Minimum 6 characters</p>
139
147
  </div>
140
148
 
149
+ <div className="flex items-start gap-2 pt-2">
150
+ <Checkbox
151
+ id="terms"
152
+ checked={acceptedTerms}
153
+ onCheckedChange={setAcceptedTerms}
154
+ className="mt-0.5"
155
+ />
156
+ <label
157
+ htmlFor="terms"
158
+ className="text-sm text-gray-600 dark:text-gray-400 cursor-pointer"
159
+ >
160
+ I agree to the{' '}
161
+ <span
162
+ onClick={(e) => { e.preventDefault(); navigate('/terms'); }}
163
+ className="underline underline-offset-4 cursor-pointer text-gray-900 dark:text-white"
164
+ >
165
+ Terms of Service
166
+ </span>
167
+ {', '}
168
+ <span
169
+ onClick={(e) => { e.preventDefault(); navigate('/eula'); }}
170
+ className="underline underline-offset-4 cursor-pointer text-gray-900 dark:text-white"
171
+ >
172
+ EULA
173
+ </span>
174
+ {', and '}
175
+ <span
176
+ onClick={(e) => { e.preventDefault(); navigate('/privacy'); }}
177
+ className="underline underline-offset-4 cursor-pointer text-gray-900 dark:text-white"
178
+ >
179
+ Privacy Policy
180
+ </span>
181
+ </label>
182
+ </div>
183
+
141
184
  <button
142
185
  type="submit"
143
186
  className="relative group w-full text-white px-8 py-4 rounded-xl font-semibold text-lg transition-all duration-300 shadow-xl backdrop-blur-sm overflow-hidden cursor-pointer"