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

@@ -10,9 +10,9 @@
"dependencies": {
"next": "14.2.3",
"next-auth": "^4.24.7",
"postcss": "^8.4.39",
"react": "^18",
"react-dom": "^18",
"sass": "^1.77.4"
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
@@ -20,6 +20,7 @@
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"sass": "^1.77.8",
"typescript": "^5"
}
},
@@ -628,6 +629,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"devOptional": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -868,6 +870,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"devOptional": true,
"engines": {
"node": ">=8"
},
@@ -889,6 +892,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true,
"dependencies": {
"fill-range": "^7.1.1"
},
@@ -936,9 +940,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001625",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz",
"integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==",
"version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"funding": [
{
"type": "opencollective",
@@ -952,7 +956,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "4.1.2",
@@ -974,6 +979,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"devOptional": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -997,6 +1003,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"devOptional": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -1887,6 +1894,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -1965,6 +1973,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -2283,7 +2292,8 @@
"node_modules/immutable": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz",
"integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ=="
"integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==",
"devOptional": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
@@ -2388,6 +2398,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"devOptional": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -2469,6 +2480,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2513,6 +2525,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"devOptional": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -2548,6 +2561,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"devOptional": true,
"engines": {
"node": ">=0.12.0"
}
@@ -3057,10 +3071,39 @@
}
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3376,6 +3419,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"devOptional": true,
"engines": {
"node": ">=8.6"
},
@@ -3393,9 +3437,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"funding": [
{
"type": "opencollective",
@@ -3410,10 +3454,11 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -3529,6 +3574,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"devOptional": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -3721,9 +3767,11 @@
}
},
"node_modules/sass": {
"version": "1.77.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.4.tgz",
"integrity": "sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==",
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -4115,6 +4163,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"devOptional": true,
"dependencies": {
"is-number": "^7.0.0"
},

View File

@@ -3,17 +3,17 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3016",
"build": "next build",
"start": "next start",
"start": "next start -p 3016",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.3",
"next-auth": "^4.24.7",
"postcss": "^8.4.39",
"react": "^18",
"react-dom": "^18",
"sass": "^1.77.4"
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
@@ -21,6 +21,7 @@
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"sass": "^1.77.8",
"typescript": "^5"
}
}

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;