Improve appearance/behaviour of login/signup/forgot modals

This commit is contained in:
Toby Zerner 2015-05-26 16:25:25 +09:30
parent 5fc2f3aeee
commit 85ba97ed5c
10 changed files with 209 additions and 143 deletions

View File

@ -1,46 +1,41 @@
import Component from 'flarum/component'; import FormModal from 'flarum/components/form-modal';
import LoadingIndicator from 'flarum/components/loading-indicator'; import LoadingIndicator from 'flarum/components/loading-indicator';
import Alert from 'flarum/components/alert'; import Alert from 'flarum/components/alert';
import icon from 'flarum/helpers/icon'; import icon from 'flarum/helpers/icon';
export default class ForgotPasswordModal extends Component { export default class ForgotPasswordModal extends FormModal {
constructor(props) { constructor(props) {
super(props); super(props);
this.email = m.prop(); this.email = m.prop(this.props.email || '');
this.loading = m.prop(false);
this.success = m.prop(false); this.success = m.prop(false);
} }
view() { view() {
return m('div.modal-dialog.modal-sm.modal-forgot-password', [ if (this.success()) {
m('div.modal-content', [ var emailProviderName = this.email().split('@')[1];
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')), return super.view({
this.props.message ? m('div.modal-alert.alert', this.props.message) : '', className: 'modal-sm forgot-password',
m('div.modal-body', [ title: 'Forgot Password',
m('div.form-centered', this.success() ? 'Sent!' : [ body: this.success()
m('div.form-group', [ ? [
m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email)}) 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('div.form-group', [ m('a.btn.btn-primary.btn-block', {href: 'http://'+emailProviderName}, 'Go to '+emailProviderName)
m('button.btn.btn-primary.btn-block[type=submit]', 'Recover Password')
])
])
]) ])
]) ]
]), : [
LoadingIndicator.component({className: 'modal-loading'+(this.loading() ? ' active' : '')}) 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()})
]),
ready($modal) { m('div.form-group', [
$modal.find('[name=email]').focus(); m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Recover Password')
} ])
]
hide() { });
app.modal.close();
} }
onsubmit(e) { onsubmit(e) {
@ -51,16 +46,23 @@ export default class ForgotPasswordModal extends Component {
method: 'POST', method: 'POST',
url: app.config['api_url']+'/forgot', url: app.config['api_url']+'/forgot',
data: {email: this.email()}, 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 => { }).then(response => {
this.loading(false); this.loading(false);
this.success(true); this.success(true);
this.alert = null;
m.redraw(); m.redraw();
}, response => { }, response => {
this.loading(false); this.loading(false);
m.redraw(); m.redraw();
app.alerts.dismiss(this.errorAlert); this.ready();
app.alerts.show(this.errorAlert = new Alert({ type: 'warning', message: 'Invalid credentials.' }));
}); });
} }
} }

View File

@ -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();
}
}

View File

