@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.
- package/package.json +5 -2
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +19 -6
- package/src/commands/new.ts +56 -4
- package/src/commands/publish.ts +1 -1
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +15 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +13 -6
- package/src/lib/managed-down.ts +66 -45
- package/src/lib/progress.ts +76 -5
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +21 -31
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/zip-packager.ts +36 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +172 -5
- package/templates/miniapp/.jack.json +1 -3
- package/templates/saas/.jack.json +154 -0
- package/templates/saas/AGENTS.md +333 -0
- package/templates/saas/bun.lock +925 -0
- package/templates/saas/components.json +21 -0
- package/templates/saas/index.html +12 -0
- package/templates/saas/package.json +75 -0
- package/templates/saas/public/icon.png +0 -0
- package/templates/saas/public/og.png +0 -0
- package/templates/saas/schema.sql +73 -0
- package/templates/saas/src/auth.ts +77 -0
- package/templates/saas/src/client/App.tsx +63 -0
- package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
- package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
- package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
- package/templates/saas/src/client/components/ui/alert.tsx +60 -0
- package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
- package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
- package/templates/saas/src/client/components/ui/badge.tsx +39 -0
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
- package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
- package/templates/saas/src/client/components/ui/button.tsx +60 -0
- package/templates/saas/src/client/components/ui/card.tsx +75 -0
- package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
- package/templates/saas/src/client/components/ui/chart.tsx +326 -0
- package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
- package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
- package/templates/saas/src/client/components/ui/command.tsx +159 -0
- package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
- package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
- package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
- package/templates/saas/src/client/components/ui/empty.tsx +94 -0
- package/templates/saas/src/client/components/ui/field.tsx +232 -0
- package/templates/saas/src/client/components/ui/form.tsx +152 -0
- package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
- package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
- package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
- package/templates/saas/src/client/components/ui/input.tsx +21 -0
- package/templates/saas/src/client/components/ui/item.tsx +172 -0
- package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
- package/templates/saas/src/client/components/ui/label.tsx +21 -0
- package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
- package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
- package/templates/saas/src/client/components/ui/popover.tsx +42 -0
- package/templates/saas/src/client/components/ui/progress.tsx +26 -0
- package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
- package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
- package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
- package/templates/saas/src/client/components/ui/select.tsx +173 -0
- package/templates/saas/src/client/components/ui/separator.tsx +28 -0
- package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
- package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
- package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
- package/templates/saas/src/client/components/ui/slider.tsx +58 -0
- package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
- package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
- package/templates/saas/src/client/components/ui/switch.tsx +28 -0
- package/templates/saas/src/client/components/ui/table.tsx +90 -0
- package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
- package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
- package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
- package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
- package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
- package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
- package/templates/saas/src/client/hooks/useAuth.ts +14 -0
- package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
- package/templates/saas/src/client/index.css +165 -0
- package/templates/saas/src/client/lib/auth-client.ts +7 -0
- package/templates/saas/src/client/lib/plans.ts +82 -0
- package/templates/saas/src/client/lib/utils.ts +6 -0
- package/templates/saas/src/client/main.tsx +15 -0
- package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
- package/templates/saas/src/client/pages/HomePage.tsx +285 -0
- package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
- package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
- package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
- package/templates/saas/src/index.ts +208 -0
- package/templates/saas/tsconfig.json +18 -0
- package/templates/saas/vite.config.ts +14 -0
- 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
|
+
© {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
|
+
}
|