feat(front): login form styled
This commit is contained in:
@@ -1 +1 @@
|
||||
export { ApplicationLayout as default } from "@/modules/common/layouts/application.layout";
|
||||
export { ApplicationLayout as default } from "@/modules/common/layouts/application/application.layout";
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { RootLayout as default } from "@/modules/common/layouts/root.layout";
|
||||
export { RootLayout as default } from "@/modules/common/layouts/root/root.layout";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
123
front/src/modules/auth/components/sign-in/sign-in.module.scss
Normal file
123
front/src/modules/auth/components/sign-in/sign-in.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
front/src/modules/auth/components/sign-in/sign-in.widget.tsx
Normal file
82
front/src/modules/auth/components/sign-in/sign-in.widget.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
45
front/src/modules/common/hooks/api/useQuery.ts
Normal file
45
front/src/modules/common/hooks/api/useQuery.ts
Normal 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 };
|
||||
}
|
||||
@@ -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,
|
||||
31
front/src/modules/common/utils/timedFetch.ts
Normal file
31
front/src/modules/common/utils/timedFetch.ts
Normal 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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user