Web: LoginMobile: added module LoginMobile

This commit is contained in:
Artem Tarasov 2021-02-16 11:00:24 +03:00
parent 80cdc0dee6
commit b6882bb381
23 changed files with 1308 additions and 3 deletions

View File

@ -23,10 +23,14 @@
{
"name": "ASC.Files.Client"
"path": "./products/ASC.Files/Client"
},
{
"name": "ASC.Web.LoginMobile"
"path": "./web/ASC.Web.LoginMobile"
}
],
"settings": {
"window.zoomLevel": 1,
"window.zoomLevel": 0,
"files.autoSave": "afterDelay",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@ -7,7 +7,8 @@
"web/ASC.Web.Login",
"web/ASC.Web.Client",
"products/ASC.People/Client",
"products/ASC.Files/Client"
"products/ASC.Files/Client",
"web/ASC.Web.LoginMobile"
],
"scripts": {
"wipe": "rimraf node_modules yarn.lock web/**/node_modules products/**/node_modules",

View File

@ -0,0 +1,7 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties"
]
}

View File

@ -0,0 +1 @@
dist/* linguist-vendored=false

116
web/ASC.Web.LoginMobile/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,15 @@
FROM node:12
WORKDIR /usr/src/app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
EXPOSE 8080
CMD [ "yarn", "build:start" ]

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"components": ["@appserver/components/src/components"],
"ASCWebComponents": ["../../packages/asc-web-components/src"],
"ASCWebCommon": ["../../packages/asc-web-common/src"]
}
},
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,58 @@
{
"name": "@appserver/loginmobile",
"version": "0.1.0",
"private": "true",
"scripts": {
"start": "webpack-cli serve",
"build": "webpack --mode production",
"serve": "serve dist -p 5021",
"clean": "rm -rf dist"
},
"devDependencies": {
"@babel/core": "7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.7",
"@babel/preset-react": "7.12.10",
"@svgr/webpack": "^5.5.0",
"babel-loader": "8.2.2",
"css-loader": "^3.6.0",
"html-webpack-plugin": "4.5.0",
"i18next": "^19.8.4",
"json-loader": "^0.5.7",
"react-i18next": "^11.7.3",
"source-map-loader": "^1.1.2",
"style-loader": "1.2.1",
"webpack": "5.14.0",
"webpack-cli": "4.3.1",
"webpack-dev-server": "3.11.2",
"serve": "11.3.2"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"attr-accept": "^2.2.2",
"email-addresses": "^3.1.0",
"moment": "^2.29.1",
"prop-types": "^15.7.2",
"rc-tree": "^4.0.0-beta.2",
"re-resizable": "^6.9.0",
"react": "^16.14.0",
"react-autosize-textarea": "^7.1.0",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.14.0",
"react-dropzone": "^11.2.4",
"react-onclickoutside": "^6.9.0",
"react-redux": "^7.2.2",
"react-resize-detector": "^5.2.0",
"react-router": "^5.2.0",
"react-text-mask": "^5.4.3",
"react-toastify": "^6.1.0",
"react-tooltip": "^4.2.11",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6",
"resize-image": "^0.1.0",
"sjcl": "^1.0.8",
"styled-components": "^5.2.1"
}
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login Mobile</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from "react";
import { Provider } from "react-redux";
import Login from "./Login";
const App = () => {
return <Login />;
};
export default App;

View File

@ -0,0 +1,527 @@
import React, { Component, useEffect } from "react";
import { connect } from "react-redux";
import styled, { css } from "styled-components";
import { withTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import { PageLayout, store, api, utils } from "ASCWebCommon";
import { checkPwd } from "ASCWebCommon/desktop";
import {
Box,
Button,
Text,
TextInput,
Link,
toastr,
Checkbox,
HelpButton,
PasswordInput,
FieldContainer,
tre,
} from "ASCWebComponents";
import i18n from "./i18n";
import ForgotPasswordModalDialog from "./sub-components/forgot-password-modal-dialog";
import Register from "./sub-components/register-container";
import { fakeHashSettings } from "./api/fakeApi";
const { login, setIsLoaded, reloadPortalSettings } = store.auth.actions;
const { getLanguage, isDesktopClient } = store.auth.selectors;
const { sendInstructionsToChangePassword } = api.people;
const { createPasswordHash, tryRedirectTo } = utils;
const LoginContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 120px auto 0 auto;
max-width: 960px;
@media (max-width: 768px) {
padding: 0 16px;
max-width: 475px;
}
@media (max-width: 375px) {
margin: 72px auto 0 auto;
max-width: 311px;
}
.greeting-title {
width: 100%;
@media (max-width: 768px) {
text-align: left;
}
@media (max-width: 375px) {
font-size: 23px;
}
}
.auth-form-container {
margin: 32px 213px 0 213px;
width: 311px;
@media (max-width: 768px) {
margin: 32px 0 0 0;
width: 100%;
}
@media (max-width: 375px) {
margin: 32px 0 0 0;
width: 100%;
}
.login-forgot-wrapper {
height: 36px;
padding: 14px 0;
.login-checkbox-wrapper {
display: flex;
.login-checkbox {
float: left;
span {
font-size: 12px;
}
}
}
.login-link {
line-height: 18px;
margin-left: auto;
}
}
.login-button {
margin-bottom: 16px;
}
.login-button-dialog {
margin-right: 8px;
}
.login-bottom-border {
width: 100%;
height: 1px;
background: #eceef1;
}
.login-bottom-text {
margin: 0 8px;
}
}
`;
const LoginFormWrapper = styled.div`
display: grid;
grid-template-rows: ${(props) =>
props.enabledJoin
? props.isDesktop
? css`1fr 10px`
: css`1fr 66px`
: css`1fr`};
width: 100%;
height: calc(100vh-56px);
`;
class Form extends Component {
constructor(props) {
super(props);
this.state = {
identifierValid: true,
identifier: "",
isLoading: false,
isDisabled: false,
passwordValid: true,
password: "",
isChecked: false,
openDialog: false,
email: "",
emailError: false,
errorText: "",
socialButtons: [],
};
}
onChangeLogin = (event) => {
this.setState({ identifier: event.target.value });
!this.state.identifierValid && this.setState({ identifierValid: true });
this.state.errorText && this.setState({ errorText: "" });
};
onChangePassword = (event) => {
this.setState({ password: event.target.value });
!this.state.passwordValid && this.setState({ passwordValid: true });
this.state.errorText && this.setState({ errorText: "" });
};
onChangeEmail = (event) => {
this.setState({ email: event.target.value, emailError: false });
};
onChangeCheckbox = () => this.setState({ isChecked: !this.state.isChecked });
onClick = () => {
this.setState({
openDialog: true,
isDisabled: true,
email: this.state.identifier,
});
};
onKeyPress = (event) => {
if (event.key === "Enter") {
!this.state.isDisabled
? this.onSubmit()
: this.onSendPasswordInstructions();
}
};
onSendPasswordInstructions = () => {
if (!this.state.email.trim()) {
this.setState({ emailError: true });
} else {
this.setState({ isLoading: true });
sendInstructionsToChangePassword(this.state.email)
.then(
(res) => toastr.success(res),
(message) => toastr.error(message)
)
.finally(this.onDialogClose());
}
};
onDialogClose = () => {
this.setState({
openDialog: false,
isDisabled: false,
isLoading: false,
email: "",
emailError: false,
});
};
onSubmit = () => {
const { errorText, identifier, password } = this.state;
const {
login,
setIsLoaded,
hashSettings,
isDesktop,
defaultPage,
} = this.props;
errorText && this.setState({ errorText: "" });
let hasError = false;
const userName = identifier.trim();
if (!userName) {
hasError = true;
this.setState({ identifierValid: !hasError });
}
const pass = password.trim();
if (!pass) {
hasError = true;
this.setState({ passwordValid: !hasError });
}
if (hasError) return false;
this.setState({ isLoading: true });
const hash = createPasswordHash(pass, fakeHashSettings);
isDesktop && checkPwd();
login(userName, hash)
.then(() => {
if (!tryRedirectTo(defaultPage)) {
setIsLoaded(true);
}
})
.catch((error) => {
this.setState({
errorText: error,
identifierValid: !error,
passwordValid: !error,
isLoading: false,
});
});
};
componentDidMount() {
const {
match,
t,
hashSettings, // eslint-disable-line react/prop-types
reloadPortalSettings, // eslint-disable-line react/prop-types
organizationName,
} = this.props;
//const { error /*confirmedEmail*/ } = match.params;
document.title = `${t("Authorization")} ${organizationName}`; //TODO: implement the setDocumentTitle() utility in ASC.Web.Common
//error && this.setState({ errorText: error });
// confirmedEmail && this.setState({ identifier: confirmedEmail });
window.addEventListener("keyup", this.onKeyPress);
if (!fakeHashSettings) {
reloadPortalSettings();
}
}
componentWillUnmount() {
window.removeEventListener("keyup", this.onKeyPress);
}
settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false,
};
render() {
const { greetingTitle, match, t } = this.props;
const {
identifierValid,
identifier,
isLoading,
passwordValid,
password,
isChecked,
openDialog,
email,
emailError,
errorText,
socialButtons,
} = this.state;
//const { confirmedEmail } = match.params;
//console.log("Login render");
return (
<>
<LoginContainer>
<Text
fontSize="32px"
fontWeight={600}
textAlign="center"
className="greeting-title"
>
{greetingTitle}
</Text>
<form className="auth-form-container">
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={!identifierValid}
errorMessage={errorText ? errorText : t("RequiredFieldMessage")} //TODO: Add wrong login server error
>
<TextInput
id="login"
name="login"
hasError={!identifierValid}
value={identifier}
placeholder={t("RegistrationEmailWatermark")}
size="large"
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="username"
onChange={this.onChangeLogin}
onKeyDown={this.onKeyPress}
/>
</FieldContainer>
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={!passwordValid}
errorMessage={errorText ? "" : t("RequiredFieldMessage")} //TODO: Add wrong password server error
>
<PasswordInput
simpleView={true}
passwordSettings={this.settings}
id="password"
inputName="password"
placeholder={t("Password")}
type="password"
hasError={!passwordValid}
inputValue={password}
size="large"
scale={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={this.onChangePassword}
onKeyDown={this.onKeyPress}
/>
</FieldContainer>
<div className="login-forgot-wrapper">
<div className="login-checkbox-wrapper">
<Checkbox
className="login-checkbox"
isChecked={isChecked}
onChange={this.onChangeCheckbox}
label={<Text fontSize="13px">{t("Remember")}</Text>}
/>
{/*<HelpButton
className="login-tooltip"
helpButtonHeaderContent={t("CookieSettingsTitle")}
tooltipContent={
<Text fontSize="12px">{t("RememberHelper")}</Text>
}
/>*/}
<Link
fontSize="13px"
color="#316DAA"
className="login-link"
type="page"
isHovered={false}
onClick={this.onClick}
>
{t("ForgotPassword")}
</Link>
</div>
</div>
{openDialog && (
<ForgotPasswordModalDialog
openDialog={openDialog}
isLoading={isLoading}
email={email}
emailError={emailError}
onChangeEmail={this.onChangeEmail}
onSendPasswordInstructions={this.onSendPasswordInstructions}
onDialogClose={this.onDialogClose}
t={t}
/>
)}
<Button
id="button"
className="login-button"
primary
size="large"
scale={true}
label={isLoading ? t("LoadingProcessing") : t("LoginButton")}
tabIndex={1}
isDisabled={isLoading}
isLoading={isLoading}
onClick={this.onSubmit}
/>
{/*confirmedEmail && (
<Text isBold={true} fontSize="16px">
{t("MessageEmailConfirmed")} {t("MessageAuthorize")}
</Text>
)*/}
{/* TODO: old error indication
<Text fontSize="14px" color="#c30">
{errorText}
</Text> */}
{socialButtons.length ? (
<Box displayProp="flex" alignItems="center">
<div className="login-bottom-border"></div>
<Text className="login-bottom-text" color="#A3A9AE">
{t("Or")}
</Text>
<div className="login-bottom-border"></div>
</Box>
) : null}
</form>
</LoginContainer>
</>
);
}
}
Form.propTypes = {
login: PropTypes.func, //.isRequired,
match: PropTypes.object, //.isRequired,
hashSettings: PropTypes.object,
reloadPortalSettings: PropTypes.func,
setIsLoaded: PropTypes.func, //.isRequired,
greetingTitle: PropTypes.string, //.isRequired,
t: PropTypes.func, //.isRequired,
i18n: PropTypes.object, //.isRequired,
language: PropTypes.string, //.isRequired,
socialButtons: PropTypes.array,
organizationName: PropTypes.string,
homepage: PropTypes.string,
defaultPage: PropTypes.string,
};
Form.defaultProps = {
identifier: "",
password: "",
email: "",
};
const FormWrapper = withTranslation()(Form);
// const LoginForm = (props) => <FormWrapper i18n={i18n} {...props} />;
// export default LoginForm;
//const RegisterWrapper = withTranslation()(Register);
const LoginForm = (props) => {
const { language, enabledJoin, isDesktop } = props;
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
return (
<LoginFormWrapper enabledJoin={enabledJoin} isDesktop={isDesktop}>
<FormWrapper i18n={i18n} {...props} />
{/*<Register />*/}
</LoginFormWrapper>
);
};
LoginForm.propTypes = {
language: PropTypes.string, //.isRequired,
isLoaded: PropTypes.bool,
enabledJoin: PropTypes.bool,
isDesktop: PropTypes.bool, //.isRequired,
};
function mapStateToProps(state) {
const { isLoaded, settings, isAuthenticated } = state.auth;
const {
greetingSettings,
organizationName,
hashSettings,
enabledJoin,
defaultPage,
} = settings;
return {
isAuthenticated,
isLoaded,
isDesktop: isDesktopClient(state),
organizationName,
language: getLanguage(state),
greetingTitle: greetingSettings,
hashSettings,
enabledJoin,
defaultPage,
};
}
export default LoginForm; //connect(mapStateToProps, {
//login,
//setIsLoaded,
//reloadPortalSettings,
//})(withRouter(LoginForm));

