create-croissant 0.1.53 → 0.1.55

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-croissant",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Scaffold a new project using the Croissant Stack",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,6 +26,7 @@
26
26
  "@workspace/auth": "workspace:*",
27
27
  "@workspace/orpc": "workspace:*",
28
28
  "@workspace/ui": "workspace:*",
29
+ "better-auth": "1.6.11",
29
30
  "electron-updater": "^6.3.9",
30
31
  "lucide-react": "^1.14.0",
31
32
  "react": "19.2.5",
@@ -17,6 +17,20 @@ function createWindow(): void {
17
17
  },
18
18
  });
19
19
 
20
+ // Handle deep links
21
+ if (process.defaultApp) {
22
+ if (process.argv.length >= 2) {
23
+ app.setAsDefaultProtocolClient("desktop", process.execPath, [join(__dirname, "../../")]);
24
+ }
25
+ } else {
26
+ app.setAsDefaultProtocolClient("desktop");
27
+ }
28
+
29
+ app.on("open-url", (event, url) => {
30
+ event.preventDefault();
31
+ mainWindow.webContents.send("auth-callback", url);
32
+ });
33
+
20
34
  mainWindow.on("ready-to-show", () => {
21
35
  mainWindow.show();
22
36
  });
@@ -3,6 +3,9 @@ import type { ElectronAPI } from "@electron-toolkit/preload";
3
3
  declare global {
4
4
  interface Window {
5
5
  electron: ElectronAPI;
6
- api: unknown;
6
+ api: {
7
+ onAuthCallback: (callback: (url: string) => void) => () => void;
8
+ openExternal: (url: string) => Promise<void>;
9
+ };
7
10
  }
8
11
  }
@@ -1,8 +1,15 @@
1
- import { contextBridge } from "electron";
1
+ import { contextBridge, ipcRenderer, shell } from "electron";
2
2
  import { electronAPI } from "@electron-toolkit/preload";
3
3
 
4
4
  // Custom APIs for renderer
5
- const api = {};
5
+ const api = {
6
+ onAuthCallback: (callback: (url: string) => void) => {
7
+ const listener = (_: any, url: string) => callback(url);
8
+ ipcRenderer.on("auth-callback", listener);
9
+ return () => ipcRenderer.removeListener("auth-callback", listener);
10
+ },
11
+ openExternal: (url: string) => shell.openExternal(url),
12
+ };
6
13
 
7
14
  // Use `contextBridge` APIs to expose Electron APIs to
8
15
  // renderer only if context isolation is enabled, otherwise
@@ -58,10 +58,6 @@ export const publicNavItems = [
58
58
  title: "Login",
59
59
  url: "/login",
60
60
  },
61
- {
62
- title: "Sign Up",
63
- url: "/signup",
64
- },
65
61
  ],
66
62
  },
