Add login page - UI

This commit is contained in:
Viet Anh Nguyen 2023-12-14 13:26:28 +07:00
parent 90824fa963
commit 78427b7bb5
10 changed files with 262 additions and 20 deletions

View File

@ -0,0 +1,12 @@
<svg width="1440" height="1024" viewBox="0 0 1440 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="-839" y="430.089" width="1187" height="1606.01" transform="rotate(-33.9609 -839 430.089)" fill="#134783"/>
<rect x="638" y="-77.9104" width="1187" height="1606.01" transform="rotate(-33.9609 638 -77.9104)" fill="#84B1E3"/>
<rect x="-303" y="-22.9104" width="1187" height="1606.01" transform="rotate(-33.9609 -303 -22.9104)" fill="#103C74"/>
<rect x="-51" y="0.0895996" width="1187" height="1606.01" transform="rotate(-33.9609 -51 0.0895996)" fill="#0C3264"/>
<rect x="204" y="0.0895996" width="1187" height="1606.01" transform="rotate(-33.9609 204 0.0895996)" fill="#0B2954"/>
<path d="M281 0.0895996L1265.52 -663L2162.68 669.059L1178.16 1332.15L281 0.0895996Z" fill="#0C3265"/>
<rect x="430" y="-13.9104" width="1187" height="1606.01" transform="rotate(-33.9609 430 -13.9104)" fill="#092956"/>
<path d="M543 -57.9104L1527.52 -721L2424.68 611.059L1440.16 1274.15L543 -57.9104Z" fill="#0C3265"/>
<path d="M720 -57.9104L1704.52 -721L2601.68 611.059L1617.16 1274.15L720 -57.9104Z" fill="#0B3F7A"/>
<rect x="818" y="-169.91" width="1187" height="1606.01" transform="rotate(-33.9609 818 -169.91)" fill="#0B4C93"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +1,4 @@
import { Typography } from 'antd'; import { Typography } from "antd";
export const Brand = ({ export const Brand = ({
collapsed, collapsed,
@ -13,15 +13,21 @@ export const Brand = ({
rows: 1, rows: 1,
}} }}
style={{ style={{
margin: 12, marginTop: 3,
marginLeft: 12,
marginRight: 12,
paddingTop: 18,
paddingBottom: 18,
letterSpacing: 2, letterSpacing: 2,
borderRadius: 4, borderBottomLeftRadius: 10,
color: 'wheat', borderBottomRightRadius: 10,
textAlign: 'center', color: "#fff",
border: isBordered ? '4px solid wheat' : 'unset', textAlign: "center",
backgroundColor: "rgb(0, 106, 255)",
fontSize: 32,
}} }}
> >
{collapsed ? 'O' : 'OCR'} {collapsed ? "O" : "OCR"}
</Typography.Title> </Typography.Title>
); );
}; };

View File