View File

@ -0,0 +1,5 @@
export const fakeHashSettings = {
size: 256,
iterations: 100000,
salt: "0a783fef249b0cfa2b1ddc3825bf9a5c9c869bf65a4a45cbd5a0c93b16b5a47e",
};

View File

@ -0,0 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -0,0 +1,37 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
//import { i18nBaseSettings } from "../../constants";
const newInstance = i18n.createInstance();
const resources = {
en: {
translation: en,
},
ru: {
translation: ru,
},
};
//newInstance.init({ ...i18nBaseSettings, resources });
newInstance.init({
resources: resources,
lng: "en",
fallbackLng: "en",
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
format: function (value, format) {
if (format === "lowercase") return value.toLowerCase();
return value;
},
},
react: {
useSuspense: false,
},
});
export default newInstance;

View File

@ -0,0 +1,3 @@
body {
font-family: Arial, Helvetica, sans-serif;
}

View File

@ -0,0 +1 @@
import("./bootstrap");

View File

@ -0,0 +1,30 @@
{
"LoadingProcessing": "Loading...",
"LoginButton": "Sign In",
"Password": "Password",
"RegistrationEmailWatermark": "E-mail",
"MessageEmailConfirmed": "Your email was activated successfully.",
"MessageAuthorize": "Please authorize yourself.",
"ForgotPassword": "Forgot your password?",
"PasswordRecoveryTitle": "Password recovery",
"PasswordRecoveryPlaceholder": "Your registration e-mail",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Please enter the email you used while registering on the portal. The password recovery instructions will be send to that email address.",
"RegisterTitle": "Registration request",
"RegisterTextBodyBeforeDomainsList": "Registration is available to users with an email account at",
"RegisterTextBodyAfterDomainsList": "To register, enter your email and click on Send request. An email message with a link to activate your account will be sent to the specified email. Please enter the email address where the invitation will be sent:",
"RegisterPlaceholder": "Your registration e-mail",
"RegisterSendButton": "Send request",
"RegisterProcessSending": "Sending...",
"SendButton": "Send",
"RequiredFieldMessage": "Required field",
"CancelButton": "Cancel",
"Remember": "Remember me",
"RememberHelper": "The default session lifetime is 20 minutes. Check this option to set it to 1 year. To set your own value, go to the settings.",
"CookieSettingsTitle": "Session Lifetime",
"Authorization": "Authorization",
"Or": "OR",
"Register": "Register"
}