@ -1,62 +1,48 @@
import Component from 'flarum/component'; import FormModal from 'flarum/components/form-modal';
import LoadingIndicator from 'flarum/components/loading-indicator'; import LoadingIndicator from 'flarum/components/loading-indicator';
import ForgotPasswordModal from 'flarum/components/forgot-password-modal'; import ForgotPasswordModal from 'flarum/components/forgot-password-modal';
import SignupModal from 'flarum/components/signup-modal'; import SignupModal from 'flarum/components/signup-modal';
import Alert from 'flarum/components/alert'; import Alert from 'flarum/components/alert';
import icon from 'flarum/helpers/icon'; import icon from 'flarum/helpers/icon';
export default class LoginModal extends Component { export default class LoginModal extends FormModal {
constructor(props) { constructor(props) {
super(props); super(props);
this.email = m.prop(); this.email = m.prop();
this.password = m.prop(); this.password = m.prop();
this.loading = m.prop(false);
} }
view() { view() {
return m('div.modal-dialog.modal-sm.modal-login', [ return super.view({
m('div.modal-content', [ className: 'modal-sm login-modal',
m('button.btn.btn-icon.btn-link.close.back-control', {onclick: this.hide.bind(this)}, icon('times')), title: 'Log In',
m('form', {onsubmit: this.login.bind(this)}, [ body: [
m('div.modal-header', m('h3.title-control', 'Log In')), m('div.form-group', [
this.props.message ? m('div.modal-alert.alert', this.props.message) : '', m('input.form-control[name=email][placeholder=Username or Email]', {onchange: m.withAttr('value', this.email), disabled: this.loading()})
m('div.modal-body', [ ]),
m('div.form-centered', [ m('div.form-group', [
m('div.form-group', [ m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password), disabled: this.loading()})
m('input.form-control[name=email][placeholder=Username or Email]', {onchange: m.withAttr('value', this.email)}) ]),
]), m('div.form-group', [
m('div.form-group', [ m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Log In')
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')
])
])
]) ])
]), ],
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) { ready() {
$modal.find('[name=email]').focus(); this.email() ? this.$('[name=password]').select() : this.$('[name=email]').select();
} }
hide() { onsubmit(e) {
app.modal.close();
app.alerts.dismiss(this.errorAlert);
}
login(e) {
e.preventDefault(); e.preventDefault();
this.loading(true); this.loading(true);
app.session.login(this.email(), this.password()).then(() => { app.session.login(this.email(), this.password()).then(() => {
@ -64,9 +50,9 @@ export default class LoginModal extends Component {
this.props.callback && this.props.callback(); this.props.callback && this.props.callback();
}, response => { }, response => {
this.loading(false); this.loading(false);
this.alert = new Alert({ type: 'warning', message: 'Your login details were incorrect.' });
m.redraw(); m.redraw();
app.alerts.dismiss(this.errorAlert); this.ready();
app.alerts.show(this.errorAlert = new Alert({ type: 'warning', message: 'Invalid credentials.' }))
}); });
} }
} }

View File

@ -1,10 +1,11 @@
import Component from 'flarum/component'; import FormModal from 'flarum/components/form-modal';
import LoadingIndicator from 'flarum/components/loading-indicator'; import LoadingIndicator from 'flarum/components/loading-indicator';
import LoginModal from 'flarum/components/login-modal'; import LoginModal from 'flarum/components/login-modal';
import Alert from 'flarum/components/alert';
import icon from 'flarum/helpers/icon'; import icon from 'flarum/helpers/icon';
import avatar from 'flarum/helpers/avatar'; import avatar from 'flarum/helpers/avatar';
export default class SignupModal extends Component { export default class SignupModal extends FormModal {
constructor(props) { constructor(props) {
super(props); super(props);
@ -12,54 +13,58 @@ export default class SignupModal extends Component {
this.email = m.prop(); this.email = m.prop();
this.password = m.prop(); this.password = m.prop();
this.welcomeUser = m.prop(); this.welcomeUser = m.prop();
this.loading = m.prop(false);
} }
view() { view() {
var welcomeUser = this.welcomeUser(); var welcomeUser = this.welcomeUser();
var emailProviderName = welcomeUser && welcomeUser.email().split('@')[1]; var emailProviderName = welcomeUser && welcomeUser.email().split('@')[1];
return m('div.modal-dialog.modal-sm.modal-signup', [ var vdom = super.view({
m('div.modal-content', [ className: 'modal-sm signup-modal'+(welcomeUser ? ' signup-modal-success' : ''),
m('button.btn.btn-icon.btn-link.close.back-control', {onclick: app.modal.close.bind(app.modal)}, icon('times')), title: 'Sign Up',
m('form', {onsubmit: this.signup.bind(this)}, [ body: [
m('div.modal-header', m('h3.title-control', 'Sign Up')), m('div.form-group', [
m('div.modal-body', [ m('input.form-control[name=username][placeholder=Username]', {onchange: m.withAttr('value', this.username), disabled: this.loading()})
m('div.form-centered', [ ]),
m('div.form-group', [ m('div.form-group', [
m('input.form-control[name=username][placeholder=Username]', {onchange: m.withAttr('value', this.username)}) m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email), disabled: this.loading()})
]), ]),
m('div.form-group', [ m('div.form-group', [
m('input.form-control[name=email][placeholder=Email]', {onchange: m.withAttr('value', this.email)}) m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password), disabled: this.loading()})
]), ]),
m('div.form-group', [ m('div.form-group', [
m('input.form-control[type=password][name=password][placeholder=Password]', {onchange: m.withAttr('value', this.password)}) m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Sign Up')
]), ])
m('div.form-group', [ ],
m('button.btn.btn-primary.btn-block[type=submit]', 'Sign Up') footer: [
]) m('p.log-in-link', [
]) 'Already have an account? ',
]), m('a[href=javascript:;]', {onclick: () => app.modal.show(new LoginModal())}, 'Log In')
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')
]) 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), return vdom;
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))
]
: ''
]) : ''
])
} }
fadeIn(element, isInitialized) { fadeIn(element, isInitialized) {
@ -67,14 +72,9 @@ export default class SignupModal extends Component {
$(element).hide().fadeIn(); $(element).hide().fadeIn();
} }
ready($modal) { onsubmit(e) {
$modal.find('[name=username]').focus();
}
signup(e) {
e.preventDefault(); e.preventDefault();
this.loading(true); this.loading(true);
var self = this;
app.store.createRecord('users').save({ app.store.createRecord('users').save({
username: this.username(), username: this.username(),
@ -86,7 +86,9 @@ export default class SignupModal extends Component {
m.redraw(); m.redraw();
}, response => { }, response => {
this.loading(false); 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(); m.redraw();
this.$('[name='+response.errors[0].path+']').select();
}); });
} }
} }

View File

@ -24,7 +24,6 @@ export default function(app) {
return component; return component;
} else if (!app.session.user()) { } else if (!app.session.user()) {
app.modal.show(new LoginModal({ 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)) callback: () => app.current.one('loaded', this.replyAction.bind(this, goToLast, forceRefresh))
})); }));
} }

