@jonsoc/web 1.1.34

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.
Files changed (44) hide show
  1. package/.env.example +3 -0
  2. package/components.json +24 -0
  3. package/index.html +13 -0
  4. package/package.json +48 -0
  5. package/public/apple-touch-icon-v3.png +1 -0
  6. package/public/apple-touch-icon.png +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/robots.txt +6 -0
  14. package/public/site.webmanifest +1 -0
  15. package/public/social-share-zen.png +1 -0
  16. package/public/social-share.png +1 -0
  17. package/public/theme.json +183 -0
  18. package/public/web-app-manifest-192x192.png +1 -0
  19. package/public/web-app-manifest-512x512.png +1 -0
  20. package/src/components/header.tsx +30 -0
  21. package/src/components/loader.tsx +9 -0
  22. package/src/components/mode-toggle.tsx +24 -0
  23. package/src/components/sign-in-form.tsx +123 -0
  24. package/src/components/sign-up-form.tsx +148 -0
  25. package/src/components/theme-provider.tsx +8 -0
  26. package/src/components/ui/button.tsx +51 -0
  27. package/src/components/ui/card.tsx +72 -0
  28. package/src/components/ui/checkbox.tsx +26 -0
  29. package/src/components/ui/dropdown-menu.tsx +235 -0
  30. package/src/components/ui/input.tsx +20 -0
  31. package/src/components/ui/label.tsx +20 -0
  32. package/src/components/ui/skeleton.tsx +7 -0
  33. package/src/components/ui/sonner.tsx +39 -0
  34. package/src/components/user-menu.tsx +50 -0
  35. package/src/index.css +127 -0
  36. package/src/lib/auth-client.ts +8 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/main.tsx +42 -0
  39. package/src/routeTree.gen.ts +77 -0
  40. package/src/routes/__root.tsx +47 -0
  41. package/src/routes/dashboard.tsx +39 -0
  42. package/src/routes/index.tsx +46 -0
  43. package/tsconfig.json +18 -0
  44. package/vite.config.ts +17 -0
