@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 +110 -0
- package/Header.jsx +17 -0
- package/LandingView.jsx +35 -0
- package/Layout.jsx +38 -0
- package/NotFound.jsx +13 -0
- package/README.md +22 -0
- package/SettingsView.jsx +144 -0
- package/Sheet.jsx +36 -0
- package/SignInView.jsx +128 -0
- package/SignUpView.jsx +132 -0
- package/StripeView.jsx +87 -0
- package/TabBar.jsx +51 -0
- package/TextView.jsx +21 -0
- package/Utilities.js +277 -0
- package/components.json +22 -0
- package/index.js +1 -0
- package/jsconfig.json +9 -0
- package/lucide-react/LICENSE +15 -0
- package/lucide-react/README.md +73 -0
- package/lucide-react/dynamic.d.ts +35438 -0
- package/lucide-react/dynamic.mjs +10 -0
- package/lucide-react/dynamicIconImports.d.ts +35409 -0
- package/lucide-react/dynamicIconImports.mjs +1 -0
- package/package.json +87 -0
- package/shadcn/hooks/use-mobile.js +19 -0
- package/shadcn/lib/utils.js +6 -0
- package/shadcn/ui/button.jsx +55 -0
- package/shadcn/ui/card.jsx +101 -0
- package/shadcn/ui/input.jsx +24 -0
- package/shadcn/ui/label.jsx +21 -0
- package/shadcn/ui/separator.jsx +27 -0
- package/shadcn/ui/sheet.jsx +139 -0
- package/shadcn/ui/sidebar.jsx +681 -0
- package/shadcn/ui/skeleton.jsx +15 -0
- package/shadcn/ui/tooltip.jsx +55 -0
- package/styles/globals.css +124 -0
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;
|
package/LandingView.jsx
ADDED
|
@@ -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">© {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
|
+
|
package/SettingsView.jsx
ADDED
|
@@ -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'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
|
+
}
|