@getjack/jack 0.1.19 → 0.1.22

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 (105) hide show
  1. package/package.json +5 -2
  2. package/src/commands/down.ts +11 -1
  3. package/src/commands/init.ts +19 -6
  4. package/src/commands/new.ts +56 -4
  5. package/src/commands/publish.ts +1 -1
  6. package/src/lib/agents.ts +3 -1
  7. package/src/lib/auth/ensure-auth.test.ts +3 -3
  8. package/src/lib/control-plane.ts +15 -1
  9. package/src/lib/deploy-upload.ts +26 -1
  10. package/src/lib/hooks.ts +232 -1
  11. package/src/lib/managed-deploy.ts +13 -6
  12. package/src/lib/managed-down.ts +66 -45
  13. package/src/lib/progress.ts +76 -5
  14. package/src/lib/project-list.ts +6 -1
  15. package/src/lib/project-operations.ts +21 -31
  16. package/src/lib/project-resolver.ts +1 -1
  17. package/src/lib/zip-packager.ts +36 -7
  18. package/src/templates/index.ts +1 -1
  19. package/src/templates/types.ts +16 -0
  20. package/templates/CLAUDE.md +172 -5
  21. package/templates/miniapp/.jack.json +1 -3
  22. package/templates/saas/.jack.json +154 -0
  23. package/templates/saas/AGENTS.md +333 -0
  24. package/templates/saas/bun.lock +925 -0
  25. package/templates/saas/components.json +21 -0
  26. package/templates/saas/index.html +12 -0
  27. package/templates/saas/package.json +75 -0
  28. package/templates/saas/public/icon.png +0 -0
  29. package/templates/saas/public/og.png +0 -0
  30. package/templates/saas/schema.sql +73 -0
  31. package/templates/saas/src/auth.ts +77 -0
  32. package/templates/saas/src/client/App.tsx +63 -0
  33. package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
  34. package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
  35. package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
  36. package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
  37. package/templates/saas/src/client/components/ui/alert.tsx +60 -0
  38. package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
  39. package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
  40. package/templates/saas/src/client/components/ui/badge.tsx +39 -0
  41. package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
  42. package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
  43. package/templates/saas/src/client/components/ui/button.tsx +60 -0
  44. package/templates/saas/src/client/components/ui/card.tsx +75 -0
  45. package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
  46. package/templates/saas/src/client/components/ui/chart.tsx +326 -0
  47. package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
  48. package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
  49. package/templates/saas/src/client/components/ui/command.tsx +159 -0
  50. package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
  51. package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
  52. package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
  53. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
  54. package/templates/saas/src/client/components/ui/empty.tsx +94 -0
  55. package/templates/saas/src/client/components/ui/field.tsx +232 -0
  56. package/templates/saas/src/client/components/ui/form.tsx +152 -0
  57. package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
  58. package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
  59. package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
  60. package/templates/saas/src/client/components/ui/input.tsx +21 -0
  61. package/templates/saas/src/client/components/ui/item.tsx +172 -0
  62. package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
  63. package/templates/saas/src/client/components/ui/label.tsx +21 -0
  64. package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
  65. package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
  66. package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
  67. package/templates/saas/src/client/components/ui/popover.tsx +42 -0
  68. package/templates/saas/src/client/components/ui/progress.tsx +26 -0
  69. package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
  70. package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
  71. package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
  72. package/templates/saas/src/client/components/ui/select.tsx +173 -0
  73. package/templates/saas/src/client/components/ui/separator.tsx +28 -0
  74. package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
  75. package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
  76. package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
  77. package/templates/saas/src/client/components/ui/slider.tsx +58 -0
  78. package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
  79. package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
  80. package/templates/saas/src/client/components/ui/switch.tsx +28 -0
  81. package/templates/saas/src/client/components/ui/table.tsx +90 -0
  82. package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
  83. package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
  84. package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
  85. package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
  86. package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
  87. package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
  88. package/templates/saas/src/client/hooks/useAuth.ts +14 -0
  89. package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
  90. package/templates/saas/src/client/index.css +165 -0
  91. package/templates/saas/src/client/lib/auth-client.ts +7 -0
  92. package/templates/saas/src/client/lib/plans.ts +82 -0
  93. package/templates/saas/src/client/lib/utils.ts +6 -0
  94. package/templates/saas/src/client/main.tsx +15 -0
  95. package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
  96. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
  97. package/templates/saas/src/client/pages/HomePage.tsx +285 -0
  98. package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
  99. package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
  100. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
  101. package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
  102. package/templates/saas/src/index.ts +208 -0
  103. package/templates/saas/tsconfig.json +18 -0
  104. package/templates/saas/vite.config.ts +14 -0
  105. package/templates/saas/wrangler.jsonc +20 -0
