client/users: add basic users listing

This commit is contained in:
rr- 2016-04-10 22:13:01 +02:00
parent bb3f280c7f
commit d6daf84da0
11 changed files with 274 additions and 6 deletions

View File

@ -47,7 +47,7 @@ a {
}
#content-holder {
margin-top: 2em;
margin: 2em;
text-align: center;
}
#content-holder>.content-wrapper {
@ -154,9 +154,30 @@ nav.text-nav ul li.active a {
background-repeat: no-repeat;
background-size: cover;
background-position: center;
display: inline-block;
}
.thumbnail img {
opacity: 0;
width: 100%;
height: 100%;
}
.flexbox-dummy {
height: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.pager nav .disabled {
opacity: .5;
}
.pager nav .prev span,
.pager nav .next span {
opacity: 0;
position: absolute;
display: block;
width: 0;
height: 0;
}

View File

@ -65,8 +65,6 @@
}
#user-edit form {
width: 100%;
}
#user-edit form {
display: flex;
}
#user-edit .left {
@ -87,3 +85,33 @@
#user-delete form {
width: 100%;
}
.user-list ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-content: flex-end;
flex-wrap: wrap;
}
.user-list li {
width: 20em;
margin: 0.5em;
padding: 0.75em;
vertical-align: top;
background: var(--top-nav-color);
text-align: left;
}
.user-list .wrapper {
display: flex;
}
.user-list .details {
font-size: 90%;
line-height: 130%;
}
.user-list .thumbnail {
width: 3em;
height: 3em;
margin: 0 0.6em 0 0;
}

View File

@ -0,0 +1,5 @@
<div class='pager'>
<div class='messages'></div>
<div class='page-content-holder'></div>
<div class='page-nav'></div>
</div>

View File

