client/users: add basic users listing
This commit is contained in:
parent
bb3f280c7f
commit
d6daf84da0
11 changed files with 274 additions and 6 deletions
|
@ -47,7 +47,7 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
#content-holder {
|
#content-holder {
|
||||||
margin-top: 2em;
|
margin: 2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#content-holder>.content-wrapper {
|
#content-holder>.content-wrapper {
|
||||||
|
@ -154,9 +154,30 @@ nav.text-nav ul li.active a {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.thumbnail img {
|
.thumbnail img {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
||||||
|
}
|
||||||
|
|
|
@ -65,8 +65,6 @@
|
||||||
}
|
}
|
||||||
#user-edit form {
|
#user-edit form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
#user-edit form {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
#user-edit .left {
|
#user-edit .left {
|
||||||
|
@ -87,3 +85,33 @@
|
||||||
#user-delete form {
|
#user-delete form {
|
||||||
width: 100%;
|
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;
|
||||||
|
}
|
||||||
|
|
5
client/html/manual_pager.hbs
Normal file
5
client/html/manual_pager.hbs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div class='pager'>
|
||||||
|
<div class='messages'></div>
|
||||||
|
<div class='page-content-holder'></div>
|
||||||
|
<div class='page-nav'></div>
|
||||||
|
</div>
|
39
client/html/manual_pager_nav.hbs
Normal file
39
client/html/manual_pager_nav.hbs
Normal 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>…</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
17
client/html/user_list.hbs
Normal 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>
|
16
client/js/controllers/page_controller.js
Normal file
16
client/js/controllers/page_controller.js
Normal 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();
|
|
@ -7,20 +7,26 @@ const events = require('../events.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const views = require('../util/views.js');
|
const views = require('../util/views.js');
|
||||||
const topNavController = require('../controllers/top_nav_controller.js');
|
const topNavController = require('../controllers/top_nav_controller.js');
|
||||||
|
const pageController = require('../controllers/page_controller.js');
|
||||||
const RegistrationView = require('../views/registration_view.js');
|
const RegistrationView = require('../views/registration_view.js');
|
||||||
const UserView = require('../views/user_view.js');
|
const UserView = require('../views/user_view.js');
|
||||||
|
const UserListView = require('../views/user_list_view.js');
|
||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
|
|
||||||
class UsersController {
|
class UsersController {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.registrationView = new RegistrationView();
|
this.registrationView = new RegistrationView();
|
||||||
this.userView = new UserView();
|
this.userView = new UserView();
|
||||||
|
this.userListView = new UserListView();
|
||||||
this.emptyView = new EmptyView();
|
this.emptyView = new EmptyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRoutes() {
|
registerRoutes() {
|
||||||
page('/register', () => { this.createUserRoute(); });
|
page('/register', () => { this.createUserRoute(); });
|
||||||
page('/users', () => { this.listUsersRoute(); });
|
page(
|
||||||
|
'/users/:query?',
|
||||||
|
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
||||||
|
(ctx, next) => { this.listUsersRoute(ctx, next); });
|
||||||
page(
|
page(
|
||||||
'/user/:name',
|
'/user/:name',
|
||||||
(ctx, next) => { this.loadUserRoute(ctx, next); },
|
(ctx, next) => { this.loadUserRoute(ctx, next); },
|
||||||
|
@ -36,8 +42,21 @@ class UsersController {
|
||||||
page.exit('/user/', (ctx, next) => { this.user = null; });
|
page.exit('/user/', (ctx, next) => { this.user = null; });
|
||||||
}
|
}
|
||||||
|
|
||||||
listUsersRoute() {
|
listUsersRoute(ctx, next) {
|
||||||
topNavController.activate('users');
|
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() {
|
createUserRoute() {
|
||||||
|
|
|
@ -14,7 +14,7 @@ handlebars.registerHelper('reltime', function(time) {
|
||||||
|
|
||||||
handlebars.registerHelper('thumbnail', function(url) {
|
handlebars.registerHelper('thumbnail', function(url) {
|
||||||
return new handlebars.SafeString(
|
return new handlebars.SafeString(
|
||||||
views.makeNonVoidElement('div', {
|
views.makeNonVoidElement('span', {
|
||||||
class: 'thumbnail',
|
class: 'thumbnail',
|
||||||
style: 'background-image: url(\'{0}\')'.format(url)
|
style: 'background-image: url(\'{0}\')'.format(url)
|
||||||
}, views.makeVoidElement('img', {alt: 'thumbnail', src: url})));
|
}, views.makeVoidElement('img', {alt: 'thumbnail', src: url})));
|
||||||
|
@ -100,3 +100,9 @@ handlebars.registerHelper('emailInput', function(options) {
|
||||||
options.hash.inputType = 'email';
|
options.hash.inputType = 'email';
|
||||||
return handlebars.helpers.input(options);
|
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(''));
|
||||||
|
});
|
||||||
|
|
|
@ -52,7 +52,25 @@ function formatRelativeTime(timeString) {
|
||||||
return future ? 'in ' + text : text + ' ago';
|
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 = {
|
module.exports = {
|
||||||
range: range,
|
range: range,
|
||||||
|
parseSearchQuery: parseSearchQuery,
|
||||||
|
parseSearchQueryRoute: parseSearchQueryRoute,
|
||||||
formatRelativeTime: formatRelativeTime,
|
formatRelativeTime: formatRelativeTime,
|
||||||
};
|
};
|
||||||
|
|
81
client/js/views/manual_page_view.js
Normal file
81
client/js/views/manual_page_view.js
Normal 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;
|
18
client/js/views/user_list_view.js
Normal file
18
client/js/views/user_list_view.js
Normal 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;
|
Loading…
Reference in a new issue