client/users: support avatar changing

This commit is contained in:
rr- 2016-04-10 15:55:56 +02:00
parent c788061976
commit 4dcee37567
9 changed files with 242 additions and 69 deletions

View File

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

View File

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

View File

@ -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>

View File

@ -1,5 +1,6 @@
<div id='user-edit'> <div id='user-edit'>
<form class='tabular'> <form class='tabular'>
<div class='left'>
<div class='input'> <div class='input'>
<ul> <ul>
{{#if this.canEditName}} {{#if this.canEditName}}
@ -26,11 +27,25 @@
</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>

View File

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

View File

@ -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 => {

View File

@ -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', {

View File

@ -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;

View File

@ -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 avatarContentField = source.querySelector('#avatar-content');
views.decorateValidator(form);
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 => {
const rankField = source.querySelector('#user-rank'); const rankField = source.querySelector('#user-rank');
const emailField = source.querySelector('#user-email'); const emailField = source.querySelector('#user-email');
const userNameField = source.querySelector('#user-name'); const userNameField = source.querySelector('#user-name');
const passwordField = source.querySelector('#user-password'); const passwordField = source.querySelector('#user-password');
const avatarStyleField = source.querySelector('#avatar-style'); const avatarStyleField = source.querySelector(
'[name=avatar-style]:checked');
views.decorateValidator(form);
/* TODO: avatar */
form.addEventListener('submit', e => {
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); });
}); });