feat(front): login form styled

This commit is contained in:
2024-07-22 22:51:14 +02:00
parent f61bdac457
commit 231db52718
16 changed files with 401 additions and 58 deletions

View File

@@ -1 +1 @@
export { ApplicationLayout as default } from "@/modules/common/layouts/application.layout";
export { ApplicationLayout as default } from "@/modules/common/layouts/application/application.layout";

View File

@@ -1,9 +1,17 @@
import { SignInWidget } from "@/modules/auth/components/sign-in.widget";
import { SignInWidget } from "@/modules/auth/components/sign-in/sign-in.widget";
import ThemeSwitcher from "@/modules/common/theme-switcher";
export default function Home() {
return (
<main>
<main
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
}}
>
Main page
<ThemeSwitcher />
<SignInWidget />

View File

@@ -1 +1 @@
export { RootLayout as default } from "@/modules/common/layouts/root.layout";
export { RootLayout as default } from "@/modules/common/layouts/root/root.layout";

View File

@@ -1,30 +0,0 @@
"use client";
import { signIn, useSession } from "next-auth/react";
export const SignInWidget = () => {
const { data } = useSession();
return (
<div>
<button
onClick={async () => {
const result = await signIn("credentials", {
redirect: false,
username: "dqnid",
password: "1234",
callbackUrl: "/",
});
console.debug("Login result:", result);
if (result?.error === "CredentialsSignin") {
return "Invalid credentials";
}
}}
>
LOGIN
</button>
{JSON.stringify(data)}
</div>
);
};

View File

