client/posts: add proof of concept for post list
This commit is contained in:
parent
28009bf46d
commit
f8e6d07fea
|
@ -1,5 +1,88 @@
|
||||||
@import colors
|
@import colors
|
||||||
|
|
||||||
|
.post-list
|
||||||
|
ul
|
||||||
|
list-style-type: none
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
display: flex
|
||||||
|
align-content: flex-end
|
||||||
|
flex-wrap: wrap
|
||||||
|
margin: 0 -0.25em
|
||||||
|
|
||||||
|
li
|
||||||
|
position: relative
|
||||||
|
flex-grow: 1
|
||||||
|
margin: 0 0.25em 0.5em 0.25em
|
||||||
|
display: inline-block
|
||||||
|
text-align: left
|
||||||
|
min-width: 10em
|
||||||
|
width: 15vw
|
||||||
|
&:not(.flexbox-dummy)
|
||||||
|
min-height: 7.5em
|
||||||
|
height: 11.25vw
|
||||||
|
|
||||||
|
a
|
||||||
|
display: inline-block
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
line-height: 100%
|
||||||
|
font-size: 80%
|
||||||
|
color: white
|
||||||
|
|
||||||
|
.type
|
||||||
|
position: absolute
|
||||||
|
left: -1px
|
||||||
|
bottom: -1px
|
||||||
|
padding: 1em 4em 0.5em 0.5em
|
||||||
|
background-image: radial-gradient(
|
||||||
|
ellipse 100% 100% at bottom left,
|
||||||
|
rgba(0,0,0,0.5),
|
||||||
|
rgba(0,0,0,0));
|
||||||
|
&[data-type=image]
|
||||||
|
display: none
|
||||||
|
|
||||||
|
.stats
|
||||||
|
position: absolute
|
||||||
|
right: -1px
|
||||||
|
bottom: -1px
|
||||||
|
text-align: right
|
||||||
|
/*padding: 0.5em
|
||||||
|
background: rgba(0,0,0,0.5);*/
|
||||||
|
padding: 1em 0.5em 0.5em 4em
|
||||||
|
background-image: radial-gradient(
|
||||||
|
ellipse 100% 100% at bottom right,
|
||||||
|
rgba(0,0,0,0.5),
|
||||||
|
rgba(0,0,0,0));
|
||||||
|
i
|
||||||
|
margin-right: 0.5em
|
||||||
|
.icon:not(:first-of-type)
|
||||||
|
margin-left: 1em
|
||||||
|
|
||||||
|
.thumbnail
|
||||||
|
border: 1px solid $inactive-link-color
|
||||||
|
background-position: 50% 30%
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
margin: 0 0.6em 0 0
|
||||||
|
&:hover
|
||||||
|
background: $main-color
|
||||||
|
.thumbnail
|
||||||
|
opacity: .9
|
||||||
|
border: 1px solid $main-color
|
||||||
|
outline: 2px solid $main-color
|
||||||
|
|
||||||
|
.post-list-header
|
||||||
|
text-align: left
|
||||||
|
form
|
||||||
|
width: auto
|
||||||
|
input[name=search-text]
|
||||||
|
width: 25em
|
||||||
|
max-width: 90vw
|
||||||
|
.append
|
||||||
|
font-size: 0.95em
|
||||||
|
color: $inactive-link-color
|
||||||
|
|
||||||
.post-container
|
.post-container
|
||||||
text-align: center
|
text-align: center
|
||||||
.post-content
|
.post-content
|
||||||
|
|
|
@ -69,12 +69,11 @@
|
||||||
.user-list
|
.user-list
|
||||||
ul
|
ul
|
||||||
list-style-type: none
|
list-style-type: none
|
||||||
margin: 0
|
|
||||||
padding: 0
|
padding: 0
|
||||||
display: flex
|
display: flex
|
||||||
align-content: flex-end
|
align-content: flex-end
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
margin: 0 -0.5em 0 -0.5em
|
margin: 0 -0.5em
|
||||||
li
|
li
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
width: 20em
|
width: 20em
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class='post-list-header'>
|
||||||
|
<form class='horizontal'>
|
||||||
|
<div class='input'>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<%= ctx.makeTextInput({id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class='buttons'>
|
||||||
|
<input type='submit' value='Search'/>
|
||||||
|
<a class='button append' href='/help/search/posts'>Syntax help</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<div class='post-list'>
|
||||||
|
<% if (ctx.results.length) { %>
|
||||||
|
<ul>
|
||||||
|
<% _.each(ctx.results, post => { %>
|
||||||
|
<li>
|
||||||
|
<a href='/post/<%= post.id %>' title='@<%= post.id %> (<%= post.type %>) Tags: <%= post.tags.map(tag => '#' + tag).join(' ') %>'>
|
||||||
|
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
|
||||||
|
<span class='type' data-type='<%= post.type %>'>
|
||||||
|
<%= post.type %>
|
||||||
|
</span>
|
||||||
|
<% if (post.score || post.favoriteCount || post.commentCount) { %>
|
||||||
|
<span class='stats'>
|
||||||
|
<% if (post.score) { %>
|
||||||
|
<span class='icon'>
|
||||||
|
<i class='fa fa-star'></i>
|
||||||
|
<%= post.score %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
<% if (post.favoriteCount) { %>
|
||||||
|
<span class='icon'>
|
||||||
|
<i class='fa fa-heart'></i>
|
||||||
|
<%= post.favoriteCount %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
<% if (post.commentCount) { %>
|
||||||
|
<span class='icon'>
|
||||||
|
<i class='fa fa-commenting'></i>
|
||||||
|
<%= post.commentCount %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
<%= ctx.makeFlexboxAlign() %>
|
||||||
|
</ul>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
|
@ -2,15 +2,24 @@
|
||||||
|
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const page = require('page');
|
const page = require('page');
|
||||||
|
const api = require('../api.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 PostsHeaderView = require('../views/posts_header_view.js');
|
||||||
|
const PostsPageView = require('../views/posts_page_view.js');
|
||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
|
|
||||||
class PostsController {
|
class PostsController {
|
||||||
|
constructor() {
|
||||||
|
this._postsHeaderView = new PostsHeaderView();
|
||||||
|
this._postsPageView = new PostsPageView();
|
||||||
|
}
|
||||||
|
|
||||||
registerRoutes() {
|
registerRoutes() {
|
||||||
page('/upload', (ctx, next) => { this._uploadPostsRoute(); });
|
page('/upload', (ctx, next) => { this._uploadPostsRoute(); });
|
||||||
page('/posts/:query?',
|
page('/posts/:query?',
|
||||||
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
||||||
(ctx, next) => { this._listPostsRoute(); });
|
(ctx, next) => { this._listPostsRoute(ctx); });
|
||||||
page(
|
page(
|
||||||
'/post/:id',
|
'/post/:id',
|
||||||
(ctx, next) => { this._showPostRoute(ctx.params.id); });
|
(ctx, next) => { this._showPostRoute(ctx.params.id); });
|
||||||
|
@ -25,9 +34,23 @@ class PostsController {
|
||||||
this._emptyView.render();
|
this._emptyView.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
_listPostsRoute() {
|
_listPostsRoute(ctx) {
|
||||||
topNavController.activate('posts');
|
topNavController.activate('posts');
|
||||||
this._emptyView.render();
|
|
||||||
|
pageController.run({
|
||||||
|
state: ctx.state,
|
||||||
|
requestPage: page => {
|
||||||
|
const text = ctx.searchQuery.text;
|
||||||
|
return api.get(
|
||||||
|
`/posts/?query=${text}&page=${page}&pageSize=40&_fields=` +
|
||||||
|
`id,type,tags,score,favoriteCount,commentCount,thumbnailUrl`);
|
||||||
|
},
|
||||||
|
clientUrl: '/posts/' + misc.formatSearchQuery({
|
||||||
|
text: ctx.searchQuery.text, page: '{page}'}),
|
||||||
|
searchQuery: ctx.searchQuery,
|
||||||
|
headerRenderer: this._postsHeaderView,
|
||||||
|
pageRenderer: this._postsPageView,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_showPostRoute(id) {
|
_showPostRoute(id) {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const page = require('page');
|
||||||
|
const keyboard = require('../util/keyboard.js');
|
||||||
|
const misc = require('../util/misc.js');
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
const TagAutoCompleteControl =
|
||||||
|
require('../controls/tag_auto_complete_control.js');
|
||||||
|
|
||||||
|
class PostsHeaderView {
|
||||||
|
constructor() {
|
||||||
|
this._template = views.getTemplate('posts-header');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx) {
|
||||||
|
const target = ctx.target;
|
||||||
|
const source = this._template(ctx);
|
||||||
|
|
||||||
|
const form = source.querySelector('form');
|
||||||
|
const searchTextInput = form.querySelector('[name=search-text]');
|
||||||
|
|
||||||
|
if (searchTextInput) {
|
||||||
|
new TagAutoCompleteControl(searchTextInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard.bind('q', () => {
|
||||||
|
form.querySelector('input').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = searchTextInput.value;
|
||||||
|
searchTextInput.blur();
|
||||||
|
page('/posts/' + misc.formatSearchQuery({text: text}));
|
||||||
|
});
|
||||||
|
|
||||||
|
views.showView(target, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PostsHeaderView;
|
|
@ -0,0 +1,17 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
|
||||||
|
class PostsPageView {
|
||||||
|
constructor() {
|
||||||
|
this._template = views.getTemplate('posts-page');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx) {
|
||||||
|
const target = ctx.target;
|
||||||
|
const source = this._template(ctx);
|
||||||
|
views.showView(target, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PostsPageView;
|
Loading…
Reference in New Issue