@ -1,8 +1,8 @@
import { Segmented } from 'antd'; import { Segmented } from "antd";
import styled from 'styled-components'; import styled from "styled-components";
import { useLocalStorage } from 'usehooks-ts'; import { useLocalStorage } from "usehooks-ts";
import { LOCALE_KEY } from '../../consts'; import { LOCALE_KEY } from "../../consts";
import { dynamicActivate, getLocale } from '../../i18n'; import { dynamicActivate, getLocale } from "../../i18n";
const StyledSegmented = styled(Segmented)` const StyledSegmented = styled(Segmented)`
& label.ant-segmented-item.ant-segmented-item-selected, & label.ant-segmented-item.ant-segmented-item-selected,
@ -23,12 +23,12 @@ export const LanguageSelect = () => {
<StyledSegmented <StyledSegmented
options={[ options={[
{ {
label: 'EN', label: "EN",
value: 'en', value: "en",
}, },
{ {
label: 'VI', label: "VI",
value: 'vi', value: "vi",
}, },
]} ]}
value={locale} value={locale}

View File

@ -105,7 +105,7 @@ export function MainLayout() {
alignItems: 'center', alignItems: 'center',
}} }}
> >
<LanguageSelect /> {/* <LanguageSelect /> */}
<Typography.Paragraph <Typography.Paragraph
style={{ style={{
marginBottom: 0, marginBottom: 0,

View File

@ -19,6 +19,7 @@
"Confirm": "Confirm", "Confirm": "Confirm",
"Copied!": "Copied!", "Copied!": "Copied!",
"Copy": "Copy", "Copy": "Copy",
"Could not login with provided username and password!": "Could not login with provided username and password!",
"Create Template": "Create Template", "Create Template": "Create Template",
"Create template error: Extracted fields must have unique labels.": "Create template error: Extracted fields must have unique labels.", "Create template error: Extracted fields must have unique labels.": "Create template error: Extracted fields must have unique labels.",
"Create template error: {0}": "Create template error: {0}", "Create template error: {0}": "Create template error: {0}",
@ -43,12 +44,16 @@
"Form number": "Form number", "Form number": "Form number",
"Home Town": "Home Town", "Home Town": "Home Town",
"ID Card": "ID Card", "ID Card": "ID Card",
"Intelligent Document Processing Solutions": "Intelligent Document Processing Solutions",
"Invoice": "Invoice", "Invoice": "Invoice",
"Invoice number": "Invoice number", "Invoice number": "Invoice number",
"Issued By": "Issued By", "Issued By": "Issued By",
"Key Information Extraction": "Key Information Extraction", "Key Information Extraction": "Key Information Extraction",
"Label": "Label", "Label": "Label",
"Label of extracted field must not have more than 255 characters": "Label of extracted field must not have more than 255 characters", "Label of extracted field must not have more than 255 characters": "Label of extracted field must not have more than 255 characters",
"Language": "Language",
"Login": "Login",
"Login with  <0>CMC Account</0>?": "Login with  <0>CMC Account</0>?",
"Name": "Name", "Name": "Name",
"Nation": "Nation", "Nation": "Nation",
"Nationality": "Nationality", "Nationality": "Nationality",
@ -58,8 +63,11 @@
"No response.": "No response.", "No response.": "No response.",
"Number": "Number", "Number": "Number",
"Other Documents": "Other Documents", "Other Documents": "Other Documents",
"Password": "Password",
"Please do not leave the label of extracted fields empty": "Please do not leave the label of extracted fields empty", "Please do not leave the label of extracted fields empty": "Please do not leave the label of extracted fields empty",
"Please draw at least 1 box of extracted field": "Please draw at least 1 box of extracted field", "Please draw at least 1 box of extracted field": "Please draw at least 1 box of extracted field",
"Please specify a password": "Please specify a password",
"Please specify a username": "Please specify a username",
"Please, specify a template name.": "Please, specify a template name.", "Please, specify a template name.": "Please, specify a template name.",
"Rank": "Rank", "Rank": "Rank",
"Refresh": "Refresh", "Refresh": "Refresh",
@ -92,7 +100,8 @@
"Update template successfully.": "Update template successfully.", "Update template successfully.": "Update template successfully.",
"Upload file validation": "Upload file validation", "Upload file validation": "Upload file validation",
"Uploaded image is not match with template.": "Uploaded image is not match with template.", "Uploaded image is not match with template.": "Uploaded image is not match with template.",
"VAT Invoice": "VAT Invoice", "User log in successfully": "User log in successfully",
"Username": "Username",
"VAT amount": "VAT amount", "VAT amount": "VAT amount",
"Value": "Value", "Value": "Value",
"You are not allowed to create more than {0} templates! Please contact us for further support.": "You are not allowed to create more than {0} templates! Please contact us for further support.", "You are not allowed to create more than {0} templates! Please contact us for further support.": "You are not allowed to create more than {0} templates! Please contact us for further support.",

View File

@ -19,6 +19,7 @@
"Confirm": "Xác nhận", "Confirm": "Xác nhận",
"Copied!": "Đã sao chép", "Copied!": "Đã sao chép",
"Copy": "Sao chép", "Copy": "Sao chép",
"Could not login with provided username and password!": "",
"Create Template": "Tạo mẫu tài liệu", "Create Template": "Tạo mẫu tài liệu",
"Create template error: Extracted fields must have unique labels.": "Tạo mẫu tài liệu lỗi: Các trường thông tin trích xuất phải có nhãn khác nhau.", "Create template error: Extracted fields must have unique labels.": "Tạo mẫu tài liệu lỗi: Các trường thông tin trích xuất phải có nhãn khác nhau.",
"Create template error: {0}": "Tạo mẫu tài liệu lỗi: {0}", "Create template error: {0}": "Tạo mẫu tài liệu lỗi: {0}",
@ -43,12 +44,16 @@
"Form number": "Số mẫu", "Form number": "Số mẫu",
"Home Town": "Quê quán", "Home Town": "Quê quán",
"ID Card": "Căn cước công dân", "ID Card": "Căn cước công dân",
"Intelligent Document Processing Solutions": "",
"Invoice": "Hóa đơn", "Invoice": "Hóa đơn",
"Invoice number": "Số hóa đơn", "Invoice number": "Số hóa đơn",
"Issued By": "Nơi cấp", "Issued By": "Nơi cấp",
"Key Information Extraction": "Trích xuất thông tin", "Key Information Extraction": "Trích xuất thông tin",
"Label": "Nhãn", "Label": "Nhãn",
"Label of extracted field must not have more than 255 characters": "Độ dài nhãn của trường thông tin trích xuất không được vượt quá 255 kí tự", "Label of extracted field must not have more than 255 characters": "Độ dài nhãn của trường thông tin trích xuất không được vượt quá 255 kí tự",
"Language": "",
"Login": "",
"Login with  <0>CMC Account</0>?": "",
"Name": "Tên", "Name": "Tên",
"Nation": "Dân tộc", "Nation": "Dân tộc",
"Nationality": "Quốc tịch", "Nationality": "Quốc tịch",
@ -58,8 +63,11 @@
"No response.": "Không có kết quả", "No response.": "Không có kết quả",
"Number": "Số", "Number": "Số",
"Other Documents": "Các tài liệu khác", "Other Documents": "Các tài liệu khác",
"Password": "",
"Please do not leave the label of extracted fields empty": "Vui lòng không bỏ trống nhãn của trường thông tin trích xuất", "Please do not leave the label of extracted fields empty": "Vui lòng không bỏ trống nhãn của trường thông tin trích xuất",
"Please draw at least 1 box of extracted field": "Vui lòng vẽ ít nhất một hình cho việc trích xuất thông tin", "Please draw at least 1 box of extracted field": "Vui lòng vẽ ít nhất một hình cho việc trích xuất thông tin",
"Please specify a password": "",
"Please specify a username": "",
"Please, specify a template name.": "Vui lòng nhập tên mẫu", "Please, specify a template name.": "Vui lòng nhập tên mẫu",
"Rank": "Hạng", "Rank": "Hạng",
"Refresh": "Tải lại", "Refresh": "Tải lại",
@ -92,7 +100,8 @@
"Update template successfully.": "Sửa mẫu tài liệu thành công.", "Update template successfully.": "Sửa mẫu tài liệu thành công.",
"Upload file validation": "Xác thực tệp tải lên", "Upload file validation": "Xác thực tệp tải lên",
"Uploaded image is not match with template.": "Ảnh được chọn không khớp với mẫu.", "Uploaded image is not match with template.": "Ảnh được chọn không khớp với mẫu.",
"VAT Invoice": "Hóa đơn VAT", "User log in successfully": "",
"Username": "",
"VAT amount": "Tổng tiền VAT", "VAT amount": "Tổng tiền VAT",
"Value": "Giá trị", "Value": "Giá trị",
"You are not allowed to create more than {0} templates! Please contact us for further support.": "Hệ thống không cho phép tạo nhiều hơn {0} mẫu tài liệu. Vui lòng liên hệ chúng tôi để được hỗ trợ thêm.", "You are not allowed to create more than {0} templates! Please contact us for further support.": "Hệ thống không cho phép tạo nhiều hơn {0} mẫu tài liệu. Vui lòng liên hệ chúng tôi để được hỗ trợ thêm.",

View File

@ -6,6 +6,7 @@ import DriverLicensePage from '../views/driver-license';
import IDCardPage from '../views/id-card'; import IDCardPage from '../views/id-card';
import InvoicePage from '../views/invoice'; import InvoicePage from '../views/invoice';
import OtherDocumentsPage from '../views/other-documents'; import OtherDocumentsPage from '../views/other-documents';
import LoginPage from '../views/login';
import { PrivateRoute } from './guard-route'; import { PrivateRoute } from './guard-route';
const ConfigTemplatePage = React.lazy(() => import('../views/config-template')); const ConfigTemplatePage = React.lazy(() => import('../views/config-template'));
@ -28,6 +29,10 @@ export function createRouter() {
/> />
), ),
}, },
{
path: '/login',
element: <LoginPage />,
},
{ {
path: '/error/401', path: '/error/401',
element: ( element: (

View File

@ -31,5 +31,5 @@ export function PrivateRoute({
return <>{element}</>; return <>{element}</>;
} }
return <Navigate to="/error/401" />; return <Navigate to="/login" />;
} }

View File

@ -0,0 +1,197 @@
import { LockOutlined, UserOutlined } from "@ant-design/icons";
import { t, Trans } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import {
Button,
Col,
Form,
FormProps,
Input,
notification,
Row,
Typography,
} from "antd";
import { AxiosError } from "axios";
import { useEffect, useState } from "react";
import styled from "styled-components";
import { LanguageSelect } from "../../components/language-select";
import { TOKEN_KEY } from "../../consts";
import "./style.css";
export const DIVERGE_FORM_ICON_COLOR = "rgba(0, 0, 0, 0.25)";
export function getErrorMessage(error: any) {
if (error instanceof AxiosError) {
return error.response?.data?.message;
}
return "";
}
export interface LogInPayload {
username: string;
password: string;
}
const StyledLoginContainer = styled.div`
width: 100%;
height: 100%;
background-image: url(/login_bg.svg);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
position: relative;
`;
const StyledFormContainer = styled(Form)`
.login-form__title {
color: white;
text-align: justify;
font-size: 52px;
margin-bottom: 0;
}
.login-form__subtitle {
color: #84b1e3;
text-align: right;
font-weight: 300;
margin-top: 0 !important;
}
.login-form__form-title {
color: white;
text-align: right;
font-weight: 400;
margin-top: 0.8em !important;
}
.ant-form-item-control-input .ant-input-affix-wrapper {
border: none;
}
.login-form__helper-text {
font-size: 18px;
color: #83afdf;
}
.login-form__submit-button {
display: block;
width: 100%;
}
a {
color: white;
text-decoration: underline;
}
`;
export default function LoginPage() {
const { i18n } = useLingui();
const [isAuthenticating, setIsAuthenticating] = useState(false);
useEffect(() => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
window.location.href = "/";
}
}, []);
const handleLogin: FormProps<LogInPayload>["onFinish"] = async (payload) => {
setIsAuthenticating(true);
try {
const resp = await fetch("/api/ctel/login/", {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(payload),
});
const message = await resp.json();
if (!resp.ok) {
throw new Error(message?.detail);
}
let token = message?.token || "";
token = token.trim();
localStorage.setItem(TOKEN_KEY, JSON.stringify(token));
notification.success({
message: t(i18n)`User log in successfully`,
});
setTimeout(() => {
window.location.href = "/";
}, 1000);
} catch (error) {
setIsAuthenticating(false);
notification.error({
message: t(i18n)`Could not login with provided username and password!`,
});
}
};
return (
<StyledLoginContainer>
<StyledFormContainer className="login-form" onFinish={handleLogin as any}>
<Typography.Title level={1} className="login-form__title">
OCR APIs
</Typography.Title>
<Typography.Title level={3} className="login-form__subtitle">
<Trans>Intelligent Document Processing Solutions</Trans>
</Typography.Title>
<Typography.Title level={2} className="login-form__form-title">
<Trans>Login</Trans>
</Typography.Title>
<Form.Item
name="username"
rules={[
{
required: true,
message: t(i18n)`Please specify a username`,
whitespace: true,
},
]}
>
<Input
autoComplete="username"
prefix={<UserOutlined style={{ color: DIVERGE_FORM_ICON_COLOR }} />}
placeholder={t(i18n)`Username`}
size="large"
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{
required: true,
message: t(i18n)`Please specify a password`,
whitespace: true,
},
]}
>
<Input.Password
autoComplete="current-password"
prefix={<LockOutlined style={{ color: DIVERGE_FORM_ICON_COLOR }} />}
placeholder={t(i18n)`Password`}
size="large"
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form__submit-button"
size="large"
loading={isAuthenticating}
>
{t(i18n)`Login`}
</Button>
</Form.Item>
</StyledFormContainer>
</StyledLoginContainer>
);
}

View File

@ -0,0 +1,4 @@
#root {
min-height: 100%;
height: 100%;
}