Skip to content
Snippets Groups Projects
Commit 9d408eec authored by Matthias Feyll's avatar Matthias Feyll :cookie:
Browse files

(ui): improved login screen

parent fd6ef7ea
No related branches found
No related tags found
1 merge request!1203improve login screen
Pipeline #260234 passed
import { BasicProp } from '@shared/types/interfaces.type'
import React, { useRef } from 'react'
import { Alert, Button, Col, Container, Form, Image, Row, Spinner } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import useLoginViewModel from '../viewmodel/login.viewmodel'
import './login.scss'
import logo from '/public/logo.svg'
// login.view.tsx
import { BasicProp } from "@shared/types/interfaces.type";
import React from "react";
import {
Alert,
Button,
Col,
Container,
Form,
Image,
Row,
Spinner,
} from "react-bootstrap";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import useLoginViewModel from "../viewmodel/login.viewmodel";
import "./login.scss";
import { LoginFormInputs } from "./login.types";
import logo from "/public/logo.svg";
const LoginView: React.FC<BasicProp> = () => {
const { t } = useTranslation('common')
const { login, handleErrorMessageRendering, displayFormFieldChecks, loginLoading } = useLoginViewModel();
export const LoginView: React.FC<BasicProp> = () => {
const { t } = useTranslation("common");
const { handleLogin, getErrorMessage, loginLoading } = useLoginViewModel();
const usernameRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginFormInputs>({
mode: "onSubmit",
defaultValues: {
username: "",
password: "",
},
});
const triggerLogin = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const username = usernameRef.current!.value;
const password = passwordRef.current!.value;
const invalidForm = (
<Alert variant="warning">{t("login.form.invalid")}</Alert>
);
const invalidCredentials = (
<Alert variant="danger">{t("login.form.failed")}</Alert>
);
login(username, password);
}
return (
<Container className="vh-100 d-flex flex-column justify-content-center login-container">
<Row className="align-items-center">
<Image src={logo} alt="logo" height={150} />
</Row>
<Row className="mt-2 justify-content-center">
<Col md={6} sm={10} className="c-box p-4 bg-white">
<h1 className="text-center h2">goSDN - Web</h1>
const invalidForm = (<Alert variant="warning">{t('login.form.invalid')}</Alert>)
const invalidCredentials = (<Alert variant="danger">{t('login.form.failed')}</Alert>)
{getErrorMessage(invalidForm, invalidCredentials)}
<Form className="mt-4" onSubmit={handleSubmit(handleLogin)}>
<Form.Group className="mb-3" controlId="loginForm.username">
<Form.Label>{t("login.form.username.label")}</Form.Label>
<Form.Control
type="text"
isInvalid={!!errors.username}
{...register("username", {
required: t("global.form.empty_field"),
})}
autoComplete="on"
autoFocus={true}
/>
<Form.Control.Feedback type="invalid">
{errors.username?.message}
</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3" controlId="loginForm.password">
<Form.Label>{t("login.form.password.label")}</Form.Label>
<Form.Control
type="password"
isInvalid={!!errors.password}
{...register("password", {
required: t("global.form.empty_field"),
})}
/>
<Form.Control.Feedback type="invalid">
{errors.password?.message}
</Form.Control.Feedback>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100 mt-3"
disabled={loginLoading}
>
{t("global.form.submit")}
{loginLoading && (
<Spinner animation="border" size="sm" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
)}
</Button>
</Form>
</Col>
</Row>
</Container>
);
};
return (
<Container className="vh-100 d-flex flex-column justify-content-center login-container">
<Row className="align-items-center">
<Image src={logo} alt="logo" height={150} />
</Row>
<Row className="mt-2 justify-content-center">
<Col md={6} sm={10} className="c-box p-4 bg-white">
<h1 className="text-center h2">goSDN - Web</h1>
{handleErrorMessageRendering(invalidForm, invalidCredentials)}
<Form className="mt-4" noValidate validated={displayFormFieldChecks()} onSubmit={triggerLogin}>
<Form.Group
className="mb-3"
controlId="loginForm.username"
>
<Form.Label>{t('login.form.username.label')}</Form.Label>
<Form.Control
type="text"
ref={usernameRef}
required
autoComplete='on'
autoFocus={true}
/>
<Form.Control.Feedback type="invalid">
{t('global.form.empty_field')}
</Form.Control.Feedback>
</Form.Group>
<Form.Group
className="mb-3"
controlId="loginForm.pasword"
>
<Form.Label>{t('login.form.password.label')}</Form.Label>
<Form.Control type="password" ref={passwordRef} required />
<Form.Control.Feedback type="invalid">
{t('global.form.empty_field')}
</Form.Control.Feedback>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100 mt-3"
disabled={loginLoading}
>
{t('global.form.submit')}
{loginLoading &&
<Spinner animation="border" size="sm" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
}
</Button>
</Form>
</Col>
</Row>
</Container>
)
}
export default LoginView
export default LoginView;
import { useAuth } from "@provider/auth.provider";
import { useState } from "react";
export interface PageLoginState {
submitted: boolean,
valid: boolean,
export interface LoginFormInputs {
username: string;
password: string;
}
// login.viewmodel.ts
import { useAuth } from "@provider/auth.provider";
export default function useLoginViewModel() {
const { login, loginProperties } = useAuth();
const { isLoading: loginLoading, error: loginError, reset: resetLogin } = loginProperties;
const [localFormState, updateLocalFormState] = useState({
submitted: false,
valid: false,
});
const handleLogin = async (data: LoginFormInputs) => {
resetLogin();
await login(data.username, data.password);
};
const handleErrorMessageRendering = (formInvalidError: JSX.Element, backendResponseError: JSX.Element): JSX.Element | null => {
// backend response check
const getErrorMessage = (formInvalidError: JSX.Element, backendResponseError: JSX.Element): JSX.Element | null => {
if (loginError) {
return backendResponseError;
}
// form invalid check
if (localFormState.submitted && !localFormState.valid) {
return formInvalidError;
}
return null;
}
const isFormValid = (username: string | undefined, password: string | undefined): boolean => {
return !!username && !!password;
}
/**
* Tries to `/login` by using the input fields.
*
* @description The fields are getting validated against null values
* @param event Submit event
*/
const loginHandler = (username: string | undefined, password: string | undefined) => {
resetLogin();
const valid = isFormValid(username, password);
updateLocalFormState({ ...localFormState, valid, submitted: true })
if (valid) {
//executeLogin(username!, password!);
login(username!, password!);
}
}
const displayFormFieldChecks = (): boolean => {
return localFormState.submitted && !loginError;
}
};
return {
displayFormFieldChecks,
login: loginHandler,
handleErrorMessageRendering: handleErrorMessageRendering,
handleLogin,
getErrorMessage,
loginLoading,
}
}
loginError
};
}
\ No newline at end of file
import { faGripVertical, IconDefinition } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import UpdateIndicator from "@layout/grid.layout/update-inidicator.layout/update-indicator.layout"
import { Category, CategoryType } from "@shared/types/category.type"
import { Col, Container, Row } from "react-bootstrap"
import './gridBox.view.scss'
import {
faGripVertical,
IconDefinition,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import UpdateIndicator from "@layout/grid.layout/update-inidicator.layout/update-indicator.layout";
import { Category, CategoryType } from "@shared/types/category.type";
import { Col, Container, Row } from "react-bootstrap";
interface GridBoxProps {
title: string,
title_icon: IconDefinition,
children: React.ReactNode,
className?: string,
title: string;
title_icon: IconDefinition;
children: React.ReactNode;
className?: string;
}
export const GridBox: React.FC<GridBoxProps> = ({ children, title, title_icon, className = "" }) => {
return (
<div className="grid-box h-100">
<Container fluid className={`c-box d-flex flex-column h-100 ${className}`}>
<div>
<UpdateIndicator
category={Category.DEVICE as CategoryType}
updateInterval={15000}
/>
<FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
<Row className="mb-0">
<Col xs={12}>
<h4 className='c-box-title'><FontAwesomeIcon icon={title_icon} size="1x" className="me-2 text-primary" />{title}</h4>
</Col>
</Row>
</div>
<div className="flex-grow-1 content">
{children}
</div>
</Container>
export const GridBox: React.FC<GridBoxProps> = ({
children,
title,
title_icon,
className = "",
}) => {
return (
<div className="grid-box h-100">
<Container
fluid
className={`c-box d-flex flex-column h-100 ${className}`}
>
<div>
<UpdateIndicator
category={Category.DEVICE as CategoryType}
updateInterval={15000}
/>
<FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
<Row className="mb-0">
<Col xs={12}>
<h4 className="c-box-title">
<FontAwesomeIcon
icon={title_icon}
size="1x"
className="me-2 text-primary"
/>
{title}
</h4>
</Col>
</Row>
</div>
)
}
\ No newline at end of file
<div className="flex-grow-1 content">{children}</div>
</Container>
</div>
);
};
......@@ -3,5 +3,6 @@
@import "./utils.scss";
@import "./toast.scss";
@import "./skeleton.scss";
@import "../components/box/gridBox.view.scss";
@import "/node_modules/react-loading-skeleton/dist/skeleton.css";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment