diff --git a/client/css/main.css b/client/css/main.css
index 7ff46206..0b339667 100644
--- a/client/css/main.css
+++ b/client/css/main.css
@@ -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;
+}
diff --git a/client/css/users.css b/client/css/users.css
index 01b6d651..85e0e2f8 100644
--- a/client/css/users.css
+++ b/client/css/users.css
@@ -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;
+}
diff --git a/client/html/manual_pager.hbs b/client/html/manual_pager.hbs
new file mode 100644
index 00000000..4bef2437
--- /dev/null
+++ b/client/html/manual_pager.hbs
@@ -0,0 +1,5 @@
+
diff --git a/client/html/manual_pager_nav.hbs b/client/html/manual_pager_nav.hbs
new file mode 100644
index 00000000..7586526d
--- /dev/null
+++ b/client/html/manual_pager_nav.hbs
@@ -0,0 +1,39 @@
+
diff --git a/client/html/user_list.hbs b/client/html/user_list.hbs
new file mode 100644
index 00000000..c6baa34d
--- /dev/null
+++ b/client/html/user_list.hbs
@@ -0,0 +1,17 @@
+
+
{{#each this.users}}-
+
+
{{/each}}{{alignFlexbox}}
+
diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js
new file mode 100644
index 00000000..783f70b3
--- /dev/null
+++ b/client/js/controllers/page_controller.js
@@ -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();
diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js
index 0e22fe20..c5046b0b 100644
--- a/client/js/controllers/users_controller.js
+++ b/client/js/controllers/users_controller.js
@@ -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() {
diff --git a/client/js/util/handlebars-helpers.js b/client/js/util/handlebars-helpers.js
index 7632bfcb..514509ad 100644
--- a/client/js/util/handlebars-helpers.js
+++ b/client/js/util/handlebars-helpers.js
@@ -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(() => '').join(''));
+});
diff --git a/client/js/util/misc.js b/client/js/util/misc.js
index 1909ceb8..9e8f13ac 100644
--- a/client/js/util/misc.js
+++ b/client/js/util/misc.js
@@ -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,
};
diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js
new file mode 100644
index 00000000..d458257e
--- /dev/null
+++ b/client/js/views/manual_page_view.js
@@ -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;
diff --git a/client/js/views/user_list_view.js b/client/js/views/user_list_view.js
new file mode 100644
index 00000000..ab36637c
--- /dev/null
+++ b/client/js/views/user_list_view.js
@@ -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;