@@ -0,0 +1,153 @@
1
+ import { useState } from "react";
2
+ import { authClient } from "../lib/auth-client";
3
+ import { toast } from "sonner";
4
+ import { ThemeToggle } from "../components/ThemeToggle";
5
+ import { Button } from "../components/ui/button";
6
+
7
+ interface ForgotPasswordPageProps {
8
+ navigate: (route: "/" | "/login" | "/signup" | "/pricing" | "/dashboard" | "/forgot-password" | "/reset-password") => void;
9
+ }
10
+
11
+ export default function ForgotPasswordPage({ navigate }: ForgotPasswordPageProps) {
12
+ const [email, setEmail] = useState("");
13
+ const [isLoading, setIsLoading] = useState(false);
14
+ const [submitted, setSubmitted] = useState(false);
15
+
16
+ const handleSubmit = async (e: React.FormEvent) => {
17
+ e.preventDefault();
18
+ setIsLoading(true);
19
+
20
+ try {
21
+ const result = await authClient.requestPasswordReset({
22
+ email,
23
+ redirectTo: `${window.location.origin}/#/reset-password`,
24
+ });
25
+
26
+ if (result.error) {
27
+ toast.error("Failed to send reset email", {
28
+ description: result.error.message || "Please try again.",
29
+ });
30
+ setIsLoading(false);
31
+ return;
32
+ }
33
+
34
+ setSubmitted(true);
35
+ toast.success("Reset email sent", {
36
+ description: "Check your inbox for the reset link.",
37
+ });
38
+ } catch (err) {
39
+ toast.error("An unexpected error occurred");
40
+ }
41
+
42
+ setIsLoading(false);
43
+ };
44
+
45
+ return (
46
+ <div className="min-h-screen bg-background flex flex-col">
47
+ {/* Navigation */}
48
+ <nav className="border-b border-border">
49
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
50
+ <div className="flex justify-between items-center h-16">
51
+ <button
52
+ type="button"
53
+ onClick={() => navigate("/")}
54
+ className="text-xl font-bold hover:opacity-80 transition-opacity"
55
+ >
56
+ jack-template
57
+ </button>
58
+ <ThemeToggle />
59
+ </div>
60
+ </div>
61
+ </nav>
62
+
63
+ {/* Form */}
64
+ <div className="flex-1 flex items-center justify-center px-4 py-12">
65
+ <div className="w-full max-w-md">
66
+ <div className="text-center mb-8">
67
+ <h1 className="text-2xl font-bold mb-2">Reset your password</h1>
68
+ <p className="text-muted-foreground">
69
+ {submitted
70
+ ? "Check your email for a reset link"
71
+ : "Enter your email and we'll send you a reset link"}
72
+ </p>
73
+ </div>
74
+
75
+ {submitted ? (
76
+ <div className="space-y-4">
77
+ <div className="p-4 bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 rounded-lg text-center">
78
+ <p className="text-green-800 dark:text-green-200">
79
+ If an account exists for {email}, you'll receive an email with instructions.
80
+ </p>
81
+ </div>
82
+ <Button
83
+ type="button"
84
+ variant="outline"
85
+ className="w-full"
86
+ onClick={() => navigate("/login")}
87
+ >
88
+ Back to login
89
+ </Button>
90
+ </div>
91
+ ) : (
92
+ <form onSubmit={handleSubmit} className="space-y-4">
93
+ <div>
94
+ <label htmlFor="email" className="block text-sm font-medium mb-2">
95
+ Email
96
+ </label>
97
+ <input
98
+ id="email"
99
+ type="email"
100
+ value={email}
101
+ onChange={(e) => setEmail(e.target.value)}
102
+ placeholder="you@example.com"
103
+ required
104
+ className="w-full px-3 py-2 border border-border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
105
+ />
106
+ </div>
107
+
108
+ <Button type="submit" className="w-full" disabled={isLoading}>
109
+ {isLoading ? (
110
+ <>
111
+ <svg
112
+ className="animate-spin -ml-1 mr-2 h-4 w-4"
113
+ fill="none"
114
+ viewBox="0 0 24 24"
115
+ >
116
+ <circle
117
+ className="opacity-25"
118
+ cx="12"
119
+ cy="12"
120
+ r="10"
121
+ stroke="currentColor"
122
+ strokeWidth="4"
123
+ />
124
+ <path
125
+ className="opacity-75"
126
+ fill="currentColor"
127
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
128
+ />
129
+ </svg>
130
+ Sending...
131
+ </>
132
+ ) : (
133
+ "Send reset link"
134
+ )}
135
+ </Button>
136
+
137
+ <p className="text-center text-sm text-muted-foreground">
138
+ Remember your password?{" "}
139
+ <button
140
+ type="button"
141
+ onClick={() => navigate("/login")}
142
+ className="text-primary hover:underline"
143
+ >
144
+ Sign in
145
+ </button>
146
+ </p>
147
+ </form>
148
+ )}
149
+ </div>
150
+ </div>
151
+ </div>
152
+ );
153
+ }
@@ -0,0 +1,285 @@
1
+ import { useEffect, useState } from "react";
2
+ import { authClient } from "../lib/auth-client";
3
+ import { ThemeToggle } from "../components/ThemeToggle";
4
+
5
+ interface HomePageProps {
6
+ navigate: (route: "/" | "/login" | "/signup" | "/pricing" | "/dashboard" | "/forgot-password" | "/reset-password") => void;
7
+ }
8
+
9
+ export default function HomePage({ navigate }: HomePageProps) {
10
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
11
+ const [isLoading, setIsLoading] = useState(true);
12
+
13
+ useEffect(() => {
14
+ authClient.getSession().then((result) => {
15
+ setIsLoggedIn(!!result.data?.user);
16
+ setIsLoading(false);
17
+ });
18
+ }, []);
19
+
20
+ return (
21
+ <div className="min-h-screen bg-background">
22
+ {/* Navigation */}
23
+ <nav className="border-b border-border">
24
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
25
+ <div className="flex justify-between items-center h-16">
26
+ <div className="flex items-center">
27
+ <span className="text-xl font-bold">jack-template</span>
28
+ </div>
29
+ <div className="flex items-center gap-4">
30
+ <button
31
+ type="button"
32
+ onClick={() => navigate("/pricing")}
33
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
34
+ >
35
+ Pricing
36
+ </button>
37
+ <ThemeToggle />
38
+ {!isLoading && !isLoggedIn && (
39
+ <button
40
+ type="button"
41
+ onClick={() => navigate("/login")}
42
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
43
+ >
44
+ Log in
45
+ </button>
46
+ )}
47
+ <button
48
+ type="button"
49
+ onClick={() => navigate(isLoggedIn ? "/dashboard" : "/signup")}
50
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:opacity-90 transition-opacity text-sm font-medium min-w-[100px]"
51
+ >
52
+ {isLoggedIn ? "Dashboard" : "Get started"}
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </nav>
58
+
59
+ {/* Hero Section */}
60
+ <section className="py-20 px-4 sm:px-6 lg:px-8">
61
+ <div className="max-w-4xl mx-auto text-center">
62
+ <h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight mb-6">
63
+ Build your SaaS faster than ever
64
+ </h1>
65
+ <p className="text-lg sm:text-xl text-muted-foreground mb-10 max-w-2xl mx-auto">
66
+ A production-ready template with authentication, payments, and everything you need to
67
+ launch your next project in minutes, not months.
68
+ </p>
69
+ <div className="flex flex-col sm:flex-row gap-4 justify-center">
70
+ <button
71
+ type="button"
72
+ onClick={() => navigate(isLoggedIn ? "/dashboard" : "/signup")}
73
+ className="px-8 py-3 bg-primary text-primary-foreground rounded-md hover:opacity-90 transition-opacity text-base font-medium min-w-[160px]"
74
+ >
75
+ {isLoggedIn ? "Go to Dashboard" : "Start for free"}
76
+ </button>
77
+ <button
78
+ type="button"
79
+ onClick={() => navigate("/pricing")}
80
+ className="px-8 py-3 border border-border rounded-md hover:bg-accent transition-colors text-base font-medium"
81
+ >
82
+ View pricing
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </section>
87
+
88
+ {/* Features Section */}
89
+ <section className="py-20 px-4 sm:px-6 lg:px-8 bg-muted/50">
90
+ <div className="max-w-6xl mx-auto">
91
+ <div className="text-center mb-16">
92
+ <h2 className="text-3xl font-bold mb-4">Everything you need to ship</h2>
93
+ <p className="text-muted-foreground max-w-2xl mx-auto">
94
+ Focus on building your product, not reinventing authentication, payments, or
95
+ infrastructure.
96
+ </p>
97
+ </div>
98
+ <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
99
+ {/* Feature 1 */}
100
+ <div className="p-6 bg-card rounded-lg border border-border">
101
+ <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
102
+ <svg
103
+ className="w-6 h-6 text-primary"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ viewBox="0 0 24 24"
107
+ >
108
+ <path
109
+ strokeLinecap="round"
110
+ strokeLinejoin="round"
111
+ strokeWidth={2}
112
+ d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
113
+ />
114
+ </svg>
115
+ </div>
116
+ <h3 className="font-semibold mb-2">Authentication</h3>
117
+ <p className="text-sm text-muted-foreground">
118
+ Secure email/password auth with sessions, powered by Better Auth.
119
+ </p>
120
+ </div>
121
+
122
+ {/* Feature 2 */}
123
+ <div className="p-6 bg-card rounded-lg border border-border">
124
+ <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
125
+ <svg
126
+ className="w-6 h-6 text-primary"
127
+ fill="none"
128
+ stroke="currentColor"
129
+ viewBox="0 0 24 24"
130
+ >
131
+ <path
132
+ strokeLinecap="round"
133
+ strokeLinejoin="round"
134
+ strokeWidth={2}
135
+ d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
136
+ />
137
+ </svg>
138
+ </div>
139
+ <h3 className="font-semibold mb-2">Payments</h3>
140
+ <p className="text-sm text-muted-foreground">
141
+ Stripe integration for subscriptions, one-time payments, and billing.
142
+ </p>
143
+ </div>
144
+
145
+ {/* Feature 3 */}
146
+ <div className="p-6 bg-card rounded-lg border border-border">
147
+ <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
148
+ <svg
149
+ className="w-6 h-6 text-primary"
150
+ fill="none"
151
+ stroke="currentColor"
152
+ viewBox="0 0 24 24"
153
+ >
154
+ <path
155
+ strokeLinecap="round"
156
+ strokeLinejoin="round"
157
+ strokeWidth={2}
158
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"
159
+ />
160
+ </svg>
161
+ </div>
162
+ <h3 className="font-semibold mb-2">Database</h3>
163
+ <p className="text-sm text-muted-foreground">
164
+ Cloudflare D1 database with Drizzle ORM for type-safe queries.
165
+ </p>
166
+ </div>
167
+
168
+ {/* Feature 4 */}
169
+ <div className="p-6 bg-card rounded-lg border border-border">
170
+ <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
171
+ <svg
172
+ className="w-6 h-6 text-primary"
173
+ fill="none"
174
+ stroke="currentColor"
175
+ viewBox="0 0 24 24"
176
+ >
177
+ <path
178
+ strokeLinecap="round"
179
+ strokeLinejoin="round"
180
+ strokeWidth={2}
181
+ d="M13 10V3L4 14h7v7l9-11h-7z"
182
+ />
183
+ </svg>
184
+ </div>
185
+ <h3 className="font-semibold mb-2">Edge Deployment</h3>
186
+ <p className="text-sm text-muted-foreground">
187
+ Deploy globally on Cloudflare Workers for blazing fast performance.
188
+ </p>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </section>
193
+
194
+ {/* Pricing Preview Section */}
195
+ <section className="py-20 px-4 sm:px-6 lg:px-8">
196
+ <div className="max-w-4xl mx-auto text-center">
197
+ <h2 className="text-3xl font-bold mb-4">Simple, transparent pricing</h2>
198
+ <p className="text-muted-foreground mb-10">
199
+ Start free and upgrade as you grow. No hidden fees, no surprises.
200
+ </p>
201
+ <div className="flex flex-col sm:flex-row gap-6 justify-center items-center">
202
+ <div className="p-6 bg-card rounded-lg border border-border text-left w-full sm:w-64">
203
+ <h3 className="font-semibold mb-1">Free</h3>
204
+ <p className="text-2xl font-bold mb-2">
205
+ $0<span className="text-sm font-normal text-muted-foreground">/mo</span>
206
+ </p>
207
+ <p className="text-sm text-muted-foreground">Perfect for getting started</p>
208
+ </div>
209
+ <div className="p-6 bg-card rounded-lg border-2 border-primary text-left w-full sm:w-64">
210
+ <h3 className="font-semibold mb-1">Pro</h3>
211
+ <p className="text-2xl font-bold mb-2">
212
+ $19<span className="text-sm font-normal text-muted-foreground">/mo</span>
213
+ </p>
214
+ <p className="text-sm text-muted-foreground">For growing businesses</p>
215
+ </div>
216
+ <div className="p-6 bg-card rounded-lg border border-border text-left w-full sm:w-64">
217
+ <h3 className="font-semibold mb-1">Enterprise</h3>
218
+ <p className="text-2xl font-bold mb-2">
219
+ $99<span className="text-sm font-normal text-muted-foreground">/mo</span>
220
+ </p>
221
+ <p className="text-sm text-muted-foreground">For large scale operations</p>
222
+ </div>
223
+ </div>
224
+ <button
225
+ type="button"
226
+ onClick={() => navigate("/pricing")}
227
+ className="mt-8 text-sm text-primary hover:underline"
228
+ >
229
+ See full pricing details
230
+ </button>
231
+ </div>
232
+ </section>
233
+
234
+ {/* Footer */}
235
+ <footer className="border-t border-border py-12 px-4 sm:px-6 lg:px-8">
236
+ <div className="max-w-6xl mx-auto">
237
+ <div className="flex flex-col md:flex-row justify-between items-center gap-6">
238
+ <div className="flex items-center gap-2">
239
+ <span className="font-bold">jack-template</span>
240
+ <span className="text-muted-foreground text-sm">Built with Jack</span>
241
+ </div>
242
+ <div className="flex gap-6">
243
+ <button
244
+ type="button"
245
+ onClick={() => navigate("/pricing")}
246
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
247
+ >
248
+ Pricing
249
+ </button>
250
+ {isLoggedIn ? (
251
+ <button
252
+ type="button"
253
+ onClick={() => navigate("/dashboard")}
254
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
255
+ >
256
+ Dashboard
257
+ </button>
258
+ ) : (
259
+ <>
260
+ <button
261
+ type="button"
262
+ onClick={() => navigate("/login")}
263
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
264
+ >
265
+ Log in
266
+ </button>
267
+ <button
268
+ type="button"
269
+ onClick={() => navigate("/signup")}
270
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
271
+ >
272
+ Sign up
273
+ </button>
274
+ </>
275
+ )}
276
+ </div>
277
+ </div>
278
+ <div className="mt-8 pt-8 border-t border-border text-center text-sm text-muted-foreground">
279
+ &copy; {new Date().getFullYear()} jack-template. All rights reserved.
280
+ </div>
281
+ </div>
282
+ </footer>
283
+ </div>
284
+ );
285
+ }
@@ -0,0 +1,169 @@
1
+ import { useState } from "react";
2
+ import { authClient } from "../lib/auth-client";
3
+ import { ThemeToggle } from "../components/ThemeToggle";
4
+
5
+ interface LoginPageProps {
6
+ navigate: (route: "/" | "/login" | "/signup" | "/pricing" | "/dashboard" | "/forgot-password" | "/reset-password") => void;
7
+ }
8
+
9
+ export default function LoginPage({ navigate }: LoginPageProps) {
10
+ const [email, setEmail] = useState("");
11
+ const [password, setPassword] = useState("");
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [isLoading, setIsLoading] = useState(false);
14
+
15
+ const handleSubmit = async (e: React.FormEvent) => {
16
+ e.preventDefault();
17
+ setError(null);
18
+ setIsLoading(true);
19
+
20
+ try {
21
+ const result = await authClient.signIn.email({
22
+ email,
23
+ password,
24
+ });
25
+
26
+ if (result.error) {
27
+ setError(result.error.message || "Failed to sign in. Please check your credentials.");
28
+ setIsLoading(false);
29
+ return;
30
+ }
31
+
32
+ navigate("/dashboard");
33
+ } catch (err) {
34
+ setError("An unexpected error occurred. Please try again.");
35
+ setIsLoading(false);
36
+ }
37
+ };
38
+
39
+ return (
40
+ <div className="min-h-screen bg-background flex flex-col">
41
+ {/* Navigation */}
42
+ <nav className="border-b border-border">
43
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
44
+ <div className="flex justify-between items-center h-16">
45
+ <button
46
+ type="button"
47
+ onClick={() => navigate("/")}
48
+ className="text-xl font-bold hover:opacity-80 transition-opacity"
49
+ >
50
+ jack-template
51
+ </button>
52
+ <ThemeToggle />
53
+ </div>
54
+ </div>
55
+ </nav>
56
+
57
+ {/* Login Form */}
58
+ <div className="flex-1 flex items-center justify-center px-4 py-12">
59
+ <div className="w-full max-w-md">
60
+ <div className="text-center mb-8">
61
+ <h1 className="text-2xl font-bold mb-2">Welcome back</h1>
62
+ <p className="text-muted-foreground">Sign in to your account to continue</p>
63
+ </div>
64
+
65
+ <form onSubmit={handleSubmit} className="space-y-4">
66
+ {error && (
67
+ <div className="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
68
+ <p className="text-sm text-destructive">{error}</p>
69
+ </div>
70
+ )}
71
+
72
+ <div>
73
+ <label htmlFor="email" className="block text-sm font-medium mb-2">
74
+ Email
75
+ </label>
76
+ <input
77
+ id="email"
78
+ type="email"
79
+ value={email}
80
+ onChange={(e) => setEmail(e.target.value)}
81
+ placeholder="you@example.com"
82
+ required
83
+ disabled={isLoading}
84
+ className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
85
+ />
86
+ </div>
87
+
88
+ <div>
89
+ <div className="flex items-center justify-between mb-2">
90
+ <label htmlFor="password" className="text-sm font-medium">
91
+ Password
92
+ </label>
93
+ <button
94
+ type="button"
95
+ onClick={() => navigate("/forgot-password")}
96
+ className="text-sm text-primary hover:underline"
97
+ >
98
+ Forgot password?
99
+ </button>
100
+ </div>
101
+ <input
102
+ id="password"
103
+ type="password"
104
+ value={password}
105
+ onChange={(e) => setPassword(e.target.value)}
106
+ placeholder="Enter your password"
107
+ required
108
+ disabled={isLoading}
109
+ className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
110
+ />
111
+ </div>
112
+
113
+ <button
114
+ type="submit"
115
+ disabled={isLoading}
116
+ className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:opacity-90 transition-opacity font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
117
+ >
118
+ {isLoading ? (
119
+ <>
120
+ <svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
121
+ <circle
122
+ className="opacity-25"
123
+ cx="12"
124
+ cy="12"
125
+ r="10"
126
+ stroke="currentColor"
127
+ strokeWidth="4"
128
+ />
129
+ <path
130
+ className="opacity-75"
131
+ fill="currentColor"
132
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
133
+ />
134
+ </svg>
135
+ Signing in...
136
+ </>
137
+ ) : (
138
+ "Sign in"
139
+ )}
140
+ </button>
141
+ </form>
142
+
143
+ <div className="mt-6 text-center">
144
+ <p className="text-sm text-muted-foreground">
145
+ Don't have an account?{" "}
146
+ <button
147
+ type="button"
148
+ onClick={() => navigate("/signup")}
149
+ className="text-primary hover:underline font-medium"
150
+ >
151
+ Sign up
152
+ </button>
153
+ </p>
154
+ </div>
155
+
156
+ <div className="mt-4 text-center">
157
+ <button
158
+ type="button"
159
+ onClick={() => navigate("/")}
160
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
161
+ >
162
+ Back to home
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ );
169
+ }