View File

@ -0,0 +1,30 @@
{
"LoadingProcessing": "Загрузка...",
"LoginButton": "Войти",
"Password": "Пароль",
"RegistrationEmailWatermark": "Регистрационный email",
"MessageEmailConfirmed": "Ваш email успешно активирован.",
"MessageAuthorize": "Пожалуйста авторизуйтесь.",
"ForgotPassword": "Забыли пароль?",
"PasswordRecoveryTitle": "Восстановление пароля",
"PasswordRecoveryPlaceholder": "Ваш регистрационный email",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Пожалуйста, введите адрес электронной почты, указанный при регистрации на портале. Инструкции для восстановления пароля будут отправлены на этот адрес электронной почты.",
"RegisterTitle": "Запрос на регистрацию",
"RegisterTextBodyBeforeDomainsList": "Регистрация доступна для пользователей, которые имеют почтовый ящик на",
"RegisterTextBodyAfterDomainsList": "Чтобы зарегистрироваться, введите свой email и нажмите кнопку Отправить запрос. Сообщение со ссылкой для активации вашей учётной записи будет отправлено на указанный адрес.",
"RegisterPlaceholder": "Ваш регистрационный email",
"RegisterSendButton": "Отправить запрос",
"RegisterProcessSending": "Отправка...",
"SendButton": "Отправить",
"RequiredFieldMessage": "Обязательное поле",
"CancelButton": "Отмена",
"Remember": "Запомнить",
"RememberHelper": "Время существования сессии по умолчанию составляет 20 минут. Отметьте эту опцию, чтобы установить значение 1 год. Чтобы задать собственное значение, перейдите в настройки.",
"CookieSettingsTitle": "Время жизни сессии",
"Authorization": "Авторизация",
"Or": "ИЛИ",
"Register": "Регистрация"
}

