client/users: add basic users listing
This commit is contained in:
parent
bb3f280c7f
commit
d6daf84da0
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<div class='pager'>
|
||||
<div class='messages'></div>
|
||||
<div class='page-content-holder'></div>
|
||||
<div class='page-nav'></div>
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 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() {
|
||||
|
|
|
@ -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(''));
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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 New Issue