Dental Care
This commit is contained in:
104
app/(auth)/forgot-password/forgot-password-form.tsx
Normal file
104
app/(auth)/forgot-password/forgot-password-form.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-session/auth-client";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ForgotPasswordForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { error } = await authClient.forgetPassword({
|
||||
email,
|
||||
redirectTo: `${window.location.origin}/reset-password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error("Failed to send reset link", {
|
||||
description: error.message || "Please try again.",
|
||||
});
|
||||
} else {
|
||||
toast.success("Password reset link sent!", {
|
||||
description: "Please check your email inbox.",
|
||||
});
|
||||
setEmail(""); // Clear the form on success
|
||||
}
|
||||
} catch {
|
||||
toast.error("An unexpected error occurred", {
|
||||
description: "Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Forgot Password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email address and we'll send you a link to reset
|
||||
your password
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@gmail.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Sending..." : "Send Reset Link"}
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Remember your password?{" "}
|
||||
<a
|
||||
href="/sign-in"
|
||||
className="underline-offset-4 hover:underline"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
app/(auth)/forgot-password/page.tsx
Normal file
26
app/(auth)/forgot-password/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ForgotPasswordForm } from "@/app/(auth)/forgot-password/forgot-password-form";
|
||||
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<Link href="/" className="flex items-center gap-2 self-center font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md p-1">
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
src={"/tooth.svg"}
|
||||
alt="Dental U Care Logo"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<span>Dental U Care</span>
|
||||
</Link>
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
app/(auth)/layout.tsx
Normal file
13
app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default async function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
// Removed redundant client-side redirect here.
|
||||
// The sign-in form performs role-based redirects after login
|
||||
// and middleware protects authenticated routes. Keeping the
|
||||
// layout minimal avoids redirect race conditions.
|
||||
return <>{children}</>;
|
||||
}
|
||||
32
app/(auth)/reset-password/page.tsx
Normal file
32
app/(auth)/reset-password/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { ResetPasswordForm } from "./reset-password-form";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 self-center font-medium"
|
||||
>
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md p-1">
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
src={"/tooth.svg"}
|
||||
alt="Dental U Care Logo"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<span>Dental U Care</span>
|
||||
</Link>
|
||||
<Suspense fallback={<div className="text-center">Loading...</div>}>
|
||||
<ResetPasswordForm />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
254
app/(auth)/reset-password/reset-password-form.tsx
Normal file
254
app/(auth)/reset-password/reset-password-form.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { toast } from "sonner";
|
||||
import { useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { authClient } from "@/lib/auth-session/auth-client";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export function ResetPasswordForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
function togglePassword(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.preventDefault();
|
||||
setShowPassword((s) => !s);
|
||||
}
|
||||
|
||||
function toggleConfirm(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.preventDefault();
|
||||
setShowConfirm((s) => !s);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
if (!token) {
|
||||
toast.error("Invalid or missing reset token");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
toast.error("Password must be at least 8 characters");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Passwords do not match");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await authClient.resetPassword({
|
||||
newPassword: password,
|
||||
token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message || "Failed to reset password");
|
||||
} else {
|
||||
toast.success("Password reset successfully");
|
||||
router.push("/sign-in");
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Reset Password</CardTitle>
|
||||
<CardDescription>Enter your new password</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your new password"
|
||||
disabled={isLoading}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
aria-label={
|
||||
showPassword ? "Hide password" : "Show password"
|
||||
}
|
||||
onClick={togglePassword}
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Must be at least 8 characters
|
||||
</p>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="confirmPassword">
|
||||
Confirm Password
|
||||
</FieldLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showConfirm ? "text" : "password"}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirm your new password"
|
||||
disabled={isLoading}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
aria-label={
|
||||
showConfirm
|
||||
? "Hide confirm password"
|
||||
: "Show confirm password"
|
||||
}
|
||||
onClick={toggleConfirm}
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
||||
>
|
||||
{showConfirm ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
Resetting...
|
||||
</>
|
||||
) : (
|
||||
"Reset Password"
|
||||
)}
|
||||
</Button>
|
||||
</Field>
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/sign-up" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
||||
By clicking continue, you agree to our{" "}
|
||||
<Link href="/docs/terms-of-service">Terms of Service</Link> and{" "}
|
||||
<Link href="/docs/privacy-policy">Privacy Policy</Link>.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
app/(auth)/sign-in/page.tsx
Normal file
38
app/(auth)/sign-in/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import Image from "next/image"
|
||||
import { Metadata } from "next"
|
||||
import { LoginForm } from "@/app/(auth)/sign-in/sign-in-form"
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sign in",
|
||||
};
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col p-6 md:p-10">
|
||||
<div className="flex justify-center md:justify-start flex-shrink-0 mb-8">
|
||||
<Link href="/" className="flex items-center gap-2 font-semibold text-lg">
|
||||
<div className=" text-primary-foreground flex size-10 items-center justify-center rounded-lg p-2">
|
||||
<Image src="/tooth.svg" alt="Dental U Care" width={24} height={24} />
|
||||
</div>
|
||||
Dental U Care
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-xs">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted relative hidden lg:block">
|
||||
<Image
|
||||
src="/dentist.jpg"
|
||||
alt="Dental clinic interior"
|
||||
fill
|
||||
className="object-cover dark:brightness-80"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
335
app/(auth)/sign-in/sign-in-form.tsx
Normal file
335
app/(auth)/sign-in/sign-in-form.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { authClient } from "@/lib/auth-session/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showVerifyNotice, setShowVerifyNotice] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [resendSuccess, setResendSuccess] = useState(false);
|
||||
|
||||
function togglePassword(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.preventDefault();
|
||||
setShowPassword((s) => !s);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await authClient.signIn.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
{
|
||||
onSuccess: async (ctx) => {
|
||||
// User data is available in ctx.data
|
||||
const user = ctx.data?.user;
|
||||
const role = user?.role;
|
||||
|
||||
setShowVerifyNotice(false);
|
||||
|
||||
// Determine target based on role
|
||||
const target =
|
||||
role === "admin"
|
||||
? "/admin"
|
||||
: role === "dentist"
|
||||
? "/dentist"
|
||||
: role === "patient"
|
||||
? "/patient"
|
||||
: "/";
|
||||
|
||||
const description =
|
||||
role === "admin"
|
||||
? "Redirecting to admin panel..."
|
||||
: role === "dentist"
|
||||
? "Redirecting to dentist portal..."
|
||||
: role === "patient"
|
||||
? "Redirecting to patient portal..."
|
||||
: "Welcome back!";
|
||||
|
||||
toast.success("Login successful!", { description });
|
||||
|
||||
// Small delay to ensure cookie is set before redirect
|
||||
// This is critical for production where cookie propagation takes time
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Use window.location.href for full page reload
|
||||
window.location.href = target;
|
||||
},
|
||||
onError: (ctx) => {
|
||||
if (ctx.error.status === 403) {
|
||||
setShowVerifyNotice(true);
|
||||
toast.error("Please verify your email address", {
|
||||
description: "Check your inbox for the verification link.",
|
||||
});
|
||||
} else {
|
||||
setShowVerifyNotice(false);
|
||||
toast.error("Login failed", {
|
||||
description: ctx.error.message || "Invalid email or password.",
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
toast.error("An unexpected error occurred", {
|
||||
description: "Please try again later.",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResendVerification(
|
||||
e: React.MouseEvent<HTMLButtonElement>
|
||||
) {
|
||||
e.preventDefault();
|
||||
setResendLoading(true);
|
||||
setResendSuccess(false);
|
||||
try {
|
||||
const res = await fetch("/api/auth/resend-verification", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setResendSuccess(true);
|
||||
toast.success("Verification email sent!", {
|
||||
description: "Check your inbox for the verification link.",
|
||||
});
|
||||
} else {
|
||||
toast.error("Failed to resend verification email.");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to resend verification email.");
|
||||
} finally {
|
||||
setResendLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGoogleSignIn() {
|
||||
try {
|
||||
setIsGoogleLoading(true);
|
||||
|
||||
// Google OAuth redirects to Google, then back to /api/auth/callback/google
|
||||
// Better Auth handles the callback and creates/updates the session
|
||||
// The onAfterSignUp hook in auth.ts ensures new users get the "patient" role
|
||||
// After callback, users are redirected to root "/"
|
||||
// The auth layout then redirects to role-specific dashboard
|
||||
await authClient.signIn.social({
|
||||
provider: "google",
|
||||
});
|
||||
|
||||
// This will cause a redirect, so code after this won't execute
|
||||
} catch (error) {
|
||||
console.error("Google sign-in failed:", error);
|
||||
toast.error("Google sign-in failed", {
|
||||
description: "Please try again.",
|
||||
});
|
||||
setIsGoogleLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cn("flex flex-col gap-6", className)}
|
||||
{...props}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FieldGroup>
|
||||
{showVerifyNotice && (
|
||||
<div className="bg-yellow-100 border border-yellow-300 text-yellow-800 rounded p-3 text-center mb-2">
|
||||
<div className="mb-2">
|
||||
Your email is not verified. Please check your inbox for the
|
||||
verification link.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="underline text-sm text-blue-700 disabled:opacity-60"
|
||||
onClick={handleResendVerification}
|
||||
disabled={resendLoading || resendSuccess}
|
||||
>
|
||||
{resendLoading
|
||||
? "Resending..."
|
||||
: resendSuccess
|
||||
? "Verification Sent!"
|
||||
: "Resend Verification Email"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<h1 className="text-2xl font-bold">Login to your account</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="e.g m@gmail.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
onClick={togglePassword}
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit" disabled={isLoading || isGoogleLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
{"Logging in..."}
|
||||
</>
|
||||
) : (
|
||||
"Login"
|
||||
)}
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Field>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isLoading || isGoogleLoading}
|
||||
>
|
||||
{isGoogleLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
{"Signing in..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="-3 0 262 262"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
className="size-4 mr-2"
|
||||
>
|
||||
<path
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
fill="#EB4335"
|
||||
/>
|
||||
</svg>
|
||||
Login with Google
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/sign-up" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</Link>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
46
app/(auth)/sign-up/page.tsx
Normal file
46
app/(auth)/sign-up/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Metadata } from "next";
|
||||
import { SignupForm } from "@/app/(auth)/sign-up/sign-up-form";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
export const metadata: Metadata = {
|
||||
title: "Sign Up",
|
||||
};
|
||||
|
||||
export default function SignupPage() {
|
||||
return (
|
||||
<div className="grid h-screen lg:grid-cols-2">
|
||||
<div className="flex flex-col p-4 md:p-6 h-screen overflow-hidden">
|
||||
<div className="flex justify-center md:justify-start flex-shrink-0 mb-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 font-semibold text-lg"
|
||||
>
|
||||
<div className=" text-primary-foreground flex size-10 items-center justify-center rounded-lg p-2">
|
||||
<Image
|
||||
src="/tooth.svg"
|
||||
alt="Dental U Care"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
Dental U Care
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-xs">
|
||||
<SignupForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted relative hidden lg:block h-screen">
|
||||
<Image
|
||||
src="/doctor-image.jpg"
|
||||
alt="Doctor"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-80"
|
||||
priority
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
414
app/(auth)/sign-up/sign-up-form.tsx
Normal file
414
app/(auth)/sign-up/sign-up-form.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { authClient } from "@/lib/auth-session/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export function SignupForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showVerifyNotice, setShowVerifyNotice] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [resendSuccess, setResendSuccess] = useState(false);
|
||||
|
||||
function togglePassword(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.preventDefault();
|
||||
setShowPassword((s) => !s);
|
||||
}
|
||||
|
||||
function toggleConfirm(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.preventDefault();
|
||||
setShowConfirm((s) => !s);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
// Validate passwords match
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Passwords don't match", {
|
||||
description: "Please make sure both passwords are the same.",
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!/[^A-Za-z0-9]/.test(password)) {
|
||||
toast.error("Password must contain at least one special character", {
|
||||
description:
|
||||
"Please include at least one special character in your password.",
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
// Validate password length
|
||||
if (password.length < 8) {
|
||||
toast.error("Password too short", {
|
||||
description: "Password must be at least 8 characters long.",
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await authClient.signUp.email({
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (error.status === 403) {
|
||||
setShowVerifyNotice(true);
|
||||
toast.error("Please verify your email address", {
|
||||
description: "Check your inbox for the verification link.",
|
||||
});
|
||||
} else {
|
||||
setShowVerifyNotice(false);
|
||||
toast.error("Sign up failed", {
|
||||
description:
|
||||
error.message || "Unable to create account. Please try again.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setShowVerifyNotice(false);
|
||||
toast.success("Account created successfully!", {
|
||||
description: "Please check your email to verify your account.",
|
||||
});
|
||||
// Redirect to login page after successful signup using full page reload
|
||||
setTimeout(() => {
|
||||
window.location.href = "/sign-in";
|
||||
}, 2000);
|
||||
}
|
||||
} catch {
|
||||
toast.error("An unexpected error occurred", {
|
||||
description: "Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResendVerification(
|
||||
e: React.MouseEvent<HTMLButtonElement>
|
||||
) {
|
||||
e.preventDefault();
|
||||
setResendLoading(true);
|
||||
setResendSuccess(false);
|
||||
try {
|
||||
const res = await fetch("/api/auth/resend-verification", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setResendSuccess(true);
|
||||
toast.success("Verification email sent!", {
|
||||
description: "Check your inbox for the verification link.",
|
||||
});
|
||||
} else {
|
||||
toast.error("Failed to resend verification email.");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to resend verification email.");
|
||||
} finally {
|
||||
setResendLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGoogleSignUp() {
|
||||
try {
|
||||
setIsGoogleLoading(true);
|
||||
// Google OAuth will redirect to Google, then back to callback
|
||||
// The onAfterSignUp hook in auth.ts assigns "patient" role to new users
|
||||
// After callback, the auth layout redirects to the role-specific dashboard
|
||||
await authClient.signIn.social({
|
||||
provider: "google",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Google sign-up failed:", error);
|
||||
toast.error("Google sign-up failed", {
|
||||
description: "Please try again.",
|
||||
});
|
||||
setIsGoogleLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cn("flex flex-col gap-3", className)}
|
||||
{...props}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{showVerifyNotice && (
|
||||
<div className="bg-yellow-100 border border-yellow-300 text-yellow-800 rounded p-3 text-center mb-2">
|
||||
<div className="mb-2">
|
||||
Your email is not verified. Please check your inbox for the
|
||||
verification link.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="underline text-sm text-blue-700 disabled:opacity-60"
|
||||
onClick={handleResendVerification}
|
||||
disabled={resendLoading || resendSuccess}
|
||||
>
|
||||
{resendLoading
|
||||
? "Resending..."
|
||||
: resendSuccess
|
||||
? "Verification Sent!"
|
||||
: "Resend Verification Email"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<FieldGroup className="gap-3">
|
||||
<div className="flex flex-col items-center text-center mb-2">
|
||||
<h1 className="text-2xl font-bold">Create your account</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Fill in the form below to create your account
|
||||
</p>
|
||||
</div>
|
||||
<Field className="gap-1">
|
||||
<FieldLabel htmlFor="name" className="text-xs">
|
||||
Full Name
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field className="gap-1">
|
||||
<FieldLabel htmlFor="email" className="text-xs">
|
||||
Email
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="e.g m@gmail.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
className="h-9"
|
||||
/>
|
||||
<FieldDescription className="text-xs leading-tight">
|
||||
We'll use this to contact you.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="gap-1">
|
||||
<FieldLabel htmlFor="password" className="text-xs">
|
||||
Password
|
||||
</FieldLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
minLength={8}
|
||||
required
|
||||
className="h-9"
|
||||
/>
|
||||
<button
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
onClick={togglePassword}
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<FieldDescription className="text-xs leading-tight">
|
||||
Must be at least 8 characters long.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="gap-1">
|
||||
<FieldLabel htmlFor="confirm-password" className="text-xs">
|
||||
Confirm Password
|
||||
</FieldLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type={showConfirm ? "text" : "password"}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
minLength={8}
|
||||
required
|
||||
className="h-9"
|
||||
/>
|
||||
<button
|
||||
aria-label={
|
||||
showConfirm ? "Hide confirm password" : "Show confirm password"
|
||||
}
|
||||
onClick={toggleConfirm}
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
||||
>
|
||||
{showConfirm ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<FieldDescription className="text-xs leading-tight">
|
||||
Please confirm your password.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="gap-1">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || isGoogleLoading}
|
||||
className="h-9"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
Creating account...
|
||||
</>
|
||||
) : (
|
||||
"Create Account"
|
||||
)}
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Field>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={handleGoogleSignUp}
|
||||
disabled={isLoading || isGoogleLoading}
|
||||
>
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="-3 0 262 262"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<path
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
fill="#EB4335"
|
||||
/>
|
||||
</svg>
|
||||
{isGoogleLoading ? "Signing up..." : "Sign up with Google"}
|
||||
</Button>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
Already have an account? <Link href="/sign-in">Sign in</Link>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user