client/users: implement account settings
(Without avatars yet.)
This commit is contained in:
parent
90d4401024
commit
7871c69aa3
13 changed files with 242 additions and 36 deletions
|
@ -21,6 +21,9 @@ form ul {
|
|||
form ul li {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
form .input {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
form .buttons {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -144,6 +147,7 @@ input[type=checkbox]:focus + .checkbox:before {
|
|||
/*
|
||||
* Regular inputs
|
||||
*/
|
||||
select,
|
||||
textarea,
|
||||
input[type=text],
|
||||
input[type=email],
|
||||
|
@ -161,6 +165,7 @@ input[type=password] {
|
|||
transition: border-color 0.1s linear, background-color 0.1s linear;
|
||||
}
|
||||
|
||||
select:disabled,
|
||||
textarea:disabled,
|
||||
input[type=text]:disabled,
|
||||
input[type=email]:disabled,
|
||||
|
@ -170,6 +175,7 @@ input[type=password]:disabled {
|
|||
color: var(--input-disabled-text-color);
|
||||
}
|
||||
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
input[type=text]:focus,
|
||||
input[type=email]:focus,
|
||||
|
@ -217,6 +223,9 @@ input[type=button]:focus,
|
|||
input[type=submit]:focus {
|
||||
outline: 2px solid var(--text-color);
|
||||
}
|
||||
select:-moz-focusring {
|
||||
text-shadow: 0;
|
||||
}
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
|
|
|
@ -38,6 +38,7 @@ body {
|
|||
|
||||
h1, h2, h3 {
|
||||
font-weight: normal;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -26,8 +26,10 @@
|
|||
#user-registration .info p:first-child {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
#user-registration p.hint {
|
||||
#user .hint,
|
||||
#user-registration .hint {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0;
|
||||
color: var(--inactive-link-color);
|
||||
font-size: 80%;
|
||||
line-height: 120%;
|
||||
|
@ -45,6 +47,7 @@
|
|||
}
|
||||
#user-summary img {
|
||||
width: 6em;
|
||||
height: 6em;
|
||||
margin: 0 1.5em 1.5em 0;
|
||||
float: left;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<div class='messages'></div>
|
||||
|
||||
<div class='content-wrapper' id='user'>
|
||||
<h1>{{this.name}}</h1>
|
||||
<h1>{{this.user.name}}</h1>
|
||||
<nav class='text-nav'><!--
|
||||
--><ul><!--
|
||||
--><li data-name='summary'><a href='/user/{{this.name}}'>Summary</a></li><!--
|
||||
--><li data-name='edit'><a href='/user/{{this.name}}/edit'>Account settings</a></li><!--
|
||||
--><li data-name='summary'><a href='/user/{{this.user.name}}'>Summary</a></li><!--
|
||||
-->{{#if this.canEditAnything}}<!--
|
||||
--><li data-name='edit'><a href='/user/{{this.user.name}}/edit'>Account settings</a></li><!--
|
||||
-->{{/if}}<!--
|
||||
--></ul><!--
|
||||
--></nav>
|
||||
<div id='user-content-holder'></div>
|
||||
|
|
|
@ -1,3 +1,45 @@
|
|||
<div id='user-edit'>
|
||||
<strong>Placeholder for account settings form</strong>
|
||||
<form>
|
||||
<fieldset class='input'>
|
||||
<ul>
|
||||
{{#if this.canEditName}}
|
||||
<li>
|
||||
<label for='user-name'>User name</label>
|
||||
<input id='user-name' name='name' type='text' value='{{this.user.name}}'/>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditPassword}}
|
||||
<li>
|
||||
<label for='user-password'>Password</label>
|
||||
<input id='user-password' name='password' type='password'/>
|
||||
<p class='hint'>Leave empty to keep the password unchanged.</p>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditEmail}}
|
||||
<li>
|
||||
<label for='user-email'>Email</label>
|
||||
<input id='user-email' name='email' type='email' value='{{this.user.email}}'/>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditRank}}
|
||||
<li>
|
||||
<label for='user-rank'>Rank</label>
|
||||
<select id='user-rank' name='rank'>
|
||||
{{#each this.ranks}}
|
||||
<option value='{{@key}}'>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<!-- TODO: avatar -->
|
||||
</fieldset>
|
||||
<fieldset class='messages'></fieldset>
|
||||
<fieldset class='buttons'>
|
||||
<input type='submit' value='Save settings'/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</ul>
|
||||
</nav>
|
||||
|
||||
{{#if this.isPrivate}}
|
||||
{{#if this.isLoggedIn}}
|
||||
<nav class='plain-nav'>
|
||||
<p><strong>Only visible to you</strong></p>
|
||||
<ul>
|
||||
|
|
|
@ -22,6 +22,11 @@ class Api {
|
|||
return this._process(fullUrl, () => request.post(fullUrl).send(data));
|
||||
}
|
||||
|
||||
put(url, data) {
|
||||
const fullUrl = this.getFullUrl(url);
|
||||
return this._process(fullUrl, () => request.put(fullUrl).send(data));
|
||||
}
|
||||
|
||||
_process(url, requestFactory) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let req = requestFactory();
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
const page = require('page');
|
||||
const api = require('../api.js');
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const topNavController = require('../controllers/top_nav_controller.js');
|
||||
const RegistrationView = require('../views/registration_view.js');
|
||||
const UserView = require('../views/user_view.js');
|
||||
|
@ -33,9 +35,29 @@ class UsersController {
|
|||
|
||||
createUserRoute() {
|
||||
topNavController.activate('register');
|
||||
this.registrationView.render({register: (...args) => {
|
||||
return this._register(...args);
|
||||
}});
|
||||
this.registrationView.render({
|
||||
register: (...args) => {
|
||||
return this._register(...args);
|
||||
}});
|
||||
}
|
||||
|
||||
loadUserRoute(ctx, next) {
|
||||
if (ctx.state.user) {
|
||||
next();
|
||||
} else if (this.user && this.user.name == ctx.params.name) {
|
||||
ctx.state.user = this.user;
|
||||
next();
|
||||
} else {
|
||||
api.get('/user/' + ctx.params.name).then(response => {
|
||||
ctx.state.user = response.user;
|
||||
ctx.save();
|
||||
this.user = response.user;
|
||||
next();
|
||||
}).catch(response => {
|
||||
this.userView.empty();
|
||||
events.notify(events.Error, response.description);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_register(name, password, email) {
|
||||
|
@ -59,34 +81,79 @@ class UsersController {
|
|||
});
|
||||
}
|
||||
|
||||
loadUserRoute(ctx, next) {
|
||||
if (ctx.state.user) {
|
||||
next();
|
||||
} else if (this.user && this.user.name == ctx.params.name) {
|
||||
ctx.state.user = this.user;
|
||||
next();
|
||||
} else {
|
||||
api.get('/user/' + ctx.params.name).then(response => {
|
||||
ctx.state.user = response.user;
|
||||
ctx.save();
|
||||
this.user = response.user;
|
||||
next();
|
||||
}).catch(response => {
|
||||
this.userView.empty();
|
||||
events.notify(events.Error, response.description);
|
||||
});
|
||||
}
|
||||
_edit(user, newName, newPassword, newEmail, newRank) {
|
||||
const data = {};
|
||||
if (newName) { data.name = newName; }
|
||||
if (newPassword) { data.password = newPassword; }
|
||||
if (newEmail) { data.email = newEmail; }
|
||||
if (newRank) { data.rank = newRank; }
|
||||
/* TODO: avatar */
|
||||
const isLoggedIn = api.isLoggedIn() && api.user.id == user.id;
|
||||
return new Promise((resolve, reject) => {
|
||||
api.put('/user/' + user.name, data)
|
||||
.then(response => {
|
||||
const next = () => {
|
||||
resolve();
|
||||
page('/user/' + newName + '/edit');
|
||||
events.notify(events.Success, 'Settings updated');
|
||||
};
|
||||
if (isLoggedIn) {
|
||||
api.login(
|
||||
newName,
|
||||
newPassword || api.userPassword,
|
||||
false)
|
||||
.then(next)
|
||||
.catch(response => {
|
||||
reject();
|
||||
events.notify(
|
||||
events.Error, response.description);
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}).catch(response => {
|
||||
reject();
|
||||
events.notify(events.Error, response.description);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_show(user, section) {
|
||||
const isPrivate = api.isLoggedIn() && user.name == api.userName;
|
||||
if (isPrivate) {
|
||||
const isLoggedIn = api.isLoggedIn() && api.user.id == user.id;
|
||||
const infix = isLoggedIn ? 'self' : 'any';
|
||||
|
||||
const myRankIdx = api.user ? config.ranks.indexOf(api.user.rank) : 0;
|
||||
const rankNames = Object.values(config.rankNames);
|
||||
let ranks = {};
|
||||
for (let rankIdx of misc.range(config.ranks.length)) {
|
||||
const rankIdentifier = config.ranks[rankIdx];
|
||||
if (rankIdentifier === 'anonymous') {
|
||||
continue;
|
||||
}
|
||||
if (rankIdx > myRankIdx) {
|
||||
continue;
|
||||
}
|
||||
ranks[rankIdentifier] = rankNames[rankIdx];
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
topNavController.activate('account');
|
||||
} else {
|
||||
topNavController.activate('users');
|
||||
}
|
||||
this.userView.render({
|
||||
user: user, section: section, isPrivate: isPrivate});
|
||||
user: user,
|
||||
section: section,
|
||||
isLoggedIn: isLoggedIn,
|
||||
canEditName: api.hasPrivilege('users:edit:' + infix + ':name'),
|
||||
canEditPassword: api.hasPrivilege('users:edit:' + infix + ':pass'),
|
||||
canEditEmail: api.hasPrivilege('users:edit:' + infix + ':email'),
|
||||
canEditRank: api.hasPrivilege('users:edit:' + infix + ':rank'),
|
||||
canEditAvatar: api.hasPrivilege('users:edit:' + infix + ':avatar'),
|
||||
canEditAnything: api.hasPrivilege('users:edit:' + infix),
|
||||
ranks: ranks,
|
||||
edit: (...args) => { return this._edit(user, ...args); },
|
||||
});
|
||||
}
|
||||
|
||||
showUserRoute(ctx, next) {
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
function* range(start=0, end=null, step=1) {
|
||||
if (end == null) {
|
||||
end = start;
|
||||
start = 0;
|
||||
}
|
||||
|
||||
for (let i = start; i < end; i += step) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
function formatRelativeTime(timeString) {
|
||||
if (!timeString) {
|
||||
return 'never';
|
||||
|
@ -41,4 +52,7 @@ function formatRelativeTime(timeString) {
|
|||
return future ? 'in ' + text : text + ' ago';
|
||||
}
|
||||
|
||||
module.exports = {formatRelativeTime: formatRelativeTime};
|
||||
module.exports = {
|
||||
range: range,
|
||||
formatRelativeTime: formatRelativeTime,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const keys = Reflect.ownKeys;
|
||||
const reduce = Function.bind.call(Function.call, Array.prototype.reduce);
|
||||
const concat = Function.bind.call(Function.call, Array.prototype.concat);
|
||||
const isEnumerable = Function.bind.call(
|
||||
Function.call, Object.prototype.propertyIsEnumerable);
|
||||
|
||||
if (!Object.values) {
|
||||
Object.values = function values(O) {
|
||||
return reduce(keys(O), (v, k) => concat(
|
||||
v, typeof k === 'string' && isEnumerable(O, k) ?
|
||||
[O[k]] :
|
||||
[]), []);
|
||||
};
|
||||
}
|
||||
|
||||
if (!Object.entries) {
|
||||
Object.entries = function entries(O) {
|
||||
return reduce(
|
||||
keys(O), (e, k) =>
|
||||
concat(e, typeof k === 'string' && isEnumerable(O, k) ?
|
||||
[[k, O[k]]] :
|
||||
[]), []);
|
||||
};
|
||||
}
|
||||
|
||||
// fix iterating over NodeList in Chrome and Opera
|
||||
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
|
||||
class UserEditView extends BaseView {
|
||||
|
@ -9,7 +10,47 @@ class UserEditView extends BaseView {
|
|||
}
|
||||
|
||||
render(options) {
|
||||
options.target.innerHTML = this.template(options.user);
|
||||
options.target.innerHTML = this.template(options);
|
||||
|
||||
const form = options.target.querySelector('form');
|
||||
const rankField = options.target.querySelector('#user-rank');
|
||||
const emailField = options.target.querySelector('#user-email');
|
||||
const userNameField = options.target.querySelector('#user-name');
|
||||
const passwordField = options.target.querySelector('#user-password');
|
||||
|
||||
this.decorateValidator(form);
|
||||
|
||||
if (userNameField) {
|
||||
userNameField.setAttribute(
|
||||
'pattern',
|
||||
config.userNameRegex + /|^$/.source);
|
||||
}
|
||||
|
||||
if (passwordField) {
|
||||
passwordField.setAttribute(
|
||||
'pattern',
|
||||
config.passwordRegex + /|^$/.source);
|
||||
}
|
||||
|
||||
if (rankField) {
|
||||
rankField.value = options.user.rank;
|
||||
}
|
||||
|
||||
/* TODO: avatar */
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.clearMessages();
|
||||
this.disableForm(form);
|
||||
options
|
||||
.edit(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
emailField.value,
|
||||
rankField.value)
|
||||
.then(user => { this.enableForm(form); })
|
||||
.catch(() => { this.enableForm(form); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@ class UserSummaryView extends BaseView {
|
|||
}
|
||||
|
||||
render(options) {
|
||||
options.target.innerHTML = this.template({
|
||||
user: options.user, isPrivate: options.isPrivate});
|
||||
options.target.innerHTML = this.template(options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class UserView extends BaseView {
|
|||
view = this.summaryView;
|
||||
}
|
||||
|
||||
this.showView(this.template(options.user));
|
||||
this.showView(this.template(options));
|
||||
|
||||
options.target = this.contentHolder.querySelector(
|
||||
'#user-content-holder');
|
||||
|
|
Loading…
Reference in a new issue