mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-17 23:12:23 +00:00
refactor(auth)!: remove components and rewrite sign-in page
This commit is contained in:
parent
1f21cad4d1
commit
a3ef5d88e6
@ -1,78 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { signIn } from "@/lib/auth";
|
|
||||||
import { authSchema } from "@/lib/zod";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form";
|
|
||||||
import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form";
|
|
||||||
|
|
||||||
const saltRounds = 10;
|
|
||||||
|
|
||||||
export async function signInWithCredentials(formData: CredentialsSignInFormValues, redirectTo?: string) {
|
|
||||||
const t = await getTranslations("signInWithCredentials");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse credentials using authSchema for validation
|
|
||||||
const { email, password } = await authSchema.parseAsync(formData);
|
|
||||||
|
|
||||||
// Find user by email
|
|
||||||
const user = await prisma.user.findUnique({ where: { email } });
|
|
||||||
|
|
||||||
// Check if the user exists
|
|
||||||
if (!user) {
|
|
||||||
throw new Error(t("userNotFound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user has a password
|
|
||||||
if (!user.password) {
|
|
||||||
throw new Error(t("invalidCredentials"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the password matches
|
|
||||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
|
||||||
if (!passwordMatch) {
|
|
||||||
throw new Error(t("incorrectPassword"));
|
|
||||||
}
|
|
||||||
|
|
||||||
await signIn("credentials", { ...formData, redirectTo, redirect: !!redirectTo });
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { error: error instanceof Error ? error.message : t("signInFailedFallback") };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) {
|
|
||||||
const t = await getTranslations("signUpWithCredentials");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const validatedData = await authSchema.parseAsync(formData);
|
|
||||||
|
|
||||||
// Check if user already exists
|
|
||||||
const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } });
|
|
||||||
if (existingUser) {
|
|
||||||
throw new Error(t("userAlreadyExists"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash password and create user
|
|
||||||
const pwHash = await bcrypt.hash(validatedData.password, saltRounds);
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: { email: validatedData.email, password: pwHash },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assign admin role if first user
|
|
||||||
const userCount = await prisma.user.count();
|
|
||||||
if (userCount === 1) {
|
|
||||||
await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } });
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { error: error instanceof Error ? error.message : t("registrationFailedFallback") };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function signInWithGithub(redirectTo?: string) {
|
|
||||||
await signIn("github", { redirectTo, redirect: !!redirectTo });
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { CodeIcon } from "lucide-react";
|
|
||||||
|
|
||||||
interface AuthLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function AuthLayout({
|
|
||||||
children
|
|
||||||
}: AuthLayoutProps) {
|
|
||||||
return (
|
|
||||||
<div className="grid min-h-svh lg:grid-cols-2">
|
|
||||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
|
||||||
<div className="flex justify-center gap-2 md:justify-start">
|
|
||||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
|
||||||
<CodeIcon className="size-4" />
|
|
||||||
</div>
|
|
||||||
Judge4c
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 items-center justify-center">
|
|
||||||
<div className="w-full max-w-sm">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative hidden bg-muted lg:block">
|
|
||||||
<Image
|
|
||||||
src="/placeholder.svg"
|
|
||||||
alt="Image"
|
|
||||||
fill
|
|
||||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,5 +1,98 @@
|
|||||||
import { SignInForm } from "@/components/sign-in-form";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { CodeIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { providerMap, signIn } from "@/lib/auth";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { FaGithub, FaGoogle } from "react-icons/fa";
|
||||||
|
|
||||||
export default function SignInPage() {
|
interface ProviderIconProps {
|
||||||
return <SignInForm />;
|
providerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProviderIcon = ({ providerId }: ProviderIconProps) => {
|
||||||
|
switch (providerId) {
|
||||||
|
case "github":
|
||||||
|
return <FaGithub />;
|
||||||
|
case "google":
|
||||||
|
return <FaGoogle />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SignInPageProps {
|
||||||
|
searchParams: Promise<{
|
||||||
|
callbackUrl: string | undefined;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SignInPage({ searchParams }: SignInPageProps) {
|
||||||
|
const { callbackUrl } = await searchParams;
|
||||||
|
const t = await getTranslations("SignInForm");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid min-h-svh lg:grid-cols-2">
|
||||||
|
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||||
|
<div className="flex justify-center gap-2 md:justify-start">
|
||||||
|
<Link
|
||||||
|
href={callbackUrl ?? "/"}
|
||||||
|
className="flex items-center gap-2 font-medium"
|
||||||
|
>
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||||
|
<CodeIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
Judge4c
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center justify-center">
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col items-center gap-2 text-center">
|
||||||
|
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
||||||
|
<p className="text-balance text-sm text-muted-foreground line-through">
|
||||||
|
{t("description")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
||||||
|
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
||||||
|
{t("or")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{Object.values(providerMap).map((provider) => {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
key={provider.id}
|
||||||
|
action={async () => {
|
||||||
|
"use server";
|
||||||
|
await signIn(provider.id, {
|
||||||
|
redirectTo: callbackUrl ?? "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full flex items-center justify-center gap-4"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<ProviderIcon providerId={provider.id} />
|
||||||
|
{t("oauth", { provider: provider.name })}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative hidden bg-muted lg:block">
|
||||||
|
<Image
|
||||||
|
src="/placeholder.svg"
|
||||||
|
alt="Image"
|
||||||
|
fill
|
||||||
|
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import { SignUpForm } from "@/components/sign-up-form";
|
|
||||||
|
|
||||||
export default function SignUpPage() {
|
|
||||||
return <SignUpForm />;
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { authSchema } from "@/lib/zod";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useState, useTransition } from "react";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { signInWithCredentials } from "@/actions/auth";
|
|
||||||
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
|
||||||
|
|
||||||
export type CredentialsSignInFormValues = z.infer<typeof authSchema>;
|
|
||||||
|
|
||||||
export function CredentialsSignInForm() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const redirectTo = searchParams.get("redirectTo");
|
|
||||||
const t = useTranslations("CredentialsSignInForm");
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
|
|
||||||
const form = useForm<CredentialsSignInFormValues>({
|
|
||||||
resolver: zodResolver(authSchema),
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleVisibility = () => setIsVisible((prev) => !prev);
|
|
||||||
|
|
||||||
const onSubmit = (data: CredentialsSignInFormValues) => {
|
|
||||||
startTransition(async () => {
|
|
||||||
const result = await signInWithCredentials(data);
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
toast.error(t("signInFailed"), {
|
|
||||||
description: result.error,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.success(t("signInSuccess"));
|
|
||||||
router.push(redirectTo || "/");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("email")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input className="peer pe-9" {...field} />
|
|
||||||
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 peer-disabled:opacity-50">
|
|
||||||
<MailIcon size={16} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("password")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
className="pe-9"
|
|
||||||
type={isVisible ? "text" : "password"}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
type="button"
|
|
||||||
onClick={toggleVisibility}
|
|
||||||
aria-label={isVisible ? t("hidePassword") : t("showPassword")}
|
|
||||||
aria-pressed={isVisible}
|
|
||||||
aria-controls="password"
|
|
||||||
>
|
|
||||||
{isVisible ? (
|
|
||||||
<EyeOffIcon size={16} aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<EyeIcon size={16} aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending} className="w-full">
|
|
||||||
{isPending ? t("signingIn") : t("signIn")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { authSchema } from "@/lib/zod";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useState, useTransition } from "react";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { signUpWithCredentials } from "@/actions/auth";
|
|
||||||
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
|
||||||
|
|
||||||
export type CredentialsSignUpFormValues = z.infer<typeof authSchema>;
|
|
||||||
|
|
||||||
export function CredentialsSignUpForm() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const redirectTo = searchParams.get("redirectTo");
|
|
||||||
const t = useTranslations("CredentialsSignUpForm");
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
|
|
||||||
const form = useForm<CredentialsSignUpFormValues>({
|
|
||||||
resolver: zodResolver(authSchema),
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleVisibility = () => setIsVisible((prev) => !prev);
|
|
||||||
|
|
||||||
const onSubmit = (data: CredentialsSignUpFormValues) => {
|
|
||||||
startTransition(async () => {
|
|
||||||
const result = await signUpWithCredentials(data);
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
toast.error(t("signUpFailed"), {
|
|
||||||
description: result.error,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.success(t("signUpSuccess"), {
|
|
||||||
description: t("signUpSuccessDescription"),
|
|
||||||
});
|
|
||||||
router.push(`/sign-in?${redirectTo}`)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("email")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input className="peer pe-9" {...field} />
|
|
||||||
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 peer-disabled:opacity-50">
|
|
||||||
<MailIcon size={16} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("password")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
className="pe-9"
|
|
||||||
type={isVisible ? "text" : "password"}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
type="button"
|
|
||||||
onClick={toggleVisibility}
|
|
||||||
aria-label={isVisible ? t("hidePassword") : t("showPassword")}
|
|
||||||
aria-pressed={isVisible}
|
|
||||||
aria-controls="password"
|
|
||||||
>
|
|
||||||
{isVisible ? (
|
|
||||||
<EyeOffIcon size={16} aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<EyeIcon size={16} aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending} className="w-full">
|
|
||||||
{isPending ? t("creatingAccount") : t("signUp")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { signInWithGithub } from "@/actions/auth";
|
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
|
|
||||||
export function GithubSignInForm() {
|
|
||||||
const t = useTranslations();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const redirectTo = searchParams.get("redirectTo");
|
|
||||||
const signInAction = signInWithGithub.bind(null, redirectTo || "/");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form action={signInAction}>
|
|
||||||
<Button variant="outline" className="w-full" type="submit">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{t("GithubSignInForm")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import { GithubSignInForm } from "@/components/github-sign-in-form";
|
|
||||||
import { CredentialsSignInForm } from "@/components/credentials-sign-in-form";
|
|
||||||
|
|
||||||
export function SignInForm() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const t = useTranslations("SignInForm");
|
|
||||||
|
|
||||||
const handleSignUp = () => {
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
router.push(`/sign-up?${params.toString()}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
|
||||||
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
||||||
<p className="text-balance text-sm text-muted-foreground">
|
|
||||||
{t("description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<CredentialsSignInForm />
|
|
||||||
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
|
||||||
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
|
||||||
{t("or")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<GithubSignInForm />
|
|
||||||
<div className="text-center text-sm">
|
|
||||||
{t("noAccount")}{" "}
|
|
||||||
<button
|
|
||||||
onClick={handleSignUp}
|
|
||||||
className="underline underline-offset-4"
|
|
||||||
>
|
|
||||||
{t("signUp")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import { GithubSignInForm } from "@/components/github-sign-in-form";
|
|
||||||
import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form";
|
|
||||||
|
|
||||||
export function SignUpForm() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const t = useTranslations("SignUpForm");
|
|
||||||
|
|
||||||
const handleSignIn = () => {
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
router.push(`/sign-in?${params.toString()}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
|
||||||
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
||||||
<p className="text-balance text-sm text-muted-foreground">
|
|
||||||
{t("description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<CredentialsSignUpForm />
|
|
||||||
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
|
||||||
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
|
||||||
{t("or")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<GithubSignInForm />
|
|
||||||
<div className="text-center text-sm">
|
|
||||||
{t("haveAccount")}{" "}
|
|
||||||
<button
|
|
||||||
onClick={handleSignIn}
|
|
||||||
className="underline underline-offset-4"
|
|
||||||
>
|
|
||||||
{t("signIn")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const authSchema = z.object({
|
|
||||||
email: z
|
|
||||||
.string()
|
|
||||||
.nonempty("Email is required")
|
|
||||||
.email("Invalid email"),
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.nonempty("Password is required")
|
|
||||||
.min(8, "Password must be at least 8 characters")
|
|
||||||
.max(32, "Password must be less than 32 characters"),
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user