create-croissant 0.1.49 → 0.1.50
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/package.json +1 -1
- package/template/apps/desktop/electron.vite.config.ts +11 -1
- package/template/apps/desktop/package.json +19 -1
- package/template/apps/desktop/src/renderer/index.html +1 -1
- package/template/apps/desktop/src/renderer/src/components/app-sidebar.tsx +186 -0
- package/template/apps/desktop/src/renderer/src/components/login-form.tsx +154 -0
- package/template/apps/desktop/src/renderer/src/components/signup-form.tsx +201 -0
- package/template/apps/desktop/src/renderer/src/lib/auth-client.ts +3 -0
- package/template/apps/desktop/src/renderer/src/lib/orpc.ts +12 -0
- package/template/apps/desktop/src/renderer/src/main.tsx +15 -4
- package/template/apps/desktop/src/renderer/src/routeTree.gen.ts +240 -0
- package/template/apps/desktop/src/renderer/src/routes/__root.tsx +29 -0
- package/template/apps/desktop/src/renderer/src/routes/_auth/account.tsx +267 -0
- package/template/apps/desktop/src/renderer/src/routes/_auth/dashboard.tsx +46 -0
- package/template/apps/desktop/src/renderer/src/routes/_auth/examples/client-orpc-auth.tsx +35 -0
- package/template/apps/desktop/src/renderer/src/routes/_auth.tsx +35 -0
- package/template/apps/desktop/src/renderer/src/routes/_public/examples/client-orpc.tsx +306 -0
- package/template/apps/desktop/src/renderer/src/routes/_public/index.tsx +54 -0
- package/template/apps/desktop/src/renderer/src/routes/_public/login.tsx +16 -0
- package/template/apps/desktop/src/renderer/src/routes/_public/signup.tsx +16 -0
- package/template/apps/desktop/src/renderer/src/routes/_public.tsx +23 -0
- package/template/apps/desktop/tsconfig.web.json +1 -0
- package/template/package.json +3 -0
- package/template/packages/ui/package.json +4 -2
- package/template/apps/desktop/src/renderer/src/App.tsx +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { defineConfig } from "electron-vite";
|
|
3
3
|
import react from "@vitejs/plugin-react";
|
|
4
|
+
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
|
5
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
6
|
|
|
5
7
|
export default defineConfig({
|
|
6
8
|
main: {},
|
|
@@ -8,9 +10,17 @@ export default defineConfig({
|
|
|
8
10
|
renderer: {
|
|
9
11
|
resolve: {
|
|
10
12
|
alias: {
|
|
13
|
+
"@": resolve("src/renderer/src"),
|
|
11
14
|
"@renderer": resolve("src/renderer/src"),
|
|
12
15
|
},
|
|
13
16
|
},
|
|
14
|
-
plugins: [
|
|
17
|
+
plugins: [
|
|
18
|
+
tanstackRouter({
|
|
19
|
+
routesDirectory: resolve("src/renderer/src/routes"),
|
|
20
|
+
generatedRouteTree: resolve("src/renderer/src/routeTree.gen.ts"),
|
|
21
|
+
}),
|
|
22
|
+
tailwindcss(),
|
|
23
|
+
react(),
|
|
24
|
+
],
|
|
15
25
|
},
|
|
16
26
|
});
|
|
@@ -21,10 +21,28 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@electron-toolkit/preload": "^3.0.2",
|
|
23
23
|
"@electron-toolkit/utils": "^4.0.0",
|
|
24
|
-
"
|
|
24
|
+
"@orpc/client": "^1.14.3",
|
|
25
|
+
"@orpc/server": "^1.14.3",
|
|
26
|
+
"@orpc/tanstack-query": "^1.14.3",
|
|
27
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
28
|
+
"@tanstack/react-form": "^1.32.0",
|
|
29
|
+
"@tanstack/react-query": "^5.100.10",
|
|
30
|
+
"@tanstack/react-router": "^1.169.2",
|
|
31
|
+
"@workspace/auth": "workspace:*",
|
|
32
|
+
"@workspace/orpc": "workspace:*",
|
|
33
|
+
"@workspace/ui": "workspace:*",
|
|
34
|
+
"better-auth": "^1.6.11",
|
|
35
|
+
"electron-updater": "^6.3.9",
|
|
36
|
+
"lucide-react": "^1.14.0",
|
|
37
|
+
"react": "19.2.5",
|
|
38
|
+
"react-dom": "19.2.5",
|
|
39
|
+
"sonner": "^2.0.7",
|
|
40
|
+
"tailwindcss": "^4.2.4"
|
|
25
41
|
},
|
|
26
42
|
"devDependencies": {
|
|
27
43
|
"@electron-toolkit/tsconfig": "^2.0.0",
|
|
44
|
+
"@tanstack/react-router-devtools": "^1.166.13",
|
|
45
|
+
"@tanstack/router-plugin": "^1.167.35",
|
|
28
46
|
"@types/node": "^22.19.1",
|
|
29
47
|
"@vitejs/plugin-react": "^5.1.1",
|
|
30
48
|
"@workspace/config-typescript": "workspace:*",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
|
7
7
|
<meta
|
|
8
8
|
http-equiv="Content-Security-Policy"
|
|
9
|
-
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data
|
|
9
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://platform.localhost"
|
|
10
10
|
/>
|
|
11
11
|
</head>
|
|
12
12
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Link } from "@tanstack/react-router";
|
|
3
|
+
import { LogOut, Settings, User } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Sidebar,
|
|
7
|
+
SidebarContent,
|
|
8
|
+
SidebarFooter,
|
|
9
|
+
SidebarGroup,
|
|
10
|
+
SidebarGroupContent,
|
|
11
|
+
SidebarGroupLabel,
|
|
12
|
+
SidebarHeader,
|
|
13
|
+
SidebarMenu,
|
|
14
|
+
SidebarMenuButton,
|
|
15
|
+
SidebarMenuItem,
|
|
16
|
+
SidebarRail,
|
|
17
|
+
} from "@workspace/ui/components/sidebar";
|
|
18
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/avatar";
|
|
19
|
+
import { ModeToggle } from "@workspace/ui/components/mode-toggle";
|
|
20
|
+
|
|
21
|
+
import { authClient } from "@/lib/auth-client";
|
|
22
|
+
|
|
23
|
+
// This is sample data.
|
|
24
|
+
export const authNavItems = [
|
|
25
|
+
{
|
|
26
|
+
title: "Dashboard",
|
|
27
|
+
items: [
|
|
28
|
+
{
|
|
29
|
+
title: "Overview",
|
|
30
|
+
url: "/dashboard",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Account",
|
|
34
|
+
url: "/account",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: "Examples",
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
title: "Client + oRPC (Auth)",
|
|
43
|
+
url: "/examples/client-orpc-auth",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const publicNavItems = [
|
|
50
|
+
{
|
|
51
|
+
title: "Welcome",
|
|
52
|
+
items: [
|
|
53
|
+
{
|
|
54
|
+
title: "Home",
|
|
55
|
+
url: "/",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: "Login",
|
|
59
|
+
url: "/login",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Sign Up",
|
|
63
|
+
url: "/signup",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: "Examples",
|
|
69
|
+
items: [
|
|
70
|
+
{
|
|
71
|
+
title: "Client + oRPC",
|
|
72
|
+
url: "/examples/client-orpc",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|
79
|
+
items?: Array<{
|
|
80
|
+
title: string;
|
|
81
|
+
items: Array<{
|
|
82
|
+
title: string;
|
|
83
|
+
url: string;
|
|
84
|
+
}>;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function AuthSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
89
|
+
return <AppSidebar items={authNavItems} {...props} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function PublicSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
93
|
+
return <AppSidebar items={publicNavItems} {...props} />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function AppSidebar({ items = authNavItems, ...props }: AppSidebarProps) {
|
|
97
|
+
const { data: session } = authClient.useSession();
|
|
98
|
+
|
|
99
|
+
const user = session?.user || null;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Sidebar {...props}>
|
|
103
|
+
<SidebarHeader>
|
|
104
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
105
|
+
<span className="font-bold">Croissant Desktop</span>
|
|
106
|
+
<ModeToggle />
|
|
107
|
+
</div>
|
|
108
|
+
</SidebarHeader>
|
|
109
|
+
<SidebarContent>
|
|
110
|
+
{items.map((item) => (
|
|
111
|
+
<SidebarGroup key={item.title}>
|
|
112
|
+
<SidebarGroupLabel>{item.title}</SidebarGroupLabel>
|
|
113
|
+
<SidebarGroupContent>
|
|
114
|
+
<SidebarMenu>
|
|
115
|
+
{item.items.map((subItem) => (
|
|
116
|
+
<SidebarMenuItem key={subItem.title}>
|
|
117
|
+
<SidebarMenuButton
|
|
118
|
+
render={
|
|
119
|
+
<Link to={subItem.url} activeProps={{ className: "bg-sidebar-accent" }} />
|
|
120
|
+
}
|
|
121
|
+
>
|
|
122
|
+
{subItem.title}
|
|
123
|
+
</SidebarMenuButton>
|
|
124
|
+
</SidebarMenuItem>
|
|
125
|
+
))}
|
|
126
|
+
</SidebarMenu>
|
|
127
|
+
</SidebarGroupContent>
|
|
128
|
+
</SidebarGroup>
|
|
129
|
+
))}
|
|
130
|
+
</SidebarContent>
|
|
131
|
+
<SidebarFooter>
|
|
132
|
+
<SidebarMenu>
|
|
133
|
+
<SidebarMenuItem>
|
|
134
|
+
{user ? (
|
|
135
|
+
<SidebarMenuButton
|
|
136
|
+
size="lg"
|
|
137
|
+
render={<div />}
|
|
138
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
139
|
+
>
|
|
140
|
+
<div className="flex items-center gap-2 w-full">
|
|
141
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
142
|
+
<AvatarImage src={user.image || ""} alt={user.name} />
|
|
143
|
+
<AvatarFallback className="rounded-lg">
|
|
144
|
+
{user.name.charAt(0) || "U"}
|
|
145
|
+
</AvatarFallback>
|
|
146
|
+
</Avatar>
|
|
147
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
148
|
+
<span className="truncate font-semibold">{user.name}</span>
|
|
149
|
+
<span className="truncate text-xs">{user.email}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="flex items-center gap-1 ml-auto">
|
|
152
|
+
<SidebarMenuButton
|
|
153
|
+
render={<Link to="/account" />}
|
|
154
|
+
className="p-1 h-auto w-auto rounded hover:bg-sidebar-accent-foreground/10"
|
|
155
|
+
title="Account Settings"
|
|
156
|
+
>
|
|
157
|
+
<Settings className="h-4 w-4" />
|
|
158
|
+
</SidebarMenuButton>
|
|
159
|
+
<SidebarMenuButton
|
|
160
|
+
onClick={async (e) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
await authClient.signOut();
|
|
164
|
+
window.location.reload();
|
|
165
|
+
}}
|
|
166
|
+
className="p-1 h-auto w-auto rounded hover:bg-sidebar-accent-foreground/10"
|
|
167
|
+
title="Sign Out"
|
|
168
|
+
>
|
|
169
|
+
<LogOut className="h-4 w-4" />
|
|
170
|
+
</SidebarMenuButton>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</SidebarMenuButton>
|
|
174
|
+
) : (
|
|
175
|
+
<SidebarMenuButton render={<Link to="/login" className="flex items-center gap-2" />}>
|
|
176
|
+
<User className="h-4 w-4" />
|
|
177
|
+
<span>Sign In</span>
|
|
178
|
+
</SidebarMenuButton>
|
|
179
|
+
)}
|
|
180
|
+
</SidebarMenuItem>
|
|
181
|
+
</SidebarMenu>
|
|
182
|
+
</SidebarFooter>
|
|
183
|
+
<SidebarRail />
|
|
184
|
+
</Sidebar>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { cn } from "@workspace/ui/lib/utils";
|
|
2
|
+
import { Button } from "@workspace/ui/components/button";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "@workspace/ui/components/card";
|
|
10
|
+
import {
|
|
11
|
+
Field,
|
|
12
|
+
FieldDescription,
|
|
13
|
+
FieldError,
|
|
14
|
+
FieldGroup,
|
|
15
|
+
FieldLabel,
|
|
16
|
+
} from "@workspace/ui/components/field";
|
|
17
|
+
import { Input } from "@workspace/ui/components/input";
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
20
|
+
import { useForm } from "@tanstack/react-form";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
import { authClient } from "@/lib/auth-client";
|
|
24
|
+
|
|
25
|
+
const loginSchema = z.object({
|
|
26
|
+
email: z.string().email("Invalid email address"),
|
|
27
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const navigate = useNavigate();
|
|
34
|
+
|
|
35
|
+
const form = useForm({
|
|
36
|
+
defaultValues: {
|
|
37
|
+
email: "",
|
|
38
|
+
password: "",
|
|
39
|
+
},
|
|
40
|
+
validators: {
|
|
41
|
+
onChange: loginSchema,
|
|
42
|
+
},
|
|
43
|
+
onSubmit: async ({ value }) => {
|
|
44
|
+
setLoading(true);
|
|
45
|
+
setError(null);
|
|
46
|
+
const { error: signInError } = await authClient.signIn.email({
|
|
47
|
+
email: value.email,
|
|
48
|
+
password: value.password,
|
|
49
|
+
});
|
|
50
|
+
if (signInError) {
|
|
51
|
+
setError(signInError.message || "Failed to sign in");
|
|
52
|
+
} else {
|
|
53
|
+
navigate({ to: "/dashboard" });
|
|
54
|
+
}
|
|
55
|
+
setLoading(false);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
|
61
|
+
<Card>
|
|
62
|
+
<CardHeader>
|
|
63
|
+
<CardTitle>Login to your account</CardTitle>
|
|
64
|
+
<CardDescription>Enter your email below to login to your account</CardDescription>
|
|
65
|
+
</CardHeader>
|
|
66
|
+
<CardContent>
|
|
67
|
+
<form
|
|
68
|
+
onSubmit={(e) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
form.handleSubmit();
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<FieldGroup>
|
|
75
|
+
{error && <div className="rounded bg-red-100 p-2 text-sm text-red-600">{error}</div>}
|
|
76
|
+
<form.Field
|
|
77
|
+
name="email"
|
|
78
|
+
children={(field) => {
|
|
79
|
+
const isInvalid =
|
|
80
|
+
field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
81
|
+
return (
|
|
82
|
+
<Field data-invalid={isInvalid}>
|
|
83
|
+
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
|
84
|
+
<Input
|
|
85
|
+
id={field.name}
|
|
86
|
+
name={field.name}
|
|
87
|
+
type="email"
|
|
88
|
+
placeholder="m@example.com"
|
|
89
|
+
value={field.state.value}
|
|
90
|
+
onBlur={field.handleBlur}
|
|
91
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
92
|
+
required
|
|
93
|
+
/>
|
|
94
|
+
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
95
|
+
</Field>
|
|
96
|
+
);
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
<form.Field
|
|
100
|
+
name="password"
|
|
101
|
+
children={(field) => {
|
|
102
|
+
const isInvalid =
|
|
103
|
+
field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
104
|
+
return (
|
|
105
|
+
<Field data-invalid={isInvalid}>
|
|
106
|
+
<div className="flex items-center">
|
|
107
|
+
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
|
108
|
+
<a
|
|
109
|
+
href="#"
|
|
110
|
+
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
|
111
|
+
>
|
|
112
|
+
Forgot your password?
|
|
113
|
+
</a>
|
|
114
|
+
</div>
|
|
115
|
+
<Input
|
|
116
|
+
id={field.name}
|
|
117
|
+
name={field.name}
|
|
118
|
+
type="password"
|
|
119
|
+
value={field.state.value}
|
|
120
|
+
onBlur={field.handleBlur}
|
|
121
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
122
|
+
required
|
|
123
|
+
/>
|
|
124
|
+
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
125
|
+
</Field>
|
|
126
|
+
);
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
<Field>
|
|
130
|
+
<form.Subscribe
|
|
131
|
+
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
|
132
|
+
children={([canSubmit, isSubmitting]) => (
|
|
133
|
+
<Button type="submit" disabled={!canSubmit || isSubmitting || loading}>
|
|
134
|
+
{isSubmitting || loading ? "Logging in..." : "Login"}
|
|
135
|
+
</Button>
|
|
136
|
+
)}
|
|
137
|
+
/>
|
|
138
|
+
<Button variant="outline" type="button" disabled={loading}>
|
|
139
|
+
Login with Google
|
|
140
|
+
</Button>
|
|
141
|
+
<FieldDescription className="text-center">
|
|
142
|
+
Don't have an account?{" "}
|
|
143
|
+
<Link to="/signup" className="underline">
|
|
144
|
+
Sign up
|
|
145
|
+
</Link>
|
|
146
|
+
</FieldDescription>
|
|
147
|
+
</Field>
|
|
148
|
+
</FieldGroup>
|
|
149
|
+
</form>
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Button } from "@workspace/ui/components/button";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "@workspace/ui/components/card";
|
|
9
|
+
import {
|
|
10
|
+
Field,
|
|
11
|
+
FieldDescription,
|
|
12
|
+
FieldError,
|
|
13
|
+
FieldGroup,
|
|
14
|
+
FieldLabel,
|
|
15
|
+
} from "@workspace/ui/components/field";
|
|
16
|
+
import { Input } from "@workspace/ui/components/input";
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
19
|
+
import { useForm } from "@tanstack/react-form";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { authClient } from "@/lib/auth-client";
|
|
22
|
+
|
|
23
|
+
const signupSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
name: z.string().min(1, "Name is required"),
|
|
26
|
+
email: z.string().email("Invalid email address"),
|
|
27
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
28
|
+
confirmPassword: z.string().min(1, "Confirm password is required"),
|
|
29
|
+
})
|
|
30
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
31
|
+
message: "Passwords do not match",
|
|
32
|
+
path: ["confirmPassword"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export function SignupForm({ ...props }: React.ComponentProps<typeof Card>) {
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
|
|
40
|
+
const form = useForm({
|
|
41
|
+
defaultValues: {
|
|
42
|
+
name: "",
|
|
43
|
+
email: "",
|
|
44
|
+
password: "",
|
|
45
|
+
confirmPassword: "",
|
|
46
|
+
},
|
|
47
|
+
validators: {
|
|
48
|
+
onChange: signupSchema,
|
|
49
|
+
},
|
|
50
|
+
onSubmit: async ({ value }) => {
|
|
51
|
+
setLoading(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
const { error: signUpError } = await authClient.signUp.email({
|
|
54
|
+
email: value.email,
|
|
55
|
+
password: value.password,
|
|
56
|
+
name: value.name,
|
|
57
|
+
});
|
|
58
|
+
if (signUpError) {
|
|
59
|
+
setError(signUpError.message || "Failed to sign up");
|
|
60
|
+
} else {
|
|
61
|
+
navigate({ to: "/dashboard" });
|
|
62
|
+
}
|
|
63
|
+
setLoading(false);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Card {...props}>
|
|
69
|
+
<CardHeader>
|
|
70
|
+
<CardTitle>Create an account</CardTitle>
|
|
71
|
+
<CardDescription>Enter your information below to create your account</CardDescription>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent>
|
|
74
|
+
<form
|
|
75
|
+
onSubmit={(e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
form.handleSubmit();
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<FieldGroup>
|
|
82
|
+
{error && <div className="rounded bg-red-100 p-2 text-sm text-red-600">{error}</div>}
|
|
83
|
+
<form.Field
|
|
84
|
+
name="name"
|
|
85
|
+
children={(field) => {
|
|
86
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
87
|
+
return (
|
|
88
|
+
<Field data-invalid={isInvalid}>
|
|
89
|
+
<FieldLabel htmlFor={field.name}>Full Name</FieldLabel>
|
|
90
|
+
<Input
|
|
91
|
+
id={field.name}
|
|
92
|
+
name={field.name}
|
|
93
|
+
type="text"
|
|
94
|
+
placeholder="John Doe"
|
|
95
|
+
value={field.state.value}
|
|
96
|
+
onBlur={field.handleBlur}
|
|
97
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
98
|
+
required
|
|
99
|
+
/>
|
|
100
|
+
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
101
|
+
</Field>
|
|
102
|
+
);
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
<form.Field
|
|
106
|
+
name="email"
|
|
107
|
+
children={(field) => {
|
|
108
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
109
|
+
return (
|
|
110
|
+
<Field data-invalid={isInvalid}>
|
|
111
|
+
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
|
112
|
+
<Input
|
|
113
|
+
id={field.name}
|
|
114
|
+
name={field.name}
|
|
115
|
+
type="email"
|
|
116
|
+
placeholder="m@example.com"
|
|
117
|
+
value={field.state.value}
|
|
118
|
+
onBlur={field.handleBlur}
|
|
119
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
120
|
+
required
|
|
121
|
+
/>
|
|
122
|
+
<FieldDescription>
|
|
123
|
+
We'll use this to contact you. We will not share your email with anyone
|
|
124
|
+
else.
|
|
125
|
+
</FieldDescription>
|
|
126
|
+
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
127
|
+
</Field>
|
|
128
|
+
);
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
<form.Field
|
|
132
|
+
name="password"
|
|
133
|
+
children={(field) => {
|
|
134
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
135
|
+
return (
|
|
136
|
+
<Field data-invalid={isInvalid}>
|
|
137
|
+
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
|
138
|
+
<Input
|
|
139
|
+
id={field.name}
|
|
140
|
+
name={field.name}
|
|
141
|
+
type="password"
|
|
142
|
+
value={field.state.value}
|
|
143
|
+
onBlur={field.handleBlur}
|
|
144
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
145
|
+
required
|
|
146
|
+
/>
|
|
147
|
+
<FieldDescription>Must be at least 8 characters long.</FieldDescription>
|
|
148
|
+
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
149
|
+
</Field>
|
|
150
|
+
);
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
<form.Field
|
|
154
|
+
name="confirmPassword"
|
|
155
|
+
children={(field) => {
|
|
156
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
157
|
+
return (
|
|
158
|
+
<Field data-invalid={isInvalid}>
|
|
159
|
+
<FieldLabel htmlFor={field.name}>Confirm Password</FieldLabel>
|
|
160
|
+
<Input
|
|
161
|
+
id={field.name}
|
|
162
|
+
name={field.name}
|
|
163
|
+
type="password"
|
|
164
|
+
value={field.state.value}
|
|
165
|
+
onBlur={field.handleBlur}
|
|
166
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
167
|
+
required
|
|
168
|
+
/>
|
|
169
|
+
<FieldDescription>Please confirm your password.</FieldDescription>
|
|
170
|
+
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
171
|
+
</Field>
|
|
172
|
+
);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
<FieldGroup>
|
|
176
|
+
<Field>
|
|
177
|
+
<form.Subscribe
|
|
178
|
+
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
|
179
|
+
children={([canSubmit, isSubmitting]) => (
|
|
180
|
+
<Button type="submit" disabled={!canSubmit || isSubmitting || loading}>
|
|
181
|
+
{isSubmitting || loading ? "Creating..." : "Create Account"}
|
|
182
|
+
</Button>
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
<Button variant="outline" type="button" disabled={loading}>
|
|
186
|
+
Sign up with Google
|
|
187
|
+
</Button>
|
|
188
|
+
<FieldDescription className="px-6 text-center">
|
|
189
|
+
Already have an account?{" "}
|
|
190
|
+
<Link to="/login" className="underline">
|
|
191
|
+
Sign in
|
|
192
|
+
</Link>
|
|
193
|
+
</FieldDescription>
|
|
194
|
+
</Field>
|
|
195
|
+
</FieldGroup>
|
|
196
|
+
</FieldGroup>
|
|
197
|
+
</form>
|
|
198
|
+
</CardContent>
|
|
199
|
+
</Card>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createORPCClient } from "@orpc/client";
|
|
2
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
3
|
+
import { RouterClient } from "@orpc/server";
|
|
4
|
+
import { router } from "@workspace/orpc/router";
|
|
5
|
+
|
|
6
|
+
const link = new RPCLink({
|
|
7
|
+
// In a real desktop app, this might point to a local or remote server
|
|
8
|
+
// For now, we assume it's the same origin as the platform's API
|
|
9
|
+
url: `https://platform.localhost/api/rpc`,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const orpc: RouterClient<typeof router> = createORPCClient(link)
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import "./assets/main.css";
|
|
2
|
-
|
|
3
1
|
import { StrictMode } from "react";
|
|
4
2
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import
|
|
3
|
+
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
|
4
|
+
|
|
5
|
+
// Import the generated route tree
|
|
6
|
+
import { routeTree } from "./routeTree.gen";
|
|
7
|
+
|
|
8
|
+
// Create a new router instance
|
|
9
|
+
const router = createRouter({ routeTree });
|
|
10
|
+
|
|
11
|
+
// Register the router instance for type safety
|
|
12
|
+
declare module "@tanstack/react-router" {
|
|
13
|
+
interface Register {
|
|
14
|
+
router: typeof router;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
createRoot(document.getElementById("root")!).render(
|
|
8
19
|
<StrictMode>
|
|
9
|
-
<
|
|
20
|
+
<RouterProvider router={router} />
|
|
10
21
|
</StrictMode>,
|
|
11
22
|
);
|