@@ -0,0 +1,123 @@
.container {
max-width: 20em;
display: flex;
flex-direction: column;
gap: 1em;
font-size: 1.6rem;
overflow: hidden;
border-radius: 0.4rem;
background-image: linear-gradient(
16deg,
rgba($color-primary-light-rgb, 0.3),
rgba($color-primary-light-rgb, 0.2)
);
.heading {
font-size: 3.4rem;
text-align: center;
background-color: #e5e5f7;
opacity: 0.8;
text-shadow: rgba($color-grey-dark, 0.3);
padding: 2rem 4rem 2rem;
--s: 80px;
background:
repeating-conic-gradient(
from 30deg,
#0000 0 120deg,
$color-white 0 180deg
)
calc(0.5 * var(--s)) calc(0.5 * var(--s) * 0.577),
repeating-conic-gradient(
from 30deg,
$color-grey-medium 0 60deg,
$color-grey-light 0 120deg,
$color-white 0 180deg
);
background-size: var(--s) calc(var(--s) * 0.577);
}
.form {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5em;
padding: 1.5em 4rem 2.5em;
&__group {
display: flex;
flex-direction: column;
font-size: 1.6rem;
gap: 0.2em;
}
&__label {
font-size: inherit;
margin-left: 0.8em;
color: $color-grey-dark;
transition: all 0.3s;
}
&__input {
font-size: inherit;
padding: 0.8em;
color: $color-foreground;
background: rgba($color-primary-light-rgb, 0.5);
border: none;
border-bottom: 2px solid transparent;
border-radius: 2px;
transition: all 0.2s;
&::placeholder {
color: $color-grey-dark;
}
&:focus {
outline: none;
border-bottom: 2px solid $color-primary;
box-shadow: 0 1px 4px rgba($color-primary-dark-rgb, 0.3);
}
}
&__group:has(.form__input:placeholder-shown) .form__label {
opacity: 0;
transform: translateY(1rem);
}
&__actions {
width: fit-content;
margin-top: 1em;
& .submit__button {
font-size: 2rem;
padding: 0.5em 1em;
border-radius: 2px;
border: none;
background-color: $color-primary;
color: $color-foreground;
transition: all 0.2s;
&:hover {
cursor: pointer;
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba($color-primary-dark-rgb, 0.4);
}
&:active {
transform: translateY(0);
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
"use client";
import { FormEvent, useState } from "react";
import styles from "./sign-in.module.scss";
import { signIn, useSession } from "next-auth/react";
type SingInWidgetsProps = {
afterSuccess?: Function;
};
export const SignInWidget: React.FC<SingInWidgetsProps> = ({
afterSuccess,
}) => {
const { data } = useSession();
const [loginStatus, setLoginStatus] = useState<
"idle" | "check" | "confirm" | "error"
>("idle");
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoginStatus("check");
const username = (
event.currentTarget.elements.namedItem("username") as HTMLInputElement
).value;
const password = (
event.currentTarget.elements.namedItem("password") as HTMLInputElement
).value;
const result = await signIn("credentials", {
redirect: false,
username: username,
password: password,
});
if (!result?.ok) {
setLoginStatus("error");
} else {
setLoginStatus("confirm");
}
};
return (
<div className={styles.container}>
<h1 className={styles["heading"]}>Sign in</h1>
<form className={styles.form} onSubmit={submit}>
<div className={`${styles["form__group"]}`}>
<label htmlFor="username" className={styles["form__label"]}>
username
</label>
<input
type="text"
id="username"
name="username"
placeholder="username"
className={styles["form__input"]}
required
/>
</div>
<div className={styles["form__group"]}>
<label htmlFor="password" className={styles["form__label"]}>
password
</label>
<input
type="password"
id="password"
name="password"
placeholder="password"
className={styles["form__input"]}
required
/>
</div>
<div className={styles["form__actions"]}>
<button type="submit" className={styles["submit__button"]}>
Submit
</button>
</div>
</form>
</div>
);
};

View File

@@ -16,7 +16,6 @@ export const authOptions: AuthOptions = {
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
console.log("__credentials", credentials);
const user: User = {
id: credentials?.password ?? "asdf",
role: "admin",
@@ -26,7 +25,7 @@ export const authOptions: AuthOptions = {
accessToken: credentials?.password ?? "asdf",
},
};
return user;
return credentials?.password === "secure-password" ? user : null;
},
}),
],

View File

@@ -0,0 +1,45 @@
import { useEffect, useState } from "react";
import { timedFetch } from "../../utils/timedFetch";
type QueryReturn<T> = {
data?: T;
status?: number;
isLoading: boolean;
isError: boolean;
};
type QueryInput = {
url: string;
options: RequestInit;
timeout: number;
};
export function useQuery<DataType>({
url,
options,
timeout,
}: QueryInput): QueryReturn<DataType> {
const [response, setResponse] = useState<{
data: DataType;
status: number;
}>();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
useEffect(() => {
setIsLoading(true);
setIsError(false);
async () => {
const _response = await timedFetch<DataType>(url, options, timeout);
if (_response) {
setResponse(_response);
setIsLoading(false);
} else {
setIsLoading(false);
setIsError(true);
}
};
}, [url, options, timeout]);
return { ...response, isLoading, isError };
}

View File

@@ -1,5 +1,5 @@
import { PropsWithChildren } from "react";
import { ApplicationProvider } from "../providers/application.provider";
import { ApplicationProvider } from "../../providers/application.provider";
export const ApplicationLayout: React.FC<PropsWithChildren> = ({
children,

View File

View File

@@ -0,0 +1,31 @@
class HttpError extends Error {
constructor(public response: Response) {
super(`HTTP error ${response.status}`);
}
}
async function timedFetch<ResponseType = any>(
url: string,
options: RequestInit = {},
timeout: number = 5000,
) {
try {
const result = await fetch(url, {
signal: AbortSignal.timeout(timeout),
...options,
});
if (!result.ok) {
throw new HttpError(result);
}
return {
data: (await result.json()) as ResponseType,
status: result.status,
};
} catch (error) {
if ((error as Error).name === "AbortError") {
console.log(`Fetch aborted by timeout (${timeout}ms)`);
}
}
}
export { timedFetch };

View File

@@ -1,8 +1,18 @@
$default-font-size: 1.6rem;
$color-primary: #ff7730;
$color-primary: var(--color-primary);
$color-primary-light: var(--color-primary-light);
$color-primary-dark: var(--color-primary-dark);
$color-primary-rgb: var(--color-primary-rgb);
$color-primary-light-rgb: var(--color-primary-light-rgb);
$color-primary-dark-rgb: var(--color-primary-dark-rgb);
$color-secondary: #ff7730;
$color-background: var(--color-white);
$color-foreground: var(--color-black);
$color-white: var(--color-white);
$color-grey-light: var(--color-grey-light);
$color-grey-medium: var(--color-grey-medium);
$color-grey-dark: var(--color-grey-dark);

View File

@@ -1,12 +1,36 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap");
// For variable definition
:root {
--color-white: #fafafa;
--color-black: #101010;
--color-grey-dark: #495057;
--color-grey-medium: #adb5bd;
--color-grey-light: #dee2e6;
--color-primary: #9c89b8;
--color-primary-light: #b8bedd;
--color-primary-dark: #56577d;
--color-primary-rgb: 156, 137, 184;
--color-primary-light-rgb: 184, 190, 221;
--color-primary-dark-rgb: 86, 87, 125;
}
[data-theme="dark"] {
--color-white: #101010;
--color-black: #fafafa;
--color-grey-dark: #dee2e6;
--color-grey-medium: #adb5bd;
--color-grey-light: #495057;
--color-primary: #56577d;
--color-primary-light: #b8bedd;
--color-primary-dark: #9c89b8;
--color-primary-rgb: 86, 87, 125;
--color-primary-light-rgb: 184, 190, 221;
--color-primary-dark-rgb: 156, 137, 184;
}
*,
@@ -18,6 +42,7 @@
}
html {
font-family: "Roboto", sans-serif;
max-width: 100vw;
overflow-x: hidden;