@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.
- package/App.jsx +15 -10
- package/AppSidebar.jsx +97 -34
- package/CHANGELOG.md +21 -0
- package/Context.jsx +3 -1
- package/LandingView.jsx +6 -14
- package/Layout.jsx +1 -1
- package/PaymentView.jsx +3 -1
- package/README.md +50 -0
- package/SettingsView.jsx +58 -12
- package/Sheet.jsx +14 -14
- package/SignInView.jsx +3 -1
- package/SignUpView.jsx +44 -1
- package/SkeletonLoader.jsx +41 -0
- package/TabBar.jsx +3 -1
- package/TextView.jsx +3 -1
- package/ThemeToggle.jsx +17 -35
- package/Toast.jsx +11 -0
- package/USAGE.md +266 -0
- package/UpgradeSheet.jsx +10 -10
- package/package.json +10 -1
- package/shadcn/ui/accordion.jsx +1 -1
- package/shadcn/ui/alert-dialog.jsx +2 -2
- package/shadcn/ui/alert.jsx +1 -1
- package/shadcn/ui/aspect-ratio.jsx +1 -1
- package/shadcn/ui/avatar.jsx +1 -1
- package/shadcn/ui/badge.jsx +1 -1
- package/shadcn/ui/breadcrumb.jsx +1 -1
- package/shadcn/ui/button-group.jsx +2 -2
- package/shadcn/ui/button.jsx +1 -1
- package/shadcn/ui/calendar.jsx +2 -2
- package/shadcn/ui/card.jsx +1 -1
- package/shadcn/ui/carousel.jsx +2 -2
- package/shadcn/ui/chart.jsx +1 -1
- package/shadcn/ui/checkbox.jsx +1 -1
- package/shadcn/ui/command.jsx +3 -3
- package/shadcn/ui/context-menu.jsx +1 -1
- package/shadcn/ui/dialog.jsx +2 -2
- package/shadcn/ui/drawer.jsx +1 -1
- package/shadcn/ui/dropdown-menu.jsx +1 -1
- package/shadcn/ui/empty.jsx +1 -1
- package/shadcn/ui/field.jsx +3 -3
- package/shadcn/ui/hover-card.jsx +1 -1
- package/shadcn/ui/input-group.jsx +4 -4
- package/shadcn/ui/input.jsx +1 -1
- package/shadcn/ui/item.jsx +2 -2
- package/shadcn/ui/kbd.jsx +1 -1
- package/shadcn/ui/label.jsx +1 -1
- package/shadcn/ui/menubar.jsx +2 -2
- package/shadcn/ui/navigation-menu.jsx +1 -1
- package/shadcn/ui/pagination.jsx +2 -2
- package/shadcn/ui/popover.jsx +1 -1
- package/shadcn/ui/progress.jsx +1 -1
- package/shadcn/ui/radio-group.jsx +1 -1
- package/shadcn/ui/resizable.jsx +1 -1
- package/shadcn/ui/scroll-area.jsx +1 -1
- package/shadcn/ui/select.jsx +1 -1
- package/shadcn/ui/separator.jsx +1 -1
- package/shadcn/ui/sheet.jsx +2 -2
- package/shadcn/ui/sidebar.jsx +8 -8
- package/shadcn/ui/skeleton.jsx +1 -1
- package/shadcn/ui/slider.jsx +1 -1
- package/shadcn/ui/spinner.jsx +1 -1
- package/shadcn/ui/switch.jsx +1 -1
- package/shadcn/ui/table.jsx +1 -1
- package/shadcn/ui/tabs.jsx +1 -1
- package/shadcn/ui/textarea.jsx +1 -1
- package/shadcn/ui/toggle-group.jsx +2 -2
- package/shadcn/ui/toggle.jsx +1 -1
- package/shadcn/ui/tooltip.jsx +1 -1
- package/styles.css +4 -1
- 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
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
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
|
|
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
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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">
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./shadcn/ui/
|
|
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
|
-
<
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
</
|
|
27
|
-
<
|
|
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
|
-
</
|
|
31
|
-
</
|
|
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
|
|
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
|
|
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"
|