View File

@ -18,11 +18,12 @@ class App {
} }
request(options) { request(options) {
options.extract = options.extract || function(xhr, xhrOptions) { var extract = options.extract;
options.extract = function(xhr, xhrOptions) {
if (xhr.status === 500) { if (xhr.status === 500) {
throw new ServerError; throw new ServerError;
} }
return xhr.responseText; return extract ? extract(xhr.responseText) : (xhr.responseText.length === 0 ? null : xhr.responseText);
}; };
return m.request(options).then(response => { return m.request(options).then(response => {

View File

@ -4,20 +4,35 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
border-radius: @border-radius-base - 1px; border-radius: @border-radius-base;
padding: 60px; padding: 60px 30px;
text-align: center; text-align: center;
color: #fff; color: #fff;
font-size: 14px;
.drawer-components();
& .avatar { & .avatar {
.avatar-size(64px); .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 { & h3, & p {
margin-bottom: 20px; margin-bottom: 30px;
} }
& .btn-default { & .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; color: #fff;
} }
} }

View File

@ -10,13 +10,33 @@
} }
.modal-alert { .modal-alert {
text-align: center; text-align: center;
& .alert {
border-radius: 0;
}
} }
.modal-body { .modal-body {
background-color: @fl-body-secondary-color; background-color: @fl-body-secondary-color;
padding: 25px; padding: 25px 30px;
color: @fl-body-muted-color;
& .form-control { & .form-control {
background-color: #fff; 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 { .modal-footer {
@ -46,9 +66,11 @@
text-align: center; text-align: center;
& .form-control, & .btn { & .form-control, & .btn {
width: 220px;
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
height: 50px;
padding: 15px 20px;
font-size: 15px;
} }
} }
@ -108,8 +130,8 @@
& .close { & .close {
position: absolute; position: absolute;
right: 5px; right: 10px;
top: 5px; top: 10px;
z-index: 1; z-index: 1;
} }
} }
@ -117,6 +139,7 @@
border: 0; border: 0;
border-radius: @border-radius-base; border-radius: @border-radius-base;
.box-shadow(0 7px 15px @fl-shadow-color); .box-shadow(0 7px 15px @fl-shadow-color);
overflow: hidden;
} }
.modal-sm { .modal-sm {
width: 375px; width: 375px;
@ -127,7 +150,7 @@
padding: 25px; padding: 25px;
& h3 { & h3 {
font-size: 22px; font-size: 20px;
margin: 0; margin: 0;
} }
} }

View File

@ -10,11 +10,7 @@ class RegisterUserCommandHandler
public function handle($command) public function handle($command)
{ {
// Assert the the current user has permission to create a user. In the // @todo check whether or not registration is open (config)
// 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');
// Create a new User entity, persist it, and dispatch domain events. // Create a new User entity, persist it, and dispatch domain events.
// Before persistance, though, fire an event to give plugins an // Before persistance, though, fire an event to give plugins an

View File

@ -21,7 +21,7 @@ class LoginAction extends BaseAction
$response = app('Flarum\Api\Actions\TokenAction') $response = app('Flarum\Api\Actions\TokenAction')
->handle(new ApiRequest($request->only('identification', 'password'))); ->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)); $response->withCookie($this->makeRememberCookie($data->token));
event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token)); event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token));