luxlabs 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/interface-boilerplate/app/api/auth/[...all]/route.ts +52 -0
- package/templates/interface-boilerplate/app/auth/callback/page.tsx +22 -31
- package/templates/interface-boilerplate/app/dashboard/page.tsx +41 -214
- package/templates/interface-boilerplate/app/favicon.ico +0 -0
- package/templates/interface-boilerplate/app/globals.css +18 -113
- package/templates/interface-boilerplate/app/layout.tsx +21 -33
- package/templates/interface-boilerplate/app/page.tsx +37 -116
- package/templates/interface-boilerplate/app/settings/page.tsx +71 -0
- package/templates/interface-boilerplate/app/sign-in/page.tsx +19 -0
- package/templates/interface-boilerplate/app/sign-up/page.tsx +19 -0
- package/templates/interface-boilerplate/components/auth/sign-in-form.tsx +137 -0
- package/templates/interface-boilerplate/components/auth/sign-up-form.tsx +225 -0
- package/templates/interface-boilerplate/components/ui/badge.tsx +18 -14
- package/templates/interface-boilerplate/components/ui/button.tsx +29 -24
- package/templates/interface-boilerplate/components/ui/card.tsx +76 -57
- package/templates/interface-boilerplate/components/ui/input.tsx +8 -9
- package/templates/interface-boilerplate/eslint.config.mjs +18 -0
- package/templates/interface-boilerplate/lib/auth-client.ts +18 -0
- package/templates/interface-boilerplate/lib/auth.config.ts +111 -0
- package/templates/interface-boilerplate/lib/utils.ts +3 -3
- package/templates/interface-boilerplate/middleware.ts +60 -0
- package/templates/interface-boilerplate/next.config.ts +7 -0
- package/templates/interface-boilerplate/package-lock.json +6855 -0
- package/templates/interface-boilerplate/package.json +18 -37
- package/templates/interface-boilerplate/postcss.config.mjs +7 -0
- package/templates/interface-boilerplate/tsconfig.json +18 -4
- package/templates/interface-boilerplate/.env.example +0 -2
- package/templates/interface-boilerplate/.eslintrc.json +0 -4
- package/templates/interface-boilerplate/app/login/page.tsx +0 -178
- package/templates/interface-boilerplate/app/signup/page.tsx +0 -199
- package/templates/interface-boilerplate/components/AnalyticsProvider.tsx +0 -142
- package/templates/interface-boilerplate/components/AuthGuard.tsx +0 -76
- package/templates/interface-boilerplate/components/ErrorBoundary.tsx +0 -106
- package/templates/interface-boilerplate/components/theme-provider.tsx +0 -9
- package/templates/interface-boilerplate/components/theme-toggle.tsx +0 -39
- package/templates/interface-boilerplate/components/ui/avatar.tsx +0 -46
- package/templates/interface-boilerplate/components/ui/checkbox.tsx +0 -27
- package/templates/interface-boilerplate/components/ui/dialog.tsx +0 -100
- package/templates/interface-boilerplate/components/ui/dropdown-menu.tsx +0 -173
- package/templates/interface-boilerplate/components/ui/index.ts +0 -53
- package/templates/interface-boilerplate/components/ui/label.tsx +0 -20
- package/templates/interface-boilerplate/components/ui/progress.tsx +0 -24
- package/templates/interface-boilerplate/components/ui/select.tsx +0 -149
- package/templates/interface-boilerplate/components/ui/separator.tsx +0 -25
- package/templates/interface-boilerplate/components/ui/skeleton.tsx +0 -12
- package/templates/interface-boilerplate/components/ui/switch.tsx +0 -28
- package/templates/interface-boilerplate/components/ui/tabs.tsx +0 -54
- package/templates/interface-boilerplate/components/ui/textarea.tsx +0 -22
- package/templates/interface-boilerplate/components/ui/tooltip.tsx +0 -29
- package/templates/interface-boilerplate/lib/analytics.ts +0 -182
- package/templates/interface-boilerplate/lib/auth-context.tsx +0 -83
- package/templates/interface-boilerplate/lib/auth.ts +0 -199
- package/templates/interface-boilerplate/lib/callFlow.ts +0 -234
- package/templates/interface-boilerplate/lib/flowTracer.ts +0 -195
- package/templates/interface-boilerplate/lib/hooks/.gitkeep +0 -0
- package/templates/interface-boilerplate/lib/stores/.gitkeep +0 -0
- package/templates/interface-boilerplate/next.config.js +0 -6
- package/templates/interface-boilerplate/postcss.config.js +0 -6
- package/templates/interface-boilerplate/tailwind.config.js +0 -103
|
@@ -1,128 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { motion } from 'framer-motion';
|
|
5
|
-
import { ArrowRight, Zap, Shield, Sparkles } from 'lucide-react';
|
|
6
|
-
import { Button } from '@/components/ui/button';
|
|
7
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
8
|
-
import { ThemeToggle } from '@/components/theme-toggle';
|
|
9
|
-
|
|
10
|
-
const features = [
|
|
11
|
-
{
|
|
12
|
-
icon: Zap,
|
|
13
|
-
title: 'Lightning Fast',
|
|
14
|
-
description: 'Built for speed with optimized performance at every level.',
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
icon: Shield,
|
|
18
|
-
title: 'Secure by Default',
|
|
19
|
-
description: 'Enterprise-grade security with end-to-end encryption.',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
icon: Sparkles,
|
|
23
|
-
title: 'Modern Design',
|
|
24
|
-
description: 'Beautiful, accessible UI that users love.',
|
|
25
|
-
},
|
|
26
|
-
];
|
|
1
|
+
import Link from "next/link";
|
|
27
2
|
|
|
28
3
|
export default function Home() {
|
|
29
|
-
const [mounted, setMounted] = useState(false);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
setMounted(true);
|
|
33
|
-
}, []);
|
|
34
|
-
|
|
35
|
-
if (!mounted) return null;
|
|
36
|
-
|
|
37
4
|
return (
|
|
38
|
-
<div className="min-h-screen bg-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<div className="flex items-center space-x-4">
|
|
47
|
-
<ThemeToggle />
|
|
48
|
-
<Button variant="ghost" asChild>
|
|
49
|
-
<a href="/login">Login</a>
|
|
50
|
-
</Button>
|
|
51
|
-
<Button asChild>
|
|
52
|
-
<a href="/dashboard">Get Started</a>
|
|
53
|
-
</Button>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</header>
|
|
57
|
-
|
|
58
|
-
{/* Hero Section */}
|
|
59
|
-
<section className="container mx-auto px-4 py-24 text-center">
|
|
60
|
-
<motion.div
|
|
61
|
-
initial={{ opacity: 0, y: 20 }}
|
|
62
|
-
animate={{ opacity: 1, y: 0 }}
|
|
63
|
-
transition={{ duration: 0.5 }}
|
|
64
|
-
>
|
|
65
|
-
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
|
|
66
|
-
Build amazing products
|
|
67
|
-
<br />
|
|
68
|
-
<span className="text-primary">with Lux</span>
|
|
5
|
+
<div className="min-h-screen flex flex-col bg-white">
|
|
6
|
+
<main className="flex-1 flex flex-col items-center justify-center px-6">
|
|
7
|
+
<div className="max-w-2xl w-full text-center">
|
|
8
|
+
<p className="text-sm font-medium tracking-widest text-zinc-400 uppercase mb-4">
|
|
9
|
+
Boilerplate
|
|
10
|
+
</p>
|
|
11
|
+
<h1 className="text-6xl font-semibold tracking-tight text-black mb-6">
|
|
12
|
+
Lux
|
|
69
13
|
</h1>
|
|
70
|
-
<p className="
|
|
71
|
-
|
|
72
|
-
All your backend logic lives in Flows.
|
|
14
|
+
<p className="text-lg text-zinc-500 mb-12 max-w-md mx-auto">
|
|
15
|
+
Authentication, database, and UI components. Everything you need to start building.
|
|
73
16
|
</p>
|
|
74
|
-
<div className="mt-10 flex items-center justify-center gap-4">
|
|
75
|
-
<Button size="lg" asChild>
|
|
76
|
-
<a href="/dashboard">
|
|
77
|
-
Get Started <ArrowRight className="ml-2 h-4 w-4" />
|
|
78
|
-
</a>
|
|
79
|
-
</Button>
|
|
80
|
-
<Button size="lg" variant="outline">
|
|
81
|
-
Learn More
|
|
82
|
-
</Button>
|
|
83
|
-
</div>
|
|
84
|
-
</motion.div>
|
|
85
|
-
</section>
|
|
86
17
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Everything you need
|
|
92
|
-
</h2>
|
|
93
|
-
<p className="mt-4 text-muted-foreground">
|
|
94
|
-
Powerful features to help you build and scale.
|
|
95
|
-
</p>
|
|
96
|
-
</div>
|
|
97
|
-
<div className="mt-16 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
98
|
-
{features.map((feature, index) => (
|
|
99
|
-
<motion.div
|
|
100
|
-
key={feature.title}
|
|
101
|
-
initial={{ opacity: 0, y: 20 }}
|
|
102
|
-
animate={{ opacity: 1, y: 0 }}
|
|
103
|
-
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
18
|
+
<div className="flex gap-4 justify-center mb-16">
|
|
19
|
+
<Link
|
|
20
|
+
href="/sign-in"
|
|
21
|
+
className="px-8 py-3 bg-black text-white text-sm font-medium rounded-full hover:bg-zinc-800 transition-colors"
|
|
104
22
|
>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
</CardContent>
|
|
115
|
-
</Card>
|
|
116
|
-
</motion.div>
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
</section>
|
|
23
|
+
Sign In
|
|
24
|
+
</Link>
|
|
25
|
+
<Link
|
|
26
|
+
href="/sign-up"
|
|
27
|
+
className="px-8 py-3 border border-zinc-200 text-black text-sm font-medium rounded-full hover:bg-zinc-50 transition-colors"
|
|
28
|
+
>
|
|
29
|
+
Sign Up
|
|
30
|
+
</Link>
|
|
31
|
+
</div>
|
|
120
32
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
33
|
+
<div className="flex items-center justify-center gap-8 text-sm text-zinc-400">
|
|
34
|
+
<span>Next.js</span>
|
|
35
|
+
<span className="w-1 h-1 bg-zinc-300 rounded-full" />
|
|
36
|
+
<span>Auth</span>
|
|
37
|
+
<span className="w-1 h-1 bg-zinc-300 rounded-full" />
|
|
38
|
+
<span>Database</span>
|
|
39
|
+
<span className="w-1 h-1 bg-zinc-300 rounded-full" />
|
|
40
|
+
<span>Tailwind</span>
|
|
41
|
+
</div>
|
|
125
42
|
</div>
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<footer className="py-6 text-center text-sm text-zinc-400">
|
|
46
|
+
<p>Ready to ship.</p>
|
|
126
47
|
</footer>
|
|
127
48
|
</div>
|
|
128
49
|
);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSession, signOut } from "@/lib/auth-client";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
|
|
7
|
+
export default function SettingsPage() {
|
|
8
|
+
const { data: session, isPending } = useSession();
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!isPending && !session) {
|
|
13
|
+
router.push("/sign-in");
|
|
14
|
+
}
|
|
15
|
+
}, [session, isPending, router]);
|
|
16
|
+
|
|
17
|
+
if (isPending) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
20
|
+
<div className="text-gray-500">Loading...</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!session) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleSignOut = async () => {
|
|
30
|
+
await signOut();
|
|
31
|
+
router.push("/sign-in");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="min-h-screen bg-gray-50">
|
|
36
|
+
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
37
|
+
<div className="mb-8">
|
|
38
|
+
<h1 className="text-3xl font-bold text-gray-900">Settings</h1>
|
|
39
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
40
|
+
Manage your account settings
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="bg-white shadow rounded-lg p-6">
|
|
45
|
+
<h2 className="text-lg font-medium text-gray-900 mb-4">Account</h2>
|
|
46
|
+
|
|
47
|
+
<div className="space-y-4">
|
|
48
|
+
<div>
|
|
49
|
+
<label className="block text-sm font-medium text-gray-700">Name</label>
|
|
50
|
+
<p className="mt-1 text-sm text-gray-900">{session.user.name}</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div>
|
|
54
|
+
<label className="block text-sm font-medium text-gray-700">Email</label>
|
|
55
|
+
<p className="mt-1 text-sm text-gray-900">{session.user.email}</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
60
|
+
<button
|
|
61
|
+
onClick={handleSignOut}
|
|
62
|
+
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
|
63
|
+
>
|
|
64
|
+
Sign Out
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SignInForm } from "@/components/auth/sign-in-form";
|
|
2
|
+
|
|
3
|
+
export default function SignInPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
6
|
+
<div className="max-w-md w-full space-y-8">
|
|
7
|
+
<div className="text-center">
|
|
8
|
+
<h2 className="text-3xl font-bold text-gray-900">Sign in</h2>
|
|
9
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
10
|
+
Welcome back! Please sign in to continue.
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
<div className="mt-8 bg-white py-8 px-6 shadow rounded-lg">
|
|
14
|
+
<SignInForm />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SignUpForm } from "@/components/auth/sign-up-form";
|
|
2
|
+
|
|
3
|
+
export default function SignUpPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
6
|
+
<div className="max-w-md w-full space-y-8">
|
|
7
|
+
<div className="text-center">
|
|
8
|
+
<h2 className="text-3xl font-bold text-gray-900">Create account</h2>
|
|
9
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
10
|
+
Get started by creating your account
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
<div className="mt-8 bg-white py-8 px-6 shadow rounded-lg">
|
|
14
|
+
<SignUpForm />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { authClient } from "@/lib/auth-client";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export function SignInForm() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [email, setEmail] = useState("");
|
|
10
|
+
const [password, setPassword] = useState("");
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState("");
|
|
13
|
+
|
|
14
|
+
const handleEmailPassword = async (e: React.FormEvent) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
setError("");
|
|
17
|
+
setIsLoading(true);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await authClient.signIn.email({
|
|
21
|
+
email,
|
|
22
|
+
password,
|
|
23
|
+
callbackURL: "/dashboard",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
router.push("/dashboard");
|
|
27
|
+
} catch (err) {
|
|
28
|
+
setError("Invalid email or password");
|
|
29
|
+
} finally {
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleGoogleSignIn = async () => {
|
|
35
|
+
setError("");
|
|
36
|
+
try {
|
|
37
|
+
await authClient.signIn.social({
|
|
38
|
+
provider: "google",
|
|
39
|
+
callbackURL: "/dashboard",
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError("Failed to sign in with Google");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-6">
|
|
48
|
+
{/* Google Sign In */}
|
|
49
|
+
<button
|
|
50
|
+
onClick={handleGoogleSignIn}
|
|
51
|
+
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|
52
|
+
>
|
|
53
|
+
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
|
54
|
+
<path
|
|
55
|
+
fill="#4285F4"
|
|
56
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
57
|
+
/>
|
|
58
|
+
<path
|
|
59
|
+
fill="#34A853"
|
|
60
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
61
|
+
/>
|
|
62
|
+
<path
|
|
63
|
+
fill="#FBBC05"
|
|
64
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
65
|
+
/>
|
|
66
|
+
<path
|
|
67
|
+
fill="#EA4335"
|
|
68
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
69
|
+
/>
|
|
70
|
+
</svg>
|
|
71
|
+
Continue with Google
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
<div className="relative">
|
|
75
|
+
<div className="absolute inset-0 flex items-center">
|
|
76
|
+
<div className="w-full border-t border-gray-300" />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="relative flex justify-center text-sm">
|
|
79
|
+
<span className="bg-white px-2 text-gray-500">Or continue with</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Email/Password Form */}
|
|
84
|
+
<form onSubmit={handleEmailPassword} className="space-y-4">
|
|
85
|
+
<div>
|
|
86
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
87
|
+
Email
|
|
88
|
+
</label>
|
|
89
|
+
<input
|
|
90
|
+
type="email"
|
|
91
|
+
value={email}
|
|
92
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
93
|
+
required
|
|
94
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
95
|
+
placeholder="you@example.com"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div>
|
|
100
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
101
|
+
Password
|
|
102
|
+
</label>
|
|
103
|
+
<input
|
|
104
|
+
type="password"
|
|
105
|
+
value={password}
|
|
106
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
107
|
+
required
|
|
108
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
109
|
+
placeholder="••••••••"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{error && (
|
|
114
|
+
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
|
115
|
+
{error}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<button
|
|
120
|
+
type="submit"
|
|
121
|
+
disabled={isLoading}
|
|
122
|
+
className="w-full px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
123
|
+
>
|
|
124
|
+
{isLoading ? "Signing in..." : "Sign in"}
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
{/* Sign Up Link */}
|
|
129
|
+
<div className="text-center text-sm text-gray-600">
|
|
130
|
+
Don't have an account?{" "}
|
|
131
|
+
<a href="/sign-up" className="text-gray-900 hover:underline font-medium">
|
|
132
|
+
Sign up
|
|
133
|
+
</a>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { authClient } from "@/lib/auth-client";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export function SignUpForm() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [name, setName] = useState("");
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [password, setPassword] = useState("");
|
|
12
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
13
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
14
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState("");
|
|
17
|
+
|
|
18
|
+
const handleAccountSubmit = async (e: React.FormEvent) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setError("");
|
|
21
|
+
|
|
22
|
+
if (password !== confirmPassword) {
|
|
23
|
+
setError("Passwords do not match");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (password.length < 8) {
|
|
28
|
+
setError("Password must be at least 8 characters");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await authClient.signUp.email({
|
|
36
|
+
email,
|
|
37
|
+
password,
|
|
38
|
+
name,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.push("/dashboard");
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
if (err.message?.includes("already exists") || err.message?.includes("UNIQUE constraint")) {
|
|
44
|
+
setError("An account with this email already exists. Please sign in instead.");
|
|
45
|
+
} else {
|
|
46
|
+
setError(err.message || "Failed to create account");
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleGoogleSignUp = async () => {
|
|
54
|
+
setError("");
|
|
55
|
+
try {
|
|
56
|
+
await authClient.signIn.social({
|
|
57
|
+
provider: "google",
|
|
58
|
+
callbackURL: "/dashboard",
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setError("Failed to sign up with Google");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-6">
|
|
67
|
+
{/* Google Sign Up */}
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleGoogleSignUp}
|
|
70
|
+
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|
71
|
+
>
|
|
72
|
+
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
|
73
|
+
<path
|
|
74
|
+
fill="#4285F4"
|
|
75
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
76
|
+
/>
|
|
77
|
+
<path
|
|
78
|
+
fill="#34A853"
|
|
79
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
80
|
+
/>
|
|
81
|
+
<path
|
|
82
|
+
fill="#FBBC05"
|
|
83
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
84
|
+
/>
|
|
85
|
+
<path
|
|
86
|
+
fill="#EA4335"
|
|
87
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
88
|
+
/>
|
|
89
|
+
</svg>
|
|
90
|
+
Continue with Google
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<div className="relative">
|
|
94
|
+
<div className="absolute inset-0 flex items-center">
|
|
95
|
+
<div className="w-full border-t border-gray-300" />
|
|
96
|
+
</div>
|
|
97
|
+
<div className="relative flex justify-center text-sm">
|
|
98
|
+
<span className="bg-white px-2 text-gray-500">Or sign up with email</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Sign Up Form */}
|
|
103
|
+
<form onSubmit={handleAccountSubmit} className="space-y-4">
|
|
104
|
+
<div>
|
|
105
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
106
|
+
Name
|
|
107
|
+
</label>
|
|
108
|
+
<input
|
|
109
|
+
type="text"
|
|
110
|
+
value={name}
|
|
111
|
+
onChange={(e) => setName(e.target.value)}
|
|
112
|
+
required
|
|
113
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
114
|
+
placeholder="John Doe"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
120
|
+
Email
|
|
121
|
+
</label>
|
|
122
|
+
<input
|
|
123
|
+
type="email"
|
|
124
|
+
value={email}
|
|
125
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
126
|
+
required
|
|
127
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
128
|
+
placeholder="you@example.com"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div>
|
|
133
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
134
|
+
Password
|
|
135
|
+
</label>
|
|
136
|
+
<div className="relative">
|
|
137
|
+
<input
|
|
138
|
+
type={showPassword ? "text" : "password"}
|
|
139
|
+
value={password}
|
|
140
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
141
|
+
required
|
|
142
|
+
minLength={8}
|
|
143
|
+
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
144
|
+
placeholder="••••••••"
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
149
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
150
|
+
>
|
|
151
|
+
{showPassword ? (
|
|
152
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
153
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
154
|
+
</svg>
|
|
155
|
+
) : (
|
|
156
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
157
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
158
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
159
|
+
</svg>
|
|
160
|
+
)}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
164
|
+
Must be at least 8 characters
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div>
|
|
169
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
170
|
+
Confirm Password
|
|
171
|
+
</label>
|
|
172
|
+
<div className="relative">
|
|
173
|
+
<input
|
|
174
|
+
type={showConfirmPassword ? "text" : "password"}
|
|
175
|
+
value={confirmPassword}
|
|
176
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
177
|
+
required
|
|
178
|
+
minLength={8}
|
|
179
|
+
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-gray-900"
|
|
180
|
+
placeholder="••••••••"
|
|
181
|
+
/>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
185
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
186
|
+
>
|
|
187
|
+
{showConfirmPassword ? (
|
|
188
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
189
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
190
|
+
</svg>
|
|
191
|
+
) : (
|
|
192
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
193
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
194
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
195
|
+
</svg>
|
|
196
|
+
)}
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{error && (
|
|
202
|
+
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
|
203
|
+
{error}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<button
|
|
208
|
+
type="submit"
|
|
209
|
+
disabled={isLoading}
|
|
210
|
+
className="w-full px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
211
|
+
>
|
|
212
|
+
{isLoading ? "Creating account..." : "Sign Up"}
|
|
213
|
+
</button>
|
|
214
|
+
</form>
|
|
215
|
+
|
|
216
|
+
{/* Sign In Link */}
|
|
217
|
+
<div className="text-center text-sm text-gray-600">
|
|
218
|
+
Already have an account?{" "}
|
|
219
|
+
<a href="/sign-in" className="text-gray-900 hover:underline font-medium">
|
|
220
|
+
Sign in
|
|
221
|
+
</a>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|