luxlabs 1.0.0 → 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/commands/interface/boilerplate.js +16 -2
- 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 +34 -0
- package/templates/interface-boilerplate/app/dashboard/page.tsx +53 -0
- package/templates/interface-boilerplate/app/favicon.ico +0 -0
- package/templates/interface-boilerplate/app/globals.css +26 -0
- package/templates/interface-boilerplate/app/layout.tsx +34 -0
- package/templates/interface-boilerplate/app/page.tsx +50 -0
- 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 +36 -0
- package/templates/interface-boilerplate/components/ui/button.tsx +57 -0
- package/templates/interface-boilerplate/components/ui/card.tsx +76 -0
- package/templates/interface-boilerplate/components/ui/input.tsx +22 -0
- 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 +6 -0
- 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 +32 -0
- package/templates/interface-boilerplate/postcss.config.mjs +7 -0
- package/templates/interface-boilerplate/tsconfig.json +34 -0
|
@@ -3,10 +3,24 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Get the templates directory path
|
|
6
|
-
*
|
|
6
|
+
* Tries multiple paths to support both:
|
|
7
|
+
* - Electron app: electron/bin/lux-cli/commands/interface/ → 4 levels up
|
|
8
|
+
* - npm package: luxlabs/commands/interface/ → 2 levels up
|
|
7
9
|
*/
|
|
8
10
|
function getTemplatesDir() {
|
|
9
|
-
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(__dirname, '../../../../templates/interface-boilerplate'), // Electron app
|
|
13
|
+
path.join(__dirname, '../../templates/interface-boilerplate'), // npm package
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (fs.existsSync(candidate)) {
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Return first candidate for error message
|
|
23
|
+
return candidates[0];
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth API Proxy
|
|
3
|
+
*
|
|
4
|
+
* Proxies all auth requests to the Lux Studio API worker.
|
|
5
|
+
* The worker handles Better Auth with the org's database.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const API_URL = process.env.NEXT_PUBLIC_LUX_API_URL || "https://v2.uselux.ai";
|
|
9
|
+
const ORG_ID = process.env.NEXT_PUBLIC_LUX_ORG_ID || "";
|
|
10
|
+
|
|
11
|
+
async function proxyRequest(request: Request): Promise<Response> {
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const path = url.pathname.replace("/api/auth", "");
|
|
14
|
+
const targetUrl = `${API_URL}/api/auth${path}${url.search}`;
|
|
15
|
+
|
|
16
|
+
// Forward the request to the worker
|
|
17
|
+
const headers = new Headers(request.headers);
|
|
18
|
+
headers.set("X-Org-Id", ORG_ID);
|
|
19
|
+
|
|
20
|
+
const response = await fetch(targetUrl, {
|
|
21
|
+
method: request.method,
|
|
22
|
+
headers,
|
|
23
|
+
body: request.method !== "GET" && request.method !== "HEAD" ? request.body : undefined,
|
|
24
|
+
// @ts-ignore - duplex is needed for streaming body
|
|
25
|
+
duplex: "half",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Forward response back with CORS headers
|
|
29
|
+
const responseHeaders = new Headers(response.headers);
|
|
30
|
+
|
|
31
|
+
return new Response(response.body, {
|
|
32
|
+
status: response.status,
|
|
33
|
+
statusText: response.statusText,
|
|
34
|
+
headers: responseHeaders,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function GET(request: Request) {
|
|
39
|
+
return proxyRequest(request);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function POST(request: Request) {
|
|
43
|
+
return proxyRequest(request);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function PUT(request: Request) {
|
|
47
|
+
return proxyRequest(request);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function DELETE(request: Request) {
|
|
51
|
+
return proxyRequest(request);
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useSession } from "@/lib/auth-client";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auth Callback Page
|
|
9
|
+
*
|
|
10
|
+
* Handles redirect after OAuth sign-in.
|
|
11
|
+
* Simply redirects authenticated users to dashboard.
|
|
12
|
+
*/
|
|
13
|
+
export default function AuthCallbackPage() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const { data: session, isPending } = useSession();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!isPending) {
|
|
19
|
+
if (session) {
|
|
20
|
+
// Authenticated - go to dashboard
|
|
21
|
+
router.push("/dashboard");
|
|
22
|
+
} else {
|
|
23
|
+
// Not authenticated - go to sign in
|
|
24
|
+
router.push("/sign-in");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}, [session, isPending, router]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
31
|
+
<div className="text-gray-600">Completing sign in...</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useSession, signOut } from "@/lib/auth-client";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export default function DashboardPage() {
|
|
8
|
+
const { data: session, isPending } = useSession();
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!isPending && !session) {
|
|
13
|
+
router.push("/sign-in");
|
|
14
|
+
}
|
|
15
|
+
}, [isPending, session, router]);
|
|
16
|
+
|
|
17
|
+
if (isPending) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
20
|
+
<p>Loading...</p>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!session) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function handleSignOut() {
|
|
30
|
+
await signOut();
|
|
31
|
+
router.push("/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="min-h-screen bg-zinc-50 p-8">
|
|
36
|
+
<div className="max-w-2xl mx-auto bg-white p-8 rounded-lg shadow-md">
|
|
37
|
+
<h1 className="text-2xl font-semibold mb-4">Dashboard</h1>
|
|
38
|
+
<p className="text-zinc-600 mb-2">
|
|
39
|
+
Welcome, <span className="font-medium">{session.user.name}</span>!
|
|
40
|
+
</p>
|
|
41
|
+
<p className="text-zinc-600 mb-6">
|
|
42
|
+
Email: {session.user.email}
|
|
43
|
+
</p>
|
|
44
|
+
<button
|
|
45
|
+
onClick={handleSignOut}
|
|
46
|
+
className="px-4 py-2 bg-black text-white rounded-md hover:bg-zinc-800"
|
|
47
|
+
>
|
|
48
|
+
Sign Out
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #ffffff;
|
|
5
|
+
--foreground: #171717;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--font-sans: var(--font-geist-sans);
|
|
12
|
+
--font-mono: var(--font-geist-mono);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--background: #0a0a0a;
|
|
18
|
+
--foreground: #ededed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
background: var(--background);
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const geistSans = Geist({
|
|
6
|
+
variable: "--font-geist-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const geistMono = Geist_Mono({
|
|
11
|
+
variable: "--font-geist-mono",
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: "Create Next App",
|
|
17
|
+
description: "Generated by create next app",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<body
|
|
28
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
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
|
|
13
|
+
</h1>
|
|
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.
|
|
16
|
+
</p>
|
|
17
|
+
|
|
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"
|
|
22
|
+
>
|
|
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>
|
|
32
|
+
|
|
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>
|
|
42
|
+
</div>
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<footer className="py-6 text-center text-sm text-zinc-400">
|
|
46
|
+
<p>Ready to ship.</p>
|
|
47
|
+
</footer>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -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
|
+
}
|