init: Initial commit

This commit is contained in:
Björn Benouarets
2026-01-19 08:42:07 +01:00
parent 1a47930d75
commit 74232ad2d2
74 changed files with 9822 additions and 98 deletions

View File

@@ -0,0 +1,288 @@
"use client";
import * as React from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import UseAnimations from "react-useanimations";
import checkAnimation from "react-useanimations/lib/checkmark";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { SocialLoginButton } from "@/components/core/social-login-button";
import { login } from "@/components/server/login";
import { IconBrandGithub, IconBrandGoogle, IconHomeFilled, IconUserCircle, IconLogout, IconChevronRight } from "@tabler/icons-react";
import { Spinner } from "@/components/ui/spinner";
const loginSchema = z.object({
username: z.string().min(3, "Username must be at least 3 characters long"),
password: z.string().min(8, "Password must be at least 8 characters long"),
});
export interface LoginFormProps {
applicationName: string;
applicationLogo?: string;
}
export const LoginForm = () => {
const form = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
defaultValues: {
username: "",
password: "",
},
});
const onSubmit = async (data: z.infer<typeof loginSchema>) => {
const response = await login(data.username, data.password);
if (response.success) {
toast.success(response.message);
} else {
toast.error(response.message);
}
};
return (
<form id="form-login" onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6 px-6">
<div className="flex flex-col">
<FieldGroup>
<Controller
name="username"
control={form.control}
render={({ field }) => (
<Field className="flex flex-col gap-2">
<FieldLabel>Username</FieldLabel>
<Input
{...field}
type="text"
placeholder="Enter your username"
className="text-sm"
id="form-login-username"
autoComplete="username"
/>
<FieldError className="text-violet-900 text-xs" errors={form.formState.errors.username ? [form.formState.errors.username] : undefined} />
</Field>
)}
/>
<Controller
name="password"
control={form.control}
render={({ field }) => (
<Field className="flex flex-col gap-2">
<FieldLabel>Password</FieldLabel>
<Input
{...field}
type="password"
placeholder="Enter your password"
className="text-sm"
id="form-login-password"
autoComplete="current-password"
/>
<FieldError className="text-violet-900 text-xs" errors={form.formState.errors.password ? [form.formState.errors.password] : undefined} />
</Field>
)}
/>
</FieldGroup>
</div>
<Field>
<Button type="submit" className="w-full bg-violet-900 text-primary-foreground hover:bg-violet-800">Login</Button>
</Field>
</form>
);
};
export const LoginContainer = (props: LoginFormProps) => {
const { applicationName, applicationLogo } = props;
return (
<div className="flex flex-col gap-6 w-full max-w-sm">
<Card className="flex flex-col p-0">
<CardHeader className="flex flex-col gap-4 justify-center items-center pt-6">
<CardTitle className="flex flex-row items-center justify-center gap-1 text-2xl font-bold text-center">
{applicationLogo && <Image src={applicationLogo} alt={applicationName} width={100} height={100} />}
{!applicationLogo && (
<IconHomeFilled className="w-6 h-6" />
)}
<Label className="text-2xl font-bold text-center">{applicationName}</Label>
</CardTitle>
<CardDescription className="text-center">
<div className="flex flex-col gap-2">
<p className="text-zinc-700 dark:text-zinc-300 font-bold text-md">Login to your account</p>
<p className="text-zinc-500 text-sm">Welcome back! Please sign in to continue.</p>
</div>
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6 px-0">
<div className="flex flex-col gap-6 pb-4">
<div className="flex flex-row gap-2 px-6 justify-between items-center w-full">
<SocialLoginButton icon={<IconBrandGithub className="w-4 h-4" />} label="GitHub" onClick={() => {}} />
<SocialLoginButton icon={<IconBrandGoogle className="w-4 h-4" />} label="Google" onClick={() => {}} />
</div>
<Separator className="w-full" />
<div className="flex flex-col gap-6">
<LoginForm />
<div className="flex flex-col gap-4">
<Separator className="w-full" />
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 items-center justify-center">
<p className="text-zinc-500 text-sm">Don&apos;t have an account? <Link href="/register" className="text-violet-800 dark:text-violet-300 font-bold">Sign up</Link></p>
</div>
</div>
<Separator className="w-full" />
<div className="flex flex-col gap-4 items-center justify-center">
<Label className="text-zinc-700 dark:text-zinc-300 font-medium text-xs">Secured by SecNex</Label>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export const LoginLoading = (props: LoginFormProps) => {
const { applicationName, applicationLogo } = props;
return (
<div className="flex flex-col gap-6 w-full max-w-md">
<Card className="flex flex-col p-0">
<CardHeader className="flex flex-col gap-4 justify-center items-center pt-6">
<CardTitle className="flex flex-row items-center justify-center gap-1 text-2xl font-bold text-center">
{applicationLogo && <Image src={applicationLogo} alt={applicationName} width={100} height={100} />}
{!applicationLogo && (
<IconHomeFilled className="w-6 h-6" />
)}
<Label className="text-2xl font-bold text-center">{applicationName}</Label>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-6 px-0 pt-6">
<div className="flex flex-col gap-6 px-6">
<div className="flex justify-center items-center gap-6 pb-4">
<div className="flex flex-col gap-2 bg-violet-200 rounded-full p-4">
<div className="flex justify-center items-center gap-6 border-2 border-violet-900 rounded-full">
<Spinner className="size-8 animate-spin text-violet-900" />
</div>
</div>
</div>
</div>
<Separator className="w-full" />
<div className="flex flex-col gap-4 items-center justify-center w-full px-6 pb-6">
<Label className="text-zinc-700 dark:text-zinc-300 font-medium text-xs text-center">Secured by SecNex</Label>
</div>
</CardContent>
</Card>
</div>
);
};
export const LoginSuccessContainer = (props: LoginFormProps) => {
const [isLoading, setIsLoading] = React.useState(true);
const [sessionInfo, setSessionInfo] = React.useState<{ username: string, email: string, role: string } | null>(null);
const router = useRouter();
const { applicationName, applicationLogo } = props;
const handleLogout = async () => {
const response = await fetch("/api/logout");
const data = await response.json();
if (data.success) {
toast.success(data.message);
router.refresh();
} else {
toast.error(data.message);
}
setIsLoading(false);
};
React.useEffect(() => {
const checkSession = async () => {
const response = await fetch("/api/session");
const data = await response.json();
if (data.success) {
setSessionInfo(data.sessionInfo);
setIsLoading(false);
} else {
router.refresh();
}
};
checkSession();
}, [router]);
if (isLoading) {
return <LoginLoading applicationName={applicationName} applicationLogo={applicationLogo} />;
}
return (
<div className="flex flex-col gap-6 w-full max-w-md">
<Card className="flex flex-col p-0">
<CardHeader className="flex flex-col gap-4 justify-center items-center pt-6">
<CardTitle className="flex flex-row items-center justify-center gap-1 text-2xl font-bold text-center">
{applicationLogo && <Image src={applicationLogo} alt={applicationName} width={100} height={100} />}
{!applicationLogo && (
<IconHomeFilled className="w-6 h-6" />
)}
<Label className="text-2xl font-bold text-center">{applicationName}</Label>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-6 px-0 pt-6">
<div className="flex flex-col gap-6 px-6">
<div className="flex justify-center items-center gap-6 pb-4">
<div className="flex flex-col gap-2 bg-violet-200 rounded-full p-4">
<div className="flex justify-center items-center gap-6 border-2 border-violet-900 rounded-full">
<UseAnimations animation={checkAnimation} size={32} strokeColor="#44168f" fillColor="#44168f" loop={false} />
</div>
</div>
</div>
<div className="flex flex-col gap-2 items-center justify-center">
<Label className="text-zinc-700 dark:text-zinc-300 font-bold text-lg">Successfully signed in</Label>
<p className="text-zinc-700 dark:text-zinc-300 font-medium text-xs text-wrap break-all">Welcome back, <span className="font-bold">{sessionInfo?.username}</span>!</p>
</div>
<div className="flex flex-row gap-4 items-center bg-violet-50 border border-violet-200 rounded-lg p-4">
<div className="flex flex-row gap-2 items-center justify-center">
<div className="flex justify-center items-center gap-4 bg-violet-200 rounded-full p-3">
<IconUserCircle className="w-6 h-6 text-violet-900" />
</div>
</div>
<div className="flex flex-col gap-1">
<Label className="text-zinc-900 dark:text-zinc-300 text-md">{sessionInfo?.username}</Label>
<p className="text-zinc-700 dark:text-zinc-300 text-xs text-wrap break-all">{sessionInfo?.email}</p>
</div>
</div>
</div>
<div className="flex flex-col gap-4 items-center justify-center w-full px-6">
<Button variant="outline" className="border-violet-900 text-violet-900 hover:text-violet-800 hover:bg-violet-50 w-full">Go to dashboard <IconChevronRight className="w-4 h-4" /></Button>
<Button variant="outline" className="border-violet-900 bg-violet-900 text-violet-50 hover:bg-violet-800 hover:text-violet-100 w-full" onClick={handleLogout}><IconLogout className="w-4 h-4" /> Logout</Button>
</div>
<Separator className="w-full" />
<div className="flex flex-col gap-4 items-center justify-center w-full px-6 pb-6">
<Label className="text-zinc-700 dark:text-zinc-300 font-medium text-xs text-center">Secured by SecNex</Label>
</div>
</CardContent>
</Card>
</div>
);
};