mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Change prettier rules
This commit is contained in:
@@ -3,109 +3,109 @@
|
||||
// Original: http://codepen.io/vanderlanth/pen/rxpNMY
|
||||
|
||||
.page {
|
||||
margin: 80px auto 0;
|
||||
margin: 80px auto 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
margin-bottom: 50px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
animation: loadStab 1s ease-out infinite;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
margin-bottom: 50px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
animation: loadStab 1s ease-out infinite;
|
||||
}
|
||||
|
||||
.cube {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: white;
|
||||
animation: cubeRotate 1s ease-out infinite;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: white;
|
||||
animation: cubeRotate 1s ease-out infinite;
|
||||
}
|
||||
|
||||
.road {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: white;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
animation: roadStab 1s ease-out infinite;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: white;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
animation: roadStab 1s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cubeRotate {
|
||||
0% {
|
||||
transform: rotate(0deg) translate3D(0, 0, 0);
|
||||
}
|
||||
65% {
|
||||
transform: rotate(45deg) translate3D(0, -13px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: rotate(70deg) translate3D(0, -8px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(90deg) translate3D(0, 0, 0);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg) translate3D(0, 0, 0);
|
||||
}
|
||||
65% {
|
||||
transform: rotate(45deg) translate3D(0, -13px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: rotate(70deg) translate3D(0, -8px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(90deg) translate3D(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes roadStab {
|
||||
0% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
transform: translate3D(0, 2px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3D(0, 4px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
0% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
transform: translate3D(0, 2px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3D(0, 4px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadStab {
|
||||
0% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
transform: translate3D(0, -2px, 0);
|
||||
}
|
||||
95% {
|
||||
transform: translate3D(0, -2px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
0% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
transform: translate3D(0, -2px, 0);
|
||||
}
|
||||
95% {
|
||||
transform: translate3D(0, -2px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------ MOUNTAINS ---------------------
|
||||
|
||||
.rocks {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
animation: roadStab 1s ease-out infinite;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
animation: roadStab 1s ease-out infinite;
|
||||
}
|
||||
|
||||
@mixin rock($rockName, $bottom, $delay) {
|
||||
.#{$rockName} {
|
||||
position: absolute;
|
||||
border-left: 2px solid transparent;
|
||||
border-right: 2px solid transparent;
|
||||
border-bottom: 4px solid white;
|
||||
bottom: $bottom;
|
||||
right: -2%;
|
||||
animation: rockTravelling 10s $delay ease-out infinite;
|
||||
}
|
||||
.#{$rockName} {
|
||||
position: absolute;
|
||||
border-left: 2px solid transparent;
|
||||
border-right: 2px solid transparent;
|
||||
border-bottom: 4px solid white;
|
||||
bottom: $bottom;
|
||||
right: -2%;
|
||||
animation: rockTravelling 10s $delay ease-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@include rock('rockOne', 23px, 0s);
|
||||
@@ -115,133 +115,133 @@
|
||||
@include rock('rockFive', 18px, 8s);
|
||||
|
||||
@keyframes rockTravelling {
|
||||
0% {
|
||||
right: -2%;
|
||||
}
|
||||
10% {
|
||||
right: 8%;
|
||||
}
|
||||
20% {
|
||||
right: 18%;
|
||||
}
|
||||
30% {
|
||||
right: 29%;
|
||||
}
|
||||
40% {
|
||||
right: 40%;
|
||||
}
|
||||
50% {
|
||||
right: 51%;
|
||||
}
|
||||
60% {
|
||||
right: 62%;
|
||||
}
|
||||
70% {
|
||||
right: 72%;
|
||||
}
|
||||
80% {
|
||||
right: 82%;
|
||||
}
|
||||
90% {
|
||||
right: 92%;
|
||||
}
|
||||
100% {
|
||||
right: 102%;
|
||||
}
|
||||
0% {
|
||||
right: -2%;
|
||||
}
|
||||
10% {
|
||||
right: 8%;
|
||||
}
|
||||
20% {
|
||||
right: 18%;
|
||||
}
|
||||
30% {
|
||||
right: 29%;
|
||||
}
|
||||
40% {
|
||||
right: 40%;
|
||||
}
|
||||
50% {
|
||||
right: 51%;
|
||||
}
|
||||
60% {
|
||||
right: 62%;
|
||||
}
|
||||
70% {
|
||||
right: 72%;
|
||||
}
|
||||
80% {
|
||||
right: 82%;
|
||||
}
|
||||
90% {
|
||||
right: 92%;
|
||||
}
|
||||
100% {
|
||||
right: 102%;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------ CLOUDS ---------------------
|
||||
|
||||
.clouds {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
animation: roadStab 1s ease-out infinite, cloudStab 1s ease-out infinite;
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
left: -50%;
|
||||
overflow: hidden;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
animation: roadStab 1s ease-out infinite, cloudStab 1s ease-out infinite;
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
left: -50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cloud {
|
||||
position: absolute;
|
||||
will-change: animation;
|
||||
position: absolute;
|
||||
will-change: animation;
|
||||
|
||||
background-image: url('./cloud.svg');
|
||||
background-size: cover;
|
||||
background-image: url('./cloud.svg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.cloudOne {
|
||||
composes: cloud;
|
||||
composes: cloud;
|
||||
|
||||
top: 5px;
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
animation: cloudTravelling 16s linear infinite;
|
||||
top: 5px;
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
animation: cloudTravelling 16s linear infinite;
|
||||
}
|
||||
|
||||
.cloudTwo {
|
||||
composes: cloud;
|
||||
composes: cloud;
|
||||
|
||||
top: 65px;
|
||||
right: -30%;
|
||||
width: 50px;
|
||||
height: 16px;
|
||||
animation: cloudTravelling 21s 5s linear infinite;
|
||||
top: 65px;
|
||||
right: -30%;
|
||||
width: 50px;
|
||||
height: 16px;
|
||||
animation: cloudTravelling 21s 5s linear infinite;
|
||||
}
|
||||
|
||||
.cloudThree {
|
||||
composes: cloud;
|
||||
composes: cloud;
|
||||
|
||||
top: 40px;
|
||||
right: -30%;
|
||||
width: 70px;
|
||||
height: 22px;
|
||||
animation: cloudTravelling 26s 11s linear infinite;
|
||||
top: 40px;
|
||||
right: -30%;
|
||||
width: 70px;
|
||||
height: 22px;
|
||||
animation: cloudTravelling 26s 11s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes cloudTravelling {
|
||||
0% {
|
||||
right: -30%;
|
||||
}
|
||||
100% {
|
||||
right: 110%;
|
||||
}
|
||||
0% {
|
||||
right: -30%;
|
||||
}
|
||||
100% {
|
||||
right: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloudStab {
|
||||
0% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
transform: translate3D(0, 2px, 0);
|
||||
}
|
||||
85% {
|
||||
transform: translate3D(0, 2px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
0% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
transform: translate3D(0, 2px, 0);
|
||||
}
|
||||
85% {
|
||||
transform: translate3D(0, 2px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3D(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
%text {
|
||||
font-family: $font-family-title;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
line-height: 1.2;
|
||||
font-family: $font-family-title;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.text {
|
||||
@extend %text;
|
||||
@extend %text;
|
||||
|
||||
font-size: 24px;
|
||||
margin-top: 25px;
|
||||
color: #666;
|
||||
font-size: 24px;
|
||||
margin-top: 25px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.subText {
|
||||
@extend %text;
|
||||
@extend %text;
|
||||
|
||||
font-size: 16px;
|
||||
margin-top: 5px;
|
||||
color: #9a9a9a;
|
||||
font-size: 16px;
|
||||
margin-top: 5px;
|
||||
color: #9a9a9a;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Page not found",
|
||||
"nothingHere": "This is not a place that you are looking for",
|
||||
"returnToTheHomePage": "Try to go back to the {link}",
|
||||
"homePage": "main page"
|
||||
"title": "Page not found",
|
||||
"nothingHere": "This is not a place that you are looking for",
|
||||
"returnToTheHomePage": "Try to go back to the {link}",
|
||||
"homePage": "main page"
|
||||
}
|
||||
|
||||
@@ -9,47 +9,45 @@ import messages from './PageNotFound.intl.json';
|
||||
import profileStyles from '../profile/profile.scss';
|
||||
|
||||
const PageNotFound: ComponentType = () => (
|
||||
<div className={styles.page}>
|
||||
<Message {...messages.title}>
|
||||
{(pageTitle) => <Helmet title={pageTitle as string} />}
|
||||
</Message>
|
||||
<div className={styles.page}>
|
||||
<Message {...messages.title}>{(pageTitle) => <Helmet title={pageTitle as string} />}</Message>
|
||||
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.cube} />
|
||||
<div className={styles.road} />
|
||||
<div className={styles.rocks}>
|
||||
<span className={styles.rockOne} />
|
||||
<span className={styles.rockTwo} />
|
||||
<span className={styles.rockThree} />
|
||||
<span className={styles.rockFour} />
|
||||
<span className={styles.rockFive} />
|
||||
</div>
|
||||
<div className={styles.clouds}>
|
||||
<span className={styles.cloudOne} />
|
||||
<span className={styles.cloudTwo} />
|
||||
<span className={styles.cloudThree} />
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.text}>
|
||||
<Message {...messages.nothingHere} />
|
||||
</p>
|
||||
<p className={styles.subText}>
|
||||
<Message
|
||||
{...messages.returnToTheHomePage}
|
||||
values={{
|
||||
link: (
|
||||
<Link to="/">
|
||||
<Message {...messages.homePage} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.cube} />
|
||||
<div className={styles.road} />
|
||||
<div className={styles.rocks}>
|
||||
<span className={styles.rockOne} />
|
||||
<span className={styles.rockTwo} />
|
||||
<span className={styles.rockThree} />
|
||||
<span className={styles.rockFour} />
|
||||
<span className={styles.rockFive} />
|
||||
</div>
|
||||
<div className={styles.clouds}>
|
||||
<span className={styles.cloudOne} />
|
||||
<span className={styles.cloudTwo} />
|
||||
<span className={styles.cloudThree} />
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.text}>
|
||||
<Message {...messages.nothingHere} />
|
||||
</p>
|
||||
<p className={styles.subText}>
|
||||
<Message
|
||||
{...messages.returnToTheHomePage}
|
||||
values={{
|
||||
link: (
|
||||
<Link to="/">
|
||||
<Message {...messages.homePage} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div className={profileStyles.footer}>
|
||||
<FooterMenu />
|
||||
<div className={profileStyles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PageNotFound;
|
||||
|
||||
@@ -29,82 +29,54 @@ import styles from './auth.scss';
|
||||
let isSidebarHiddenCache = false;
|
||||
|
||||
const AuthPage: ComponentType = () => {
|
||||
const [isSidebarHidden, setIsSidebarHidden] = useState<boolean>(
|
||||
isSidebarHiddenCache,
|
||||
);
|
||||
const client = useSelector((state: RootState) => state.auth.client);
|
||||
const [isSidebarHidden, setIsSidebarHidden] = useState<boolean>(isSidebarHiddenCache);
|
||||
const client = useSelector((state: RootState) => state.auth.client);
|
||||
|
||||
const goToAuth = useCallback(() => {
|
||||
isSidebarHiddenCache = true;
|
||||
setIsSidebarHidden(true);
|
||||
}, []);
|
||||
const goToAuth = useCallback(() => {
|
||||
isSidebarHiddenCache = true;
|
||||
setIsSidebarHidden(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={isSidebarHidden ? styles.hiddenSidebar : styles.sidebar}>
|
||||
<AppInfo {...client} onGoToAuth={goToAuth} />
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<div className={isSidebarHidden ? styles.hiddenSidebar : styles.sidebar}>
|
||||
<AppInfo {...client} onGoToAuth={goToAuth} />
|
||||
</div>
|
||||
|
||||
<div className={styles.content} data-e2e-content>
|
||||
<Switch>
|
||||
<Route path="/login" render={renderPanelTransition(Login)} />
|
||||
<Route path="/mfa" render={renderPanelTransition(Mfa)} />
|
||||
<Route path="/password" render={renderPanelTransition(Password)} />
|
||||
<Route path="/register" render={renderPanelTransition(Register)} />
|
||||
<Route
|
||||
path="/activation/:key?"
|
||||
render={renderPanelTransition(Activation)}
|
||||
/>
|
||||
<Route
|
||||
path="/resend-activation"
|
||||
render={renderPanelTransition(ResendActivation)}
|
||||
/>
|
||||
<Route
|
||||
path="/oauth/permissions"
|
||||
render={renderPanelTransition(Permissions)}
|
||||
/>
|
||||
<Route
|
||||
path="/choose-account"
|
||||
render={renderPanelTransition(ChooseAccount)}
|
||||
/>
|
||||
<Route
|
||||
path="/oauth/choose-account"
|
||||
render={renderPanelTransition(ChooseAccount)}
|
||||
/>
|
||||
<Route path="/oauth/finish" component={Finish} />
|
||||
<Route
|
||||
path="/accept-rules"
|
||||
render={renderPanelTransition(AcceptRules)}
|
||||
/>
|
||||
<Route
|
||||
path="/forgot-password"
|
||||
render={renderPanelTransition(ForgotPassword)}
|
||||
/>
|
||||
<Route
|
||||
path="/recover-password/:key?"
|
||||
render={renderPanelTransition(RecoverPassword)}
|
||||
/>
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={styles.content} data-e2e-content>
|
||||
<Switch>
|
||||
<Route path="/login" render={renderPanelTransition(Login)} />
|
||||
<Route path="/mfa" render={renderPanelTransition(Mfa)} />
|
||||
<Route path="/password" render={renderPanelTransition(Password)} />
|
||||
<Route path="/register" render={renderPanelTransition(Register)} />
|
||||
<Route path="/activation/:key?" render={renderPanelTransition(Activation)} />
|
||||
<Route path="/resend-activation" render={renderPanelTransition(ResendActivation)} />
|
||||
<Route path="/oauth/permissions" render={renderPanelTransition(Permissions)} />
|
||||
<Route path="/choose-account" render={renderPanelTransition(ChooseAccount)} />
|
||||
<Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} />
|
||||
<Route path="/oauth/finish" component={Finish} />
|
||||
<Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} />
|
||||
<Route path="/forgot-password" render={renderPanelTransition(ForgotPassword)} />
|
||||
<Route path="/recover-password/:key?" render={renderPanelTransition(RecoverPassword)} />
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function renderPanelTransition(
|
||||
factory: Factory,
|
||||
): (props: RouteComponentProps<any>) => ReactNode {
|
||||
const { Title, Body, Footer, Links } = factory();
|
||||
function renderPanelTransition(factory: Factory): (props: RouteComponentProps<any>) => ReactNode {
|
||||
const { Title, Body, Footer, Links } = factory();
|
||||
|
||||
return (props) => (
|
||||
<PanelTransition
|
||||
key="panel-transition"
|
||||
Title={<Title />}
|
||||
Body={<Body {...props} />}
|
||||
Footer={<Footer />}
|
||||
Links={<Links />}
|
||||
/>
|
||||
);
|
||||
return (props) => (
|
||||
<PanelTransition
|
||||
key="panel-transition"
|
||||
Title={<Title />}
|
||||
Body={<Body {...props} />}
|
||||
Footer={<Footer />}
|
||||
Links={<Links />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthPage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"title": "Authorization successful",
|
||||
"applicationAuth": "Application authorization",
|
||||
"authorizationSuccessful": "Authorization has been successfully completed.",
|
||||
"authorizationForAppSuccessful": "Authorization for {appName} has been successfully completed.",
|
||||
"youCanCloseThisPage": "You can close this window and return to your application."
|
||||
"title": "Authorization successful",
|
||||
"applicationAuth": "Application authorization",
|
||||
"authorizationSuccessful": "Authorization has been successfully completed.",
|
||||
"authorizationForAppSuccessful": "Authorization for {appName} has been successfully completed.",
|
||||
"youCanCloseThisPage": "You can close this window and return to your application."
|
||||
}
|
||||
|
||||
@@ -10,68 +10,66 @@ import styles from './success-oauth.scss';
|
||||
import messages from './SuccessOauthPage.intl.json';
|
||||
|
||||
export default class SuccessOauthPage extends React.Component<{
|
||||
location: {
|
||||
query: Query<'appName'>;
|
||||
};
|
||||
location: {
|
||||
query: Query<'appName'>;
|
||||
};
|
||||
}> {
|
||||
componentDidMount() {
|
||||
this.onPageUpdate();
|
||||
componentDidMount() {
|
||||
this.onPageUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// try to close window if possible
|
||||
// @ts-ignore
|
||||
window.open('', '_self').close();
|
||||
} catch (err) {
|
||||
// don't care
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// try to close window if possible
|
||||
// @ts-ignore
|
||||
window.open('', '_self').close();
|
||||
} catch (err) {
|
||||
// don't care
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
onPageUpdate() {
|
||||
loader.hide();
|
||||
}
|
||||
onPageUpdate() {
|
||||
loader.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const appName = this.props.location.query.get('appName');
|
||||
render() {
|
||||
const appName = this.props.location.query.get('appName');
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Message {...messages.title}>
|
||||
{(pageTitle) => <Helmet title={pageTitle as string} />}
|
||||
</Message>
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Message {...messages.title}>{(pageTitle) => <Helmet title={pageTitle as string} />}</Message>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<Link to="/" className={styles.logo}>
|
||||
<Message {...rootMessages.siteName} />
|
||||
</Link>
|
||||
<div className={styles.wrapper}>
|
||||
<Link to="/" className={styles.logo}>
|
||||
<Message {...rootMessages.siteName} />
|
||||
</Link>
|
||||
|
||||
<div className={styles.title}>
|
||||
<Message {...messages.applicationAuth} />
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<Message {...messages.applicationAuth} />
|
||||
</div>
|
||||
|
||||
<div className={styles.checkmark} />
|
||||
<div className={styles.checkmark} />
|
||||
|
||||
<div className={styles.description}>
|
||||
{appName ? (
|
||||
<Message
|
||||
{...messages.authorizationForAppSuccessful}
|
||||
values={{
|
||||
appName: <b>{appName}</b>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Message {...messages.authorizationSuccessful} />
|
||||
)}
|
||||
|
||||
<Message {...messages.youCanCloseThisPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={styles.description}>
|
||||
{appName ? (
|
||||
<Message
|
||||
{...messages.authorizationForAppSuccessful}
|
||||
values={{
|
||||
appName: <b>{appName}</b>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Message {...messages.authorizationSuccessful} />
|
||||
)}
|
||||
|
||||
<Message {...messages.youCanCloseThisPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,47 +3,47 @@
|
||||
$sidebar-width: 320px;
|
||||
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 50px;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 50px;
|
||||
z-index: 10;
|
||||
|
||||
background: $black;
|
||||
background: $black;
|
||||
}
|
||||
|
||||
.hiddenSidebar {
|
||||
composes: sidebar;
|
||||
composes: sidebar;
|
||||
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
max-width: 340px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
max-width: 340px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 350px) {
|
||||
.content {
|
||||
padding: 55px 0;
|
||||
}
|
||||
.content {
|
||||
padding: 55px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.content {
|
||||
padding: 55px 50px;
|
||||
margin-left: $sidebar-width;
|
||||
}
|
||||
.content {
|
||||
padding: 55px 50px;
|
||||
margin-left: $sidebar-width;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
right: auto;
|
||||
.sidebar {
|
||||
right: auto;
|
||||
|
||||
width: $sidebar-width;
|
||||
}
|
||||
width: $sidebar-width;
|
||||
}
|
||||
|
||||
.hiddenSidebar {
|
||||
display: block;
|
||||
}
|
||||
.hiddenSidebar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,69 +2,69 @@
|
||||
@import '~app/components/ui/colors.scss';
|
||||
|
||||
.page {
|
||||
border-top: 50px solid #ddd8ce;
|
||||
border-top: 50px solid #ddd8ce;
|
||||
|
||||
padding: 85px 10px;
|
||||
padding: 85px 10px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
position: relative;
|
||||
|
||||
margin: 0 auto;
|
||||
padding: 55px 25px;
|
||||
max-width: 330px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: 55px 25px;
|
||||
max-width: 330px;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
border: 3px solid #ddd8ce;
|
||||
background: #fff;
|
||||
border: 3px solid #ddd8ce;
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
$borderWidth: 3px;
|
||||
$borderWidth: 3px;
|
||||
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
padding: 0 20px;
|
||||
padding: 0 20px;
|
||||
|
||||
font-family: $font-family-title;
|
||||
font-size: 33px;
|
||||
line-height: 50px - $borderWidth * 2;
|
||||
color: #fff;
|
||||
background: $green;
|
||||
border: 3px solid darker($green);
|
||||
|
||||
&:hover {
|
||||
font-family: $font-family-title;
|
||||
font-size: 33px;
|
||||
line-height: 50px - $borderWidth * 2;
|
||||
color: #fff;
|
||||
background: $green;
|
||||
border: 3px solid darker($green);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: $green;
|
||||
border: 3px solid darker($green);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
|
||||
color: lighter($green);
|
||||
font-size: 66px;
|
||||
margin-bottom: 28px;
|
||||
color: lighter($green);
|
||||
font-size: 66px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
color: #9a9a9a;
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
color: #9a9a9a;
|
||||
line-height: 1.4;
|
||||
|
||||
b {
|
||||
color: #666;
|
||||
}
|
||||
b {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +1,87 @@
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchAvailableApps,
|
||||
resetApp,
|
||||
deleteApp,
|
||||
} from 'app/components/dev/apps/actions';
|
||||
import { fetchAvailableApps, resetApp, deleteApp } from 'app/components/dev/apps/actions';
|
||||
import ApplicationsIndex from 'app/components/dev/apps/ApplicationsIndex';
|
||||
import { User } from 'app/components/user';
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
user: User;
|
||||
apps: OauthAppResponse[];
|
||||
fetchAvailableApps: () => Promise<void>;
|
||||
deleteApp: (clientId: string) => Promise<void>;
|
||||
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<void>;
|
||||
user: User;
|
||||
apps: OauthAppResponse[];
|
||||
fetchAvailableApps: () => Promise<void>;
|
||||
deleteApp: (clientId: string) => Promise<void>;
|
||||
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
type State = {
|
||||
isLoading: boolean;
|
||||
forceUpdate: boolean;
|
||||
isLoading: boolean;
|
||||
forceUpdate: boolean;
|
||||
};
|
||||
|
||||
class ApplicationsListPage extends React.Component<Props, State> {
|
||||
state = {
|
||||
isLoading: false,
|
||||
forceUpdate: false,
|
||||
};
|
||||
state = {
|
||||
isLoading: false,
|
||||
forceUpdate: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
!this.props.user.isGuest && this.loadApplicationsList();
|
||||
}
|
||||
|
||||
componentDidUpdate({ user }: Props) {
|
||||
if (this.props.user !== user) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ forceUpdate: true });
|
||||
this.loadApplicationsList();
|
||||
componentDidMount() {
|
||||
!this.props.user.isGuest && this.loadApplicationsList();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, apps, resetApp, deleteApp, location } = this.props;
|
||||
const { isLoading, forceUpdate } = this.state;
|
||||
const clientId = location.hash.substr(1) || null;
|
||||
|
||||
return (
|
||||
<ApplicationsIndex
|
||||
displayForGuest={user.isGuest}
|
||||
applications={forceUpdate ? [] : apps}
|
||||
isLoading={isLoading}
|
||||
deleteApp={deleteApp}
|
||||
resetApp={resetApp}
|
||||
clientId={clientId}
|
||||
resetClientId={this.resetClientId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
loadApplicationsList = async () => {
|
||||
this.setState({ isLoading: true });
|
||||
await this.props.fetchAvailableApps();
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
forceUpdate: false,
|
||||
});
|
||||
};
|
||||
|
||||
resetClientId = () => {
|
||||
const { history, location } = this.props;
|
||||
|
||||
if (location.hash) {
|
||||
history.push({ ...location, hash: '' });
|
||||
componentDidUpdate({ user }: Props) {
|
||||
if (this.props.user !== user) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ forceUpdate: true });
|
||||
this.loadApplicationsList();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { user, apps, resetApp, deleteApp, location } = this.props;
|
||||
const { isLoading, forceUpdate } = this.state;
|
||||
const clientId = location.hash.substr(1) || null;
|
||||
|
||||
return (
|
||||
<ApplicationsIndex
|
||||
displayForGuest={user.isGuest}
|
||||
applications={forceUpdate ? [] : apps}
|
||||
isLoading={isLoading}
|
||||
deleteApp={deleteApp}
|
||||
resetApp={resetApp}
|
||||
clientId={clientId}
|
||||
resetClientId={this.resetClientId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
loadApplicationsList = async () => {
|
||||
this.setState({ isLoading: true });
|
||||
await this.props.fetchAvailableApps();
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
forceUpdate: false,
|
||||
});
|
||||
};
|
||||
|
||||
resetClientId = () => {
|
||||
const { history, location } = this.props;
|
||||
|
||||
if (location.hash) {
|
||||
history.push({ ...location, hash: '' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: RootState) => ({
|
||||
user: state.user,
|
||||
apps: state.apps.available,
|
||||
}),
|
||||
{
|
||||
fetchAvailableApps,
|
||||
resetApp,
|
||||
deleteApp,
|
||||
},
|
||||
(state: RootState) => ({
|
||||
user: state.user,
|
||||
apps: state.apps.available,
|
||||
}),
|
||||
{
|
||||
fetchAvailableApps,
|
||||
resetApp,
|
||||
deleteApp,
|
||||
},
|
||||
)(ApplicationsListPage);
|
||||
|
||||
@@ -7,63 +7,62 @@ import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import { ApplicationType } from 'app/components/dev/apps';
|
||||
|
||||
const app: OauthAppResponse = {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
countUsers: 0,
|
||||
createdAt: 0,
|
||||
type: 'application',
|
||||
name: '',
|
||||
description: '',
|
||||
websiteUrl: '',
|
||||
redirectUri: '',
|
||||
minecraftServerIp: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
countUsers: 0,
|
||||
createdAt: 0,
|
||||
type: 'application',
|
||||
name: '',
|
||||
description: '',
|
||||
websiteUrl: '',
|
||||
redirectUri: '',
|
||||
minecraftServerIp: '',
|
||||
};
|
||||
|
||||
interface State {
|
||||
type: ApplicationType | null;
|
||||
type: ApplicationType | null;
|
||||
}
|
||||
|
||||
export default class CreateNewApplicationPage extends Component<{}, State> {
|
||||
state: State = {
|
||||
type: null,
|
||||
};
|
||||
state: State = {
|
||||
type: null,
|
||||
};
|
||||
|
||||
form: FormModel = new FormModel();
|
||||
form: FormModel = new FormModel();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ApplicationForm
|
||||
form={this.form}
|
||||
displayTypeSwitcher
|
||||
onSubmit={this.onSubmit}
|
||||
type={this.state.type}
|
||||
setType={this.setType}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit = async () => {
|
||||
const { form } = this;
|
||||
const { type } = this.state;
|
||||
|
||||
if (!type) {
|
||||
throw new Error('Form was submitted without specified type');
|
||||
render() {
|
||||
return (
|
||||
<ApplicationForm
|
||||
form={this.form}
|
||||
displayTypeSwitcher
|
||||
onSubmit={this.onSubmit}
|
||||
type={this.state.type}
|
||||
setType={this.setType}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
form.beginLoading();
|
||||
const result = await oauth.create(type, form.serialize());
|
||||
form.endLoading();
|
||||
onSubmit = async () => {
|
||||
const { form } = this;
|
||||
const { type } = this.state;
|
||||
|
||||
this.goToMainPage(result.data.clientId);
|
||||
};
|
||||
if (!type) {
|
||||
throw new Error('Form was submitted without specified type');
|
||||
}
|
||||
|
||||
setType = (type: ApplicationType) => {
|
||||
this.setState({
|
||||
type,
|
||||
});
|
||||
};
|
||||
form.beginLoading();
|
||||
const result = await oauth.create(type, form.serialize());
|
||||
form.endLoading();
|
||||
|
||||
goToMainPage = (hash?: string) =>
|
||||
browserHistory.push(`/dev/applications${hash ? `#${hash}` : ''}`);
|
||||
this.goToMainPage(result.data.clientId);
|
||||
};
|
||||
|
||||
setType = (type: ApplicationType) => {
|
||||
this.setState({
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
goToMainPage = (hash?: string) => browserHistory.push(`/dev/applications${hash ? `#${hash}` : ''}`);
|
||||
}
|
||||
|
||||
@@ -9,31 +9,20 @@ import CreateNewApplicationPage from './CreateNewApplicationPage';
|
||||
import UpdateApplicationPage from './UpdateApplicationPage';
|
||||
|
||||
const DevPage: ComponentType = () => (
|
||||
<div className={styles.container}>
|
||||
<div data-e2e-content>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/dev/applications"
|
||||
exact
|
||||
component={ApplicationsListPage}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/dev/applications/new"
|
||||
exact
|
||||
component={CreateNewApplicationPage}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/dev/applications/:clientId"
|
||||
component={UpdateApplicationPage}
|
||||
/>
|
||||
<Redirect to="/dev/applications" />
|
||||
</Switch>
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div data-e2e-content>
|
||||
<Switch>
|
||||
<Route path="/dev/applications" exact component={ApplicationsListPage} />
|
||||
<PrivateRoute path="/dev/applications/new" exact component={CreateNewApplicationPage} />
|
||||
<PrivateRoute path="/dev/applications/:clientId" component={UpdateApplicationPage} />
|
||||
<Redirect to="/dev/applications" />
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DevPage;
|
||||
|
||||
@@ -7,114 +7,103 @@ import { browserHistory } from 'app/services/history';
|
||||
import oauth from 'app/services/api/oauth';
|
||||
import * as loader from 'app/services/loader';
|
||||
import PageNotFound from 'app/pages/404/PageNotFound';
|
||||
import {
|
||||
getApp,
|
||||
fetchApp as fetchAppAction,
|
||||
} from 'app/components/dev/apps/actions';
|
||||
import { getApp, fetchApp as fetchAppAction } from 'app/components/dev/apps/actions';
|
||||
import ApplicationForm from 'app/components/dev/apps/applicationForm/ApplicationForm';
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
type OwnProps = RouteComponentProps<{
|
||||
clientId: string;
|
||||
clientId: string;
|
||||
}>;
|
||||
|
||||
interface Props extends OwnProps {
|
||||
app: OauthAppResponse | null;
|
||||
fetchApp: (app: string) => Promise<void>;
|
||||
app: OauthAppResponse | null;
|
||||
fetchApp: (app: string) => Promise<void>;
|
||||
}
|
||||
|
||||
class UpdateApplicationPage extends React.Component<
|
||||
Props,
|
||||
{
|
||||
isNotFound: boolean;
|
||||
}
|
||||
Props,
|
||||
{
|
||||
isNotFound: boolean;
|
||||
}
|
||||
> {
|
||||
form: FormModel = new FormModel();
|
||||
form: FormModel = new FormModel();
|
||||
|
||||
state = {
|
||||
isNotFound: false,
|
||||
};
|
||||
state = {
|
||||
isNotFound: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.app === null && this.fetchApp();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { app } = this.props;
|
||||
|
||||
if (this.state.isNotFound) {
|
||||
return <PageNotFound />;
|
||||
componentDidMount() {
|
||||
this.props.app === null && this.fetchApp();
|
||||
}
|
||||
|
||||
if (!app) {
|
||||
// we are loading
|
||||
return null;
|
||||
render() {
|
||||
const { app } = this.props;
|
||||
|
||||
if (this.state.isNotFound) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
if (!app) {
|
||||
// we are loading
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ApplicationForm form={this.form} onSubmit={this.onSubmit} app={app} type={app.type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ApplicationForm
|
||||
form={this.form}
|
||||
onSubmit={this.onSubmit}
|
||||
app={app}
|
||||
type={app.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
async fetchApp() {
|
||||
const { fetchApp, match } = this.props;
|
||||
|
||||
async fetchApp() {
|
||||
const { fetchApp, match } = this.props;
|
||||
try {
|
||||
loader.show();
|
||||
await fetchApp(match.params.clientId);
|
||||
} catch (resp) {
|
||||
const { status } = resp.originalResponse;
|
||||
|
||||
try {
|
||||
loader.show();
|
||||
await fetchApp(match.params.clientId);
|
||||
} catch (resp) {
|
||||
const { status } = resp.originalResponse;
|
||||
if (status === 403) {
|
||||
this.goToMainPage();
|
||||
|
||||
if (status === 403) {
|
||||
this.goToMainPage();
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (status === 404) {
|
||||
this.setState({
|
||||
isNotFound: true,
|
||||
});
|
||||
|
||||
if (status === 404) {
|
||||
this.setState({
|
||||
isNotFound: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.unexpected('Error fetching app', resp);
|
||||
} finally {
|
||||
loader.hide();
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = async () => {
|
||||
const { form } = this;
|
||||
const { app } = this.props;
|
||||
|
||||
if (!app || !app.clientId) {
|
||||
throw new Error('Form has an invalid state');
|
||||
logger.unexpected('Error fetching app', resp);
|
||||
} finally {
|
||||
loader.hide();
|
||||
}
|
||||
}
|
||||
|
||||
form.beginLoading();
|
||||
const result = await oauth.update(app.clientId, form.serialize());
|
||||
form.endLoading();
|
||||
onSubmit = async () => {
|
||||
const { form } = this;
|
||||
const { app } = this.props;
|
||||
|
||||
this.goToMainPage(result.data.clientId);
|
||||
};
|
||||
if (!app || !app.clientId) {
|
||||
throw new Error('Form has an invalid state');
|
||||
}
|
||||
|
||||
goToMainPage = (hash?: string) =>
|
||||
browserHistory.push(`/dev/applications${hash ? `#${hash}` : ''}`);
|
||||
form.beginLoading();
|
||||
const result = await oauth.update(app.clientId, form.serialize());
|
||||
form.endLoading();
|
||||
|
||||
this.goToMainPage(result.data.clientId);
|
||||
};
|
||||
|
||||
goToMainPage = (hash?: string) => browserHistory.push(`/dev/applications${hash ? `#${hash}` : ''}`);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: RootState, props: OwnProps) => ({
|
||||
app: getApp(state, props.match.params.clientId),
|
||||
}),
|
||||
{
|
||||
fetchApp: fetchAppAction,
|
||||
},
|
||||
(state: RootState, props: OwnProps) => ({
|
||||
app: getApp(state, props.match.params.clientId),
|
||||
}),
|
||||
{
|
||||
fetchApp: fetchAppAction,
|
||||
},
|
||||
)(UpdateApplicationPage);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
.container {
|
||||
padding: 55px 0 65px;
|
||||
padding: 55px 0 65px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,116 +2,99 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { RouteComponentProps, Redirect } from 'react-router-dom';
|
||||
import FormModel from 'app/components/ui/form/FormModel';
|
||||
import ChangeEmail, {
|
||||
ChangeEmailStep,
|
||||
} from 'app/components/profile/changeEmail/ChangeEmail';
|
||||
import {
|
||||
requestEmailChange,
|
||||
setNewEmail,
|
||||
confirmNewEmail,
|
||||
} from 'app/services/api/accounts';
|
||||
import ChangeEmail, { ChangeEmailStep } from 'app/components/profile/changeEmail/ChangeEmail';
|
||||
import { requestEmailChange, setNewEmail, confirmNewEmail } from 'app/services/api/accounts';
|
||||
import { RootState } from 'app/reducers';
|
||||
import Context from 'app/components/profile/Context';
|
||||
|
||||
interface RouteParams {
|
||||
step: 'step1' | 'step2' | 'step3';
|
||||
code: string;
|
||||
step: 'step1' | 'step2' | 'step3';
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<RouteParams> {
|
||||
lang: string;
|
||||
email: string;
|
||||
lang: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
class ChangeEmailPage extends React.Component<Props> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
|
||||
render() {
|
||||
const { step = 'step1', code } = this.props.match.params;
|
||||
render() {
|
||||
const { step = 'step1', code } = this.props.match.params;
|
||||
|
||||
if (step && !/^step[123]$/.test(step)) {
|
||||
// wrong param value
|
||||
return <Redirect to="/404" />;
|
||||
if (step && !/^step[123]$/.test(step)) {
|
||||
// wrong param value
|
||||
return <Redirect to="/404" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChangeEmail
|
||||
onSubmit={this.onSubmit}
|
||||
email={this.props.email}
|
||||
lang={this.props.lang}
|
||||
step={(Number(step.slice(-1)) - 1) as ChangeEmailStep}
|
||||
onChangeStep={this.onChangeStep}
|
||||
code={code}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChangeEmail
|
||||
onSubmit={this.onSubmit}
|
||||
email={this.props.email}
|
||||
lang={this.props.lang}
|
||||
step={(Number(step.slice(-1)) - 1) as ChangeEmailStep}
|
||||
onChangeStep={this.onChangeStep}
|
||||
code={code}
|
||||
/>
|
||||
);
|
||||
}
|
||||
onChangeStep = (step: number) => {
|
||||
this.props.history.push(`/profile/change-email/step${++step}`);
|
||||
};
|
||||
|
||||
onChangeStep = (step: number) => {
|
||||
this.props.history.push(`/profile/change-email/step${++step}`);
|
||||
};
|
||||
onSubmit = (step: number, form: FormModel): Promise<void> => {
|
||||
return this.context
|
||||
.onSubmit({
|
||||
form,
|
||||
sendData: () => {
|
||||
const { userId } = this.context;
|
||||
const data = form.serialize();
|
||||
|
||||
onSubmit = (step: number, form: FormModel): Promise<void> => {
|
||||
return this.context
|
||||
.onSubmit({
|
||||
form,
|
||||
sendData: () => {
|
||||
const { userId } = this.context;
|
||||
const data = form.serialize();
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
return requestEmailChange(userId, data.password).catch(
|
||||
handleErrors(),
|
||||
);
|
||||
case 1:
|
||||
return setNewEmail(userId, data.email, data.key).catch(
|
||||
handleErrors('/profile/change-email'),
|
||||
);
|
||||
case 2:
|
||||
return confirmNewEmail(userId, data.key).catch(
|
||||
handleErrors('/profile/change-email'),
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unsupported step ${step}`);
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
step > 1 && this.context.goToProfile();
|
||||
});
|
||||
};
|
||||
switch (step) {
|
||||
case 0:
|
||||
return requestEmailChange(userId, data.password).catch(handleErrors());
|
||||
case 1:
|
||||
return setNewEmail(userId, data.email, data.key).catch(
|
||||
handleErrors('/profile/change-email'),
|
||||
);
|
||||
case 2:
|
||||
return confirmNewEmail(userId, data.key).catch(handleErrors('/profile/change-email'));
|
||||
default:
|
||||
throw new Error(`Unsupported step ${step}`);
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
step > 1 && this.context.goToProfile();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleErrors(
|
||||
repeatUrl?: string,
|
||||
): <T extends { errors: Record<string, any> }>(resp: T) => Promise<T> {
|
||||
return (resp) => {
|
||||
if (resp.errors) {
|
||||
if (resp.errors.key) {
|
||||
resp.errors.key = {
|
||||
type: resp.errors.key,
|
||||
payload: {},
|
||||
};
|
||||
function handleErrors(repeatUrl?: string): <T extends { errors: Record<string, any> }>(resp: T) => Promise<T> {
|
||||
return (resp) => {
|
||||
if (resp.errors) {
|
||||
if (resp.errors.key) {
|
||||
resp.errors.key = {
|
||||
type: resp.errors.key,
|
||||
payload: {},
|
||||
};
|
||||
|
||||
if (
|
||||
['error.key_not_exists', 'error.key_expire'].includes(
|
||||
resp.errors.key.type,
|
||||
) &&
|
||||
repeatUrl
|
||||
) {
|
||||
Object.assign(resp.errors.key.payload, {
|
||||
repeatUrl,
|
||||
});
|
||||
if (['error.key_not_exists', 'error.key_expire'].includes(resp.errors.key.type) && repeatUrl) {
|
||||
Object.assign(resp.errors.key.payload, {
|
||||
repeatUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
};
|
||||
return Promise.reject(resp);
|
||||
};
|
||||
}
|
||||
|
||||
export default connect((state: RootState) => ({
|
||||
email: state.user.email,
|
||||
lang: state.user.lang,
|
||||
email: state.user.email,
|
||||
lang: state.user.lang,
|
||||
}))(ChangeEmailPage);
|
||||
|
||||
@@ -8,36 +8,36 @@ import { updateUser } from 'app/components/user/actions';
|
||||
import Context from 'app/components/profile/Context';
|
||||
|
||||
interface Props {
|
||||
updateUser: (fields: Partial<User>) => void;
|
||||
updateUser: (fields: Partial<User>) => void;
|
||||
}
|
||||
|
||||
class ChangePasswordPage extends React.Component<Props> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
|
||||
form = new FormModel();
|
||||
form = new FormModel();
|
||||
|
||||
render() {
|
||||
return <ChangePassword onSubmit={this.onSubmit} form={this.form} />;
|
||||
}
|
||||
render() {
|
||||
return <ChangePassword onSubmit={this.onSubmit} form={this.form} />;
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { form } = this;
|
||||
onSubmit = () => {
|
||||
const { form } = this;
|
||||
|
||||
return this.context
|
||||
.onSubmit({
|
||||
form,
|
||||
sendData: () => changePassword(this.context.userId, form.serialize()),
|
||||
})
|
||||
.then(() => {
|
||||
this.props.updateUser({
|
||||
passwordChangedAt: Date.now() / 1000,
|
||||
});
|
||||
this.context.goToProfile();
|
||||
});
|
||||
};
|
||||
return this.context
|
||||
.onSubmit({
|
||||
form,
|
||||
sendData: () => changePassword(this.context.userId, form.serialize()),
|
||||
})
|
||||
.then(() => {
|
||||
this.props.updateUser({
|
||||
passwordChangedAt: Date.now() / 1000,
|
||||
});
|
||||
this.context.goToProfile();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
updateUser,
|
||||
updateUser,
|
||||
})(ChangePasswordPage);
|
||||
|
||||
@@ -8,68 +8,68 @@ import ChangeUsername from 'app/components/profile/changeUsername/ChangeUsername
|
||||
import Context from 'app/components/profile/Context';
|
||||
|
||||
type Props = {
|
||||
username: string;
|
||||
updateUsername: (username: string) => void;
|
||||
username: string;
|
||||
updateUsername: (username: string) => void;
|
||||
};
|
||||
|
||||
class ChangeUsernamePage extends React.Component<Props> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
|
||||
form = new FormModel();
|
||||
form = new FormModel();
|
||||
|
||||
actualUsername: string = this.props.username;
|
||||
actualUsername: string = this.props.username;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.updateUsername(this.actualUsername);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ChangeUsername
|
||||
form={this.form}
|
||||
onSubmit={this.onSubmit}
|
||||
onChange={this.onUsernameChange}
|
||||
username={this.props.username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onUsernameChange = (username: string) => {
|
||||
this.props.updateUsername(username);
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { form } = this;
|
||||
|
||||
if (this.actualUsername === this.props.username) {
|
||||
this.context.goToProfile();
|
||||
|
||||
return Promise.resolve();
|
||||
componentWillUnmount() {
|
||||
this.props.updateUsername(this.actualUsername);
|
||||
}
|
||||
|
||||
return this.context
|
||||
.onSubmit({
|
||||
form,
|
||||
sendData: () => {
|
||||
const { username, password } = form.serialize();
|
||||
render() {
|
||||
return (
|
||||
<ChangeUsername
|
||||
form={this.form}
|
||||
onSubmit={this.onSubmit}
|
||||
onChange={this.onUsernameChange}
|
||||
username={this.props.username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return changeUsername(this.context.userId, username, password);
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.actualUsername = form.value('username');
|
||||
onUsernameChange = (username: string) => {
|
||||
this.props.updateUsername(username);
|
||||
};
|
||||
|
||||
this.context.goToProfile();
|
||||
});
|
||||
};
|
||||
onSubmit = () => {
|
||||
const { form } = this;
|
||||
|
||||
if (this.actualUsername === this.props.username) {
|
||||
this.context.goToProfile();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.context
|
||||
.onSubmit({
|
||||
form,
|
||||
sendData: () => {
|
||||
const { username, password } = form.serialize();
|
||||
|
||||
return changeUsername(this.context.userId, username, password);
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.actualUsername = form.value('username');
|
||||
|
||||
this.context.goToProfile();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: RootState) => ({
|
||||
username: state.user.username,
|
||||
}),
|
||||
{
|
||||
updateUsername: (username: string) => updateUser({ username }),
|
||||
},
|
||||
(state: RootState) => ({
|
||||
username: state.user.username,
|
||||
}),
|
||||
{
|
||||
updateUsername: (username: string) => updateUser({ username }),
|
||||
},
|
||||
)(ChangeUsernamePage);
|
||||
|
||||
@@ -1,81 +1,77 @@
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, Redirect } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiFactorAuth, {
|
||||
MfaStep,
|
||||
} from 'app/components/profile/multiFactorAuth';
|
||||
import MultiFactorAuth, { MfaStep } from 'app/components/profile/multiFactorAuth';
|
||||
import { FormModel } from 'app/components/ui/form';
|
||||
import { User } from 'app/components/user';
|
||||
import { RootState } from 'app/reducers';
|
||||
import Context from 'app/components/profile/Context';
|
||||
|
||||
interface Props
|
||||
extends RouteComponentProps<{
|
||||
step?: '1' | '2' | '3';
|
||||
}> {
|
||||
user: User;
|
||||
extends RouteComponentProps<{
|
||||
step?: '1' | '2' | '3';
|
||||
}> {
|
||||
user: User;
|
||||
}
|
||||
|
||||
class MultiFactorAuthPage extends React.Component<Props> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
match: {
|
||||
params: { step },
|
||||
},
|
||||
} = this.props;
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
match: {
|
||||
params: { step },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (step) {
|
||||
if (!/^[1-3]$/.test(step)) {
|
||||
// wrong param value
|
||||
return <Redirect to="/404" />;
|
||||
}
|
||||
if (step) {
|
||||
if (!/^[1-3]$/.test(step)) {
|
||||
// wrong param value
|
||||
return <Redirect to="/404" />;
|
||||
}
|
||||
|
||||
if (user.isOtpEnabled) {
|
||||
return <Redirect to="/mfa" />;
|
||||
}
|
||||
if (user.isOtpEnabled) {
|
||||
return <Redirect to="/mfa" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiFactorAuth
|
||||
isMfaEnabled={user.isOtpEnabled}
|
||||
onSubmit={this.onSubmit}
|
||||
step={this.getStep()}
|
||||
onChangeStep={this.onChangeStep}
|
||||
onComplete={this.onComplete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiFactorAuth
|
||||
isMfaEnabled={user.isOtpEnabled}
|
||||
onSubmit={this.onSubmit}
|
||||
step={this.getStep()}
|
||||
onChangeStep={this.onChangeStep}
|
||||
onComplete={this.onComplete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
getStep(): MfaStep {
|
||||
const step = Number(this.props.match.params.step) - 1;
|
||||
|
||||
getStep(): MfaStep {
|
||||
const step = Number(this.props.match.params.step) - 1;
|
||||
if (step !== 0 && step !== 1 && step !== 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (step !== 0 && step !== 1 && step !== 2) {
|
||||
return 0;
|
||||
return step;
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
onChangeStep = (step: number) => {
|
||||
this.props.history.push(`/profile/mfa/step${step + 1}`);
|
||||
};
|
||||
|
||||
onChangeStep = (step: number) => {
|
||||
this.props.history.push(`/profile/mfa/step${step + 1}`);
|
||||
};
|
||||
onSubmit = (form: FormModel, sendData: () => Promise<void>) => {
|
||||
return this.context.onSubmit({
|
||||
form,
|
||||
sendData,
|
||||
});
|
||||
};
|
||||
|
||||
onSubmit = (form: FormModel, sendData: () => Promise<void>) => {
|
||||
return this.context.onSubmit({
|
||||
form,
|
||||
sendData,
|
||||
});
|
||||
};
|
||||
|
||||
onComplete = () => {
|
||||
this.context.goToProfile();
|
||||
};
|
||||
onComplete = () => {
|
||||
this.context.goToProfile();
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(({ user }: RootState) => ({ user }))(
|
||||
MultiFactorAuthPage,
|
||||
);
|
||||
export default connect(({ user }: RootState) => ({ user }))(MultiFactorAuthPage);
|
||||
|
||||
@@ -14,200 +14,160 @@ import { ComponentLoader } from 'app/components/ui/loader';
|
||||
|
||||
import styles from './profile.scss';
|
||||
|
||||
const Profile = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-profile-index" */ 'app/components/profile/Profile'
|
||||
),
|
||||
);
|
||||
const Profile = React.lazy(() => import(/* webpackChunkName: "page-profile-index" */ 'app/components/profile/Profile'));
|
||||
const ChangePasswordPage = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-profile-change-password" */ 'app/pages/profile/ChangePasswordPage'
|
||||
),
|
||||
import(/* webpackChunkName: "page-profile-change-password" */ 'app/pages/profile/ChangePasswordPage'),
|
||||
);
|
||||
const ChangeUsernamePage = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-profile-change-username" */ 'app/pages/profile/ChangeUsernamePage'
|
||||
),
|
||||
import(/* webpackChunkName: "page-profile-change-username" */ 'app/pages/profile/ChangeUsernamePage'),
|
||||
);
|
||||
const ChangeEmailPage = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-profile-change-email" */ 'app/pages/profile/ChangeEmailPage'
|
||||
),
|
||||
import(/* webpackChunkName: "page-profile-change-email" */ 'app/pages/profile/ChangeEmailPage'),
|
||||
);
|
||||
const MultiFactorAuthPage = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-profile-mfa" */ 'app/pages/profile/MultiFactorAuthPage'
|
||||
),
|
||||
import(/* webpackChunkName: "page-profile-mfa" */ 'app/pages/profile/MultiFactorAuthPage'),
|
||||
);
|
||||
|
||||
interface Props {
|
||||
userId: number;
|
||||
onSubmit: (options: {
|
||||
form: FormModel;
|
||||
sendData: () => Promise<any>;
|
||||
}) => Promise<void>;
|
||||
refreshUserData: () => Promise<any>;
|
||||
userId: number;
|
||||
onSubmit: (options: { form: FormModel; sendData: () => Promise<any> }) => Promise<void>;
|
||||
refreshUserData: () => Promise<any>;
|
||||
}
|
||||
|
||||
class ProfilePage extends React.Component<Props> {
|
||||
render() {
|
||||
const { userId, onSubmit } = this.props;
|
||||
render() {
|
||||
const { userId, onSubmit } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Provider
|
||||
value={{
|
||||
userId,
|
||||
onSubmit,
|
||||
goToProfile: this.goToProfile,
|
||||
}}
|
||||
>
|
||||
<React.Suspense fallback={<ComponentLoader />}>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/profile/mfa/step:step([1-3])"
|
||||
component={MultiFactorAuthPage}
|
||||
/>
|
||||
<Route
|
||||
path="/profile/mfa"
|
||||
exact
|
||||
component={MultiFactorAuthPage}
|
||||
/>
|
||||
<Route
|
||||
path="/profile/change-password"
|
||||
exact
|
||||
component={ChangePasswordPage}
|
||||
/>
|
||||
<Route
|
||||
path="/profile/change-username"
|
||||
exact
|
||||
component={ChangeUsernamePage}
|
||||
/>
|
||||
<Route
|
||||
path="/profile/change-email/:step?/:code?"
|
||||
component={ChangeEmailPage}
|
||||
/>
|
||||
<Route path="/profile" exact component={Profile} />
|
||||
<Route path="/" exact component={Profile} />
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Provider
|
||||
value={{
|
||||
userId,
|
||||
onSubmit,
|
||||
goToProfile: this.goToProfile,
|
||||
}}
|
||||
>
|
||||
<React.Suspense fallback={<ComponentLoader />}>
|
||||
<Switch>
|
||||
<Route path="/profile/mfa/step:step([1-3])" component={MultiFactorAuthPage} />
|
||||
<Route path="/profile/mfa" exact component={MultiFactorAuthPage} />
|
||||
<Route path="/profile/change-password" exact component={ChangePasswordPage} />
|
||||
<Route path="/profile/change-username" exact component={ChangeUsernamePage} />
|
||||
<Route path="/profile/change-email/:step?/:code?" component={ChangeEmailPage} />
|
||||
<Route path="/profile" exact component={Profile} />
|
||||
<Route path="/" exact component={Profile} />
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
goToProfile = async () => {
|
||||
await this.props.refreshUserData();
|
||||
goToProfile = async () => {
|
||||
await this.props.refreshUserData();
|
||||
|
||||
browserHistory.push('/');
|
||||
};
|
||||
browserHistory.push('/');
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: RootState) => ({
|
||||
userId: state.user.id!,
|
||||
}),
|
||||
{
|
||||
refreshUserData,
|
||||
onSubmit: ({
|
||||
form,
|
||||
sendData,
|
||||
}: {
|
||||
form: FormModel;
|
||||
sendData: () => Promise<any>;
|
||||
}) => (dispatch: Dispatch) => {
|
||||
form.beginLoading();
|
||||
(state: RootState) => ({
|
||||
userId: state.user.id!,
|
||||
}),
|
||||
{
|
||||
refreshUserData,
|
||||
onSubmit: ({ form, sendData }: { form: FormModel; sendData: () => Promise<any> }) => (dispatch: Dispatch) => {
|
||||
form.beginLoading();
|
||||
|
||||
return sendData()
|
||||
.catch((resp) => {
|
||||
const requirePassword = resp.errors && !!resp.errors.password;
|
||||
return sendData()
|
||||
.catch((resp) => {
|
||||
const requirePassword = resp.errors && !!resp.errors.password;
|
||||
|
||||
// prevalidate user input, because requestPassword popup will block the
|
||||
// entire form from input, so it must be valid
|
||||
if (resp.errors) {
|
||||
delete resp.errors.password;
|
||||
// prevalidate user input, because requestPassword popup will block the
|
||||
// entire form from input, so it must be valid
|
||||
if (resp.errors) {
|
||||
delete resp.errors.password;
|
||||
|
||||
if (resp.errors.email && resp.data && resp.data.canRepeatIn) {
|
||||
resp.errors.email = {
|
||||
type: resp.errors.email,
|
||||
payload: {
|
||||
msLeft: resp.data.canRepeatIn * 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(resp.errors).length) {
|
||||
form.setErrors(resp.errors);
|
||||
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
|
||||
if (requirePassword) {
|
||||
return requestPassword(form);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
})
|
||||
.catch((resp) => {
|
||||
if (!resp || !resp.errors) {
|
||||
logger.warn('Unexpected profile editing error', {
|
||||
resp,
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
})
|
||||
.finally(() => form.endLoading());
|
||||
|
||||
function requestPassword(form: FormModel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch(
|
||||
createPopup({
|
||||
Popup(props: { onClose: () => Promise<any> }) {
|
||||
const onSubmit = () => {
|
||||
form.beginLoading();
|
||||
|
||||
sendData()
|
||||
.then(resolve)
|
||||
.then(props.onClose)
|
||||
.catch((resp) => {
|
||||
if (resp.errors) {
|
||||
form.setErrors(resp.errors);
|
||||
|
||||
const parentFormHasErrors =
|
||||
Object.keys(resp.errors).filter(
|
||||
(name) => name !== 'password',
|
||||
).length > 0;
|
||||
|
||||
if (parentFormHasErrors) {
|
||||
// something wrong with parent form, hiding popup and show that form
|
||||
props.onClose();
|
||||
reject(resp);
|
||||
logger.warn(
|
||||
'Profile: can not submit password popup due to errors in source form',
|
||||
{ resp },
|
||||
);
|
||||
if (resp.errors.email && resp.data && resp.data.canRepeatIn) {
|
||||
resp.errors.email = {
|
||||
type: resp.errors.email,
|
||||
payload: {
|
||||
msLeft: resp.data.canRepeatIn * 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
})
|
||||
.finally(() => form.endLoading());
|
||||
};
|
||||
|
||||
return <PasswordRequestForm form={form} onSubmit={onSubmit} />;
|
||||
},
|
||||
disableOverlayClose: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
if (Object.keys(resp.errors).length) {
|
||||
form.setErrors(resp.errors);
|
||||
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
|
||||
if (requirePassword) {
|
||||
return requestPassword(form);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
})
|
||||
.catch((resp) => {
|
||||
if (!resp || !resp.errors) {
|
||||
logger.warn('Unexpected profile editing error', {
|
||||
resp,
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
})
|
||||
.finally(() => form.endLoading());
|
||||
|
||||
function requestPassword(form: FormModel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch(
|
||||
createPopup({
|
||||
Popup(props: { onClose: () => Promise<any> }) {
|
||||
const onSubmit = () => {
|
||||
form.beginLoading();
|
||||
|
||||
sendData()
|
||||
.then(resolve)
|
||||
.then(props.onClose)
|
||||
.catch((resp) => {
|
||||
if (resp.errors) {
|
||||
form.setErrors(resp.errors);
|
||||
|
||||
const parentFormHasErrors =
|
||||
Object.keys(resp.errors).filter((name) => name !== 'password')
|
||||
.length > 0;
|
||||
|
||||
if (parentFormHasErrors) {
|
||||
// something wrong with parent form, hiding popup and show that form
|
||||
props.onClose();
|
||||
reject(resp);
|
||||
logger.warn(
|
||||
'Profile: can not submit password popup due to errors in source form',
|
||||
{ resp },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
})
|
||||
.finally(() => form.endLoading());
|
||||
};
|
||||
|
||||
return <PasswordRequestForm form={form} onSubmit={onSubmit} />;
|
||||
},
|
||||
disableOverlayClose: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)(ProfilePage);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
.container {
|
||||
padding: 55px 10px 65px; // 65px for footer
|
||||
padding: 55px 10px 65px; // 65px for footer
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"siteName": "Ely.by"
|
||||
"siteName": "Ely.by"
|
||||
}
|
||||
|
||||
@@ -22,123 +22,103 @@ import styles from './root.scss';
|
||||
import messages from './RootPage.intl.json';
|
||||
|
||||
const ProfilePage = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-profile-all" */ 'app/pages/profile/ProfilePage'
|
||||
),
|
||||
);
|
||||
const PageNotFound = React.lazy(() =>
|
||||
import(/* webpackChunkName: "page-not-found" */ 'app/pages/404/PageNotFound'),
|
||||
);
|
||||
const RulesPage = React.lazy(() =>
|
||||
import(/* webpackChunkName: "page-rules" */ 'app/pages/rules/RulesPage'),
|
||||
);
|
||||
const DevPage = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "page-dev-applications" */ 'app/pages/dev/DevPage'
|
||||
),
|
||||
);
|
||||
const AuthPage = React.lazy(() =>
|
||||
import(/* webpackChunkName: "page-auth" */ 'app/pages/auth/AuthPage'),
|
||||
import(/* webpackChunkName: "page-profile-all" */ 'app/pages/profile/ProfilePage'),
|
||||
);
|
||||
const PageNotFound = React.lazy(() => import(/* webpackChunkName: "page-not-found" */ 'app/pages/404/PageNotFound'));
|
||||
const RulesPage = React.lazy(() => import(/* webpackChunkName: "page-rules" */ 'app/pages/rules/RulesPage'));
|
||||
const DevPage = React.lazy(() => import(/* webpackChunkName: "page-dev-applications" */ 'app/pages/dev/DevPage'));
|
||||
const AuthPage = React.lazy(() => import(/* webpackChunkName: "page-auth" */ 'app/pages/auth/AuthPage'));
|
||||
|
||||
class RootPage extends React.PureComponent<{
|
||||
account: Account | null;
|
||||
user: User;
|
||||
isPopupActive: boolean;
|
||||
onLogoClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
location: {
|
||||
pathname: string;
|
||||
};
|
||||
account: Account | null;
|
||||
user: User;
|
||||
isPopupActive: boolean;
|
||||
onLogoClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
location: {
|
||||
pathname: string;
|
||||
};
|
||||
}> {
|
||||
componentDidMount() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
onPageUpdate() {
|
||||
loader.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const { user, account, isPopupActive, onLogoClick } = this.props;
|
||||
const isRegisterPage = props.location.pathname === '/register';
|
||||
|
||||
if (document && document.body) {
|
||||
document.body.style.overflow = isPopupActive ? 'hidden' : '';
|
||||
componentDidMount() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Helmet>
|
||||
<html lang={user.lang} />
|
||||
</Helmet>
|
||||
componentDidUpdate() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
<ScrollIntoView top />
|
||||
onPageUpdate() {
|
||||
loader.hide();
|
||||
}
|
||||
|
||||
<div
|
||||
id="view-port"
|
||||
className={clsx(styles.viewPort, {
|
||||
[styles.isPopupActive]: isPopupActive,
|
||||
})}
|
||||
>
|
||||
<div className={styles.header} data-testid="toolbar">
|
||||
<div className={styles.headerContent}>
|
||||
<Link
|
||||
to="/"
|
||||
className={styles.logo}
|
||||
onClick={onLogoClick}
|
||||
data-testid="home-page"
|
||||
>
|
||||
<Message {...messages.siteName} />
|
||||
</Link>
|
||||
<div className={styles.userbar}>
|
||||
<Userbar
|
||||
account={account}
|
||||
guestAction={isRegisterPage ? 'login' : 'register'}
|
||||
/>
|
||||
</div>
|
||||
render() {
|
||||
const { props } = this;
|
||||
const { user, account, isPopupActive, onLogoClick } = this.props;
|
||||
const isRegisterPage = props.location.pathname === '/register';
|
||||
|
||||
if (document && document.body) {
|
||||
document.body.style.overflow = isPopupActive ? 'hidden' : '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Helmet>
|
||||
<html lang={user.lang} />
|
||||
</Helmet>
|
||||
|
||||
<ScrollIntoView top />
|
||||
|
||||
<div
|
||||
id="view-port"
|
||||
className={clsx(styles.viewPort, {
|
||||
[styles.isPopupActive]: isPopupActive,
|
||||
})}
|
||||
>
|
||||
<div className={styles.header} data-testid="toolbar">
|
||||
<div className={styles.headerContent}>
|
||||
<Link to="/" className={styles.logo} onClick={onLogoClick} data-testid="home-page">
|
||||
<Message {...messages.siteName} />
|
||||
</Link>
|
||||
<div className={styles.userbar}>
|
||||
<Userbar account={account} guestAction={isRegisterPage ? 'login' : 'register'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<React.Suspense fallback={<ComponentLoader />}>
|
||||
<Switch>
|
||||
<PrivateRoute path="/profile" component={ProfilePage} />
|
||||
<Route path="/404" component={PageNotFound} />
|
||||
<Route path="/rules" component={RulesPage} />
|
||||
<Route path="/dev" component={DevPage} />
|
||||
|
||||
<AuthFlowRoute
|
||||
exact
|
||||
path="/"
|
||||
key="indexPage"
|
||||
component={user.isGuest ? AuthPage : ProfilePage}
|
||||
/>
|
||||
<AuthFlowRoute path="/" component={AuthPage} />
|
||||
|
||||
<Route component={PageNotFound} />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<PopupStack />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<React.Suspense fallback={<ComponentLoader />}>
|
||||
<Switch>
|
||||
<PrivateRoute path="/profile" component={ProfilePage} />
|
||||
<Route path="/404" component={PageNotFound} />
|
||||
<Route path="/rules" component={RulesPage} />
|
||||
<Route path="/dev" component={DevPage} />
|
||||
|
||||
<AuthFlowRoute
|
||||
exact
|
||||
path="/"
|
||||
key="indexPage"
|
||||
component={user.isGuest ? AuthPage : ProfilePage}
|
||||
/>
|
||||
<AuthFlowRoute path="/" component={AuthPage} />
|
||||
|
||||
<Route component={PageNotFound} />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<PopupStack />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
(state: RootState) => ({
|
||||
user: state.user,
|
||||
account: getActiveAccount(state),
|
||||
isPopupActive: state.popup.popups.length > 0,
|
||||
}),
|
||||
{
|
||||
onLogoClick: resetAuth,
|
||||
},
|
||||
)(RootPage),
|
||||
connect(
|
||||
(state: RootState) => ({
|
||||
user: state.user,
|
||||
account: getActiveAccount(state),
|
||||
isPopupActive: state.popup.popups.length > 0,
|
||||
}),
|
||||
{
|
||||
onLogoClick: resetAuth,
|
||||
},
|
||||
)(RootPage),
|
||||
);
|
||||
|
||||
@@ -4,63 +4,63 @@
|
||||
$userBarHeight: 50px;
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.viewPort {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.isPopupActive {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.4s 0.1s ease;
|
||||
filter: blur(5px);
|
||||
transition: filter 0.4s 0.1s ease;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 756px;
|
||||
margin: 0 auto;
|
||||
max-width: 756px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: $userBarHeight;
|
||||
width: 100%;
|
||||
background: $green;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: $userBarHeight;
|
||||
width: 100%;
|
||||
background: $green;
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
composes: wrapper;
|
||||
position: relative;
|
||||
composes: wrapper;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo {
|
||||
line-height: 50px;
|
||||
padding: 0 20px;
|
||||
display: inline-block;
|
||||
background: darker($green);
|
||||
border-bottom: none;
|
||||
line-height: 50px;
|
||||
padding: 0 20px;
|
||||
display: inline-block;
|
||||
background: darker($green);
|
||||
border-bottom: none;
|
||||
|
||||
font-family: $font-family-title;
|
||||
font-size: 33px;
|
||||
color: #fff !important;
|
||||
font-family: $font-family-title;
|
||||
font-size: 33px;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.body {
|
||||
// TODO: должны ли мы здесь описать базовый шрифт, его размер и базовую линию?
|
||||
composes: wrapper;
|
||||
position: relative;
|
||||
// TODO: должны ли мы здесь описать базовый шрифт, его размер и базовую линию?
|
||||
composes: wrapper;
|
||||
position: relative;
|
||||
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-top: $userBarHeight; // place for header
|
||||
padding-top: $userBarHeight; // place for header
|
||||
}
|
||||
|
||||
.userbar {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 115px;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 115px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"title": "Site rules",
|
||||
"title": "Site rules",
|
||||
|
||||
"mainProvisions": "Main provisions",
|
||||
"mainProvision1": "{name} service was created for the organization of safety access to Ely.by's users accounts, his partners and any side project that wish to use one of the our's services.",
|
||||
"mainProvision2": "We (here and in the next points) — Ely.by project developers team that make creating qualitative services for Minecraft community.",
|
||||
"mainProvision3": "Ely.by is side project, that has nothing to do with Mojang and Microsoft companies. We don't provide support to Minecraft premium accounts, and we have nothing to do with servers that use or don't use our services.",
|
||||
"mainProvision4": "The registration of the users account at server is free. Account creation Ely.by is only possible at that page {link}.",
|
||||
"mainProvisions": "Main provisions",
|
||||
"mainProvision1": "{name} service was created for the organization of safety access to Ely.by's users accounts, his partners and any side project that wish to use one of the our's services.",
|
||||
"mainProvision2": "We (here and in the next points) — Ely.by project developers team that make creating qualitative services for Minecraft community.",
|
||||
"mainProvision3": "Ely.by is side project, that has nothing to do with Mojang and Microsoft companies. We don't provide support to Minecraft premium accounts, and we have nothing to do with servers that use or don't use our services.",
|
||||
"mainProvision4": "The registration of the users account at server is free. Account creation Ely.by is only possible at that page {link}.",
|
||||
|
||||
"emailAndNickname": "E‑mail and nickname",
|
||||
"emailAndNickname1": "Account registration with usage of temporary mail services is prohibited. We speak about services that gives random E‑mail in any quantity.",
|
||||
"emailAndNickname2": "We try to counteract it, but if you succesed in registration of account with usage of temporary mail services, there wont be any technical support for it and later, during of update of ours filters, account will be blocked with your nickname.",
|
||||
"emailAndNickname3": "There are no any moral restrictions for users nickname that will be used in game.",
|
||||
"emailAndNickname4": "Nicknames, belonging to famous persons, can be released at their favor for requirement and proves of that persons.",
|
||||
"emailAndNickname5": "Minecraft premium account owner has right to require a control restore of his nickname an if it happened you have to change your nickname in 3 days or it will be done automatically.",
|
||||
"emailAndNickname6": "If there is no any activity at your account during last 3 month, your nickname can be occupied by any user.",
|
||||
"emailAndNickname7": "We aren't responsible for losing your game progress at servers if it was result of nickname changing, including changes on our demand.",
|
||||
"emailAndNickname": "E‑mail and nickname",
|
||||
"emailAndNickname1": "Account registration with usage of temporary mail services is prohibited. We speak about services that gives random E‑mail in any quantity.",
|
||||
"emailAndNickname2": "We try to counteract it, but if you succesed in registration of account with usage of temporary mail services, there wont be any technical support for it and later, during of update of ours filters, account will be blocked with your nickname.",
|
||||
"emailAndNickname3": "There are no any moral restrictions for users nickname that will be used in game.",
|
||||
"emailAndNickname4": "Nicknames, belonging to famous persons, can be released at their favor for requirement and proves of that persons.",
|
||||
"emailAndNickname5": "Minecraft premium account owner has right to require a control restore of his nickname an if it happened you have to change your nickname in 3 days or it will be done automatically.",
|
||||
"emailAndNickname6": "If there is no any activity at your account during last 3 month, your nickname can be occupied by any user.",
|
||||
"emailAndNickname7": "We aren't responsible for losing your game progress at servers if it was result of nickname changing, including changes on our demand.",
|
||||
|
||||
"elyAccountsAsService": "{name} as service",
|
||||
"elyAccountsAsServiceDesc1": "{name} has free providing to any project, that interested in it usage for Minecraft.",
|
||||
"elyAccountsAsServiceDesc2": "Despite we do our utmost to provide fast and stable work of service, we are not saved from DDOS-attack, hosters links work interruptions, electricity disorders or any cases, that impossible to be predicted. For avoiding possible incomprehension, we obliged to discuss next agreements, that will work in case of situations mentioned before:",
|
||||
"elyAccountsAsService1": "We don't have any guarantee about fault free work time of this service.",
|
||||
"elyAccountsAsService2": "We are not responsible for delays and lost income as the result of ours service inoperability."
|
||||
"elyAccountsAsService": "{name} as service",
|
||||
"elyAccountsAsServiceDesc1": "{name} has free providing to any project, that interested in it usage for Minecraft.",
|
||||
"elyAccountsAsServiceDesc2": "Despite we do our utmost to provide fast and stable work of service, we are not saved from DDOS-attack, hosters links work interruptions, electricity disorders or any cases, that impossible to be predicted. For avoiding possible incomprehension, we obliged to discuss next agreements, that will work in case of situations mentioned before:",
|
||||
"elyAccountsAsService1": "We don't have any guarantee about fault free work time of this service.",
|
||||
"elyAccountsAsService2": "We are not responsible for delays and lost income as the result of ours service inoperability."
|
||||
}
|
||||
|
||||
@@ -7,59 +7,56 @@ import { TestContextProvider } from 'app/shell';
|
||||
import RulesPage from './RulesPage';
|
||||
|
||||
describe('RulesPage', () => {
|
||||
describe('#onRuleClick()', () => {
|
||||
const id = 'rule-1-2';
|
||||
const pathname = '/foo';
|
||||
const search = '?bar';
|
||||
let page: HTMLElement;
|
||||
let replace: Function;
|
||||
describe('#onRuleClick()', () => {
|
||||
const id = 'rule-1-2';
|
||||
const pathname = '/foo';
|
||||
const search = '?bar';
|
||||
let page: HTMLElement;
|
||||
let replace: Function;
|
||||
|
||||
beforeEach(() => {
|
||||
replace = sinon.stub().named('history.replace');
|
||||
beforeEach(() => {
|
||||
replace = sinon.stub().named('history.replace');
|
||||
|
||||
({ container: page } = render(
|
||||
<TestContextProvider>
|
||||
<RulesPage
|
||||
location={{ pathname, search } as any}
|
||||
history={{ replace }}
|
||||
/>
|
||||
</TestContextProvider>,
|
||||
));
|
||||
({ container: page } = render(
|
||||
<TestContextProvider>
|
||||
<RulesPage location={{ pathname, search } as any} history={{ replace }} />
|
||||
</TestContextProvider>,
|
||||
));
|
||||
});
|
||||
|
||||
it('should update location on rule click', () => {
|
||||
const expectedUrl = `/foo?bar#${id}`;
|
||||
|
||||
fireEvent.click(page.querySelector(`#${id}`) as HTMLElement);
|
||||
|
||||
expect(replace, 'to have a call satisfying', [expectedUrl]);
|
||||
});
|
||||
|
||||
it('should not update location if link was clicked', () => {
|
||||
fireEvent.click(screen.getByText('/register', { exact: false }));
|
||||
|
||||
expect(replace, 'was not called');
|
||||
});
|
||||
|
||||
it('should not update location if defaultPrevented', () => {
|
||||
const el = page.querySelector(`#${id}`) as HTMLElement;
|
||||
const event = createEvent.click(el);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
fireEvent(el, event);
|
||||
|
||||
expect(replace, 'was not called');
|
||||
});
|
||||
|
||||
it('should not update location if no id', () => {
|
||||
const el = page.querySelector(`#${id}`) as HTMLElement;
|
||||
|
||||
el.id = '';
|
||||
|
||||
fireEvent.click(el);
|
||||
|
||||
expect(replace, 'was not called');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update location on rule click', () => {
|
||||
const expectedUrl = `/foo?bar#${id}`;
|
||||
|
||||
fireEvent.click(page.querySelector(`#${id}`) as HTMLElement);
|
||||
|
||||
expect(replace, 'to have a call satisfying', [expectedUrl]);
|
||||
});
|
||||
|
||||
it('should not update location if link was clicked', () => {
|
||||
fireEvent.click(screen.getByText('/register', { exact: false }));
|
||||
|
||||
expect(replace, 'was not called');
|
||||
});
|
||||
|
||||
it('should not update location if defaultPrevented', () => {
|
||||
const el = page.querySelector(`#${id}`) as HTMLElement;
|
||||
const event = createEvent.click(el);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
fireEvent(el, event);
|
||||
|
||||
expect(replace, 'was not called');
|
||||
});
|
||||
|
||||
it('should not update location if no id', () => {
|
||||
const el = page.querySelector(`#${id}`) as HTMLElement;
|
||||
|
||||
el.id = '';
|
||||
|
||||
fireEvent.click(el);
|
||||
|
||||
expect(replace, 'was not called');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,163 +13,153 @@ const projectName = <Message {...appInfo.appName} />;
|
||||
import clsx from 'clsx';
|
||||
|
||||
const rules = [
|
||||
{
|
||||
title: <Message {...messages.mainProvisions} />,
|
||||
items: [
|
||||
<Message
|
||||
key="0"
|
||||
{...messages.mainProvision1}
|
||||
values={{
|
||||
name: <b>{projectName}</b>,
|
||||
}}
|
||||
/>,
|
||||
<Message key="1" {...messages.mainProvision2} />,
|
||||
<Message key="2" {...messages.mainProvision3} />,
|
||||
<Message
|
||||
key="3"
|
||||
{...messages.mainProvision4}
|
||||
values={{
|
||||
link: <Link to="/register">https://account.ely.by/register</Link>,
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: <Message {...messages.emailAndNickname} />,
|
||||
items: [
|
||||
<Message key="0" {...messages.emailAndNickname1} />,
|
||||
<Message key="1" {...messages.emailAndNickname2} />,
|
||||
<Message key="2" {...messages.emailAndNickname3} />,
|
||||
<Message key="3" {...messages.emailAndNickname4} />,
|
||||
<Message key="4" {...messages.emailAndNickname5} />,
|
||||
<Message key="5" {...messages.emailAndNickname6} />,
|
||||
<Message key="6" {...messages.emailAndNickname7} />,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Message
|
||||
{...messages.elyAccountsAsService}
|
||||
values={{
|
||||
name: projectName,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
<Message
|
||||
{...messages.elyAccountsAsServiceDesc1}
|
||||
values={{
|
||||
name: <b>{projectName}</b>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Message {...messages.elyAccountsAsServiceDesc2} />
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
<Message key="0" {...messages.elyAccountsAsService1} />,
|
||||
<Message key="1" {...messages.elyAccountsAsService2} />,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: <Message {...messages.mainProvisions} />,
|
||||
items: [
|
||||
<Message
|
||||
key="0"
|
||||
{...messages.mainProvision1}
|
||||
values={{
|
||||
name: <b>{projectName}</b>,
|
||||
}}
|
||||
/>,
|
||||
<Message key="1" {...messages.mainProvision2} />,
|
||||
<Message key="2" {...messages.mainProvision3} />,
|
||||
<Message
|
||||
key="3"
|
||||
{...messages.mainProvision4}
|
||||
values={{
|
||||
link: <Link to="/register">https://account.ely.by/register</Link>,
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: <Message {...messages.emailAndNickname} />,
|
||||
items: [
|
||||
<Message key="0" {...messages.emailAndNickname1} />,
|
||||
<Message key="1" {...messages.emailAndNickname2} />,
|
||||
<Message key="2" {...messages.emailAndNickname3} />,
|
||||
<Message key="3" {...messages.emailAndNickname4} />,
|
||||
<Message key="4" {...messages.emailAndNickname5} />,
|
||||
<Message key="5" {...messages.emailAndNickname6} />,
|
||||
<Message key="6" {...messages.emailAndNickname7} />,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Message
|
||||
{...messages.elyAccountsAsService}
|
||||
values={{
|
||||
name: projectName,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
<Message
|
||||
{...messages.elyAccountsAsServiceDesc1}
|
||||
values={{
|
||||
name: <b>{projectName}</b>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Message {...messages.elyAccountsAsServiceDesc2} />
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
<Message key="0" {...messages.elyAccountsAsService1} />,
|
||||
<Message key="1" {...messages.elyAccountsAsService2} />,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default class RulesPage extends Component<{
|
||||
location: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
};
|
||||
location: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
history: {
|
||||
replace: Function;
|
||||
};
|
||||
history: {
|
||||
replace: Function;
|
||||
};
|
||||
}> {
|
||||
render() {
|
||||
let { hash } = this.props.location;
|
||||
render() {
|
||||
let { hash } = this.props.location;
|
||||
|
||||
if (hash) {
|
||||
hash = hash.substring(1);
|
||||
}
|
||||
if (hash) {
|
||||
hash = hash.substring(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Message {...messages.title}>
|
||||
{(pageTitle) => <Helmet title={pageTitle as string} />}
|
||||
</Message>
|
||||
return (
|
||||
<div>
|
||||
<Message {...messages.title}>{(pageTitle) => <Helmet title={pageTitle as string} />}</Message>
|
||||
|
||||
<div className={styles.rules}>
|
||||
{rules.map((block, sectionIndex) => (
|
||||
<div className={styles.rulesSection} key={sectionIndex}>
|
||||
<h2
|
||||
className={clsx(styles.rulesSectionTitle, {
|
||||
[styles.target]:
|
||||
RulesPage.getTitleHash(sectionIndex) === hash,
|
||||
})}
|
||||
id={RulesPage.getTitleHash(sectionIndex)}
|
||||
>
|
||||
{block.title}
|
||||
</h2>
|
||||
<div className={styles.rules}>
|
||||
{rules.map((block, sectionIndex) => (
|
||||
<div className={styles.rulesSection} key={sectionIndex}>
|
||||
<h2
|
||||
className={clsx(styles.rulesSectionTitle, {
|
||||
[styles.target]: RulesPage.getTitleHash(sectionIndex) === hash,
|
||||
})}
|
||||
id={RulesPage.getTitleHash(sectionIndex)}
|
||||
>
|
||||
{block.title}
|
||||
</h2>
|
||||
|
||||
<div className={styles.rulesBody}>
|
||||
{block.description ? (
|
||||
<div className={styles.blockDescription}>
|
||||
{block.description}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<ol className={styles.rulesList}>
|
||||
{block.items.map((item, ruleIndex) => (
|
||||
<li
|
||||
className={clsx(styles.rulesItem, {
|
||||
[styles.target]:
|
||||
RulesPage.getRuleHash(sectionIndex, ruleIndex) ===
|
||||
hash,
|
||||
})}
|
||||
key={ruleIndex}
|
||||
id={RulesPage.getRuleHash(sectionIndex, ruleIndex)}
|
||||
onClick={this.onRuleClick.bind(this)}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className={styles.rulesBody}>
|
||||
{block.description ? (
|
||||
<div className={styles.blockDescription}>{block.description}</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<ol className={styles.rulesList}>
|
||||
{block.items.map((item, ruleIndex) => (
|
||||
<li
|
||||
className={clsx(styles.rulesItem, {
|
||||
[styles.target]:
|
||||
RulesPage.getRuleHash(sectionIndex, ruleIndex) === hash,
|
||||
})}
|
||||
key={ruleIndex}
|
||||
id={RulesPage.getRuleHash(sectionIndex, ruleIndex)}
|
||||
onClick={this.onRuleClick.bind(this)}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onRuleClick(event: React.SyntheticEvent<HTMLElement>) {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
!event.currentTarget.id ||
|
||||
event.target instanceof HTMLAnchorElement
|
||||
) {
|
||||
// some-one have already processed this event or it is a link
|
||||
return;
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = event.currentTarget;
|
||||
const newPath = `${this.props.location.pathname}${this.props.location.search}#${id}`;
|
||||
onRuleClick(event: React.SyntheticEvent<HTMLElement>) {
|
||||
if (event.defaultPrevented || !event.currentTarget.id || event.target instanceof HTMLAnchorElement) {
|
||||
// some-one have already processed this event or it is a link
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.history.replace(newPath);
|
||||
}
|
||||
const { id } = event.currentTarget;
|
||||
const newPath = `${this.props.location.pathname}${this.props.location.search}#${id}`;
|
||||
|
||||
static getTitleHash(sectionIndex: number) {
|
||||
return `rule-${sectionIndex + 1}`;
|
||||
}
|
||||
this.props.history.replace(newPath);
|
||||
}
|
||||
|
||||
static getRuleHash(sectionIndex: number, ruleIndex: number) {
|
||||
return `${RulesPage.getTitleHash(sectionIndex)}-${ruleIndex + 1}`;
|
||||
}
|
||||
static getTitleHash(sectionIndex: number) {
|
||||
return `rule-${sectionIndex + 1}`;
|
||||
}
|
||||
|
||||
static getRuleHash(sectionIndex: number, ruleIndex: number) {
|
||||
return `${RulesPage.getTitleHash(sectionIndex)}-${ruleIndex + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,97 +2,97 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.rules {
|
||||
max-width: 500px;
|
||||
margin: 30px auto 0;
|
||||
padding: 0 20px;
|
||||
max-width: 500px;
|
||||
margin: 30px auto 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.rulesSection {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.rulesSectionTitle {
|
||||
line-height: 50px;
|
||||
line-height: 50px;
|
||||
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
|
||||
background: $blue;
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
.rulesBody {
|
||||
position: relative;
|
||||
// z-index, чтобы положить :before ниже текста, но выше фона блока
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
// z-index, чтобы положить :before ниже текста, но выше фона блока
|
||||
z-index: 0;
|
||||
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
%rulesTextFormat {
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.blockDescription {
|
||||
@extend %rulesTextFormat;
|
||||
|
||||
p {
|
||||
@extend %rulesTextFormat;
|
||||
}
|
||||
|
||||
p {
|
||||
@extend %rulesTextFormat;
|
||||
}
|
||||
}
|
||||
|
||||
.rulesList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.rulesItem {
|
||||
@extend %rulesTextFormat;
|
||||
@extend %rulesTextFormat;
|
||||
|
||||
list-style: decimal;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
list-style: decimal;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.target {
|
||||
&:before {
|
||||
cursor: default;
|
||||
$border: 8px solid #ddd8ce;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -40px;
|
||||
width: calc(100% + 60px);
|
||||
height: calc(100% + 20px);
|
||||
background: $white;
|
||||
border-left: $border;
|
||||
border-right: $border;
|
||||
box-sizing: border-box;
|
||||
z-index: -1;
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #444;
|
||||
border-bottom-color: #aaa;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: #444;
|
||||
&.target {
|
||||
&:before {
|
||||
cursor: default;
|
||||
$border: 8px solid #ddd8ce;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -40px;
|
||||
width: calc(100% + 60px);
|
||||
height: calc(100% + 20px);
|
||||
background: $white;
|
||||
border-left: $border;
|
||||
border-right: $border;
|
||||
box-sizing: border-box;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #444;
|
||||
border-bottom-color: #aaa;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user