diff options
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/auth/login/page.tsx | 134 | ||||
| -rw-r--r-- | apps/web/src/app/auth/signup/page.tsx | 163 | ||||
| -rw-r--r-- | apps/web/src/components/header.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/icons.tsx | 22 | ||||
| -rw-r--r-- | apps/web/src/lib/auth-client.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/lib/auth.ts | 6 |
6 files changed, 249 insertions, 82 deletions
diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx index 5eadb85c..ebf2eb3f 100644 --- a/apps/web/src/app/auth/login/page.tsx +++ b/apps/web/src/app/auth/login/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; -import { authClient } from "@/lib/auth-client"; +import { useRouter } from "next/navigation"; +import { signIn } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,81 +13,145 @@ import { import { Suspense, useState } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { ArrowLeft, Loader2 } from "lucide-react"; +import { GoogleIcon } from "@/components/icons"; function LoginForm() { - const searchParams = useSearchParams(); const router = useRouter(); - const redirectUrl = searchParams.get("redirect"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState<string | null>(null); + const [isEmailLoading, setIsEmailLoading] = useState(false); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); const handleLogin = async () => { setError(null); - const { error } = await authClient.signIn.email({ + setIsEmailLoading(true); + + const { error } = await signIn.email({ email, password, }); if (error) { setError(error.message || "An unexpected error occurred."); + setIsEmailLoading(false); return; } - router.push(redirectUrl || "/"); + router.push("/editor"); + }; + + const handleGoogleLogin = async () => { + setError(null); + setIsGoogleLoading(true); + + try { + await signIn.social({ + provider: "google", + }); + router.push("/editor"); + } catch (error) { + setError("Failed to sign in with Google. Please try again."); + setIsGoogleLoading(false); + } }; + const isAnyLoading = isEmailLoading || isGoogleLoading; + return ( - <div className="flex flex-col space-y-4"> + <div className="flex flex-col space-y-6"> {error && ( <Alert variant="destructive"> <AlertTitle>Error</AlertTitle> <AlertDescription>{error}</AlertDescription> </Alert> )} - <div className="space-y-2"> - <Label htmlFor="email">Email</Label> - <Input - id="email" - type="email" - placeholder="m@example.com" - value={email} - onChange={(e) => setEmail(e.target.value)} - /> + + <Button + onClick={handleGoogleLogin} + variant="outline" + size="lg" + disabled={isAnyLoading} + > + {isGoogleLoading ? <Loader2 className="animate-spin" /> : (<GoogleIcon />)} Continue with Google + </Button> + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <Separator className="w-full" /> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground">Or continue with</span> + </div> </div> - <div className="space-y-2"> - <Label htmlFor="password">Password</Label> - <Input - id="password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - /> + <div className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="email">Email</Label> + <Input + id="email" + type="email" + placeholder="m@example.com" + value={email} + onChange={(e) => setEmail(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="password">Password</Label> + <Input + id="password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <Button + onClick={handleLogin} + disabled={isAnyLoading || !email || !password} + className="w-full h-11" + size="lg" + > + {isEmailLoading ? <Loader2 className="animate-spin" /> : "Sign in"} + </Button> </div> - <Button onClick={handleLogin}>Login</Button> </div> ); } export default function LoginPage() { + const router = useRouter(); + return ( - <div className="flex h-screen items-center justify-center"> - <Card className="w-[350px]"> - <CardHeader> - <CardTitle>Login</CardTitle> - <CardDescription> - Enter your email and password to login. + <div className="flex h-screen items-center justify-center relative"> + <Button + variant="ghost" + onClick={() => router.back()} + className="absolute top-6 left-6" + > + <ArrowLeft className="h-5 w-5" /> Back + </Button> + <Card className="w-[400px] shadow-lg border-0"> + <CardHeader className="text-center pb-4"> + <CardTitle className="text-2xl font-semibold">Welcome back</CardTitle> + <CardDescription className="text-base"> + Sign in to your account to continue </CardDescription> </CardHeader> - <CardContent> - <Suspense fallback={<div>Loading...</div>}> + <CardContent className="pt-0"> + <Suspense fallback={<div className="text-center"> + <Loader2 className="animate-spin" /> + </div>}> <LoginForm /> </Suspense> - <div className="mt-4 text-center text-sm"> + <div className="mt-6 text-center text-sm"> Don't have an account?{" "} - <Link href="/auth/signup" className="underline"> + <Link href="/auth/signup" className="font-medium text-primary underline-offset-4 hover:underline"> Sign up </Link> </div> diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx index f328851c..3d125a07 100644 --- a/apps/web/src/app/auth/signup/page.tsx +++ b/apps/web/src/app/auth/signup/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; -import { authClient } from "@/lib/auth-client"; +import { useRouter } from "next/navigation"; +import { signUp, signIn } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,21 +13,26 @@ import { import { Suspense, useState } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Loader2, ArrowLeft } from "lucide-react"; +import { GoogleIcon } from "@/components/icons"; function SignUpForm() { - const searchParams = useSearchParams(); const router = useRouter(); - const redirectUrl = searchParams.get("redirect"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState<string | null>(null); + const [isEmailLoading, setIsEmailLoading] = useState(false); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); const handleSignUp = async () => { setError(null); - const { error } = await authClient.signUp.email({ + setIsEmailLoading(true); + + const { error } = await signUp.email({ name, email, password, @@ -35,70 +40,138 @@ function SignUpForm() { if (error) { setError(error.message || "An unexpected error occurred."); + setIsEmailLoading(false); return; } - router.push(redirectUrl || "/"); + router.push("/auth/login"); + }; + + const handleGoogleSignUp = async () => { + setError(null); + setIsGoogleLoading(true); + + try { + await signIn.social({ + provider: "google", + }); + + router.push("/editor"); + } catch (error) { + setError("Failed to sign up with Google. Please try again."); + setIsGoogleLoading(false); + } }; + const isAnyLoading = isEmailLoading || isGoogleLoading; + return ( - <div className="flex flex-col space-y-4"> + <div className="flex flex-col space-y-6"> {error && ( <Alert variant="destructive"> <AlertTitle>Error</AlertTitle> <AlertDescription>{error}</AlertDescription> </Alert> )} - <div className="space-y-2"> - <Label htmlFor="name">Name</Label> - <Input - id="name" - type="text" - placeholder="John Doe" - value={name} - onChange={(e) => setName(e.target.value)} - /> - </div> - <div className="space-y-2"> - <Label htmlFor="email">Email</Label> - <Input - id="email" - type="email" - placeholder="m@example.com" - value={email} - onChange={(e) => setEmail(e.target.value)} - /> + + <Button + onClick={handleGoogleSignUp} + variant="outline" + size="lg" + disabled={isAnyLoading} + > + {isGoogleLoading ? <Loader2 className="animate-spin" /> : (<GoogleIcon />)} Continue with Google + </Button> + + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <Separator className="w-full" /> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground">Or continue with</span> + </div> </div> - <div className="space-y-2"> - <Label htmlFor="password">Password</Label> - <Input - id="password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - /> + + <div className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="name">Full Name</Label> + <Input + id="name" + type="text" + placeholder="John Doe" + value={name} + onChange={(e) => setName(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="email">Email</Label> + <Input + id="email" + type="email" + placeholder="m@example.com" + value={email} + onChange={(e) => setEmail(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="password">Password</Label> + <Input + id="password" + type="password" + placeholder="Create a strong password" + value={password} + onChange={(e) => setPassword(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> + </div> + <Button + onClick={handleSignUp} + disabled={isAnyLoading || !name || !email || !password} + className="w-full h-11" + size="lg" + > + {isEmailLoading ? <Loader2 className="animate-spin" /> : "Create account"} + </Button> </div> - <Button onClick={handleSignUp}>Sign Up</Button> </div> ); } export default function SignUpPage() { + const router = useRouter(); + return ( - <div className="flex h-screen items-center justify-center"> - <Card className="w-[350px]"> - <CardHeader> - <CardTitle>Sign Up</CardTitle> - <CardDescription>Create an account to get started.</CardDescription> + <div className="flex h-screen items-center justify-center relative"> + <Button + variant="ghost" + onClick={() => router.back()} + className="absolute top-6 left-6" + > + <ArrowLeft className="h-5 w-5" /> Back + </Button> + + <Card className="w-[400px] shadow-lg border-0"> + <CardHeader className="text-center pb-4"> + <CardTitle className="text-2xl font-semibold">Create your account</CardTitle> + <CardDescription className="text-base"> + Get started with your free account today + </CardDescription> </CardHeader> - <CardContent> - <Suspense fallback={<div>Loading...</div>}> + <CardContent className="pt-0"> + <Suspense fallback={<div className="text-center"> + <Loader2 className="animate-spin" /> + </div>}> <SignUpForm /> </Suspense> - <div className="mt-4 text-center text-sm"> + <div className="mt-6 text-center text-sm"> Already have an account?{" "} - <Link href="/auth/login" className="underline"> - Login + <Link href="/auth/login" className="font-medium text-primary underline-offset-4 hover:underline"> + Sign in </Link> </div> </CardContent> diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 004863d9..109d022e 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -5,8 +5,10 @@ import Image from "next/image"; import { Button } from "./ui/button";
import { ArrowRight } from "lucide-react";
import { HeaderBase } from "./header-base";
+import { useSession } from "@/lib/auth-client";
export function Header() {
+ const { data: session } = useSession();
const leftContent = (
<Link href="/" className="flex items-center gap-3">
<Image src="/logo.png" alt="OpenCut Logo" width={24} height={24} />
@@ -21,7 +23,7 @@ export function Header() { GitHub
</Button>
</Link>
- <Link href="/editor">
+ <Link href={session ? "/editor" : "/auth/login"}>
<Button size="sm" className="text-sm ml-4">
Start editing
<ArrowRight className="h-4 w-4" />
diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx new file mode 100644 index 00000000..5cc24157 --- /dev/null +++ b/apps/web/src/components/icons.tsx @@ -0,0 +1,22 @@ +export function GoogleIcon({ className }: { className?: string }) { + return ( + <svg className={className} viewBox="0 0 24 24"> + <path + fill="#4285F4" + 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" + /> + <path + fill="#34A853" + 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" + /> + <path + fill="#FBBC05" + 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" + /> + <path + fill="#EA4335" + 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" + /> + </svg> + ); +} diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index 7c467e7e..1ab4e636 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,4 +1,4 @@ import { createAuthClient } from "better-auth/react";
-export const authClient = createAuthClient({
+export const { signIn, signUp, useSession } = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL!,
});
diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index 4cfaa6f2..d0175c97 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -16,6 +16,12 @@ export const auth = betterAuth({ emailAndPassword: {
enabled: true,
},
+ socialProviders: {
+ google: {
+ clientId: process.env.GOOGLE_CLIENT_ID as string,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
+ },
+ },
appName: "OpenCut",
trustedOrigins: ["http://localhost:3000"],
});
|
