272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
// src/components/auth/LoginForm.tsx
|
|
'use client';
|
|
|
|
import * as React from 'react';
|
|
import Button from '@/components/ui/Button';
|
|
|
|
type LoginValues = {
|
|
email: string;
|
|
password: string;
|
|
rememberMe: boolean;
|
|
};
|
|
|
|
type SocialProvider = 'google' | 'github';
|
|
|
|
export interface LoginFormProps {
|
|
title?: string;
|
|
subtitle?: string;
|
|
onSubmit?: (values: LoginValues) => void | Promise<void>;
|
|
isSubmitting?: boolean;
|
|
errorMessage?: string | null;
|
|
showRememberMe?: boolean;
|
|
showSocialLogin?: boolean;
|
|
onSocialClick?: (provider: SocialProvider) => void;
|
|
}
|
|
|
|
const LoginForm: React.FC<LoginFormProps> = ({
|
|
title = 'Bitte melde dich an',
|
|
subtitle,
|
|
onSubmit,
|
|
isSubmitting,
|
|
errorMessage,
|
|
showRememberMe = true,
|
|
showSocialLogin = true,
|
|
onSocialClick,
|
|
}) => {
|
|
const [form, setForm] = React.useState<LoginValues>({
|
|
email: '',
|
|
password: '',
|
|
rememberMe: false,
|
|
});
|
|
|
|
const handleChange =
|
|
(field: keyof LoginValues) =>
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = field === 'rememberMe' ? e.target.checked : e.target.value;
|
|
setForm((prev) => ({ ...prev, [field]: value as never }));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!onSubmit) return;
|
|
await onSubmit(form);
|
|
};
|
|
|
|
return (
|
|
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
|
{/* Header */}
|
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
|
<img
|
|
alt="Your Company"
|
|
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
|
|
className="mx-auto h-10 w-auto dark:hidden"
|
|
/>
|
|
<img
|
|
alt="Your Company"
|
|
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
|
|
className="mx-auto h-10 w-auto not-dark:hidden"
|
|
/>
|
|
<h2 className="mt-6 text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
|
{title}
|
|
</h2>
|
|
{subtitle && (
|
|
<p className="mt-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
{subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Card */}
|
|
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
|
<div className="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
|
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
{/* Benutzername / E-Mail */}
|
|
<div>
|
|
<label
|
|
htmlFor="email"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Benutzername oder E-Mail
|
|
</label>
|
|
<div className="mt-2">
|
|
<input
|
|
id="email"
|
|
name="email"
|
|
type="text"
|
|
required
|
|
autoComplete="username"
|
|
value={form.email}
|
|
onChange={handleChange('email')}
|
|
className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div>
|
|
<label
|
|
htmlFor="password"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Passwort
|
|
</label>
|
|
<div className="mt-2">
|
|
<input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
required
|
|
autoComplete="current-password"
|
|
value={form.password}
|
|
onChange={handleChange('password')}
|
|
className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Remember + Forgot */}
|
|
<div className="flex items-center justify-between">
|
|
{showRememberMe && (
|
|
<div className="flex gap-3">
|
|
<div className="flex h-6 shrink-0 items-center">
|
|
<div className="group grid size-4 grid-cols-1">
|
|
<input
|
|
id="remember-me"
|
|
name="remember-me"
|
|
type="checkbox"
|
|
checked={form.rememberMe}
|
|
onChange={handleChange('rememberMe')}
|
|
className="col-start-1 row-start-1 appearance-none rounded-sm border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 indeterminate:border-indigo-600 indeterminate:bg-indigo-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:indeterminate:border-indigo-500 dark:indeterminate:bg-indigo-500 dark:focus-visible:outline-indigo-500 forced-colors:appearance-auto"
|
|
/>
|
|
<svg
|
|
fill="none"
|
|
viewBox="0 0 14 14"
|
|
className="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-disabled:stroke-gray-950/25 dark:group-has-disabled:stroke-white/25"
|
|
>
|
|
<path
|
|
d="M3 8L6 11L11 3.5"
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="opacity-0 group-has-checked:opacity-100"
|
|
/>
|
|
<path
|
|
d="M3 7H11"
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="opacity-0 group-has-indeterminate:opacity-100"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<label
|
|
htmlFor="remember-me"
|
|
className="block text-sm/6 text-gray-900 dark:text-white"
|
|
>
|
|
Angemeldet bleiben
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
<div className="text-sm/6">
|
|
<a
|
|
href="#"
|
|
className="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
>
|
|
Passwort vergessen?
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{errorMessage && (
|
|
<p className="text-sm text-red-600 dark:text-red-400">
|
|
{errorMessage}
|
|
</p>
|
|
)}
|
|
|
|
{/* Submit */}
|
|
<div>
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
size="lg"
|
|
className="w-full justify-center"
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting ? 'Wird angemeldet…' : 'Anmelden'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Social login */}
|
|
{showSocialLogin && (
|
|
<div>
|
|
<div className="mt-10 flex items-center gap-x-6">
|
|
<div className="w-full flex-1 border-t border-gray-200 dark:border-white/10" />
|
|
<p className="text-sm/6 font-medium text-nowrap text-gray-900 dark:text-white">
|
|
Oder anmelden mit
|
|
</p>
|
|
<div className="w-full flex-1 border-t border-gray-200 dark:border-white/10" />
|
|
</div>
|
|
|
|
<div className="mt-6 grid grid-cols-2 gap-4">
|
|
{/* Google */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onSocialClick?.('google')}
|
|
className="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 focus-visible:inset-ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
|
|
>
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-5 w-5">
|
|
<path
|
|
d="M12.0003 4.75C13.7703 4.75 15.3553 5.36002 16.6053 6.54998L20.0303 3.125C17.9502 1.19 15.2353 0 12.0003 0C7.31028 0 3.25527 2.69 1.28027 6.60998L5.27028 9.70498C6.21525 6.86002 8.87028 4.75 12.0003 4.75Z"
|
|
fill="#EA4335"
|
|
/>
|
|
<path
|
|
d="M23.49 12.275C23.49 11.49 23.415 10.73 23.3 10H12V14.51H18.47C18.18 15.99 17.34 17.25 16.08 18.1L19.945 21.1C22.2 19.01 23.49 15.92 23.49 12.275Z"
|
|
fill="#4285F4"
|
|
/>
|
|
<path
|
|
d="M5.26498 14.2949C5.02498 13.5699 4.88501 12.7999 4.88501 11.9999C4.88501 11.1999 5.01998 10.4299 5.26498 9.7049L1.275 6.60986C0.46 8.22986 0 10.0599 0 11.9999C0 13.9399 0.46 15.7699 1.28 17.3899L5.26498 14.2949Z"
|
|
fill="#FBBC05"
|
|
/>
|
|
<path
|
|
d="M12.0004 24.0001C15.2404 24.0001 17.9654 22.935 19.9454 21.095L16.0804 18.095C15.0054 18.82 13.6204 19.245 12.0004 19.245C8.8704 19.245 6.21537 17.135 5.2654 14.29L1.27539 17.385C3.25539 21.31 7.3104 24.0001 12.0004 24.0001Z"
|
|
fill="#34A853"
|
|
/>
|
|
</svg>
|
|
<span className="text-sm/6 font-semibold">Google</span>
|
|
</button>
|
|
|
|
{/* GitHub */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onSocialClick?.('github')}
|
|
className="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 focus-visible:inset-ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
|
|
>
|
|
<svg
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
aria-hidden="true"
|
|
className="size-5 fill-[#24292F] dark:fill-white"
|
|
>
|
|
<path
|
|
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
|
|
clipRule="evenodd"
|
|
fillRule="evenodd"
|
|
/>
|
|
</svg>
|
|
<span className="text-sm/6 font-semibold">GitHub</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LoginForm;
|