refactor: split login screen and forms

This commit is contained in:
Stavros
2025-03-06 17:30:35 +02:00
parent 9f5f4adddb
commit 61f4848f20
4 changed files with 144 additions and 124 deletions

View File

@@ -0,0 +1,46 @@
import { TextInput, PasswordInput, Button } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { LoginFormValues, loginSchema } from "../../schemas/login-schema";
interface LoginFormProps {
isLoading: boolean;
onSubmit: (values: LoginFormValues) => void;
}
export const LoginForm = (props: LoginFormProps) => {
const { isLoading, onSubmit } = props;
const form = useForm({
mode: "uncontrolled",
initialValues: {
username: "",
password: "",
},
validate: zodResolver(loginSchema),
});
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
label="Username"
placeholder="user@example.com"
required
disabled={isLoading}
key={form.key("username")}
{...form.getInputProps("username")}
/>
<PasswordInput
label="Password"
placeholder="password"
required
mt="md"
disabled={isLoading}
key={form.key("password")}
{...form.getInputProps("password")}
/>
<Button fullWidth mt="xl" type="submit" loading={isLoading}>
Login
</Button>
</form>
);
};

View File

@@ -0,0 +1,72 @@
import { Grid, Button } from "@mantine/core";
import { GithubIcon } from "../../icons/github";
import { GoogleIcon } from "../../icons/google";
import { OAuthIcon } from "../../icons/oauth";
import { TailscaleIcon } from "../../icons/tailscale";
interface OAuthButtonsProps {
oauthProviders: string[];
isLoading: boolean;
mutate: (provider: string) => void;
genericName: string;
}
export const OAuthButtons = (props: OAuthButtonsProps) => {
const { oauthProviders, isLoading, genericName, mutate } = props;
return (
<Grid mb="md" mt="md" align="center" justify="center">
{oauthProviders.includes("google") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => mutate("google")}
loading={isLoading}
>
Google
</Button>
</Grid.Col>
)}
{oauthProviders.includes("github") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<GithubIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => mutate("github")}
loading={isLoading}
>
Github
</Button>
</Grid.Col>
)}
{oauthProviders.includes("tailscale") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => mutate("tailscale")}
loading={isLoading}
>
Tailscale
</Button>
</Grid.Col>
)}
{oauthProviders.includes("generic") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => mutate("generic")}
loading={isLoading}
>
{genericName}
</Button>
</Grid.Col>
)}
</Grid>
);
};

View File

@@ -1,33 +1,22 @@
import { import { Paper, Title, Text, Divider } from "@mantine/core";
Button,
Paper,
PasswordInput,
TextInput,
Title,
Text,
Divider,
Grid,
} from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { z } from "zod";
import { useUserContext } from "../context/user-context"; import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
import { GoogleIcon } from "../icons/google";
import { GithubIcon } from "../icons/github";
import { OAuthIcon } from "../icons/oauth";
import { TailscaleIcon } from "../icons/tailscale";
import { isQueryValid } from "../utils/utils"; import { isQueryValid } from "../utils/utils";
import { OAuthButtons } from "../components/auth/oauth-buttons";
import { LoginFormValues } from "../schemas/login-schema";
import { LoginForm } from "../components/auth/login-forn";
export const LoginPage = () => { export const LoginPage = () => {
const queryString = window.location.search; const queryString = window.location.search;
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? ""; const redirectUri = params.get("redirect_uri") ?? "";
const { isLoggedIn, configuredProviders, title, genericName } = useUserContext(); const { isLoggedIn, configuredProviders, title, genericName } =
useUserContext();
const oauthProviders = configuredProviders.filter( const oauthProviders = configuredProviders.filter(
(value) => value !== "username", (value) => value !== "username",
@@ -37,24 +26,8 @@ export const LoginPage = () => {
return <Navigate to="/logout" />; return <Navigate to="/logout" />;
} }
const schema = z.object({
username: z.string(),
password: z.string(),
});
type FormValues = z.infer<typeof schema>;
const form = useForm({
mode: "uncontrolled",
initialValues: {
username: "",
password: "",
},
validate: zodResolver(schema),
});
const loginMutation = useMutation({ const loginMutation = useMutation({
mutationFn: (login: FormValues) => { mutationFn: (login: LoginFormValues) => {
return axios.post("/api/login", login); return axios.post("/api/login", login);
}, },
onError: () => { onError: () => {
@@ -105,7 +78,7 @@ export const LoginPage = () => {
}, },
}); });
const handleSubmit = (values: FormValues) => { const handleSubmit = (values: LoginFormValues) => {
loginMutation.mutate(values); loginMutation.mutate(values);
}; };
@@ -118,68 +91,12 @@ export const LoginPage = () => {
<Text size="lg" fw={500} ta="center"> <Text size="lg" fw={500} ta="center">
Welcome back, login with Welcome back, login with
</Text> </Text>
<Grid mb="md" mt="md" align="center" justify="center"> <OAuthButtons
{oauthProviders.includes("google") && ( oauthProviders={oauthProviders}
<Grid.Col span="content"> isLoading={loginOAuthMutation.isLoading}
<Button mutate={loginOAuthMutation.mutate}
radius="xl" genericName={genericName}
leftSection={ />
<GoogleIcon style={{ width: 14, height: 14 }} />
}
variant="default"
onClick={() => loginOAuthMutation.mutate("google")}
loading={loginOAuthMutation.isLoading}
>
Google
</Button>
</Grid.Col>
)}
{oauthProviders.includes("github") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={
<GithubIcon style={{ width: 14, height: 14 }} />
}
variant="default"
onClick={() => loginOAuthMutation.mutate("github")}
loading={loginOAuthMutation.isLoading}
>
Github
</Button>
</Grid.Col>
)}
{oauthProviders.includes("tailscale") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={
<TailscaleIcon style={{ width: 14, height: 14 }} />
}
variant="default"
onClick={() => loginOAuthMutation.mutate("tailscale")}
loading={loginOAuthMutation.isLoading}
>
Tailscale
</Button>
</Grid.Col>
)}
{oauthProviders.includes("generic") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={
<OAuthIcon style={{ width: 14, height: 14 }} />
}
variant="default"
onClick={() => loginOAuthMutation.mutate("generic")}
loading={loginOAuthMutation.isLoading}
>
{genericName}
</Button>
</Grid.Col>
)}
</Grid>
{configuredProviders.includes("username") && ( {configuredProviders.includes("username") && (
<Divider <Divider
label="Or continue with password" label="Or continue with password"
@@ -190,33 +107,10 @@ export const LoginPage = () => {
</> </>
)} )}
{configuredProviders.includes("username") && ( {configuredProviders.includes("username") && (
<form onSubmit={form.onSubmit(handleSubmit)}> <LoginForm
<TextInput isLoading={loginMutation.isLoading}
label="Username" onSubmit={handleSubmit}
placeholder="user@example.com" />
required
disabled={loginMutation.isLoading}
key={form.key("username")}
{...form.getInputProps("username")}
/>
<PasswordInput
label="Password"
placeholder="password"
required
mt="md"
disabled={loginMutation.isLoading}
key={form.key("password")}
{...form.getInputProps("password")}
/>
<Button
fullWidth
mt="xl"
type="submit"
loading={loginMutation.isLoading}
>
Login
</Button>
</form>
)} )}
</Paper> </Paper>
</Layout> </Layout>

View File

@@ -0,0 +1,8 @@
import { z } from "zod";
export const loginSchema = z.object({
username: z.string(),
password: z.string(),
});
export type LoginFormValues = z.infer<typeof loginSchema>;