View File

@ -0,0 +1,100 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "@appserver/components/src/components/button";
import TextInput from "@appserver/components/src/components/text-input";
import Text from "@appserver/components/src/components/text";
import ModalDialog from "@appserver/components/src/components/modal-dialog";
import FieldContainer from "@appserver/components/src/components/field-container";
import ModalDialogContainer from "./modal-dialog-container";
class ForgotPasswordModalDialog extends React.Component {
render() {
const {
openDialog,
isLoading,
email,
emailError,
onChangeEmail,
onSendPasswordInstructions,
onDialogClose,
t,
} = this.props;
return (
<ModalDialogContainer>
<ModalDialog
visible={openDialog}
bodyPadding="16px 0 0 0"
onClose={onDialogClose}
>
<ModalDialog.Header>
<Text isBold={true} fontSize="21px">
{t("PasswordRecoveryTitle")}
</Text>
</ModalDialog.Header>
<ModalDialog.Body>
<Text
key="text-body"
className="text-body"
isBold={false}
fontSize="13px"
>
{t("MessageSendPasswordRecoveryInstructionsOnEmail")}
</Text>
<FieldContainer
key="e-mail"
isVertical={true}
hasError={emailError}
labelVisible={false}
errorMessage={t("RequiredFieldMessage")}
>
<TextInput
hasError={emailError}
placeholder={t("PasswordRecoveryPlaceholder")}
isAutoFocussed={true}
id="e-mail"
name="e-mail"
type="text"
size="base"
scale={true}
tabIndex={2}
isDisabled={isLoading}
value={email}
onChange={onChangeEmail}
/>
</FieldContainer>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
className="modal-dialog-button"
key="SendBtn"
label={isLoading ? t("LoadingProcessing") : t("SendButton")}
size="big"
scale={false}
primary={true}
onClick={onSendPasswordInstructions}
isLoading={isLoading}
isDisabled={isLoading}
tabIndex={2}
/>
</ModalDialog.Footer>
</ModalDialog>
</ModalDialogContainer>
);
}
}
ForgotPasswordModalDialog.propTypes = {
openDialog: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
emailError: PropTypes.bool.isRequired,
onChangeEmail: PropTypes.func.isRequired,
onSendPasswordInstructions: PropTypes.func.isRequired,
onDialogClose: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default ForgotPasswordModalDialog;

View File

@ -0,0 +1,19 @@
import styled from "styled-components";
const ModalDialogContainer = styled.div`
.modal-dialog-aside-footer {
@media (max-width: 1024px) {
width: 90%;
}
}
.modal-dialog-button {
@media (max-width: 1024px) {
width: 100%;
}
}
.field-body {
margin-top: 16px;
}
`;
export default ModalDialogContainer;

View File

@ -0,0 +1,127 @@
import React, { useEffect, useState } from "react";
import Box from "@appserver/components/src/components/box";
import Text from "@appserver/components/src/components/text";
import toastr from "@appserver/components/src/components/toast/toastr";
import RegisterModalDialog from "./register-modal-dialog";
import styled from "styled-components";
import PropTypes from "prop-types";
import { sendRegisterRequest } from "@appserver/common/src/api/settings";
import { I18nextProvider, withTranslation } from "react-i18next";
import { getLanguage } from "@appserver/common/src/store/auth/selectors";
import { connect } from "react-redux";
import i18n from "../i18n";
const StyledRegister = styled(Box)`
display: flex;
align-items: center;
justify-content: center;
z-index: 184;
width: 100%;
height: 66px;
padding: 1.5em;
bottom: 0;
right: 0;
background-color: #f8f9f9;
cursor: pointer;
`;
const Register = ({ t }) => {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const [emailErr, setEmailErr] = useState(false);
const onRegisterClick = () => {
setVisible(true);
};
const onRegisterModalClose = () => {
setVisible(false);
setEmail("");
setEmailErr(false);
};
const onChangeEmail = (e) => {
setEmail(e.currentTarget.value);
setEmailErr(false);
};
const onSendRegisterRequest = () => {
if (!email.trim()) {
setEmailErr(true);
} else {
setLoading(true);
sendRegisterRequest(email)
.then(() => {
setLoading(false);
toastr.success("Successfully sent");
})
.catch((error) => {
setLoading(false);
toastr.error(error);
})
.finally(onRegisterModalClose);
}
};
return (
<>
<StyledRegister onClick={onRegisterClick}>
<Text color="#316DAA">{t("Register")}</Text>
</StyledRegister>
{visible && (
<RegisterModalDialog
visible={visible}
loading={loading}
email={email}
emailErr={emailErr}
t={t}
onChangeEmail={onChangeEmail}
onRegisterModalClose={onRegisterModalClose}
onSendRegisterRequest={onSendRegisterRequest}
/>
)}
</>
);
};
Register.propTypes = {
t: PropTypes.func.isRequired,
};
const RegisterTranslationWrapper = withTranslation()(Register);
const RegisterWrapper = (props) => {
const { language, isAuthenticated, enabledJoin } = props;
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
return (
<I18nextProvider i18n={i18n}>
{enabledJoin && !isAuthenticated && (
<RegisterTranslationWrapper {...props} />
)}
</I18nextProvider>
);
};
RegisterWrapper.propTypes = {
language: PropTypes.string,
isAuthenticated: PropTypes.bool,
enabledJoin: PropTypes.bool,
};
function mapStateToProps(state) {
const { isAuthenticated, settings } = state.auth;
const { enabledJoin } = settings;
return {
language: getLanguage(state),
isAuthenticated,
enabledJoin,
};
}
export default connect(mapStateToProps, null)(RegisterWrapper);

View File

@ -0,0 +1,105 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "@appserver/components/src/components/button";
import TextInput from "@appserver/components/src/components/text-input";
import Text from "@appserver/components/src/components/text";
import ModalDialog from "@appserver/components/src/components/modal-dialog";
import FieldContainer from "@appserver/components/src/components/field-container";
import ModalDialogContainer from "./modal-dialog-container";
const domains = ["mail.ru", "gmail.com", "yandex.ru"];
const domainList = domains.map((domain, i) => (
<span key={i}>
<b>
{domain}
{i === domains.length - 1 ? "." : ", "}
</b>
</span>
));
const RegisterModalDialog = ({
visible,
loading,
email,
emailErr,
t,
onChangeEmail,
onRegisterModalClose,
onSendRegisterRequest,
}) => {
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
bodyPadding="16px 0 0 0"
onClose={onRegisterModalClose}
>
<ModalDialog.Header>
<Text isBold={true} fontSize="21px">
{t("RegisterTitle")}
</Text>
</ModalDialog.Header>
<ModalDialog.Body>
<Text key="text-body" isBold={false} fontSize="13px">
{t("RegisterTextBodyBeforeDomainsList")} {domainList}{" "}
{t("RegisterTextBodyAfterDomainsList")}
</Text>
<FieldContainer
key="e-mail"
isVertical={true}
hasError={emailErr}
labelVisible={false}
errorMessage={t("RequiredFieldMessage")}
>
<TextInput
hasError={emailErr}
placeholder={t("RegisterPlaceholder")}
isAutoFocussed={true}
id="e-mail"
name="e-mail"
type="text"
size="base"
scale={true}
tabIndex={3}
isDisabled={loading}
value={email}
onChange={onChangeEmail}
/>
</FieldContainer>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
className="modal-dialog-button"
key="SendBtn"
label={
loading ? t("RegisterProcessSending") : t("RegisterSendButton")
}
size="big"
scale={false}
primary={true}
onClick={onSendRegisterRequest}
isLoading={loading}
isDisabled={loading}
tabIndex={3}
/>
</ModalDialog.Footer>
</ModalDialog>
</ModalDialogContainer>
);
};
RegisterModalDialog.propTypes = {
visible: PropTypes.bool.isRequired,
loading: PropTypes.bool.isRequired,
email: PropTypes.string,
emailErr: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
onChangeEmail: PropTypes.func.isRequired,
onSendRegisterRequest: PropTypes.func.isRequired,
onRegisterModalClose: PropTypes.func.isRequired,
};
export default RegisterModalDialog;

View File

@ -0,0 +1,83 @@
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
const deps = require("./package.json").dependencies;
module.exports = {
entry: "./src/index",
mode: "development",
devServer: {
contentBase: [path.join(__dirname, "public"), path.join(__dirname, "dist")],
port: 5021,
historyApiFallback: true,
hot: false,
hotOnly: false,
},
output: {
publicPath: "auto",
chunkFilename: "[id].[contenthash].js",
},
resolve: {
extensions: [".jsx", ".js", ".json"],
fallback: {
crypto: false,
},
alias: {
ASCWebComponents: path.resolve(
__dirname,
"../../packages/asc-web-components/src/"
),
ASCWebCommon: path.resolve(
__dirname,
"../../packages/asc-web-common/src"
),
},
},
module: {
rules: [
{
test: /\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\.react.svg$/,
use: ["@svgr/webpack"],
},
{ test: /\.json$/, loader: "json-loader" },
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-react", "@babel/preset-env"],
plugins: [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from",
],
},
},
"source-map-loader",
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};