diff --git a/.meteor/packages b/.meteor/packages index 4d7a6c497..97341908e 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -21,8 +21,8 @@ nova:notifications nova:getting-started nova:categories nova:share +nova:voting -# nova:voting # nova:migrations # nova:api # nova:email diff --git a/.meteor/versions b/.meteor/versions index ef9df58fb..e2d0b1cfa 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -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 diff --git a/packages/base-components/lib/common/Header.jsx b/packages/base-components/lib/common/Header.jsx index 6050a9563..a0607e2dd 100644 --- a/packages/base-components/lib/common/Header.jsx +++ b/packages/base-components/lib/common/Header.jsx @@ -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}) => { + + diff --git a/packages/base-components/lib/common/Icon.jsx b/packages/base-components/lib/common/Icon.jsx new file mode 100644 index 000000000..63b903a43 --- /dev/null +++ b/packages/base-components/lib/common/Icon.jsx @@ -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 ; +} + +module.exports = Icon; +export default Icon; \ No newline at end of file diff --git a/packages/base-components/lib/common/SearchForm.jsx b/packages/base-components/lib/common/SearchForm.jsx new file mode 100644 index 000000000..4b780f96f --- /dev/null +++ b/packages/base-components/lib/common/SearchForm.jsx @@ -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 ( +
+ + + +
+ ) + } +} + +SearchForm.propTypes = { + labelText: React.PropTypes.string +} + +SearchForm.defaultProps = { + labelText: "Search" +}; + +module.exports = SearchForm; +export default SearchForm; \ No newline at end of file diff --git a/packages/base-components/lib/components.js b/packages/base-components/lib/components.js index 6a7e36c3f..4e84c28aa 100644 --- a/packages/base-components/lib/components.js +++ b/packages/base-components/lib/components.js @@ -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 diff --git a/packages/base-components/lib/debug/Cheatsheet.jsx b/packages/base-components/lib/debug/Cheatsheet.jsx index 6da2e8cf5..a72d089f1 100644 --- a/packages/base-components/lib/debug/Cheatsheet.jsx +++ b/packages/base-components/lib/debug/Cheatsheet.jsx @@ -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 => {
    {_.map(Users.is, renderFunction)}
+

Methods

+
    + {_.map(methodList, (item, key) => (key.indexOf("users.") !== -1 ? renderFunction(item, key) : null))} +
@@ -53,6 +59,10 @@ const Cheatsheet = props => {
    {_.map(Posts, (item, key) => (key[0] !== "_" ? renderFunction(item, key) : null) )}
+

Methods

+
    + {_.map(methodList, (item, key) => (key.indexOf("posts.") !== -1 ? renderFunction(item, key) : null))} +
@@ -61,6 +71,10 @@ const Cheatsheet = props => {
    {_.map(Comments, (item, key) => (key[0] !== "_" ? renderFunction(item, key) : null) )}
+

Methods

+
    + {_.map(methodList, (item, key) => (key.indexOf("comments.") !== -1 ? renderFunction(item, key) : null))} +
diff --git a/packages/base-components/lib/posts/Post.jsx b/packages/base-components/lib/posts/Post.jsx index 3ad1861fd..dc74c8755 100644 --- a/packages/base-components/lib/posts/Post.jsx +++ b/packages/base-components/lib/posts/Post.jsx @@ -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 (
+ +

{post.title}

{post.commentCount} comments

{moment(post.postedAt).fromNow()}

+ + + {post.categoriesArray ? : ""}
diff --git a/packages/base-components/lib/posts/PostDaily.jsx b/packages/base-components/lib/posts/PostDaily.jsx new file mode 100644 index 000000000..8b94d8b9b --- /dev/null +++ b/packages/base-components/lib/posts/PostDaily.jsx @@ -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 ( +
+ {getLastNDates(this.state.days).map((date, index) => )} + Load More Days +
+ ) + } +} + +PostDaily.propTypes = { + days: React.PropTypes.number +} + +PostDaily.defaultProps = { + days: 5 +} + +module.exports = PostDaily; +export default PostDaily; diff --git a/packages/base-components/lib/posts/PostDay.jsx b/packages/base-components/lib/posts/PostDay.jsx new file mode 100644 index 000000000..67af16cb6 --- /dev/null +++ b/packages/base-components/lib/posts/PostDay.jsx @@ -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 ( +
+

{moment(date).format("dddd, MMMM Do YYYY")}

+ +
+ ) +} + +PostDay.propTypes = { + date: React.PropTypes.object, + number: React.PropTypes.number +} + +module.exports = PostDay; +export default PostDay; \ No newline at end of file diff --git a/packages/base-components/lib/posts/PostStats.jsx b/packages/base-components/lib/posts/PostStats.jsx new file mode 100644 index 000000000..d5d3a19df --- /dev/null +++ b/packages/base-components/lib/posts/PostStats.jsx @@ -0,0 +1,16 @@ +const PostStats = ({post}) => { + + ({Icon} = Telescope.components); + + return ( +
+ {post.score ? {Math.floor(post.score*10000)/10000} Score : ""} + {post.upvotes} Upvotes + {post.clickCount} Clicks + {post.viewCount} Views +
+ ) +} + +module.exports = PostStats; +export default PostStats; \ No newline at end of file diff --git a/packages/base-components/lib/posts/Vote.jsx b/packages/base-components/lib/posts/Vote.jsx new file mode 100644 index 000000000..24fb2a1c0 --- /dev/null +++ b/packages/base-components/lib/posts/Vote.jsx @@ -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 ( + + ) + } + +} + +Vote.propTypes = { + post: React.PropTypes.object.isRequired, // the current comment + currentUser: React.PropTypes.object, // the current user +} + +module.exports = Vote; +export default Vote; \ No newline at end of file diff --git a/packages/base-components/lib/posts/list/PostItem.jsx b/packages/base-components/lib/posts/list/PostItem.jsx index 94c1ed16e..d40ffa68d 100644 --- a/packages/base-components/lib/posts/list/PostItem.jsx +++ b/packages/base-components/lib/posts/list/PostItem.jsx @@ -40,16 +40,19 @@ class PostItem extends Component { render() { - ({UserAvatar} = Telescope.components); + ({UserAvatar, Vote, PostStats} = Telescope.components); const post = this.props.post; return (
+

{post.title}

{Users.getDisplayName(post.user)}, {moment(post.postedAt).fromNow()}, {post.commentCount} comments

+ + {this.renderCategories()} {this.renderCommenters()} {this.renderActions()} diff --git a/packages/base-components/lib/posts/list/PostList.jsx b/packages/base-components/lib/posts/list/PostList.jsx index 3e0755341..2834dc1f7 100644 --- a/packages/base-components/lib/posts/list/PostList.jsx +++ b/packages/base-components/lib/posts/list/PostList.jsx @@ -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 (
- + {showViews ? : null}
{results.map(post => )}
@@ -15,7 +15,7 @@ const PostList = ({results, currentUser, hasMore, ready, count, totalCount, load } else if (!ready) { return (
- + {showViews ? : null}
@@ -24,7 +24,7 @@ const PostList = ({results, currentUser, hasMore, ready, count, totalCount, load } else { return (
- + {showViews ? : null}
diff --git a/packages/base-components/lib/routes.jsx b/packages/base-components/lib/routes.jsx index 6177a3a40..912615ed8 100644 --- a/packages/base-components/lib/routes.jsx +++ b/packages/base-components/lib/routes.jsx @@ -24,6 +24,16 @@ FlowRouter.route('/', { } }); +FlowRouter.route('/daily/:days?', { + name: 'posts.list', + action(params, queryParams) { + + ({AppContainer, PostDaily} = Telescope.components); + + mount(AppContainer, {content: }) + } +}); + FlowRouter.route('/posts/:_id', { name: 'posts.single', action(params, queryParams) { diff --git a/packages/base-styles/lib/stylesheets/main.css b/packages/base-styles/lib/stylesheets/main.css index 5c9f10295..6b85f4025 100644 --- a/packages/base-styles/lib/stylesheets/main.css +++ b/packages/base-styles/lib/stylesheets/main.css @@ -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; } \ No newline at end of file diff --git a/packages/nova-demo/demo-app.jsx b/packages/nova-demo/demo-app.jsx index 904bb70c9..efc0764c1 100644 --- a/packages/nova-demo/demo-app.jsx +++ b/packages/nova-demo/demo-app.jsx @@ -1,5 +1,7 @@ import {mount} from 'react-mounter'; +import MoviesWrapper from './demo-components.jsx'; + ////////////////////////////////////////////////////// // Collection & Schema // ////////////////////////////////////////////////////// diff --git a/packages/nova-demo/demo-component.jsx b/packages/nova-demo/demo-components.jsx similarity index 83% rename from packages/nova-demo/demo-component.jsx rename to packages/nova-demo/demo-components.jsx index d196c3ee5..8bd4aac72 100644 --- a/packages/nova-demo/demo-component.jsx +++ b/packages/nova-demo/demo-components.jsx @@ -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 (
@@ -27,7 +27,7 @@ MoviesWrapper = React.createClass({ - +
) } -}); +} ////////////////////////////////////////////////////// // 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({
) } -}); +}; + ////////////////////////////////////////////////////// // 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 : ""}
) - }, + } render() { @@ -111,6 +111,8 @@ Movie = React.createClass({ ) } -}); +}; -const LoadMore = props => Load More ({props.count}/{props.totalCount}) \ No newline at end of file +const LoadMore = props => Load More ({props.count}/{props.totalCount}) + +export default MoviesWrapper \ No newline at end of file diff --git a/packages/nova-demo/package.js b/packages/nova-demo/package.js index 83b12d04e..169ecef40 100644 --- a/packages/nova-demo/package.js +++ b/packages/nova-demo/package.js @@ -24,7 +24,6 @@ Package.onUse(function (api) { ]); api.addFiles([ - 'demo-component.jsx', 'demo-app.jsx' ], ['client', 'server']); diff --git a/packages/nova-posts/lib/methods.js b/packages/nova-posts/lib/methods.js index 2989c263f..72bc6f28b 100644 --- a/packages/nova-posts/lib/methods.js +++ b/packages/nova-posts/lib/methods.js @@ -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"); }, diff --git a/packages/nova-posts/lib/server/publications.js b/packages/nova-posts/lib/server/publications.js index 2a523c66c..73d0aff27 100644 --- a/packages/nova-posts/lib/server/publications.js +++ b/packages/nova-posts/lib/server/publications.js @@ -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; diff --git a/packages/_nova-voting/README.md b/packages/nova-voting/README.md similarity index 100% rename from packages/_nova-voting/README.md rename to packages/nova-voting/README.md diff --git a/packages/_nova-voting/lib/custom_fields.js b/packages/nova-voting/lib/custom_fields.js similarity index 80% rename from packages/_nova-voting/lib/custom_fields.js rename to packages/nova-voting/lib/custom_fields.js index c325a1a47..bba9df9c6 100644 --- a/packages/_nova-voting/lib/custom_fields.js +++ b/packages/nova-voting/lib/custom_fields.js @@ -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"]); diff --git a/packages/_nova-voting/lib/scoring.js b/packages/nova-voting/lib/scoring.js similarity index 89% rename from packages/_nova-voting/lib/scoring.js rename to packages/nova-voting/lib/scoring.js index 1ecfb2cba..4ea2e707e 100644 --- a/packages/_nova-voting/lib/scoring.js +++ b/packages/nova-voting/lib/scoring.js @@ -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); diff --git a/packages/_nova-voting/lib/server/cron.js b/packages/nova-voting/lib/server/cron.js similarity index 94% rename from packages/_nova-voting/lib/server/cron.js rename to packages/nova-voting/lib/server/cron.js index c14dc146e..7034bf49c 100644 --- a/packages/_nova-voting/lib/server/cron.js +++ b/packages/nova-voting/lib/server/cron.js @@ -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+')'); diff --git a/packages/_nova-voting/lib/vote.js b/packages/nova-voting/lib/vote.js similarity index 100% rename from packages/_nova-voting/lib/vote.js rename to packages/nova-voting/lib/vote.js diff --git a/packages/_nova-voting/package.js b/packages/nova-voting/package.js similarity index 82% rename from packages/_nova-voting/package.js rename to packages/nova-voting/package.js index 1120fded9..8064ead71 100644 --- a/packages/_nova-voting/package.js +++ b/packages/nova-voting/package.js @@ -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',