package/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ # Convex URLs
2
+ VITE_CONVEX_URL=
3
+ VITE_CONVEX_SITE_URL=
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-lyra",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/index.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "menuColor": "default",
22
+ "menuAccent": "subtle",
23
+ "registries": {}
24
+ }
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>jonsoc</title>
7
+ </head>
8
+
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@jonsoc/web",
3
+ "version": "1.1.34",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "serve": "vite preview",
10
+ "check-types": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@base-ui/react": "catalog:",
14
+ "@convex-dev/better-auth": "catalog:",
15
+ "@hookform/resolvers": "catalog:",
16
+ "@jonsoc/convex": "workspace:*",
17
+ "@jonsoc/env": "workspace:*",
18
+ "@tailwindcss/vite": "catalog:",
19
+ "@tanstack/react-form": "catalog:",
20
+ "@tanstack/react-router": "catalog:",
21
+ "better-auth": "catalog:",
22
+ "class-variance-authority": "^0.7.1",
23
+ "clsx": "^2.1.1",
24
+ "convex": "catalog:",
25
+ "lucide-react": "catalog:",
26
+ "next-themes": "catalog:",
27
+ "react": "catalog:",
28
+ "react-dom": "catalog:",
29
+ "sonner": "catalog:",
30
+ "tailwind-merge": "^3.3.1",
31
+ "tw-animate-css": "^1.2.5",
32
+ "zod": "catalog:"
33
+ },
34
+ "devDependencies": {
35
+ "@jonsoc/config": "workspace:*",
36
+ "@tanstack/react-router-devtools": "catalog:",
37
+ "@tanstack/router-plugin": "catalog:",
38
+ "@types/react": "19.2.7",
39
+ "@types/react-dom": "19.2.3",
40
+ "@vitejs/plugin-react": "^4.3.4",
41
+ "tailwindcss": "catalog:",
42
+ "typescript": "catalog:",
43
+ "vite": "catalog:"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/apple-touch-icon-v3.png
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/apple-touch-icon.png
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/favicon-96x96-v3.png
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/favicon-96x96.png
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/favicon-v3.ico
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/favicon-v3.svg
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/favicon.ico
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/favicon.svg
@@ -0,0 +1,6 @@
1
+ User-agent: *
2
+ Allow: /
3
+
4
+ # Disallow shared content pages
5
+ Disallow: /s/
6
+ Disallow: /share/
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/site.webmanifest
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/images/social-share-zen.png
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/images/social-share.png
@@ -0,0 +1,183 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "$schema": {
6
+ "type": "string",
7
+ "description": "JSON schema reference for configuration validation"
8
+ },
9
+ "defs": {
10
+ "type": "object",
11
+ "description": "Color definitions that can be referenced in the theme",
12
+ "patternProperties": {
13
+ "^[a-zA-Z][a-zA-Z0-9_]*$": {
14
+ "oneOf": [
15
+ {
16
+ "type": "string",
17
+ "pattern": "^#[0-9a-fA-F]{6}$",
18
+ "description": "Hex color value"
19
+ },
20
+ {
21
+ "type": "integer",
22
+ "minimum": 0,
23
+ "maximum": 255,
24
+ "description": "ANSI color code (0-255)"
25
+ },
26
+ {
27
+ "type": "string",
28
+ "enum": ["none"],
29
+ "description": "No color (uses terminal default)"
30
+ }
31
+ ]
32
+ }
33
+ },
34
+ "additionalProperties": false
35
+ },
36
+ "theme": {
37
+ "type": "object",
38
+ "description": "Theme color definitions",
39
+ "properties": {
40
+ "primary": { "$ref": "#/definitions/colorValue" },
41
+ "secondary": { "$ref": "#/definitions/colorValue" },
42
+ "accent": { "$ref": "#/definitions/colorValue" },
43
+ "error": { "$ref": "#/definitions/colorValue" },
44
+ "warning": { "$ref": "#/definitions/colorValue" },
45
+ "success": { "$ref": "#/definitions/colorValue" },
46
+ "info": { "$ref": "#/definitions/colorValue" },
47
+ "text": { "$ref": "#/definitions/colorValue" },
48
+ "textMuted": { "$ref": "#/definitions/colorValue" },
49
+ "selectedListItemText": { "$ref": "#/definitions/colorValue" },
50
+ "background": { "$ref": "#/definitions/colorValue" },
51
+ "backgroundPanel": { "$ref": "#/definitions/colorValue" },
52
+ "backgroundElement": { "$ref": "#/definitions/colorValue" },
53
+ "border": { "$ref": "#/definitions/colorValue" },
54
+ "borderActive": { "$ref": "#/definitions/colorValue" },
55
+ "borderSubtle": { "$ref": "#/definitions/colorValue" },
56
+ "diffAdded": { "$ref": "#/definitions/colorValue" },
57
+ "diffRemoved": { "$ref": "#/definitions/colorValue" },
58
+ "diffContext": { "$ref": "#/definitions/colorValue" },
59
+ "diffHunkHeader": { "$ref": "#/definitions/colorValue" },
60
+ "diffHighlightAdded": { "$ref": "#/definitions/colorValue" },
61
+ "diffHighlightRemoved": { "$ref": "#/definitions/colorValue" },
62
+ "diffAddedBg": { "$ref": "#/definitions/colorValue" },
63
+ "diffRemovedBg": { "$ref": "#/definitions/colorValue" },
64
+ "diffContextBg": { "$ref": "#/definitions/colorValue" },
65
+ "diffLineNumber": { "$ref": "#/definitions/colorValue" },
66
+ "diffAddedLineNumberBg": { "$ref": "#/definitions/colorValue" },
67
+ "diffRemovedLineNumberBg": { "$ref": "#/definitions/colorValue" },
68
+ "markdownText": { "$ref": "#/definitions/colorValue" },
69
+ "markdownHeading": { "$ref": "#/definitions/colorValue" },
70
+ "markdownLink": { "$ref": "#/definitions/colorValue" },
71
+ "markdownLinkText": { "$ref": "#/definitions/colorValue" },
72
+ "markdownCode": { "$ref": "#/definitions/colorValue" },
73
+ "markdownBlockQuote": { "$ref": "#/definitions/colorValue" },
74
+ "markdownEmph": { "$ref": "#/definitions/colorValue" },
75
+ "markdownStrong": { "$ref": "#/definitions/colorValue" },
76
+ "markdownHorizontalRule": { "$ref": "#/definitions/colorValue" },
77
+ "markdownListItem": { "$ref": "#/definitions/colorValue" },
78
+ "markdownListEnumeration": { "$ref": "#/definitions/colorValue" },
79
+ "markdownImage": { "$ref": "#/definitions/colorValue" },
80
+ "markdownImageText": { "$ref": "#/definitions/colorValue" },
81
+ "markdownCodeBlock": { "$ref": "#/definitions/colorValue" },
82
+ "syntaxComment": { "$ref": "#/definitions/colorValue" },
83
+ "syntaxKeyword": { "$ref": "#/definitions/colorValue" },
84
+ "syntaxFunction": { "$ref": "#/definitions/colorValue" },
85
+ "syntaxVariable": { "$ref": "#/definitions/colorValue" },
86
+ "syntaxString": { "$ref": "#/definitions/colorValue" },
87
+ "syntaxNumber": { "$ref": "#/definitions/colorValue" },
88
+ "syntaxType": { "$ref": "#/definitions/colorValue" },
89
+ "syntaxOperator": { "$ref": "#/definitions/colorValue" },
90
+ "syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
91
+ },
92
+ "required": ["primary", "secondary", "accent", "text", "textMuted", "background"],
93
+ "additionalProperties": false
94
+ }
95
+ },
96
+ "required": ["theme"],
97
+ "additionalProperties": false,
98
+ "definitions": {
99
+ "colorValue": {
100
+ "oneOf": [
101
+ {
102
+ "type": "string",
103
+ "pattern": "^#[0-9a-fA-F]{6}$",
104
+ "description": "Hex color value (same for dark and light)"
105
+ },
106
+ {
107
+ "type": "integer",
108
+ "minimum": 0,
109
+ "maximum": 255,
110
+ "description": "ANSI color code (0-255, same for dark and light)"
111
+ },
112
+ {
113
+ "type": "string",
114
+ "enum": ["none"],
115
+ "description": "No color (uses terminal default)"
116
+ },
117
+ {
118
+ "type": "string",
119
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
120
+ "description": "Reference to another color in the theme or defs"
121
+ },
122
+ {
123
+ "type": "object",
124
+ "properties": {
125
+ "dark": {
126
+ "oneOf": [
127
+ {
128
+ "type": "string",
129
+ "pattern": "^#[0-9a-fA-F]{6}$",
130
+ "description": "Hex color value for dark mode"
131
+ },
132
+ {
133
+ "type": "integer",
134
+ "minimum": 0,
135
+ "maximum": 255,
136
+ "description": "ANSI color code for dark mode"
137
+ },
138
+ {
139
+ "type": "string",
140
+ "enum": ["none"],
141
+ "description": "No color (uses terminal default)"
142
+ },
143
+ {
144
+ "type": "string",
145
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
146
+ "description": "Reference to another color for dark mode"
147
+ }
148
+ ]
149
+ },
150
+ "light": {
151
+ "oneOf": [
152
+ {
153
+ "type": "string",
154
+ "pattern": "^#[0-9a-fA-F]{6}$",
155
+ "description": "Hex color value for light mode"
156
+ },
157
+ {
158
+ "type": "integer",
159
+ "minimum": 0,
160
+ "maximum": 255,
161
+ "description": "ANSI color code for light mode"
162
+ },
163
+ {
164
+ "type": "string",
165
+ "enum": ["none"],
166
+ "description": "No color (uses terminal default)"
167
+ },
168
+ {
169
+ "type": "string",
170
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
171
+ "description": "Reference to another color for light mode"
172
+ }
173
+ ]
174
+ }
175
+ },
176
+ "required": ["dark", "light"],
177
+ "additionalProperties": false,
178
+ "description": "Separate colors for dark and light modes"
179
+ }
180
+ ]
181
+ }
182
+ }
183
+ }
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/web-app-manifest-192x192.png
@@ -0,0 +1 @@
1
+ ../../ui/src/assets/favicon/web-app-manifest-512x512.png
@@ -0,0 +1,30 @@
1
+ import { Link } from "@tanstack/react-router"
2
+
3
+ import { ModeToggle } from "./mode-toggle"
4
+
5
+ export default function Header() {
6
+ const links = [
7
+ { to: "/", label: "Home" },
8
+ { to: "/dashboard", label: "Dashboard" },
9
+ ] as const
10
+
11
+ return (
12
+ <div>
13
+ <div className="flex flex-row items-center justify-between px-2 py-1">
14
+ <nav className="flex gap-4 text-lg">
15
+ {links.map(({ to, label }) => {
16
+ return (
17
+ <Link key={to} to={to}>
18
+ {label}
19
+ </Link>
20
+ )
21
+ })}
22
+ </nav>
23
+ <div className="flex items-center gap-2">
24
+ <ModeToggle />
25
+ </div>
26
+ </div>
27
+ <hr />
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,9 @@
1
+ import { Loader2 } from "lucide-react"
2
+
3
+ export default function Loader() {
4
+ return (
5
+ <div className="flex h-full items-center justify-center pt-8">
6
+ <Loader2 className="animate-spin" />
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,24 @@
1
+ import { Moon, Sun } from "lucide-react"
2
+
3
+ import { useTheme } from "@/components/theme-provider"
4
+ import { Button } from "@/components/ui/button"
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
6
+
7
+ export function ModeToggle() {
8
+ const { setTheme } = useTheme()
9
+
10
+ return (
11
+ <DropdownMenu>
12
+ <DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
13
+ <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
14
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
15
+ <span className="sr-only">Toggle theme</span>
16
+ </DropdownMenuTrigger>
17
+ <DropdownMenuContent align="end">
18
+ <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
19
+ <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
20
+ <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
21
+ </DropdownMenuContent>
22
+ </DropdownMenu>
23
+ )
24
+ }
@@ -0,0 +1,123 @@
1
+ import { useForm } from "@tanstack/react-form"
2
+ import { useNavigate } from "@tanstack/react-router"
3
+ import { toast } from "sonner"
4
+ import z from "zod"
5
+
6
+ import { authClient } from "@/lib/auth-client"
7
+
8
+ import { Button } from "./ui/button"
9
+ import { Input } from "./ui/input"
10
+ import { Label } from "./ui/label"
11
+
12
+ export default function SignInForm({ onSwitchToSignUp }: { onSwitchToSignUp: () => void }) {
13
+ const navigate = useNavigate({
14
+ from: "/",
15
+ })
16
+
17
+ const form = useForm({
18
+ defaultValues: {
19
+ email: "",
20
+ password: "",
21
+ },
22
+ onSubmit: async ({ value }) => {
23
+ await authClient.signIn.email(
24
+ {
25
+ email: value.email,
26
+ password: value.password,
27
+ },
28
+ {
29
+ onSuccess: () => {
30
+ navigate({
31
+ to: "/dashboard",
32
+ })
33
+ toast.success("Sign in successful")
34
+ },
35
+ onError: (error) => {
36
+ toast.error(error.error.message || error.error.statusText)
37
+ },
38
+ },
39
+ )
40
+ },
41
+ validators: {
42
+ onSubmit: z.object({
43
+ email: z.email("Invalid email address"),
44
+ password: z.string().min(8, "Password must be at least 8 characters"),
45
+ }),
46
+ },
47
+ })
48
+
49
+ return (
50
+ <div className="mx-auto w-full mt-10 max-w-md p-6">
51
+ <h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
52
+
53
+ <form
54
+ onSubmit={(e) => {
55
+ e.preventDefault()
56
+ e.stopPropagation()
57
+ form.handleSubmit()
58
+ }}
59
+ className="space-y-4"
60
+ >
61
+ <div>
62
+ <form.Field name="email">
63
+ {(field) => (
64
+ <div className="space-y-2">
65
+ <Label htmlFor={field.name}>Email</Label>
66
+ <Input
67
+ id={field.name}
68
+ name={field.name}
69
+ type="email"
70
+ value={field.state.value}
71
+ onBlur={field.handleBlur}
72
+ onChange={(e) => field.handleChange(e.target.value)}
73
+ />
74
+ {field.state.meta.errors.map((error) => (
75
+ <p key={error?.message} className="text-red-500">
76
+ {error?.message}
77
+ </p>
78
+ ))}
79
+ </div>
80
+ )}
81
+ </form.Field>
82
+ </div>
83
+
84
+ <div>
85
+ <form.Field name="password">
86
+ {(field) => (
87
+ <div className="space-y-2">
88
+ <Label htmlFor={field.name}>Password</Label>
89
+ <Input
90
+ id={field.name}
91
+ name={field.name}
92
+ type="password"
93
+ value={field.state.value}
94
+ onBlur={field.handleBlur}
95
+ onChange={(e) => field.handleChange(e.target.value)}
96
+ />
97
+ {field.state.meta.errors.map((error) => (
98
+ <p key={error?.message} className="text-red-500">
99
+ {error?.message}
100
+ </p>
101
+ ))}
102
+ </div>
103
+ )}
104
+ </form.Field>
105
+ </div>
106
+
107
+ <form.Subscribe>
108
+ {(state) => (
109
+ <Button type="submit" className="w-full" disabled={!state.canSubmit || state.isSubmitting}>
110
+ {state.isSubmitting ? "Submitting..." : "Sign In"}
111
+ </Button>
112
+ )}
113
+ </form.Subscribe>
114
+ </form>
115
+
116
+ <div className="mt-4 text-center">
117
+ <Button variant="link" onClick={onSwitchToSignUp} className="text-indigo-600 hover:text-indigo-800">
118
+ Need an account? Sign Up
119
+ </Button>
120
+ </div>
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,148 @@
1
+ import { useForm } from "@tanstack/react-form"
2
+ import { useNavigate } from "@tanstack/react-router"
3
+ import { toast } from "sonner"
4
+ import z from "zod"
5
+
6
+ import { authClient } from "@/lib/auth-client"
7
+
8
+ import { Button } from "./ui/button"
9
+ import { Input } from "./ui/input"
10
+ import { Label } from "./ui/label"
11
+
12
+ export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: () => void }) {
13
+ const navigate = useNavigate({
14
+ from: "/",
15
+ })
16
+
17
+ const form = useForm({
18
+ defaultValues: {
19
+ email: "",
20
+ password: "",
21
+ name: "",
22
+ },
23
+ onSubmit: async ({ value }) => {
24
+ await authClient.signUp.email(
25
+ {
26
+ email: value.email,
27
+ password: value.password,
28
+ name: value.name,
29
+ },
30
+ {
31
+ onSuccess: () => {
32
+ navigate({
33
+ to: "/dashboard",
34
+ })
35
+ toast.success("Sign up successful")
36
+ },
37
+ onError: (error) => {
38
+ toast.error(error.error.message || error.error.statusText)
39
+ },
40
+ },
41
+ )
42
+ },
43
+ validators: {
44
+ onSubmit: z.object({
45
+ name: z.string().min(2, "Name must be at least 2 characters"),
46
+ email: z.email("Invalid email address"),
47
+ password: z.string().min(8, "Password must be at least 8 characters"),
48
+ }),
49
+ },
50
+ })
51
+
52
+ return (
53
+ <div className="mx-auto w-full mt-10 max-w-md p-6">
54
+ <h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
55
+
56
+ <form
57
+ onSubmit={(e) => {
58
+ e.preventDefault()
59
+ e.stopPropagation()
60
+ form.handleSubmit()
61
+ }}
62
+ className="space-y-4"
63
+ >
64
+ <div>
65
+ <form.Field name="name">
66
+ {(field) => (
67
+ <div className="space-y-2">
68
+ <Label htmlFor={field.name}>Name</Label>
69
+ <Input
70
+ id={field.name}
71
+ name={field.name}
72
+ value={field.state.value}
73
+ onBlur={field.handleBlur}
74
+ onChange={(e) => field.handleChange(e.target.value)}
75
+ />
76
+ {field.state.meta.errors.map((error) => (
77
+ <p key={error?.message} className="text-red-500">
78
+ {error?.message}
79
+ </p>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </form.Field>
84
+ </div>
85
+
86
+ <div>
87
+ <form.Field name="email">
88
+ {(field) => (
89
+ <div className="space-y-2">
90
+ <Label htmlFor={field.name}>Email</Label>
91
+ <Input
92
+ id={field.name}
93
+ name={field.name}
94
+ type="email"
95
+ value={field.state.value}
96
+ onBlur={field.handleBlur}
97
+ onChange={(e) => field.handleChange(e.target.value)}
98
+ />
99
+ {field.state.meta.errors.map((error) => (
100
+ <p key={error?.message} className="text-red-500">
101
+ {error?.message}
102
+ </p>
103
+ ))}
104
+ </div>
105
+ )}
106
+ </form.Field>
107
+ </div>
108
+
109
+ <div>
110
+ <form.Field name="password">
111
+ {(field) => (
112
+ <div className="space-y-2">
113
+ <Label htmlFor={field.name}>Password</Label>
114
+ <Input
115
+ id={field.name}
116
+ name={field.name}
117
+ type="password"
118
+ value={field.state.value}
119
+ onBlur={field.handleBlur}
120
+ onChange={(e) => field.handleChange(e.target.value)}
121
+ />
122
+ {field.state.meta.errors.map((error) => (
123
+ <p key={error?.message} className="text-red-500">
124
+ {error?.message}
125
+ </p>
126
+ ))}
127
+ </div>
128
+ )}
129
+ </form.Field>
130
+ </div>
131
+
132
+ <form.Subscribe>
133
+ {(state) => (
134
+ <Button type="submit" className="w-full" disabled={!state.canSubmit || state.isSubmitting}>
135
+ {state.isSubmitting ? "Submitting..." : "Sign Up"}
136
+ </Button>
137
+ )}
138
+ </form.Subscribe>
139
+ </form>
140
+
141
+ <div className="mt-4 text-center">
142
+ <Button variant="link" onClick={onSwitchToSignIn} className="text-indigo-600 hover:text-indigo-800">
143
+ Already have an account? Sign In
144
+ </Button>
145
+ </div>
146
+ </div>
147
+ )
148
+ }