client/users: support avatar changing
This commit is contained in:
parent
c788061976
commit
4dcee37567
|
@ -20,7 +20,7 @@ form .input {
|
||||||
form .buttons {
|
form .buttons {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
form .input li:first-child label:not(.radio):not(.checkbox),
|
form .input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper),
|
||||||
form .input li:first-child {
|
form .input li:first-child {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -33,7 +33,7 @@ form.tabular ul {
|
||||||
form.tabular ul li {
|
form.tabular ul li {
|
||||||
display: table-row;
|
display: table-row;
|
||||||
}
|
}
|
||||||
form.tabular ul li label:not(.radio):not(.checkbox) {
|
form.tabular ul li label:not(.radio):not(.checkbox):not(.file-dropper) {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
width: 33%;
|
width: 33%;
|
||||||
}
|
}
|
||||||
|
@ -219,3 +219,27 @@ button::-moz-focus-inner,
|
||||||
input::-moz-focus-inner {
|
input::-moz-focus-inner {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* File dropper
|
||||||
|
*/
|
||||||
|
.file-dropper {
|
||||||
|
background: white;
|
||||||
|
border: 3px dashed #eee;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
line-height: 140%;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
input[type=file]:disabled+.file-dropper {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
input[type=file]:active+.file-dropper,
|
||||||
|
input[type=file]:focus+.file-dropper,
|
||||||
|
.file-dropper.active {
|
||||||
|
border-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
|
@ -64,11 +64,26 @@
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
#user-edit form {
|
#user-edit form {
|
||||||
width: 22.5em;
|
width: 100%;
|
||||||
|
}
|
||||||
|
#user-edit form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#user-edit .left {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
#user-edit .right {
|
||||||
|
width: 35%;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
#user-edit .file-dropper-holder {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#user-edit .file-dropper {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
#user-delete form {
|
#user-delete form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
#user-delete form label {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<div class='file-dropper-holder'>
|
||||||
|
<input type='file' name='{{this.name}}' id='{{this.id}}'/>
|
||||||
|
<label class='file-dropper' for='{{this.id}}'>
|
||||||
|
{{#if this.allowMultiple}}
|
||||||
|
Drop files here!
|
||||||
|
{{else}}
|
||||||
|
Drop file here!
|
||||||
|
{{/if}}
|
||||||
|
<br/>
|
||||||
|
Or just click on this box.
|
||||||
|
</label>
|
||||||
|
</div>
|
|
@ -1,36 +1,51 @@
|
||||||
<div id='user-edit'>
|
<div id='user-edit'>
|
||||||
<form class='tabular'>
|
<form class='tabular'>
|
||||||
<div class='input'>
|
<div class='left'>
|
||||||
<ul>
|
<div class='input'>
|
||||||
{{#if this.canEditName}}
|
<ul>
|
||||||
<li>
|
{{#if this.canEditName}}
|
||||||
{{textInput text='User name' id='user-name' name='name' value=this.user.name}}
|
<li>
|
||||||
</li>
|
{{textInput text='User name' id='user-name' name='name' value=this.user.name}}
|
||||||
{{/if}}
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.canEditPassword}}
|
{{#if this.canEditPassword}}
|
||||||
<li>
|
<li>
|
||||||
{{passwordInput text='Password' id='user-password' name='password' placeholder='leave blank if not changing'}}
|
{{passwordInput text='Password' id='user-password' name='password' placeholder='leave blank if not changing'}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.canEditEmail}}
|
{{#if this.canEditEmail}}
|
||||||
<li>
|
<li>
|
||||||
{{emailInput text='Email' id='user-email' name='email' value=this.user.email}}
|
{{emailInput text='Email' id='user-email' name='email' value=this.user.email}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.canEditRank}}
|
{{#if this.canEditRank}}
|
||||||
<li>
|
<li>
|
||||||
{{select text='Rank' id='user-rank' name='rank' keyValues=this.ranks selectedKey=this.user.rank}}
|
{{select text='Rank' id='user-rank' name='rank' keyValues=this.ranks selectedKey=this.user.rank}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
<!-- TODO: avatar -->
|
</div>
|
||||||
</div>
|
<div class='messages'></div>
|
||||||
<div class='messages'></div>
|
<div class='buttons'>
|
||||||
<div class='buttons'>
|
<input type='submit' value='Save settings'/>
|
||||||
<input type='submit' value='Save settings'/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if this.canEditAvatar}}
|
||||||
|
<div class='right'>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{{radio text='Gravatar' id='gravatar-radio' name='avatar-style' value='gravatar' selectedValue=this.user.avatarStyle}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{radio text='Manual avatar' id='avatar-radio' name='avatar-style' value='manual' selectedValue=this.user.avatarStyle}}
|
||||||
|
<div id='avatar-content'></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,28 +13,33 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
get(url) {
|
get(url) {
|
||||||
const fullUrl = this.getFullUrl(url);
|
return this._process(url, request.get);
|
||||||
return this._process(fullUrl, () => request.get(fullUrl));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
post(url, data) {
|
post(url, data, files) {
|
||||||
const fullUrl = this.getFullUrl(url);
|
return this._process(url, request.post, data, files);
|
||||||
return this._process(fullUrl, () => request.post(fullUrl).send(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
put(url, data) {
|
put(url, data, files) {
|
||||||
const fullUrl = this.getFullUrl(url);
|
return this._process(url, request.put, data, files);
|
||||||
return this._process(fullUrl, () => request.put(fullUrl).send(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(url, data) {
|
delete(url) {
|
||||||
const fullUrl = this.getFullUrl(url);
|
return this._process(url, request.delete);
|
||||||
return this._process(fullUrl, () => request.delete(fullUrl).send(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_process(url, requestFactory) {
|
_process(url, requestFactory, data, files) {
|
||||||
|
const fullUrl = this.getFullUrl(url);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let req = requestFactory();
|
let req = requestFactory(fullUrl);
|
||||||
|
if (data) {
|
||||||
|
req.attach('metadata', new Blob([JSON.stringify(data)]));
|
||||||
|
}
|
||||||
|
if (files) {
|
||||||
|
for (let key of Object.keys(files)) {
|
||||||
|
req.attach(key, files[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this.userName && this.userPassword) {
|
if (this.userName && this.userPassword) {
|
||||||
req.auth(this.userName, this.userPassword);
|
req.auth(this.userName, this.userPassword);
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,27 +102,46 @@ class UsersController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_edit(user, newName, newPassword, newEmail, newRank) {
|
_edit(user, data) {
|
||||||
const data = {};
|
let files = [];
|
||||||
if (newName) { data.name = newName; }
|
|
||||||
if (newPassword) { data.password = newPassword; }
|
if (!data.name) {
|
||||||
if (newEmail) { data.email = newEmail; }
|
delete data.name;
|
||||||
if (newRank) { data.rank = newRank; }
|
}
|
||||||
/* TODO: avatar */
|
if (!data.password) {
|
||||||
|
delete data.password;
|
||||||
|
}
|
||||||
|
if (!data.email) {
|
||||||
|
delete data.email;
|
||||||
|
}
|
||||||
|
if (!data.rank) {
|
||||||
|
delete data.rank;
|
||||||
|
}
|
||||||
|
if (!data.avatarStyle ||
|
||||||
|
(data.avatarStyle == user.avatarStyle && !data.avatarContent)) {
|
||||||
|
delete data.avatarStyle;
|
||||||
|
}
|
||||||
|
if (data.avatarContent) {
|
||||||
|
files.avatar = data.avatarContent;
|
||||||
|
}
|
||||||
|
|
||||||
const isLoggedIn = api.isLoggedIn() && api.user.id == user.id;
|
const isLoggedIn = api.isLoggedIn() && api.user.id == user.id;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
api.put('/user/' + user.name, data)
|
api.put('/user/' + user.name, data, files)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
this.user = response.user;
|
||||||
return isLoggedIn ?
|
return isLoggedIn ?
|
||||||
api.login(
|
api.login(
|
||||||
newName, newPassword || api.userPassword, false) :
|
data.name || api.userName,
|
||||||
|
data.password || api.userPassword,
|
||||||
|
false) :
|
||||||
Promise.fulfill();
|
Promise.fulfill();
|
||||||
}, response => {
|
}, response => {
|
||||||
return Promise.reject(response.description);
|
return Promise.reject(response.description);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
resolve();
|
resolve();
|
||||||
if (newName !== user.name) {
|
if (data.name && data.name !== user.name) {
|
||||||
page('/user/' + newName + '/edit');
|
page('/user/' + data.name + '/edit');
|
||||||
}
|
}
|
||||||
events.notify(events.Success, 'Settings updated.');
|
events.notify(events.Success, 'Settings updated.');
|
||||||
}, errorMessage => {
|
}, errorMessage => {
|
||||||
|
|
|
@ -31,6 +31,7 @@ handlebars.registerHelper('radio', function(options) {
|
||||||
name: options.hash.name,
|
name: options.hash.name,
|
||||||
value: options.hash.value,
|
value: options.hash.value,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
checked: options.hash.selectedValue === options.hash.value,
|
||||||
required: options.hash.required,
|
required: options.hash.required,
|
||||||
}),
|
}),
|
||||||
views.makeNonVoidElement('label', {
|
views.makeNonVoidElement('label', {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
|
||||||
|
class FileDropperControl {
|
||||||
|
constructor() {
|
||||||
|
this.template = views.getTemplate('file-dropper');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx) {
|
||||||
|
const target = ctx.target;
|
||||||
|
const source = this.template({
|
||||||
|
id: 'file-' + Math.random().toString(36).substring(7),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropper = source.querySelector('.file-dropper');
|
||||||
|
const fileInput = source.querySelector('input');
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
fileInput.multiple = ctx.allowMultiple || false;
|
||||||
|
|
||||||
|
const resolve = files => {
|
||||||
|
files = Array.from(files);
|
||||||
|
if (ctx.lock) {
|
||||||
|
dropper.innerText = files.map(file => file.name).join(', ');
|
||||||
|
}
|
||||||
|
ctx.resolve(files);
|
||||||
|
};
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
dropper.addEventListener('dragenter', e => {
|
||||||
|
dropper.classList.add('active');
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
dropper.addEventListener('dragleave', e => {
|
||||||
|
counter--;
|
||||||
|
if (counter === 0) {
|
||||||
|
dropper.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dropper.addEventListener('dragover', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
dropper.addEventListener('drop', e => {
|
||||||
|
dropper.classList.remove('active');
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.dataTransfer.files.length) {
|
||||||
|
window.alert('Only files are supported.');
|
||||||
|
}
|
||||||
|
if (!ctx.allowMultiple && e.dataTransfer.files.length > 1) {
|
||||||
|
window.alert('Cannot select multiple files.');
|
||||||
|
}
|
||||||
|
resolve(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', e => {
|
||||||
|
resolve(e.target.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
views.showView(target, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FileDropperControl;
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
const config = require('../config.js');
|
const config = require('../config.js');
|
||||||
const views = require('../util/views.js');
|
const views = require('../util/views.js');
|
||||||
|
const FileDropperControl = require('./file_dropper_control.js');
|
||||||
|
|
||||||
class UserEditView {
|
class UserEditView {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.template = views.getTemplate('user-edit');
|
this.template = views.getTemplate('user-edit');
|
||||||
|
this.fileDropperControl = new FileDropperControl();
|
||||||
}
|
}
|
||||||
|
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
|
@ -16,25 +18,39 @@ class UserEditView {
|
||||||
const source = this.template(ctx);
|
const source = this.template(ctx);
|
||||||
|
|
||||||
const form = source.querySelector('form');
|
const form = source.querySelector('form');
|
||||||
const rankField = source.querySelector('#user-rank');
|
const avatarContentField = source.querySelector('#avatar-content');
|
||||||
const emailField = source.querySelector('#user-email');
|
|
||||||
const userNameField = source.querySelector('#user-name');
|
|
||||||
const passwordField = source.querySelector('#user-password');
|
|
||||||
const avatarStyleField = source.querySelector('#avatar-style');
|
|
||||||
|
|
||||||
views.decorateValidator(form);
|
views.decorateValidator(form);
|
||||||
|
|
||||||
/* TODO: avatar */
|
let avatarContent = null;
|
||||||
|
this.fileDropperControl.render({
|
||||||
|
target: avatarContentField,
|
||||||
|
lock: true,
|
||||||
|
resolve: files => {
|
||||||
|
source.querySelector(
|
||||||
|
'[name=avatar-style][value=manual]').checked = true;
|
||||||
|
avatarContent = files[0];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
form.addEventListener('submit', e => {
|
form.addEventListener('submit', e => {
|
||||||
|
const rankField = source.querySelector('#user-rank');
|
||||||
|
const emailField = source.querySelector('#user-email');
|
||||||
|
const userNameField = source.querySelector('#user-name');
|
||||||
|
const passwordField = source.querySelector('#user-password');
|
||||||
|
const avatarStyleField = source.querySelector(
|
||||||
|
'[name=avatar-style]:checked');
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
views.clearMessages(target);
|
views.clearMessages(target);
|
||||||
views.disableForm(form);
|
views.disableForm(form);
|
||||||
ctx.edit(
|
ctx.edit({
|
||||||
userNameField.value,
|
name: userNameField.value,
|
||||||
passwordField.value,
|
password: passwordField.value,
|
||||||
emailField.value,
|
email: emailField.value,
|
||||||
rankField.value)
|
rank: rankField.value,
|
||||||
|
avatarStyle: avatarStyleField.value,
|
||||||
|
avatarContent: avatarContent})
|
||||||
.always(() => { views.enableForm(form); });
|
.always(() => { views.enableForm(form); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue