first commit

This commit is contained in:
2026-04-01 15:17:46 +02:00
commit d50ab5f7bf
19 changed files with 2554 additions and 0 deletions

163
react/LoginForm.tsx Normal file
View File

@@ -0,0 +1,163 @@
import type { FormEvent, ReactNode } from "react";
import { Alert, AlertDescription, AlertIcon, Button, Center, FormControl, FormLabel, HStack, Icon, Input, Stack } from "@chakra-ui/react";
import { FcGoogle } from "react-icons/fc";
import type { AuthProviderAvailability, AuthProviderKey, AuthSubmitValues, LoginMode } from "./types";
type LoginFormTexts = {
nameLabel: string;
emailLabel: string;
passwordLabel: string;
passwordConfirmLabel: string;
submitRegisterLabel: string;
submitSignInLabel: string;
toggleToRegisterLabel: string;
toggleToSignInLabel: string;
forgotPasswordLabel: string;
googleLabel: string;
slackLabel: string;
};
type LoginFormProps = {
mode: LoginMode;
texts: LoginFormTexts;
onSubmit: (values: AuthSubmitValues) => void | Promise<void>;
onModeToggle: () => void;
loading?: boolean;
oauthLoadingProvider?: AuthProviderKey | null;
providers?: AuthProviderAvailability;
onOAuthSignIn?: (provider: AuthProviderKey) => void | Promise<void>;
errorMessage?: string | null;
successMessage?: string | null;
footer?: ReactNode;
forgotPasswordLink?: ReactNode;
emailPlaceholder?: string;
namePlaceholder?: string;
};
export function LoginForm({
mode,
texts,
onSubmit,
onModeToggle,
loading = false,
oauthLoadingProvider = null,
providers,
onOAuthSignIn,
errorMessage,
successMessage,
footer,
forgotPasswordLink,
emailPlaceholder = "you@example.com",
namePlaceholder = "Jane Doe"
}: LoginFormProps) {
const registerMode = mode === "register";
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = new FormData(event.currentTarget);
void onSubmit({
name: String(form.get("name") ?? ""),
email: String(form.get("email") ?? ""),
password: String(form.get("password") ?? ""),
passwordConfirm: String(form.get("passwordConfirm") ?? "")
});
}
return (
<Stack spacing={5}>
{errorMessage ? (
<Alert status="error" borderRadius="md">
<AlertIcon />
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
) : null}
{successMessage ? (
<Alert status="success" borderRadius="md">
<AlertIcon />
<AlertDescription>{successMessage}</AlertDescription>
</Alert>
) : null}
<form onSubmit={handleSubmit}>
<Stack spacing={4}>
{registerMode ? (
<FormControl isRequired>
<FormLabel>{texts.nameLabel}</FormLabel>
<Input name="name" placeholder={namePlaceholder} />
</FormControl>
) : null}
<FormControl isRequired>
<FormLabel>{texts.emailLabel}</FormLabel>
<Input name="email" type="email" placeholder={emailPlaceholder} />
</FormControl>
<FormControl isRequired>
<FormLabel>{texts.passwordLabel}</FormLabel>
<Input name="password" type="password" minLength={8} />
</FormControl>
{!registerMode && forgotPasswordLink ? <Stack align="flex-end">{forgotPasswordLink}</Stack> : null}
{registerMode ? (
<FormControl isRequired>
<FormLabel>{texts.passwordConfirmLabel}</FormLabel>
<Input name="passwordConfirm" type="password" minLength={8} />
</FormControl>
) : null}
<Button type="submit" isLoading={loading}>
{registerMode ? texts.submitRegisterLabel : texts.submitSignInLabel}
</Button>
</Stack>
</form>
{registerMode || !onOAuthSignIn || (!providers?.google && !providers?.slack) ? null : (
<HStack>
{providers.google ? (
<Button
flex={1}
bg="gray.200"
color="gray.900"
borderRadius="full"
justifyContent="flex-start"
h="56px"
px={3}
fontSize={{ base: "md", md: "lg" }}
fontWeight="semibold"
iconSpacing={4}
leftIcon={
<Center boxSize="40px" bg="white" borderRadius="full" boxShadow="sm">
<Icon as={FcGoogle} boxSize={6} />
</Center>
}
_hover={{ bg: "gray.300" }}
_active={{ bg: "gray.300" }}
isLoading={oauthLoadingProvider === "google"}
onClick={() => void onOAuthSignIn("google")}
>
{texts.googleLabel}
</Button>
) : null}
{providers.slack ? (
<Button
flex={1}
variant="outline"
isLoading={oauthLoadingProvider === "slack"}
onClick={() => void onOAuthSignIn("slack")}
>
{texts.slackLabel}
</Button>
) : null}
</HStack>
)}
<Button variant="ghost" onClick={onModeToggle}>
{registerMode ? texts.toggleToSignInLabel : texts.toggleToRegisterLabel}
</Button>
{footer ?? null}
</Stack>
);
}