@ -0,0 +1,39 @@
<nav class='text-nav'>
<ul>
<li>
{{#if this.prevLinkActive}}
<a class='prev' href='{{this.prevLink}}'>
{{else}}
<a class='prev disabled'>
{{/if}}
<i class='fa fa-chevron-left'></i>
<span>Previous page</span>
</a>
</li>
{{#each this.pages}}
{{#if this.ellipsis}}
<li>&hellip;</li>
{{else}}
{{#if this.active}}
<li class='active'>
{{else}}
<li>
{{/if}}
<a href='{{this.link}}'>{{this.number}}</a>
</li>
{{/if}}
{{/each}}
<li>
{{#if this.nextLinkActive}}
<a class='next' href='{{this.nextLink}}'>
{{else}}
<a class='next disabled'>
{{/if}}
<i class='fa fa-chevron-right'></i>
<span>Next page</span>
</a>
</li>
</ul>
</nav>

17
client/html/user_list.hbs Normal file
View File

@ -0,0 +1,17 @@
<div class='user-list'>
<ul><!--
-->{{#each this.users}}<!--
--><li>
<div class='wrapper'>
<a class='image' href='/user/{{this.name}}'>{{thumbnail this.avatarUrl}}</a>
<div class='details'>
<a href='/user/{{this.name}}'>{{this.name}}</a><br/>
Registered: {{reltime this.creationTime}}<br/>
Last seen: {{reltime this.lastLoginTime}}
</div>
</div>
</li><!--
-->{{/each}}<!--
-->{{alignFlexbox}}<!--
--></ul>
</div>

View File

@ -0,0 +1,16 @@
'use strict';
const api = require('../api.js');
const ManualPageView = require('../views/manual_page_view.js');
class PageController {
constructor() {
this.pageView = new ManualPageView();
}
run(ctx) {
this.pageView.render(ctx);
}
}
module.exports = new PageController();

View File

@ -7,20 +7,26 @@ const events = require('../events.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const topNavController = require('../controllers/top_nav_controller.js');
const pageController = require('../controllers/page_controller.js');
const RegistrationView = require('../views/registration_view.js');
const UserView = require('../views/user_view.js');
const UserListView = require('../views/user_list_view.js');
const EmptyView = require('../views/empty_view.js');
class UsersController {
constructor() {
this.registrationView = new RegistrationView();
this.userView = new UserView();
this.userListView = new UserListView();
this.emptyView = new EmptyView();
}
registerRoutes() {
page('/register', () => { this.createUserRoute(); });
page('/users', () => { this.listUsersRoute(); });
page(
'/users/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { this.listUsersRoute(ctx, next); });
page(
'/user/:name',
(ctx, next) => { this.loadUserRoute(ctx, next); },
@ -36,8 +42,21 @@ class UsersController {
page.exit('/user/', (ctx, next) => { this.user = null; });
}
listUsersRoute() {
listUsersRoute(ctx, next) {
topNavController.activate('users');
pageController.run({
requestPage: page => {
return api.get(
'/users/?query={text}&page={page}&pageSize=30'.format({
text: ctx.searchQuery.text,
page: page}));
},
clientUrl: '/users/text={text};page={page}'.format({
text: ctx.searchQuery.text}),
initialPage: ctx.searchQuery.page,
pageRenderer: this.userListView,
});
}
createUserRoute() {

View File

@ -14,7 +14,7 @@ handlebars.registerHelper('reltime', function(time) {
handlebars.registerHelper('thumbnail', function(url) {
return new handlebars.SafeString(
views.makeNonVoidElement('div', {
views.makeNonVoidElement('span', {
class: 'thumbnail',
style: 'background-image: url(\'{0}\')'.format(url)
}, views.makeVoidElement('img', {alt: 'thumbnail', src: url})));
@ -100,3 +100,9 @@ handlebars.registerHelper('emailInput', function(options) {
options.hash.inputType = 'email';
return handlebars.helpers.input(options);
});
handlebars.registerHelper('alignFlexbox', function(options) {
return new handlebars.SafeString(
Array.from(misc.range(20))
.map(() => '<li class="flexbox-dummy"></li>').join(''));
});

View File

@ -52,7 +52,25 @@ function formatRelativeTime(timeString) {
return future ? 'in ' + text : text + ' ago';
}
function parseSearchQuery(query) {
let result = {};
for (let word of (query || '').split(/;/)) {
const [key, value] = word.split(/=/, 2);
result[key] = value;
}
result.text = result.text || '';
result.page = parseInt(result.page || '1');
return result;
}
function parseSearchQueryRoute(ctx, next) {
ctx.searchQuery = parseSearchQuery(ctx.params.query || '');
next();
}
module.exports = {
range: range,
parseSearchQuery: parseSearchQuery,
parseSearchQueryRoute: parseSearchQueryRoute,
formatRelativeTime: formatRelativeTime,
};

View File

@ -0,0 +1,81 @@
'use strict';
const events = require('../events.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
function removeConsecutiveDuplicates(a) {
return a.filter((item, pos, ary) => {
return !pos || item != ary[pos - 1];
});
}
class ManualPageView {
constructor() {
this.holderTemplate = views.getTemplate('manual-pager');
this.navTemplate = views.getTemplate('manual-pager-nav');
}
render(ctx) {
const target = document.getElementById('content-holder');
const source = this.holderTemplate();
const pageContentHolder = source.querySelector('.page-content-holder');
const pageNav = source.querySelector('.page-nav');
ctx.requestPage(ctx.initialPage).then(response => {
let pageRendererCtx = response;
pageRendererCtx.target = pageContentHolder;
ctx.pageRenderer.render(pageRendererCtx);
const totalPages = Math.ceil(response.total / response.pageSize);
const threshold = 2;
let pagesVisible = [];
for (let i = 1; i <= threshold; i++) {
pagesVisible.push(i);
}
for (let i = totalPages - threshold; i <= totalPages; i++) {
pagesVisible.push(i);
}
for (let i = ctx.initialPage - threshold;
i <= ctx.initialPage + threshold;
i++) {
pagesVisible.push(i);
}
pagesVisible = pagesVisible.filter((item, pos, ary) => {
return item >= 1 && item <= totalPages;
});
pagesVisible = pagesVisible.sort((a, b) => { return a - b; });
pagesVisible = removeConsecutiveDuplicates(pagesVisible);
const pages = [];
let lastPage = 0;
for (let page of pagesVisible) {
if (page !== lastPage + 1) {
pages.push({ellipsis: true});
}
pages.push({
number: page,
link: ctx.clientUrl.format({page: page}),
active: ctx.initialPage === page,
});
lastPage = page;
}
views.showView(pageNav, this.navTemplate({
prevLink: ctx.clientUrl.format({page: ctx.initialPage - 1}),
nextLink: ctx.clientUrl.format({page: ctx.initialPage + 1}),
prevLinkActive: ctx.initialPage > 1,
nextLinkActive: ctx.initialPage < totalPages,
pages: pages,
}));
views.listenToMessages(target);
views.showView(target, source);
}, response => {
views.listenToMessages(target);
views.showView(target, source);
events.notify(events.Error, response.description);
});
}
}
module.exports = ManualPageView;

View File

@ -0,0 +1,18 @@
'use strict';
const views = require('../util/views.js');
class UserListView {
constructor() {
this.template = views.getTemplate('user-list');
}
render(ctx) {
const target = ctx.target;
const source = this.template(ctx);
views.listenToMessages(target);
views.showView(target, source);
}
}
module.exports = UserListView;