67
63
  {
@@ -157,7 +153,7 @@ export function AppSidebar({ items = authNavItems, ...props }: AppSidebarProps)
157
153
  <Settings className="h-4 w-4" />
158
154
  </SidebarMenuButton>
159
155
  <SidebarMenuButton
160
- onClick={async (e) => {
156
+ onClick={async (e: React.MouseEvent) => {
161
157
  e.preventDefault();
162
158
  e.stopPropagation();
163
159
  await authClient.signOut();
@@ -7,150 +7,49 @@ import {
7
7
  CardHeader,
8
8
  CardTitle,
9
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 "@renderer/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
- });
10
+ import { useEffect, useState } from "react";
11
+ import { useNavigate } from "@tanstack/react-router";
29
12
 
30
13
  export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
31
14
  const [loading, setLoading] = useState(false);
32
- const [error, setError] = useState<string | null>(null);
33
15
  const navigate = useNavigate();
34
16
 
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 {
17
+ useEffect(() => {
18
+ const unsubscribe = window.api.onAuthCallback(async (url) => {
19
+ const token = new URL(url).searchParams.get("token");
20
+ if (token) {
21
+ setLoading(true);
22
+ // Better Auth typically handles this via the setSession/proxy
23
+ // In deep link flow, we might need to manually set the session
24
+ // or let the client handle the token.
25
+ // For now, we redirect to dashboard and let authClient.getSession() handle it
53
26
  navigate({ to: "/dashboard" });
27
+ setLoading(false);
54
28
  }
55
- setLoading(false);
56
- },
57
- });
29
+ });
30
+ return () => unsubscribe();
31
+ }, [navigate]);
32
+
33
+ const handleBrowserLogin = () => {
34
+ const platformUrl = `${import.meta.env.VITE_API_URL.replace("/api/auth", "")}/login?redirect=desktop://auth-callback`;
35
+ window.api.openExternal(platformUrl);
36
+ };
58
37
 
59
38
  return (
60
39
  <div className={cn("flex flex-col gap-6", className)} {...props}>
61
40
  <Card>
62
- <CardHeader>
63
- <CardTitle>Login to your account</CardTitle>
64
- <CardDescription>Enter your email below to login to your account</CardDescription>
41
+ <CardHeader className="text-center">
42
+ <CardTitle className="text-xl">Login to your account</CardTitle>
43
+ <CardDescription>
44
+ Click the button below to login via your web browser.
45
+ </CardDescription>
65
46
  </CardHeader>
66
47
  <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) => ({
132
- canSubmit: state.canSubmit,
133
- isSubmitting: state.isSubmitting,
134
- })}
135
- >
136
- {(state: { canSubmit: boolean; isSubmitting: boolean }) => (
137
- <Button type="submit" disabled={!state.canSubmit || state.isSubmitting || loading}>
138
- {state.isSubmitting || loading ? "Logging in..." : "Login"}
139
- </Button>
140
- )}
141
- </form.Subscribe>
142
- <Button variant="outline" type="button" disabled={loading}>
143
- Login with Google
144
- </Button>
145
- <FieldDescription className="text-center">
146
- Don&apos;t have an account?{" "}
147
- <Link to="/signup" className="underline">
148
- Sign up
149
- </Link>
150
- </FieldDescription>
151
- </Field>
152
- </FieldGroup>
153
- </form>
48
+ <div className="flex flex-col gap-4">
49
+ <Button size="lg" className="w-full" onClick={handleBrowserLogin} disabled={loading}>
50
+ {loading ? "Authenticating..." : "Login from the web"}
51
+ </Button>
52
+ </div>
154
53
  </CardContent>
155
54
  </Card>
156
55
  </div>
@@ -12,7 +12,6 @@ import { Route as rootRouteImport } from './routes/__root'
12
12
  import { Route as PublicRouteImport } from './routes/_public'
13
13
  import { Route as AuthRouteImport } from './routes/_auth'
14
14
  import { Route as PublicIndexRouteImport } from './routes/_public/index'
15
- import { Route as PublicSignupRouteImport } from './routes/_public/signup'
16
15
  import { Route as PublicLoginRouteImport } from './routes/_public/login'
17
16
  import { Route as AuthDashboardRouteImport } from './routes/_auth/dashboard'
18
17
  import { Route as AuthAccountRouteImport } from './routes/_auth/account'
@@ -32,11 +31,6 @@ const PublicIndexRoute = PublicIndexRouteImport.update({
32
31
  path: '/',
33
32
  getParentRoute: () => PublicRoute,
34
33
  } as any)
35
- const PublicSignupRoute = PublicSignupRouteImport.update({
36
- id: '/signup',
37
- path: '/signup',
38
- getParentRoute: () => PublicRoute,
39
- } as any)
40
34
  const PublicLoginRoute = PublicLoginRouteImport.update({
41
35
  id: '/login',
42
36
  path: '/login',
@@ -70,7 +64,6 @@ export interface FileRoutesByFullPath {
70
64
  '/account': typeof AuthAccountRoute
71
65
  '/dashboard': typeof AuthDashboardRoute
72
66
  '/login': typeof PublicLoginRoute
73
- '/signup': typeof PublicSignupRoute
74
67
  '/examples/client-orpc-auth': typeof AuthExamplesClientOrpcAuthRoute
75
68
  '/examples/client-orpc': typeof PublicExamplesClientOrpcRoute
76
69
  }
@@ -79,7 +72,6 @@ export interface FileRoutesByTo {
79
72
  '/account': typeof AuthAccountRoute
80
73
  '/dashboard': typeof AuthDashboardRoute
81
74
  '/login': typeof PublicLoginRoute
82
- '/signup': typeof PublicSignupRoute
83
75
  '/examples/client-orpc-auth': typeof AuthExamplesClientOrpcAuthRoute
84
76
  '/examples/client-orpc': typeof PublicExamplesClientOrpcRoute
85
77
  }
@@ -90,7 +82,6 @@ export interface FileRoutesById {
90
82
  '/_auth/account': typeof AuthAccountRoute
91
83
  '/_auth/dashboard': typeof AuthDashboardRoute
92
84
  '/_public/login': typeof PublicLoginRoute
93
- '/_public/signup': typeof PublicSignupRoute
94
85
  '/_public/': typeof PublicIndexRoute
95
86
  '/_auth/examples/client-orpc-auth': typeof AuthExamplesClientOrpcAuthRoute
96
87
  '/_public/examples/client-orpc': typeof PublicExamplesClientOrpcRoute
@@ -102,7 +93,6 @@ export interface FileRouteTypes {
102
93
  | '/account'
103
94
  | '/dashboard'
104
95
  | '/login'
105
- | '/signup'
106
96
  | '/examples/client-orpc-auth'
107
97
  | '/examples/client-orpc'
108
98
  fileRoutesByTo: FileRoutesByTo
@@ -111,7 +101,6 @@ export interface FileRouteTypes {
111
101
  | '/account'
112
102
  | '/dashboard'
113
103
  | '/login'
114
- | '/signup'
115
104
  | '/examples/client-orpc-auth'
116
105
  | '/examples/client-orpc'
117
106
  id:
@@ -121,7 +110,6 @@ export interface FileRouteTypes {
121
110
  | '/_auth/account'
122
111
  | '/_auth/dashboard'
123
112
  | '/_public/login'
124
- | '/_public/signup'
125
113
  | '/_public/'
126
114
  | '/_auth/examples/client-orpc-auth'
127
115
  | '/_public/examples/client-orpc'
@@ -155,13 +143,6 @@ declare module '@tanstack/react-router' {
155
143
  preLoaderRoute: typeof PublicIndexRouteImport
156
144
  parentRoute: typeof PublicRoute
157
145
  }
158
- '/_public/signup': {
159
- id: '/_public/signup'
160
- path: '/signup'
161
- fullPath: '/signup'
162
- preLoaderRoute: typeof PublicSignupRouteImport
163
- parentRoute: typeof PublicRoute
164
- }
165
146
  '/_public/login': {
166
147
  id: '/_public/login'
167
148
  path: '/login'
@@ -216,14 +197,12 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
216
197
 
217
198
  interface PublicRouteChildren {
218
199
  PublicLoginRoute: typeof PublicLoginRoute
219
- PublicSignupRoute: typeof PublicSignupRoute
220
200
  PublicIndexRoute: typeof PublicIndexRoute
221
201
  PublicExamplesClientOrpcRoute: typeof PublicExamplesClientOrpcRoute
222
202
  }
223
203
 
224
204
  const PublicRouteChildren: PublicRouteChildren = {
225
205
  PublicLoginRoute: PublicLoginRoute,
226
- PublicSignupRoute: PublicSignupRoute,
227
206
  PublicIndexRoute: PublicIndexRoute,
228
207
  PublicExamplesClientOrpcRoute: PublicExamplesClientOrpcRoute,
229
208
  }
@@ -1,10 +1,12 @@
1
- import { Outlet, createRootRoute } from "@tanstack/react-router";
1
+ import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
3
  import { Toaster } from "@workspace/ui/components/sonner";
4
4
  import { ThemeProvider } from "@workspace/ui/components/theme-provider";
5
5
  import { ORPCProvider } from "@workspace/orpc/react";
6
6
  import { orpc } from "@renderer/lib/orpc";
7
7
  import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
8
+ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@workspace/ui/components/empty";
9
+ import { Button, buttonVariants } from "@workspace/ui/components/button";
8
10
 
9
11
  import "@workspace/ui/globals.css";
10
12
 
@@ -12,6 +14,23 @@ const queryClient = new QueryClient();
12
14
 
13
15
  export const Route = createRootRoute({
14
16
  component: RootLayout,
17
+ notFoundComponent: () => {
18
+ return (
19
+ <Empty className="h-screen border-none">
20
+ <EmptyHeader>
21
+ <EmptyTitle>404 - Page Not Found</EmptyTitle>
22
+ <EmptyDescription>
23
+ The page you are looking for does not exist or has been moved.
24
+ </EmptyDescription>
25
+ </EmptyHeader>
26
+ <EmptyContent>
27
+ <Link to="/" className={buttonVariants({ variant: "outline" })}>
28
+ Go Home
29
+ </Link>
30
+ </EmptyContent>
31
+ </Empty>
32
+ );
33
+ },
15
34
  });
16
35
 
17
36
  function RootLayout() {
@@ -12,6 +12,7 @@
12
12
  "db:studio": "drizzle-kit studio"
13
13
  },
14
14
  "dependencies": {
15
+ "@better-auth/electron": "1.6.11",
15
16
  "@noble/ciphers": "^2.2.0",
16
17
  "@tailwindcss/vite": "^4.2.4",
17
18
  "@tanstack/react-router": "^1.168.24",
@@ -20,11 +21,12 @@
20
21
  "@workspace/auth": "workspace:*",
21
22
  "@workspace/orpc": "workspace:*",
22
23
  "@workspace/ui": "workspace:*",
24
+ "better-auth": "1.6.11",
23
25
  "lucide-react": "^1.11.0",
24
26
  "nitro": "latest",
25
- "sonner": "^2.0.7",
26
27
  "react": "19.2.5",
27
28
  "react-dom": "19.2.5",
29
+ "sonner": "^2.0.7",
28
30
  "tailwindcss": "^4.2.4",
29
31
  "vite-tsconfig-paths": "^6.1.1"
30
32
  },
@@ -16,7 +16,7 @@ import {
16
16
  } from "@workspace/ui/components/field";
17
17
  import { Input } from "@workspace/ui/components/input";
18
18
  import { useState } from "react";
19
- import { Link } from "@tanstack/react-router";
19
+ import { Link, useSearch } from "@tanstack/react-router";
20
20
  import { useForm } from "@tanstack/react-form";
21
21
  import { z } from "zod";
22
22
 
@@ -30,6 +30,7 @@ const loginSchema = z.object({
30
30
  export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
31
31
  const [loading, setLoading] = useState(false);
32
32
  const [error, setError] = useState<string | null>(null);
33
+ const search = useSearch({ from: "/_public/login" }) as { redirect?: string };
33
34
 
34
35
  const form = useForm({
35
36
  defaultValues: {
@@ -45,7 +46,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<"div">)
45
46
  const { error: signInError } = await authClient.signIn.email({
46
47
  email: value.email,
47
48
  password: value.password,
48
- callbackURL: "/dashboard",
49
+ callbackURL: search.redirect || "/dashboard",
49
50
  });
50
51
  if (signInError) {
51
52
  setError(signInError.message || "Failed to sign in");
@@ -0,0 +1,13 @@
1
+ import { createAuthClient } from "better-auth/client";
2
+ import { electronProxyClient } from "@better-auth/electron/proxy";
3
+
4
+ export const authClient = createAuthClient({
5
+ baseURL: import.meta.env.VITE_API_URL,
6
+ plugins: [
7
+ electronProxyClient({
8
+ protocol: {
9
+ scheme: "com.example.app"
10
+ },
11
+ }),
12
+ ],
13
+ });
@@ -1,3 +1,5 @@
1
1
  import { createAuthClient } from "better-auth/react";
2
2
 
3
- export const authClient = createAuthClient();
3
+ export const authClient = createAuthClient({
4
+ baseURL: typeof window !== "undefined" ? window.location.origin : "http://localhost:3000",
5
+ });
@@ -1,9 +1,11 @@
1
- import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
1
+ import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
3
  import { Toaster } from "@workspace/ui/components/sonner";
4
4
  import { ThemeProvider } from "@workspace/ui/components/theme-provider";
5
5
  import { ORPCProvider } from "@workspace/orpc/react";
6
6
  import { orpc } from "@/lib/orpc";
7
+ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@workspace/ui/components/empty";
8
+ import { Button, buttonVariants } from "@workspace/ui/components/button";
7
9
 
8
10
  import appCss from "@workspace/ui/globals.css?url";
9
11
 
@@ -31,6 +33,23 @@ export const Route = createRootRoute({
31
33
  ],
32
34
  }),
33
35
  shellComponent: RootDocument,
36
+ notFoundComponent: () => {
37
+ return (
38
+ <Empty className="h-screen border-none">
39
+ <EmptyHeader>
40
+ <EmptyTitle>404 - Page Not Found</EmptyTitle>
41
+ <EmptyDescription>
42
+ The page you are looking for does not exist or has been moved.
43
+ </EmptyDescription>
44
+ </EmptyHeader>
45
+ <EmptyContent>
46
+ <Link to="/" className={buttonVariants({ variant: "outline" })}>
47
+ Go Home
48
+ </Link>
49
+ </EmptyContent>
50
+ </Empty>
51
+ );
52
+ },
34
53
  });
35
54
 
36
55
  function RootDocument({ children }: { children: React.ReactNode }) {
@@ -25,11 +25,9 @@
25
25
  "@orpc/server": "latest",
26
26
  "@orpc/tanstack-query": "latest",
27
27
  "@tanstack/react-form": "latest",
28
- "@tanstack/react-query": "latest",
29
- "better-auth": "^1.6.11"
28
+ "@tanstack/react-query": "latest"
30
29
  },
31
30
  "devDependencies": {
32
- "@better-auth/core": "^1.6.11",
33
31
  "husky": "latest",
34
32
  "oxfmt": "latest",
35
33
  "oxlint": "latest",
@@ -12,7 +12,8 @@
12
12
  "generate": "better-auth generate --output ../db/src/lib/auth-schema.ts"
13
13
  },
14
14
  "dependencies": {
15
- "@workspace/db": "workspace:*"
15
+ "@workspace/db": "workspace:*",
16
+ "better-auth": "1.6.11"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@better-auth/cli": "^1.4.22",
@@ -1,205 +0,0 @@
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 "@renderer/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&apos;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) => ({
179
- canSubmit: state.canSubmit,
180
- isSubmitting: state.isSubmitting,
181
- })}
182
- >
183
- {(state: { canSubmit: boolean; isSubmitting: boolean }) => (
184
- <Button type="submit" disabled={!state.canSubmit || state.isSubmitting || loading}>
185
- {state.isSubmitting || loading ? "Creating..." : "Create Account"}
186
- </Button>
187
- )}
188
- </form.Subscribe>
189
- <Button variant="outline" type="button" disabled={loading}>
190
- Sign up with Google
191
- </Button>
192
- <FieldDescription className="px-6 text-center">
193
- Already have an account?{" "}
194
- <Link to="/login" className="underline">
195
- Sign in
196
- </Link>
197
- </FieldDescription>
198
- </Field>
199
- </FieldGroup>
200
- </FieldGroup>
201
- </form>
202
- </CardContent>
203
- </Card>
204
- );
205
- }
@@ -1,16 +0,0 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
- import { SignupForm } from "@renderer/components/signup-form";
3
-
4
- export const Route = createFileRoute("/_public/signup")({
5
- component: Signup,
6
- });
7
-
8
- function Signup() {
9
- return (
10
- <div className="flex min-h-svh items-center justify-center p-6">
11
- <div className="w-full max-w-sm">
12
- <SignupForm />
13
- </div>
14
- </div>
15
- );
16
- }