update with latest commits of nova

This commit is contained in:
xavizalote 2016-03-22 08:44:23 +01:00
commit c3eb63b7f4
27 changed files with 344 additions and 40 deletions

View file

@ -21,8 +21,8 @@ nova:notifications
nova:getting-started
nova:categories
nova:share
nova:voting
# nova:voting
# nova:migrations
# nova:api
# nova:email

View file

@ -104,6 +104,7 @@ nova:search@0.25.7
nova:settings@0.25.7
nova:share@0.25.7
nova:users@0.25.7
nova:voting@0.25.7
npm-bcrypt@0.7.8_2
npm-mongo@1.4.41-rc.2
oauth@1.1.8-rc.2

View file

@ -4,8 +4,8 @@ import Core from "meteor/nova:core";
const Messages = Core.Messages;
const Header = ({currentUser}) => {
({Logo, ListContainer, CategoriesList, FlashContainer, FlashMessages, ModalButton, NewDocContainer, CanCreatePost, CurrentUserContainer, NewsletterForm, HeadTags} = Telescope.components);
({Logo, ListContainer, CategoriesList, FlashContainer, FlashMessages, ModalButton, NewDocContainer, CanCreatePost, CurrentUserContainer, NewsletterForm, SearchForm, HeadTags} = Telescope.components);
const logoUrl = Telescope.settings.get("logoUrl");
const siteTitle = Telescope.settings.get("title", "Telescope");
@ -42,6 +42,8 @@ const Header = ({currentUser}) => {
<CurrentUserContainer component={NewsletterForm} />
<SearchForm/>
<FlashContainer component={FlashMessages}/>
</header>

View file

@ -0,0 +1,10 @@
const Icon = ({ name, iconClass }) => {
const icons = Telescope.utils.icons;
const iconCode = !!icons[name] ? icons[name] : name;
iconClass = (typeof iconClass === 'string') ? ' '+iconClass : '';
const c = 'icon fa fa-fw fa-' + iconCode + ' icon-' + name + iconClass;
return <i className={c} aria-hidden="true"></i>;
}
module.exports = Icon;
export default Icon;

View file

@ -0,0 +1,65 @@
import React, { PropTypes, Component } from 'react';
import Formsy from 'formsy-react';
import FRC from 'formsy-react-components';
const Input = FRC.Input;
// see: http://stackoverflow.com/questions/1909441/jquery-keyup-delay
const delay = (function(){
var timer = 0;
return function(callback, ms){
clearTimeout (timer);
timer = setTimeout(callback, ms);
};
})();
class SearchForm extends Component{
constructor() {
super();
this.search = this.search.bind(this);
}
search(data) {
if (FlowRouter.getRouteName() !== "posts.list") {
FlowRouter.go("posts.list");
}
if (data.searchQuery === '') {
data.searchQuery = null;
}
delay(function(){
FlowRouter.setQueryParams({query: data.searchQuery});
}, 700 );
}
render() {
return (
<div className="search-form">
<Formsy.Form onChange={this.search}>
<Input
name="searchQuery"
value=""
label={this.props.labelText}
type="text"
/>
</Formsy.Form>
</div>
)
}
}
SearchForm.propTypes = {
labelText: React.PropTypes.string
}
SearchForm.defaultProps = {
labelText: "Search"
};
module.exports = SearchForm;
export default SearchForm;

View file

@ -8,6 +8,8 @@ Telescope.registerComponent("Flash", require('./common/Flash.jsx'));
Telescope.registerComponent('HeadTags', require('./common/HeadTags.jsx'));
Telescope.registerComponent("FlashMessages", require('./common/FlashMessages.jsx'));
Telescope.registerComponent("NewsletterForm", require('./common/NewsletterForm.jsx'));
Telescope.registerComponent("Icon", require('./common/Icon.jsx'));
Telescope.registerComponent("SearchForm", require('./common/SearchForm.jsx'));
// posts
@ -21,6 +23,10 @@ Telescope.registerComponent("PostList", require('./posts/list/PostList.jsx'));
Telescope.registerComponent("PostCategories", require('./posts/list/PostCategories.jsx'));
Telescope.registerComponent("PostCommenters", require('./posts/list/PostCommenters.jsx'));
Telescope.registerComponent("Post", require('./posts/Post.jsx'));
Telescope.registerComponent("PostStats", require('./posts/PostStats.jsx'));
Telescope.registerComponent("PostDaily", require('./posts/PostDaily.jsx'));
Telescope.registerComponent("PostDay", require('./posts/PostDay.jsx'));
Telescope.registerComponent("Vote", require('./posts/Vote.jsx'));
// comments

View file

@ -1,3 +1,5 @@
const methodList = Meteor.isServer ? Meteor.server.method_handlers : Meteor.connection._methodHandlers;
const renderFunction = (func, name) => {
const s = func.toString();
const openParen = s.indexOf("(");
@ -45,6 +47,10 @@ const Cheatsheet = props => {
<ul>
{_.map(Users.is, renderFunction)}
</ul>
<h3>Methods</h3>
<ul>
{_.map(methodList, (item, key) => (key.indexOf("users.") !== -1 ? renderFunction(item, key) : null))}
</ul>
</div>
<div className="cheatsheet-block">
@ -53,6 +59,10 @@ const Cheatsheet = props => {
<ul>
{_.map(Posts, (item, key) => (key[0] !== "_" ? renderFunction(item, key) : null) )}
</ul>
<h3>Methods</h3>
<ul>
{_.map(methodList, (item, key) => (key.indexOf("posts.") !== -1 ? renderFunction(item, key) : null))}
</ul>
</div>
<div className="cheatsheet-block">
@ -61,6 +71,10 @@ const Cheatsheet = props => {
<ul>
{_.map(Comments, (item, key) => (key[0] !== "_" ? renderFunction(item, key) : null) )}
</ul>
<h3>Methods</h3>
<ul>
{_.map(methodList, (item, key) => (key.indexOf("comments.") !== -1 ? renderFunction(item, key) : null))}
</ul>
</div>
<div className="cheatsheet-block">

View file

@ -1,17 +1,22 @@
const Post = ({document}) => {
({ListContainer, CommentList, CommentNew, PostCategories, SocialShare, HeadTags} = Telescope.components);
const Post = ({document, currentUser}) => {
({ListContainer, CommentList, CommentNew, PostCategories, SocialShare, Vote, PostStats, HeadTags} = Telescope.components);
const post = document;
const htmlBody = {__html: post.htmlBody};
return (
<div className="post">
<Vote post={post} currentUser={currentUser}/>
<h3>{post.title}</h3>
<HeadTags url={Posts.getLink(post)} title={post.title}/>
<SocialShare url={ Posts.getLink(post) } title={ post.title }/>
<p>{post.commentCount} comments</p>
<p>{moment(post.postedAt).fromNow()}</p>
<PostStats post={post} />
{post.categoriesArray ? <PostCategories categories={post.categoriesArray} /> : ""}
<div dangerouslySetInnerHTML={htmlBody}></div>

View file

@ -0,0 +1,45 @@
import React, { PropTypes, Component } from 'react';
// for a number of days "n" return dates object for the past n days
const getLastNDates = n => {
return _.range(n).map(
i => moment().subtract(i, 'days').startOf('day').toDate()
);
};
class PostDaily extends Component{
constructor(props) {
super(props);
this.loadMoreDays = this.loadMoreDays.bind(this);
this.state = {days: props.days};
}
loadMoreDays(e) {
e.preventDefault();
this.setState({
days: this.state.days + 5
});
}
render() {
({PostDay} = Telescope.components);
return (
<div className="post-daily">
{getLastNDates(this.state.days).map((date, index) => <PostDay key={index} date={date} number={index}/>)}
<a href="#" className="button button--primary" onClick={this.loadMoreDays}>Load More Days</a>
</div>
)
}
}
PostDaily.propTypes = {
days: React.PropTypes.number
}
PostDaily.defaultProps = {
days: 5
}
module.exports = PostDaily;
export default PostDaily;

View file

@ -0,0 +1,40 @@
import React, { PropTypes, Component } from 'react';
const PostDay = ({date, number}) => {
({PostList} = Telescope.components);
const terms = {
view: "top",
date: date,
after: moment(date).format("YYYY-MM-DD"),
before: moment(date).format("YYYY-MM-DD"),
enableCache: number <= 15 ? true : false // only cache first 15 days
};
({selector, options} = Posts.parameters.get(terms));
return (
<div className="post-day">
<h2>{moment(date).format("dddd, MMMM Do YYYY")}</h2>
<ListContainer
collection={Posts}
publication="posts.list"
selector={selector}
options={options}
terms={terms}
joins={Posts.getJoins()}
component={PostList}
componentProps={{showViews: false}}
/>
</div>
)
}
PostDay.propTypes = {
date: React.PropTypes.object,
number: React.PropTypes.number
}
module.exports = PostDay;
export default PostDay;

View file

@ -0,0 +1,16 @@
const PostStats = ({post}) => {
({Icon} = Telescope.components);
return (
<div className="post-stats">
{post.score ? <span title="Score"><Icon name="score"/> {Math.floor(post.score*10000)/10000} <span className="sr-only">Score</span></span> : ""}
<span title="Upvotes"><Icon name="upvote"/> {post.upvotes} <span className="sr-only">Upvotes</span></span>
<span title="Clicks"><Icon name="clicks"/> {post.clickCount} <span className="sr-only">Clicks</span></span>
<span title="Views"><Icon name="views"/> {post.viewCount} <span className="sr-only">Views</span></span>
</div>
)
}
module.exports = PostStats;
export default PostStats;

View file

@ -0,0 +1,62 @@
import React, { PropTypes, Component } from 'react';
import Core from "meteor/nova:core";
const Messages = Core.Messages;
class Vote extends Component {
constructor() {
super();
this.upvote = this.upvote.bind(this);
}
upvote(e) {
e.preventDefault();
const post = this.props.post;
const user = this.props.currentUser;
if(!user){
Messages.flash("Please log in first");
} else if (user.hasUpvoted(post)) {
Meteor.call('posts.cancelUpvote', post._id, function(){
Events.track("post upvote cancelled", {'_id': post._id});
});
} else {
Meteor.call('posts.upvote', post._id, function(){
Events.track("post upvoted", {'_id': post._id});
});
}
}
render() {
({Icon} = Telescope.components);
const post = this.props.post;
const user = this.props.currentUser;
let actionsClass = "vote";
if (Users.hasUpvoted(user, post)) actionsClass += " voted upvoted";
if (Users.hasDownvoted(user, post)) actionsClass += " voted downvoted";
return (
<div className={actionsClass}>
<a href="#" className="button button--secondary upvote" onClick={this.upvote}>
<Icon name="upvote" />
<span className="sr-only">Upvote</span>
</a>
</div>
)
}
}
Vote.propTypes = {
post: React.PropTypes.object.isRequired, // the current comment
currentUser: React.PropTypes.object, // the current user
}
module.exports = Vote;
export default Vote;

View file

@ -40,16 +40,19 @@ class PostItem extends Component {
render() {
({UserAvatar} = Telescope.components);
({UserAvatar, Vote, PostStats} = Telescope.components);
const post = this.props.post;
return (
<div className="post-item">
<Vote post={post} currentUser={this.props.currentUser}/>
<h3 className="post-title"><a href={Posts.getLink(post)} target={Posts.getLinkTarget(post)}>{post.title}</a></h3>
<p><a href={Users.getProfileUrl(post.user)}><UserAvatar user={post.user}/>{Users.getDisplayName(post.user)}</a>, {moment(post.postedAt).fromNow()}, {post.commentCount} comments</p>
<PostStats post={post} />
{this.renderCategories()}
{this.renderCommenters()}
{this.renderActions()}

View file

@ -1,11 +1,11 @@
const PostList = ({results, currentUser, hasMore, ready, count, totalCount, loadMore}) => {
const PostList = ({results, currentUser, hasMore, ready, count, totalCount, loadMore, showViews = true}) => {
({PostItem, LoadMore, PostsLoading, NoPosts, NoMorePosts, PostViews} = Telescope.components);
if (!!results.length) {
return (
<div className="postList">
<PostViews />
{showViews ? <PostViews /> : null}
<div className="post-list-content">
{results.map(post => <PostItem post={post} currentUser={currentUser} key={post._id}/>)}
</div>
@ -15,7 +15,7 @@ const PostList = ({results, currentUser, hasMore, ready, count, totalCount, load
} else if (!ready) {
return (
<div className="postList">
<PostViews />
{showViews ? <PostViews /> : null}
<div className="post-list-content">
<PostsLoading/>
</div>
@ -24,7 +24,7 @@ const PostList = ({results, currentUser, hasMore, ready, count, totalCount, load
} else {
return (
<div className="postList">
<PostViews />
{showViews ? <PostViews /> : null}
<div className="post-list-content">
<NoPosts/>
</div>

View file

@ -24,6 +24,16 @@ FlowRouter.route('/', {
}
});
FlowRouter.route('/daily/:days?', {
name: 'posts.list',
action(params, queryParams) {
({AppContainer, PostDaily} = Telescope.components);
mount(AppContainer, {content: <PostDaily days={params.days}/>})
}
});
FlowRouter.route('/posts/:_id', {
name: 'posts.single',
action(params, queryParams) {

View file

@ -97,6 +97,7 @@ code{
}
.cheatsheet h3{
margin-bottom: 10px;
font-weight: bold;
}
.cheatsheet code{
font-size: 13px;
@ -106,4 +107,16 @@ code{
height: 24px;
width: 24px;
display: inline-block;
}
.sr-only{
display: none;
}
.upvoted .upvote{
opacity: 0.3;
}
.post-day h2{
font-weight: bold;
}

View file

@ -1,5 +1,7 @@
import {mount} from 'react-mounter';
import MoviesWrapper from './demo-components.jsx';
//////////////////////////////////////////////////////
// Collection & Schema //
//////////////////////////////////////////////////////

View file

@ -6,20 +6,20 @@ import Core from 'meteor/nova:core';
import SmartContainers from "meteor/utilities:react-list-container";
import FormContainers from "meteor/utilities:react-form-containers";
FlashContainer = Core.FlashContainer;
ModalButton = Core.ModalButton;
NewDocContainer = FormContainers.NewDocContainer;
EditDocContainer = FormContainers.EditDocContainer;
ListContainer = SmartContainers.ListContainer;
const ModalButton = Core.ModalButton;
const NewDocContainer = FormContainers.NewDocContainer;
const EditDocContainer = FormContainers.EditDocContainer;
const ListContainer = SmartContainers.ListContainer;
const FlashContainer = Telescope.components.FlashContainer;
const FlashMessages = Telescope.components.FlashMessages;
//////////////////////////////////////////////////////
// MoviesWrapper //
//////////////////////////////////////////////////////
MoviesWrapper = React.createClass({
class MoviesWrapper extends Component {
render() {
return (
<div className="wrapper">
@ -27,7 +27,7 @@ MoviesWrapper = React.createClass({
<LogInButtons />
</NoSSR>
<FlashContainer />
<FlashContainer component={FlashMessages}/>
<div className="main">
<ListContainer
@ -45,13 +45,13 @@ MoviesWrapper = React.createClass({
</div>
)
}
});
}
//////////////////////////////////////////////////////
// MoviesList //
//////////////////////////////////////////////////////
MoviesList = React.createClass({
class MoviesList extends Component {
renderNew() {
@ -62,7 +62,7 @@ MoviesList = React.createClass({
)
return !!this.props.currentUser ? component : "";
},
}
render() {
@ -74,12 +74,12 @@ MoviesList = React.createClass({
</div>
)
}
});
};
//////////////////////////////////////////////////////
// Movie //
//////////////////////////////////////////////////////
Movie = React.createClass({
class Movie extends Component {
renderEdit() {
@ -96,7 +96,7 @@ Movie = React.createClass({
{this.props.currentUser && this.props.currentUser._id === movie.userId ? component : ""}
</div>
)
},
}
render() {
@ -111,6 +111,8 @@ Movie = React.createClass({
)
}
});
};
const LoadMore = props => <a href="#" className="load-more button button--primary" onClick={props.loadMore}>Load More ({props.count}/{props.totalCount})</a>
const LoadMore = props => <a href="#" className="load-more button button--primary" onClick={props.loadMore}>Load More ({props.count}/{props.totalCount})</a>
export default MoviesWrapper

View file

@ -24,7 +24,6 @@ Package.onUse(function (api) {
]);
api.addFiles([
'demo-component.jsx',
'demo-app.jsx'
], ['client', 'server']);

View file

@ -213,6 +213,7 @@ Meteor.methods({
'posts.upvote': function (postId) {
check(postId, String);
console.log("upvote")
return Telescope.operateOnItem.call(this, Posts, postId, Meteor.user(), "upvote");
},
@ -223,6 +224,7 @@ Meteor.methods({
'posts.cancelUpvote': function (postId) {
check(postId, String);
console.log("cancelUpvote")
return Telescope.operateOnItem.call(this, Posts, postId, Meteor.user(), "cancelUpvote");
},

View file

@ -70,7 +70,8 @@ Meteor.publish('posts.list', function (terms) {
terms.currentUserId = this.userId; // add currentUserId to terms
({selector, options} = Posts.parameters.get(terms));
Counts.publish(this, 'posts.list', Posts.find(selector, options));
// disabled for now because of FlowRouterSSR issue
// Counts.publish(this, 'posts.list', Posts.find(selector, options));
options.fields = Posts.publishedFields.list;

View file

@ -1,3 +1,5 @@
import PublicationUtils from 'meteor/utilities:smart-publications';
// ------------------------------------- Posts -------------------------------- //
Posts.addField([
@ -67,8 +69,8 @@ Posts.addField([
},
]);
Telescope.utils.addToFields(Posts.publishedFields.list, ["upvotes", "downvotes", "baseScore", "score"]);
Telescope.utils.addToFields(Posts.publishedFields.single, ["upvotes", "upvoters", "downvotes", "downvoters", "baseScore", "score"]);
PublicationUtils.addToFields(Posts.publishedFields.list, ["upvotes", "upvoters", "downvotes", "downvoters", "baseScore", "score"]);
PublicationUtils.addToFields(Posts.publishedFields.single, ["upvotes", "upvoters", "downvotes", "downvoters", "baseScore", "score"]);
// ------------------------------------- Comments -------------------------------- //
@ -143,5 +145,5 @@ Comments.addField([
},
]);
Telescope.utils.addToFields(Comments.publishedFields.list, ["upvotes", "downvotes", "baseScore", "score"]);
Telescope.utils.addToFields(Comments.publishedFields.single, ["upvotes", "upvoters", "downvotes", "downvoters", "baseScore", "score"]);
PublicationUtils.addToFields(Comments.publishedFields.list, ["upvotes", "downvotes", "baseScore", "score"]);
PublicationUtils.addToFields(Comments.publishedFields.single, ["upvotes", "upvoters", "downvotes", "downvoters", "baseScore", "score"]);

View file

@ -37,10 +37,8 @@ Telescope.updateScore = function (args) {
// time decay factor
var f = 1.3;
// use baseScore if defined, if not just use the number of votes
// note: for transition period, also use votes if there are more votes than baseScore
// var baseScore = Math.max(item.votes || 0, item.baseScore || 0);
var baseScore = item.baseScore;
// use baseScore if defined, if not just use 0
var baseScore = item.baseScore || 0;
// HN algorithm
var newScore = baseScore / Math.pow(ageInHours + 2, f);

View file

@ -1,9 +1,10 @@
Meteor.startup(function () {
var scoreInterval = Settings.get("scoreUpdateInterval") || 30;
var scoreInterval = Telescope.settings.get("scoreUpdateInterval") || 30;
if (scoreInterval > 0) {
// active items get updated every N seconds
Meteor.setInterval(function () {
var updatedPosts = 0;
var updatedComments = 0;
// console.log('tick ('+scoreInterval+')');

View file

@ -11,6 +11,11 @@ Package.onUse(function (api) {
api.use(['nova:core@0.25.7']);
api.use([
'nova:posts@0.25.7',
'nova:comments@0.25.7'
], ['client', 'server']);
api.addFiles([
'lib/scoring.js',
'lib/vote.js',