@stevederico/skateboard-ui 0.5.0

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/AppSidebar.jsx ADDED
@@ -0,0 +1,110 @@
1
+ import { useNavigate, useLocation } from "react-router-dom";
2
+ import constants from "@/constants.json";
3
+ import { DynamicIcon } from "./lucide-react/dynamic"; // Verify this import
4
+ import {
5
+ Sidebar,
6
+ SidebarContent,
7
+ SidebarMenu,
8
+ SidebarRail,
9
+ SidebarFooter,
10
+ SidebarHeader,
11
+ useSidebar,
12
+ } from "./shadcn/ui/sidebar";
13
+
14
+
15
+ // Use this if your DynamicIcon import isn't working
16
+ const DynamicIconComponent = DynamicIcon
17
+
18
+ export default function AppSidebar() {
19
+ const { open, setOpen } = useSidebar();
20
+ const navigate = useNavigate();
21
+ const location = useLocation();
22
+ const currentPage = (location.pathname.split("/")[2] || "").toLowerCase();
23
+
24
+ const handleNavigation = (url) => {
25
+ navigate(url);
26
+ };
27
+
28
+ return (
29
+ <Sidebar collapsible="icon" className="min-w-[40px]">
30
+ <SidebarHeader className="p-0">
31
+ <SidebarMenu>
32
+ <div className={`flex flex-row m-2 mt-8 mb-8 items-center ${open ? "ml-4" : "justify-center ml-2"}`}>
33
+ <div className="bg-app dark:border rounded-lg flex aspect-square size-10 items-center justify-center">
34
+ <DynamicIconComponent
35
+ name={constants.appIcon}
36
+ size={28}
37
+ color="white"
38
+ strokeWidth={2}
39
+ />
40
+ </div>
41
+ {open && <div className="font-semibold ml-2 text-xl">{constants.appName}</div>}
42
+ </div>
43
+ </SidebarMenu>
44
+ </SidebarHeader>
45
+ <SidebarContent>
46
+ <ul className={`flex flex-col gap-2 p-2 ${open ? "" : "items-center"}`}>
47
+ {constants.pages.map((item) => {
48
+ const isActive = currentPage === item.url.toLowerCase();
49
+ return (
50
+ <li key={item.title}>
51
+ <div
52
+ className={`cursor-pointer items-center flex w-full p-2 rounded-lg ${open ? "h-10" : "h-10 w-8"} ${isActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"}`}
53
+ onClick={() => handleNavigation(`/app/${item.url.toLowerCase()}`)}
54
+ >
55
+ <span className="flex w-full">
56
+ <DynamicIconComponent
57
+ name={item.icon}
58
+ size={24}
59
+ strokeWidth={1.5}
60
+ className={"!size-6"}
61
+ />
62
+ {open && <span className="ml-2">{item.title}</span>}
63
+ </span>
64
+ </div>
65
+ </li>
66
+ );
67
+ })}
68
+ </ul>
69
+ </SidebarContent>
70
+ <SidebarFooter>
71
+ <ul className={`flex flex-col gap-1 ${open ? "" : "items-center"}`}>
72
+ <li>
73
+ <div
74
+ className={`cursor-pointer flex w-full p-2 ${open ? "h-10" : "h-10 w-8"}`}
75
+ onClick={() => setOpen(!open)}
76
+ >
77
+ <span className="flex w-full items-center">
78
+ <DynamicIconComponent
79
+ name="panel-left-close"
80
+ size={18}
81
+ strokeWidth={1.5}
82
+ className={"!size-5"}
83
+ />
84
+ {open && <span className="ml-2 text-sm">Collapse</span>}
85
+ </span>
86
+ </div>
87
+ </li>
88
+ <li>
89
+ <div
90
+ className={`cursor-pointer items-center rounded-lg flex w-full p-2 ${open ? "h-10" : "h-10 w-8"}
91
+ ${location.pathname.toLowerCase().includes("settings") ? "bg-sidebar-accent text-sidebar-accent-foreground" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"}`}
92
+ onClick={() => handleNavigation("/app/settings")}
93
+ >
94
+ <span className="flex w-full items-center">
95
+ <DynamicIconComponent
96
+ name="settings"
97
+ size={18}
98
+ strokeWidth={1.5}
99
+ className={"!size-5 "}
100
+ />
101
+ {open && <span className="ml-2 text-sm">Settings</span>}
102
+ </span>
103
+ </div>
104
+ </li>
105
+ </ul>
106
+ </SidebarFooter>
107
+ <SidebarRail />
108
+ </Sidebar>
109
+ );
110
+ }
package/Header.jsx ADDED
@@ -0,0 +1,17 @@
1
+ function Header(props) {
2
+ return (
3
+ <div className="flex w-full bg-background py-8 px-4 border-b ">
4
+ <span className="font-semibold text-4xl">{props.title}</span>
5
+ {typeof props.buttonTitle !== "undefined" && (
6
+ <button
7
+ className={`ml-auto bg-app text-white px-4 py-2 rounded mr-0 ${props.buttonClass || ''}`}
8
+ onClick={props.onButtonTitleClick}
9
+ >
10
+ {props.buttonTitle}
11
+ </button>
12
+ )}
13
+ </div>
14
+ );
15
+ }
16
+
17
+ export default Header;
@@ -0,0 +1,35 @@
1
+ import constants from "@/constants.json";
2
+
3
+ import { DynamicIcon } from "./lucide-react/dynamic"; // Verify this import
4
+
5
+ export default function LandingView() {
6
+ return (
7
+ <div className="flex flex-col bg-white h-screen">
8
+ <header className="py-2 ">
9
+ <div className="flex items-center m-2 mx-auto">
10
+ <div className="bg-app rounded-lg flex items-center justify-center ml-3 w-8 h-8">
11
+ <DynamicIcon name={constants.appIcon} size={18} color="white" strokeWidth={2} />
12
+ </div>
13
+ <div className="font-semibold ml-2 text-2xl text-gray-700">{constants.appName}</div>
14
+ </div>
15
+ </header>
16
+ <main className="py-24 md:py-48 bg-app">
17
+ <div className="flex flex-col items-center mb-6">
18
+ <h1 className="text-center tracking-tight font-bold text-5xl md:text-7xl mb-10 text-white">{constants.tagline}</h1>
19
+ <a href={'/app'} target="_blank" className="mx-auto bg-white font-medium text-app shadow-sm rounded-3xl px-4 md:px-8 py-4 cursor-pointer">
20
+ Get Started
21
+ </a>
22
+ </div>
23
+ </main>
24
+ <footer className="py-4 mx-3">
25
+ <div className="flex gap-3 text-gray-500 hover:text-gray-600 cursor-pointer">
26
+ <div className="mr-auto">&copy; {new Date().getFullYear()} {constants.companyName}</div>
27
+ <a href={'/privacy'} target="_blank">Privacy</a>
28
+ <a href={'/terms'} target="_blank">Terms</a>
29
+ <a href={'/eula'} target="_blank" className="mr-3">EULA</a>
30
+ </div>
31
+
32
+ </footer>
33
+ </div>
34
+ )
35
+ }
package/Layout.jsx ADDED
@@ -0,0 +1,38 @@
1
+ import { Outlet } from 'react-router-dom';
2
+ import TabBar from './TabBar.jsx'
3
+ import { SidebarProvider, SidebarTrigger } from "./shadcn/ui/sidebar"
4
+ import AppSidebar from "./AppSidebar"
5
+ import { useEffect } from 'react';
6
+
7
+ export default function Layout({ children }) {
8
+
9
+ useEffect(() => {
10
+ const root = document.documentElement;
11
+ let theme = localStorage.getItem('theme')
12
+ if (theme === 'dark') {
13
+ root.classList.add('dark');
14
+ } else {
15
+ root.classList.remove('dark');
16
+ }
17
+ localStorage.setItem('theme', theme);
18
+ }, []);
19
+
20
+ return (
21
+ <div className="min-h-screen">
22
+ <div className="fixed inset-0 flex overflow-hidden pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]">
23
+ <SidebarProvider>
24
+ <AppSidebar />
25
+ <main className="flex-1 relative overflow-y-auto scrollbar-hide">
26
+ <Outlet />
27
+ </main>
28
+ </SidebarProvider>
29
+ </div>
30
+ <TabBar className="md:hidden" />
31
+ </div>
32
+ );
33
+ }
34
+
35
+
36
+
37
+
38
+
package/NotFound.jsx ADDED
@@ -0,0 +1,13 @@
1
+ export default function NotFound() {
2
+
3
+ return (
4
+ <>
5
+ <div className="w-full py-6">
6
+ <h1 className="text-center font-bold leading-tight my-6 text-4xl">Page Not Found</h1>
7
+ <p className="text-center font-light leading-tight my-6 text-black text-xl">Are you sure you have the right URL?</p>
8
+ </div>
9
+ </>
10
+ )
11
+ }
12
+
13
+
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # skateboard-ui
2
+
3
+ skateboard-ui is the component library for the @stevederico/skatebaord react boilerplate.
4
+
5
+
6
+ 0.5.0
7
+ * added lucide-react
8
+ 0.4.2
9
+ * removed sign out and billing from noLogin apps
10
+ 0.4.1
11
+ - fixed subscribe on Settings
12
+ 0.4.0
13
+ - fixed stripe showCheckout
14
+ 0.3.9
15
+ - fixed token expiration
16
+ 0.3.8
17
+ - updated cookie handling
18
+
19
+
20
+
21
+
22
+
@@ -0,0 +1,144 @@
1
+ import { useNavigate } from 'react-router-dom';
2
+ import { getState } from '@/context.jsx';
3
+ import { useEffect, useState } from 'react';
4
+ import { DynamicIcon } from "./lucide-react/dynamic"; // Verify this import
5
+ import constants from "@/constants.json";
6
+ import pkg from '@package';
7
+ import { showCheckout } from './Utilities';
8
+ import Header from './Header.jsx';
9
+
10
+ export default function SettingsView() {
11
+ const navigate = useNavigate();
12
+ const { state, dispatch } = getState();
13
+
14
+ function signOutClicked() {
15
+ dispatch({ type: 'CLEAR_USER', payload: null });
16
+ navigate('/signin');
17
+ }
18
+
19
+ return (
20
+ <div className="h-full min-h-screen flex flex-col">
21
+ {/* Navbar */}
22
+ <div className="flex border-b w-full items-center">
23
+ <Header
24
+ buttonClass=""
25
+ title={"Settings"}
26
+ >
27
+ </Header>
28
+ <div className="ml-auto mr-5 pt-1">
29
+ <ThemeToggle />
30
+ </div>
31
+ </div>
32
+
33
+ {/* Main content */}
34
+ <div className="flex flex-col flex-1 items-center p-4 gap-6">
35
+ {(constants.noLogin == false || typeof constants.noLogin === 'undefined') && (
36
+
37
+ <div className="w-full bg-accent p-6 rounded flex items-center justify-between">
38
+ <div className="w-10 h-10 bg-app dark:text-black text-white flex justify-center items-center rounded-full">
39
+ <span className="uppercase">{state.user?.name?.split(' ').map(word => word[0]).join('') || "NA"}</span>
40
+ </div>
41
+ <div className="ml-4">
42
+ <div className="text font-medium block mb-1 capitalize">{state.user?.name || "No User"}</div>
43
+ <div className="text-sm text-gray-500">{state.user?.email || "no@user.com"}</div>
44
+ </div>
45
+ <div className="ml-auto">
46
+ <button className="bg-sidebar-background text-center border-foreground border ml-2 px-3 py-2 rounded text-sm border cursor-pointer" onClick={() => {
47
+ signOutClicked()
48
+ }}>Sign Out</button>
49
+ </div>
50
+ </div>
51
+ )}
52
+
53
+ {/* SUPPORT */}
54
+ <div className="flex gap-6 w-full">
55
+ <div className="bg-accent p-6 rounded flex-1">
56
+ <div className="flex items-center">
57
+ <div>
58
+ <div className="mb-2 font-medium">Contact Support</div>
59
+ <div className="text-sm text-gray-500">How can we help you?</div>
60
+ </div>
61
+ <div className="ml-auto">
62
+ <div onClick={() => { window.location.href = `mailto:${constants.companyEmail}`; }} className="bg-sidebar-background text-center border-foreground border ml-2 px-3 py-2 rounded text-sm whitespace-nowrap cursor-pointer">Support</div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ {/* BILLING */}
69
+ {(constants.noLogin == false || typeof constants.noLogin === 'undefined') && (
70
+ <div className="flex gap-6 mb-10 w-full">
71
+ <div className="bg-accent p-6 rounded flex-1">
72
+ <div className="flex items-center">
73
+ <div>
74
+ <div className="mb-2 font-medium">Billing</div>
75
+ <div className="text-sm text-gray-500">
76
+ {state.user?.subStatus === null || typeof state.user?.subStatus === 'undefined'
77
+ ? "Your plan is free"
78
+ : ["active", "canceled"].includes(state.user?.subStatus)
79
+ ? `Your plan ${state.user?.subStatus === "active" ? "renews" : "ends"} ${new Date(state.user.expires * 1000).toLocaleDateString('en-US')}`
80
+ : `Your plan is ${state.user?.subStatus}`
81
+ }
82
+ </div>
83
+ </div>
84
+
85
+ <div className="ml-auto">
86
+ {state.user?.stripeID ? (
87
+ <div onClick={() => { showManage(state.user?.stripeID) }} className="bg-sidebar-background border-foreground border ml-2 px-3 py-2 rounded text-sm whitespace-nowrap cursor-pointer text-center">Manage</div>
88
+ ) : (
89
+ <div onClick={() => { showCheckout(state.user?.email) }} className="bg-app text-white border-app border ml-2 px-3 py-2 rounded text-sm whitespace-nowrap cursor-pointer">Subscribe</div>
90
+ )}
91
+ </div>
92
+
93
+ </div>
94
+ </div>
95
+ </div>
96
+ )}
97
+ </div>
98
+
99
+ {/* Footer Links */}
100
+ <div className="mt-auto text-center">
101
+ <div className="m-2 mb-4 block text-sm text-gray-500 pb-24 md:pb-0">v{pkg.version}</div>
102
+ </div>
103
+ </div>
104
+
105
+
106
+ );
107
+ }
108
+
109
+
110
+ const ThemeToggle = () => {
111
+ const [theme, setTheme] = useState(() => {
112
+ return localStorage.getItem('theme') || 'light';
113
+ });
114
+
115
+ useEffect(() => {
116
+ const root = document.documentElement;
117
+ if (theme === 'dark') {
118
+ root.classList.add('dark');
119
+ } else {
120
+ root.classList.remove('dark');
121
+ }
122
+ localStorage.setItem('theme', theme);
123
+ }, [theme]);
124
+
125
+
126
+ const toggleTheme = () => {
127
+ setTheme((prevTheme) => (prevTheme === 'dark' ? 'light' : 'dark'));
128
+ };
129
+
130
+ return (
131
+ <button onClick={toggleTheme} className="cursor-pointer">
132
+ {theme === 'dark' &&
133
+ <span className="text-gray-500">
134
+ <DynamicIcon name={'sun'} size={24} />
135
+ </span>
136
+ }
137
+ {theme !== 'dark' &&
138
+ <span className="text-gray-500">
139
+ <DynamicIcon name={'moon'} size={24} />
140
+ </span>
141
+ }
142
+ </button>
143
+ );
144
+ };
package/Sheet.jsx ADDED
@@ -0,0 +1,36 @@
1
+ import { useState, useRef, useImperativeHandle, forwardRef } from 'react';
2
+ import {
3
+ Sheet,
4
+ SheetContent,
5
+ SheetHeader,
6
+ SheetTitle,
7
+ } from "./shadcn/ui/sheet"
8
+
9
+ const MySheet = forwardRef(function MySheet(props, ref) {
10
+ const { title = "", minHeight = "auto", children } = props;
11
+ const [isOpen, setIsOpen] = useState(false);
12
+
13
+ useImperativeHandle(ref, () => ({
14
+ show: () => setIsOpen(true),
15
+ hide: () => setIsOpen(false),
16
+ open: () => setIsOpen(true),
17
+ close: () => setIsOpen(false),
18
+ toggle: () => setIsOpen(prev => !prev)
19
+ }));
20
+
21
+ return (
22
+ <Sheet open={isOpen} onOpenChange={setIsOpen}>
23
+ <SheetContent className="bg-background w-full overflow-y-auto" side="bottom" style={{ minHeight }}>
24
+ <SheetHeader className={"mb-0"}>
25
+ <SheetTitle>{title}</SheetTitle>
26
+ </SheetHeader>
27
+ <span className="mx-4 mb-4">{children}</span>
28
+
29
+
30
+ </SheetContent>
31
+ </Sheet>
32
+ );
33
+ });
34
+
35
+ export default MySheet;
36
+
package/SignInView.jsx ADDED
@@ -0,0 +1,128 @@
1
+ import { cn } from "./shadcn/lib/utils"
2
+ import { Button } from "./shadcn/ui/button"
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "./shadcn/ui/card"
10
+ import { Input } from "./shadcn/ui/input"
11
+ import { Label } from "./shadcn/ui/label"
12
+ import { DynamicIcon } from "./lucide-react/dynamic"; // Verify this import
13
+
14
+ import { useState } from 'react';
15
+ import { useNavigate } from 'react-router-dom';
16
+ import { getState } from '@/context.jsx';
17
+ import constants from "@/constants.json";
18
+ import { getBackendURL } from './Utilities'
19
+
20
+ export default function LoginForm({
21
+ className,
22
+ ...props
23
+ }) {
24
+ const [email, setEmail] = useState('');
25
+ const [password, setPassword] = useState('');
26
+ const [isSubmitting, setIsSubmitting] = useState(false);
27
+ const navigate = useNavigate();
28
+ const { dispatch } = getState();
29
+
30
+ const [errorMessage, setErrorMessage] = useState('')
31
+
32
+ async function signInClicked(e) {
33
+ e.preventDefault();
34
+ if (isSubmitting) return;
35
+ setIsSubmitting(true);
36
+ try {
37
+ const uri = `${getBackendURL()}/signin`;
38
+ console.log("URI ", uri)
39
+ const response = await fetch(uri, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ email, password })
43
+ });
44
+
45
+ if (response.ok) {
46
+ const data = await response.json();
47
+ document.cookie = `token=${data.token}; path=/; Secure; SameSite=Strict; expires=${new Date(data.tokenExpires * 1000).toUTCString()}`;
48
+ delete data.token;
49
+ dispatch({ type: 'SET_USER', payload: data });
50
+ navigate('/app');
51
+ } else {
52
+ setErrorMessage('Invalid Credentials');
53
+ }
54
+ } catch (error) {
55
+ setErrorMessage('Server Error');
56
+ } finally {
57
+ setIsSubmitting(false);
58
+ }
59
+ }
60
+
61
+ return (
62
+ <div className={cn("flex flex-col gap-6 p-4 md:max-w-[50%] mx-auto", className)} {...props}>
63
+ <Card>
64
+ <CardHeader>
65
+ <div className="flex flex-row items-center m-2 mx-auto">
66
+ <div className="bg-app dark:border rounded-lg flex aspect-square size-10 items-center justify-center">
67
+ <DynamicIcon name={constants.appIcon} size={24} color="white" strokeWidth={2} />
68
+ </div>
69
+ <div className="font-semibold ml-2 text-4xl">{constants.appName}</div>
70
+ </div>
71
+ {errorMessage && (
72
+ <div className="text-red-500 text-center font-semibold rounded-xl py-2">
73
+ {errorMessage}
74
+ </div>
75
+ )}
76
+ </CardHeader>
77
+ <CardContent>
78
+ <form onSubmit={signInClicked}>
79
+ <div className="flex flex-col gap-6">
80
+ <div className="grid gap-3">
81
+ <Label htmlFor="email">Email</Label>
82
+ <Input
83
+ id="email"
84
+ type="email"
85
+ placeholder="mcfly@example.com"
86
+ required
87
+ value={email}
88
+ onChange={(e) => {
89
+ setEmail(e.target.value);
90
+ setErrorMessage('');
91
+ }}
92
+ />
93
+ </div>
94
+ <div className="grid gap-3">
95
+ <div className="flex items-center">
96
+ <Label htmlFor="password">Password</Label>
97
+ </div>
98
+ <Input
99
+ id="password"
100
+ type="password"
101
+ placeholder="password"
102
+ required
103
+ value={password}
104
+ onChange={(e) => setPassword(e.target.value)}
105
+ />
106
+ </div>
107
+ <div className="flex flex-col gap-3">
108
+ <Button
109
+ type="submit"
110
+ className="w-full cursor-pointer py-6"
111
+ disabled={isSubmitting}
112
+ >
113
+ {isSubmitting ? "Signing in..." : "Sign In"}
114
+ </Button>
115
+ </div>
116
+ </div>
117
+ <div className="mt-6 text-center text-sm">
118
+ Don&apos;t have an account?{" "}
119
+ <span onClick={() => navigate('/signup')} className="underline underline-offset-4 cursor-pointer">
120
+ Sign Up
121
+ </span>
122
+ </div>
123
+ </form>
124
+ </CardContent>
125
+ </Card>
126
+ </div>
127
+ );
128
+ }
package/SignUpView.jsx ADDED
@@ -0,0 +1,132 @@
1
+ import { cn } from "./shadcn/lib/utils"
2
+ import { Button } from "./shadcn/ui/button"
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardHeader,
7
+ } from "./shadcn/ui/card"
8
+ import { Input } from "./shadcn/ui/input"
9
+ import { Label } from "./shadcn/ui/label"
10
+ import { DynamicIcon } from "./lucide-react/dynamic"; // Verify this import
11
+ import { useState } from 'react';
12
+ import { useNavigate } from 'react-router-dom';
13
+ import constants from "@/constants.json";
14
+ import { getState } from '@/context.jsx';
15
+ import { getBackendURL } from './Utilities'
16
+
17
+ export default function LoginForm({
18
+ className,
19
+ ...props
20
+ }) {
21
+ const [email, setEmail] = useState('');
22
+ const [password, setPassword] = useState('');
23
+ const [name, setName] = useState('');
24
+ const navigate = useNavigate();
25
+ const { state, dispatch } = getState();
26
+ const [errorMessage, setErrorMessage] = useState('')
27
+
28
+ async function signUpClicked() {
29
+ try {
30
+ console.log(`${getBackendURL()}/signup`);
31
+ console.log(`name: ${name}`);
32
+ const response = await fetch(`${getBackendURL()}/signup`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ email, password, name })
36
+ });
37
+
38
+ if (response.ok) {
39
+ const data = await response.json();
40
+
41
+ const expireDate = new Date();
42
+ expireDate.setTime(expireDate.getTime() + 24 * 60 * 60 * 1000);
43
+ const tokenValue = encodeURIComponent(data.token);
44
+ const hostname = window.location.hostname;
45
+ document.cookie = `token=${tokenValue}; path=/; domain=${hostname}; expires=${expireDate.toUTCString()}`;
46
+
47
+ delete data.token
48
+ dispatch({ type: 'SET_USER', payload: data });
49
+ navigate('/app');
50
+ } else {
51
+ setErrorMessage('Invalid Credentials')
52
+ console.log("error with /signup")
53
+ }
54
+ } catch (error) {
55
+ console.error('Signup failed:', error);
56
+ setErrorMessage('Server Error')
57
+ }
58
+ }
59
+
60
+ return (
61
+ (<div className={cn("flex flex-col gap-6 p-4 md:max-w-[50%] mx-auto", className)} {...props}>
62
+ <Card>
63
+ <CardHeader>
64
+ <div className="flex flex-row items-center m-2 mx-auto">
65
+ <div className="bg-app dark:border rounded-lg flex aspect-square size-10 items-center justify-center">
66
+ <DynamicIcon name={constants.appIcon} size={24} color="white" strokeWidth={2} />
67
+ </div>
68
+ <div className="font-semibold ml-2 text-4xl">{constants.appName}</div>
69
+ </div>
70
+ {errorMessage !== '' && (
71
+ <div className=" text-red-500 text-center font-semibold rounded-xl py-2">
72
+ {errorMessage}
73
+ </div>
74
+ )}
75
+ </CardHeader>
76
+ <CardContent>
77
+ <form>
78
+ <div className="flex flex-col gap-6">
79
+ <div className="grid gap-3">
80
+ <Label htmlFor="name">Name</Label>
81
+ <Input id="name" placeholder="John Smith" required value={name}
82
+ onChange={(e) => {
83
+ setName(e.target.value);
84
+ setErrorMessage('');
85
+ }} />
86
+ </div>
87
+ <div className="grid gap-3">
88
+ <Label htmlFor="email">Email</Label>
89
+ <Input id="email" type="email" placeholder="mcfly@example.com" required value={email}
90
+ onChange={(e) => {
91
+ setEmail(e.target.value);
92
+ setErrorMessage('');
93
+ }} />
94
+ </div>
95
+ <div className="grid gap-3">
96
+ <div className="flex items-center">
97
+ <Label htmlFor="password">Password</Label>
98
+ </div>
99
+ <Input id="password" type="password" placeholder="password" required value={password} onChange={(e) => setPassword(e.target.value)} />
100
+ </div>
101
+ <div className="flex flex-col gap-3">
102
+ <Button onClick={(e) => { e.preventDefault(); signUpClicked() }} className="w-full cursor-pointer py-6">
103
+ Sign Up
104
+ </Button>
105
+ </div>
106
+ </div>
107
+ <div className="mt-6 text-center text-sm">
108
+ Already have an account?{" "}
109
+ <span onClick={(e) => { e.preventDefault(); navigate('/signin'); }} className="underline underline-offset-4 cursor-pointer">
110
+ Sign In
111
+ </span>
112
+ </div>
113
+
114
+ </form>
115
+ </CardContent>
116
+ </Card>
117
+
118
+ <div className="mt-6 text-center text-sm">
119
+ By registering you agree to our
120
+ <span onClick={(e) => { e.preventDefault(); navigate('/terms'); }} className="ml-1 underline underline-offset-4 cursor-pointer">
121
+ Terms of Service
122
+ </span>,
123
+ <span onClick={(e) => { e.preventDefault(); navigate('/eula'); }} className="ml-1 underline underline-offset-4 cursor-pointer">
124
+ EULA
125
+ </span>,
126
+ <span onClick={(e) => { e.preventDefault(); navigate('/privacy'); }} className="ml-1 underline underline-offset-4 cursor-pointer">
127
+ Privacy Policy
128
+ </span>
129
+ </div>
130
+ </div>)
131
+ );
132
+ }