From 95e5a2d69d3f622287d12bd631c476abb65cae89 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 26 May 2015 16:25:25 +0930 Subject: [PATCH] Improve appearance/behaviour of login/signup/forgot modals --- .../src/components/forgot-password-modal.js | 70 ++++++------- .../js/forum/src/components/form-modal.js | 42 ++++++++ .../js/forum/src/components/login-modal.js | 70 ++++++------- .../js/forum/src/components/signup-modal.js | 98 ++++++++++--------- .../src/initializers/discussion-controls.js | 1 - framework/core/js/lib/utils/app.js | 5 +- framework/core/less/forum/signup.less | 25 ++++- framework/core/less/lib/modals.less | 33 ++++++- .../Commands/RegisterUserCommandHandler.php | 6 +- .../core/src/Forum/Actions/LoginAction.php | 2 +- 10 files changed, 209 insertions(+), 143 deletions(-) create mode 100644 framework/core/js/forum/src/components/form-modal.js diff --git a/framework/core/js/forum/src/components/forgot-password-modal.js b/framework/core/js/forum/src/components/forgot-password-modal.js index 8da5611f5..cdfa74593 100644 --- a/framework/core/js/forum/src/components/forgot-password-modal.js +++ b/framework/core/js/forum/src/components/forgot-password-modal.js @@ -1,46 +1,41 @@ -import Component from 'flarum/component'; +import FormModal from 'flarum/components/form-modal'; import LoadingIndicator from 'flarum/components/loading-indicator'; import Alert from 'flarum/components/alert'; import icon from 'flarum/helpers/icon'; -export default class ForgotPasswordModal extends Component { +export default class ForgotPasswordModal extends FormModal { constructor(props) { super(props); - this.email = m.prop(); - this.loading = m.prop(false); + this.email = m.prop(this.props.email || ''); this.success = m.prop(false); } view() { - return m('div.modal-dialog.modal-sm.modal-forgot-password', [ - m('div.modal-content', [ - m('button.btn.btn-icon.btn-link.close.back-control', {onclick: this.hide.bind(this)}, icon('times')), - m('form', {onsubmit: this.onsubmit.bind(this)}, [ - m('div.modal-header', m('h3.title-control', 'Forgot Password')), - this.props.message ? m('div.modal-alert.alert', this.props.message) : '', - m('div.modal-body', [ - m('div.form-centered', this.success() ? 'Sent!' : [ - m('div.form-group', [ - m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email)}) - ]), - m('div.form-group', [ - m('button.btn.btn-primary.btn-block[type=submit]', 'Recover Password') - ]) - ]) + if (this.success()) { + var emailProviderName = this.email().split('@')[1]; + } + + return super.view({ + className: 'modal-sm forgot-password', + title: 'Forgot Password', + body: this.success() + ? [ + m('p.help-text', 'OK, we\'ve sent you an email containing a link to reset your password. Check your spam folder if you don\'t receive it within the next minute or two. Yeah, sometimes we get put through to spam - can you believe it?!'), + m('div.form-group', [ + m('a.btn.btn-primary.btn-block', {href: 'http://'+emailProviderName}, 'Go to '+emailProviderName) ]) - ]) - ]), - LoadingIndicator.component({className: 'modal-loading'+(this.loading() ? ' active' : '')}) - ]) - } - - ready($modal) { - $modal.find('[name=email]').focus(); - } - - hide() { - app.modal.close(); + ] + : [ + m('p.help-text', 'Forgot your password? Don\'t worry, it happens all the time. Simply enter your email address and we\'ll send you instructions on how to set up a new one.'), + m('div.form-group', [ + m('input.form-control[name=email][placeholder=Email]', {value: this.email(), onchange: m.withAttr('value', this.email), disabled: this.loading()}) + ]), + m('div.form-group', [ + m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Recover Password') + ]) + ] + }); } onsubmit(e) { @@ -51,16 +46,23 @@ export default class ForgotPasswordModal extends Component { method: 'POST', url: app.config['api_url']+'/forgot', data: {email: this.email()}, - background: true + background: true, + extract: xhr => { + if (xhr.status === 404) { + this.alert = new Alert({ type: 'warning', message: 'That email wasn\'t found in our database.' }); + throw new Error; + } + return null; + } }).then(response => { this.loading(false); this.success(true); + this.alert = null; m.redraw(); }, response => { this.loading(false); m.redraw(); - app.alerts.dismiss(this.errorAlert); - app.alerts.show(this.errorAlert = new Alert({ type: 'warning', message: 'Invalid credentials.' })); + this.ready(); }); } } diff --git a/framework/core/js/forum/src/components/form-modal.js b/framework/core/js/forum/src/components/form-modal.js new file mode 100644 index 000000000..b32babd2c --- /dev/null +++ b/framework/core/js/forum/src/components/form-modal.js @@ -0,0 +1,42 @@ +import Component from 'flarum/component'; +import LoadingIndicator from 'flarum/components/loading-indicator'; +import Alert from 'flarum/components/alert'; +import icon from 'flarum/helpers/icon'; + +export default class FormModal extends Component { + constructor(props) { + super(props); + + this.alert = null; + this.loading = m.prop(false); + } + + view(options) { + if (this.alert) { + this.alert.props.dismissible = false; + } + + return m('div.modal-dialog', {className: options.className, config: this.element}, [ + m('div.modal-content', [ + m('a[href=javascript:;].btn.btn-icon.btn-link.close.back-control', {onclick: this.hide.bind(this)}, icon('times')), + m('form', {onsubmit: this.onsubmit.bind(this)}, [ + m('div.modal-header', m('h3.title-control', options.title)), + this.alert ? m('div.modal-alert', this.alert.view()) : '', + m('div.modal-body', [ + m('div.form-centered', options.body) + ]), + options.footer ? m('div.modal-footer', options.footer) : '' + ]) + ]), + LoadingIndicator.component({className: 'modal-loading'+(this.loading() ? ' active' : '')}) + ]) + } + + ready() { + this.$(':input:first').select(); + } + + hide() { + app.modal.close(); + } +} diff --git a/framework/core/js/forum/src/components/login-modal.js b/framework/core/js/forum/src/components/login-modal.js index cc9316b4c..8b794409d 100644 --- a/framework/core/js/forum/src/components/login-modal.js +++ b/framework/core/js/forum/src/components/login-modal.js @@ -1,62 +1,48 @@ -import Component from 'flarum/component'; +import FormModal from 'flarum/components/form-modal'; import LoadingIndicator from 'flarum/components/loading-indicator'; import ForgotPasswordModal from 'flarum/components/forgot-password-modal'; import SignupModal from 'flarum/components/signup-modal'; import Alert from 'flarum/components/alert'; import icon from 'flarum/helpers/icon'; -export default class LoginModal extends Component { +export default class LoginModal extends FormModal { constructor(props) { super(props); this.email = m.prop(); this.password = m.prop(); - this.loading = m.prop(false); } view() { - return m('div.modal-dialog.modal-sm.modal-login', [ - m('div.modal-content', [ - m('button.btn.btn-icon.btn-link.close.back-control', {onclick: this.hide.bind(this)}, icon('times')), - m('form', {onsubmit: this.login.bind(this)}, [ - m('div.modal-header', m('h3.title-control', 'Log In')), - this.props.message ? m('div.modal-alert.alert', this.props.message) : '', - m('div.modal-body', [ - m('div.form-centered', [ - m('div.form-group', [ - m('input.form-control[name=email][placeholder=Username or Email]', {onchange: m.withAttr('value', this.email)}) - ]), - m('div.form-group', [ - m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password)}) - ]), - m('div.form-group', [ - m('button.btn.btn-primary.btn-block[type=submit]', 'Log In') - ]) - ]) - ]), - m('div.modal-footer', [ - m('p.forgot-password-link', m('a[href=javascript:;]', {onclick: () => app.modal.show(new ForgotPasswordModal())}, 'Forgot password?')), - m('p.sign-up-link', [ - 'Don\'t have an account? ', - m('a[href=javascript:;]', {onclick: () => app.modal.show(new SignupModal())}, 'Sign Up') - ]) - ]) + return super.view({ + className: 'modal-sm login-modal', + title: 'Log In', + body: [ + m('div.form-group', [ + m('input.form-control[name=email][placeholder=Username or Email]', {onchange: m.withAttr('value', this.email), disabled: this.loading()}) + ]), + m('div.form-group', [ + m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password), disabled: this.loading()}) + ]), + m('div.form-group', [ + m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Log In') ]) - ]), - LoadingIndicator.component({className: 'modal-loading'+(this.loading() ? ' active' : '')}) - ]) + ], + footer: [ + m('p.forgot-password-link', m('a[href=javascript:;]', {onclick: () => app.modal.show(new ForgotPasswordModal({email: this.email()}))}, 'Forgot password?')), + m('p.sign-up-link', [ + 'Don\'t have an account? ', + m('a[href=javascript:;]', {onclick: () => app.modal.show(new SignupModal())}, 'Sign Up') + ]) + ] + }); } - ready($modal) { - $modal.find('[name=email]').focus(); + ready() { + this.email() ? this.$('[name=password]').select() : this.$('[name=email]').select(); } - hide() { - app.modal.close(); - app.alerts.dismiss(this.errorAlert); - } - - login(e) { + onsubmit(e) { e.preventDefault(); this.loading(true); app.session.login(this.email(), this.password()).then(() => { @@ -64,9 +50,9 @@ export default class LoginModal extends Component { this.props.callback && this.props.callback(); }, response => { this.loading(false); + this.alert = new Alert({ type: 'warning', message: 'Your login details were incorrect.' }); m.redraw(); - app.alerts.dismiss(this.errorAlert); - app.alerts.show(this.errorAlert = new Alert({ type: 'warning', message: 'Invalid credentials.' })) + this.ready(); }); } } diff --git a/framework/core/js/forum/src/components/signup-modal.js b/framework/core/js/forum/src/components/signup-modal.js index 18412ac85..a7cadf3be 100644 --- a/framework/core/js/forum/src/components/signup-modal.js +++ b/framework/core/js/forum/src/components/signup-modal.js @@ -1,10 +1,11 @@ -import Component from 'flarum/component'; +import FormModal from 'flarum/components/form-modal'; import LoadingIndicator from 'flarum/components/loading-indicator'; import LoginModal from 'flarum/components/login-modal'; +import Alert from 'flarum/components/alert'; import icon from 'flarum/helpers/icon'; import avatar from 'flarum/helpers/avatar'; -export default class SignupModal extends Component { +export default class SignupModal extends FormModal { constructor(props) { super(props); @@ -12,54 +13,58 @@ export default class SignupModal extends Component { this.email = m.prop(); this.password = m.prop(); this.welcomeUser = m.prop(); - this.loading = m.prop(false); } view() { var welcomeUser = this.welcomeUser(); var emailProviderName = welcomeUser && welcomeUser.email().split('@')[1]; - return m('div.modal-dialog.modal-sm.modal-signup', [ - m('div.modal-content', [ - m('button.btn.btn-icon.btn-link.close.back-control', {onclick: app.modal.close.bind(app.modal)}, icon('times')), - m('form', {onsubmit: this.signup.bind(this)}, [ - m('div.modal-header', m('h3.title-control', 'Sign Up')), - m('div.modal-body', [ - m('div.form-centered', [ - m('div.form-group', [ - m('input.form-control[name=username][placeholder=Username]', {onchange: m.withAttr('value', this.username)}) - ]), - m('div.form-group', [ - m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email)}) - ]), - m('div.form-group', [ - m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password)}) - ]), - m('div.form-group', [ - m('button.btn.btn-primary.btn-block[type=submit]', 'Sign Up') - ]) - ]) - ]), - m('div.modal-footer', [ - m('p.log-in-link', [ - 'Already have an account? ', - m('a[href=javascript:;]', {onclick: () => app.modal.show(new LoginModal())}, 'Log In') - ]) + var vdom = super.view({ + className: 'modal-sm signup-modal'+(welcomeUser ? ' signup-modal-success' : ''), + title: 'Sign Up', + body: [ + m('div.form-group', [ + m('input.form-control[name=username][placeholder=Username]', {onchange: m.withAttr('value', this.username), disabled: this.loading()}) + ]), + m('div.form-group', [ + m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email), disabled: this.loading()}) + ]), + m('div.form-group', [ + m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password), disabled: this.loading()}) + ]), + m('div.form-group', [ + m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Sign Up') + ]) + ], + footer: [ + m('p.log-in-link', [ + 'Already have an account? ', + m('a[href=javascript:;]', {onclick: () => app.modal.show(new LoginModal())}, 'Log In') + ]) + ] + }); + + if (welcomeUser) { + vdom.children.push( + m('div.signup-welcome', {style: 'background: '+this.welcomeUser().color(), config: this.fadeIn}, [ + m('div.darken-overlay'), + m('div.container', [ + avatar(welcomeUser), + m('h3', 'Welcome, '+welcomeUser.username()+'!'), + !welcomeUser.isConfirmed() + ? [ + m('p', ['We\'ve sent a confirmation email to ', m('strong', welcomeUser.email()), '. If it doesn\'t arrive soon, check your spam folder.']), + m('p', m('a.btn.btn-default', {href: 'http://'+emailProviderName}, 'Go to '+emailProviderName)) + ] + : [ + m('p', m('a.btn.btn-default', {onclick: this.hide.bind(this)}, 'Dismiss')) + ] ]) ]) - ]), - LoadingIndicator.component({className: 'modal-loading'+(this.loading() ? ' active' : '')}), - welcomeUser ? m('div.signup-welcome', {style: 'background: '+this.welcomeUser().color(), config: this.fadeIn}, [ - avatar(welcomeUser), - m('h3', 'Welcome, '+welcomeUser.username()+'!'), - !welcomeUser.isConfirmed() - ? [ - m('p', ['We\'ve sent a confirmation email to ', m('strong', welcomeUser.email()), '. If it doesn\'t arrive soon, check your spam folder.']), - m('p', m('a.btn.btn-default', {href: 'http://'+emailProviderName}, 'Go to '+emailProviderName)) - ] - : '' - ]) : '' - ]) + ) + } + + return vdom; } fadeIn(element, isInitialized) { @@ -67,14 +72,9 @@ export default class SignupModal extends Component { $(element).hide().fadeIn(); } - ready($modal) { - $modal.find('[name=username]').focus(); - } - - signup(e) { + onsubmit(e) { e.preventDefault(); this.loading(true); - var self = this; app.store.createRecord('users').save({ username: this.username(), @@ -86,7 +86,9 @@ export default class SignupModal extends Component { m.redraw(); }, response => { this.loading(false); + this.alert = new Alert({ type: 'warning', message: response.errors.map((error, k) => [error.detail, k < response.errors.length - 1 ? m('br') : '']) }); m.redraw(); + this.$('[name='+response.errors[0].path+']').select(); }); } } diff --git a/framework/core/js/forum/src/initializers/discussion-controls.js b/framework/core/js/forum/src/initializers/discussion-controls.js index 298fdc4e3..cd52f19ed 100644 --- a/framework/core/js/forum/src/initializers/discussion-controls.js +++ b/framework/core/js/forum/src/initializers/discussion-controls.js @@ -24,7 +24,6 @@ export default function(app) { return component; } else if (!app.session.user()) { app.modal.show(new LoginModal({ - message: 'You must be logged in to do that.', callback: () => app.current.one('loaded', this.replyAction.bind(this, goToLast, forceRefresh)) })); } diff --git a/framework/core/js/lib/utils/app.js b/framework/core/js/lib/utils/app.js index 0194dc7cf..8857a96d7 100644 --- a/framework/core/js/lib/utils/app.js +++ b/framework/core/js/lib/utils/app.js @@ -18,11 +18,12 @@ class App { } request(options) { - options.extract = options.extract || function(xhr, xhrOptions) { + var extract = options.extract; + options.extract = function(xhr, xhrOptions) { if (xhr.status === 500) { throw new ServerError; } - return xhr.responseText; + return extract ? extract(xhr.responseText) : (xhr.responseText.length === 0 ? null : xhr.responseText); }; return m.request(options).then(response => { diff --git a/framework/core/less/forum/signup.less b/framework/core/less/forum/signup.less index 5af51278b..1205dfef9 100644 --- a/framework/core/less/forum/signup.less +++ b/framework/core/less/forum/signup.less @@ -4,20 +4,35 @@ left: 0; right: 0; bottom: 0; - border-radius: @border-radius-base - 1px; - padding: 60px; + border-radius: @border-radius-base; + padding: 60px 30px; text-align: center; color: #fff; + font-size: 14px; + + .drawer-components(); & .avatar { .avatar-size(64px); - .box-shadow(0 0 0 4px #fff); + border: 4px solid @fl-body-bg; + .box-shadow(0 2px 6px @fl-shadow-color); } & h3, & p { - margin-bottom: 20px; + margin-bottom: 30px; } & .btn-default { - background: fade(#000, 10%); + font-size: 15px; + height: 50px; + padding: 15px 20px; + } + & .container { + width: auto !important; + padding: 0 !important; + position: relative; + } +} +.signup-modal-success { + & .close { color: #fff; } } diff --git a/framework/core/less/lib/modals.less b/framework/core/less/lib/modals.less index a3aede95c..c3004bbb4 100644 --- a/framework/core/less/lib/modals.less +++ b/framework/core/less/lib/modals.less @@ -10,13 +10,33 @@ } .modal-alert { text-align: center; + + & .alert { + border-radius: 0; + } } .modal-body { background-color: @fl-body-secondary-color; - padding: 25px; + padding: 25px 30px; + color: @fl-body-muted-color; & .form-control { background-color: #fff; + color: @fl-body-color; + + &:focus { + border: 2px solid @fl-body-primary-color; + } + } + + & .help-text { + font-size: 14px; + line-height: 1.5em; + margin-bottom: 25px; + } + + & :last-child { + margin-bottom: 0; } } .modal-footer { @@ -46,9 +66,11 @@ text-align: center; & .form-control, & .btn { - width: 220px; margin: 0 auto; text-align: center; + height: 50px; + padding: 15px 20px; + font-size: 15px; } } @@ -108,8 +130,8 @@ & .close { position: absolute; - right: 5px; - top: 5px; + right: 10px; + top: 10px; z-index: 1; } } @@ -117,6 +139,7 @@ border: 0; border-radius: @border-radius-base; .box-shadow(0 7px 15px @fl-shadow-color); + overflow: hidden; } .modal-sm { width: 375px; @@ -127,7 +150,7 @@ padding: 25px; & h3 { - font-size: 22px; + font-size: 20px; margin: 0; } } diff --git a/framework/core/src/Core/Handlers/Commands/RegisterUserCommandHandler.php b/framework/core/src/Core/Handlers/Commands/RegisterUserCommandHandler.php index c83cbf3b6..a473681ee 100644 --- a/framework/core/src/Core/Handlers/Commands/RegisterUserCommandHandler.php +++ b/framework/core/src/Core/Handlers/Commands/RegisterUserCommandHandler.php @@ -10,11 +10,7 @@ class RegisterUserCommandHandler public function handle($command) { - // Assert the the current user has permission to create a user. In the - // case of a guest trying to register an account, this will depend on - // whether or not registration is open. If the user is an admin, though, - // it will be allowed. - $command->forum->assertCan($command->user, 'register'); + // @todo check whether or not registration is open (config) // Create a new User entity, persist it, and dispatch domain events. // Before persistance, though, fire an event to give plugins an diff --git a/framework/core/src/Forum/Actions/LoginAction.php b/framework/core/src/Forum/Actions/LoginAction.php index a34a1880b..b410538af 100644 --- a/framework/core/src/Forum/Actions/LoginAction.php +++ b/framework/core/src/Forum/Actions/LoginAction.php @@ -21,7 +21,7 @@ class LoginAction extends BaseAction $response = app('Flarum\Api\Actions\TokenAction') ->handle(new ApiRequest($request->only('identification', 'password'))); - if (($data = $response->getData()) && ! empty($data->token)) { + if ($response->getStatusCode() === 200 && ($data = $response->getData()) && ! empty($data->token)) { $response->withCookie($this->makeRememberCookie($data->token)); event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token));