Merge branch 'devel'

This commit is contained in:
Sacha Greif 2016-06-16 11:07:31 +09:00
commit 57bc830fc9
81 changed files with 645 additions and 491 deletions

View file

@ -50,19 +50,18 @@ jparker:gravatar@0.4.1
jquery@1.11.8
kadira:debug@3.2.2
kadira:dochead@1.5.0
kadira:flow-router-ssr@3.13.0
kadira:runtime-dev@0.0.1
livedata@1.0.18
localstorage@1.0.9
logging@1.0.12
matb33:collection-hooks@0.8.1
mdg:validation-error@0.2.0
mdg:validation-error@0.5.1
meteor@1.1.14
meteor-base@1.0.4
meteorhacks:fast-render@2.14.0
meteorhacks:inject-data@2.0.0
meteorhacks:inject-initial@1.0.4
meteorhacks:kadira@2.28.7
meteorhacks:kadira@2.28.5
meteorhacks:kadira-binary-deps@1.4.0
meteorhacks:kadira-profiler@1.2.1
meteorhacks:meteorx@1.4.1
@ -123,13 +122,14 @@ rate-limit@1.0.4
react-meteor-data@0.2.9
reactive-dict@1.1.7
reactive-var@1.0.9
reactrouter:react-router-ssr@3.1.3
reload@1.1.8
retry@1.0.7
routepolicy@1.0.10
service-configuration@1.0.9
session@1.1.5
sha@1.0.7
softwarerero:accounts-t9n@1.3.4
softwarerero:accounts-t9n@1.3.3
spacebars@1.0.11
spacebars-compiler@1.0.11
srp@1.0.8
@ -147,7 +147,7 @@ ui@1.0.11
underscore@1.0.8
url@1.0.9
utilities:react-list-container@0.1.12
utilities:smart-methods@0.1.4
utilities:smart-methods@0.1.5
utilities:smart-publications@0.1.4
webapp@1.2.8
webapp-hashing@1.0.9

View file

@ -10,6 +10,7 @@
"formsy-react": "^0.18.0",
"formsy-react-components": "^0.7.1",
"handlebars": "^4.0.5",
"history": "^3.0.0",
"html-to-text": "^2.1.0",
"intl": "^1.2.4",
"intl-locales-supported": "^1.0.0",
@ -25,14 +26,17 @@
"react-bootstrap-datetimepicker": "0.0.22",
"react-cookie": "^0.4.6",
"react-dom": "^15.0.0",
"react-intl": "^2.1.2",
"react-intl": "^2.1.3",
"react-komposer": "^1.8.0",
"react-mounter": "^1.2.0",
"react-no-ssr": "^1.1.0",
"react-router": "^3.0.0-alpha.1",
"react-router-bootstrap": "^0.23.0",
"sanitize-html": "^1.11.4",
"speakingurl": "^9.0.0",
"tracker-component": "^1.3.14",
"url": "^0.11.0"
"url": "^0.11.0",
"use-named-routes": "^0.3.1"
},
"private": true,
"devDependencies": {

View file

@ -1,5 +0,0 @@
const Actions = {}
Actions.call = Meteor.call;
export default Actions;

View file

@ -2,8 +2,8 @@ import React, { PropTypes, Component } from 'react';
import { FormattedMessage } from 'react-intl';
import NovaForm from "meteor/nova:forms";
import { DocumentContainer } from "meteor/utilities:react-list-container";
import { Messages } from "meteor/nova:core";
import Actions from "../actions.js";
//import { Messages } from "meteor/nova:core";
//import Actions from "../actions.js";
class CategoriesEditForm extends Component{
@ -15,11 +15,11 @@ class CategoriesEditForm extends Component{
deleteCategory() {
const category = this.props.category;
if (window.confirm(`Delete category “${category.name}”?`)) {
Actions.call("categories.deleteById", category._id, (error, result) => {
this.context.actions.call("categories.deleteById", category._id, (error, result) => {
if (error) {
Messages.flash(error.message, "error");
this.context.messages.flash(error.message, "error");
} else {
Messages.flash(`Category “${category.name}” deleted and removed from ${result} posts.`, "success");
this.context.messages.flash(`Category “${category.name}” deleted and removed from ${result} posts.`, "success");
}
});
}
@ -35,7 +35,7 @@ class CategoriesEditForm extends Component{
currentUser={this.context.currentUser}
methodName="categories.edit"
successCallback={(category)=>{
Messages.flash("Category edited.", "success");
this.context.messages.flash("Category edited.", "success");
}}
/>
<hr/>
@ -50,7 +50,9 @@ CategoriesEditForm.propTypes = {
}
CategoriesEditForm.contextTypes = {
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
messages: React.PropTypes.object
};
module.exports = CategoriesEditForm;

View file

@ -1,8 +1,9 @@
import React, { PropTypes, Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, DropdownButton, MenuItem, Modal } from 'react-bootstrap';
import Router from "../router.js"
import { ModalTrigger, ContextPasser } from "meteor/nova:core";
import { /* ModalTrigger, */ ContextPasser } from "meteor/nova:core";
import { withRouter } from 'react-router'
import { LinkContainer } from 'react-router-bootstrap';
// note: cannot use ModalTrigger component because of https://github.com/react-bootstrap/react-bootstrap/issues/1808
@ -40,7 +41,7 @@ class CategoriesList extends Component {
<Modal.Title><FormattedMessage id="categories.edit"/></Modal.Title>
</Modal.Header>
<Modal.Body>
<ContextPasser currentUser={this.context.currentUser} closeCallback={this.closeModal}>
<ContextPasser currentUser={this.context.currentUser} messages={this.context.messages} closeCallback={this.closeModal}>
<Telescope.components.CategoriesEditForm category={category}/>
</ContextPasser>
</Modal.Body>
@ -56,7 +57,7 @@ class CategoriesList extends Component {
<Modal.Title><FormattedMessage id="categories.new"/></Modal.Title>
</Modal.Header>
<Modal.Body>
<ContextPasser currentUser={this.context.currentUser} closeCallback={this.closeModal}>
<ContextPasser currentUser={this.context.currentUser} messages={this.context.messages} closeCallback={this.closeModal}>
<Telescope.components.CategoriesNewForm/>
</ContextPasser>
</Modal.Body>
@ -78,9 +79,8 @@ class CategoriesList extends Component {
const categories = this.props.categories;
const context = this.context;
const currentRoute = context.currentRoute;
const currentCategorySlug = currentRoute.queryParams.cat;
const currentQuery = _.clone(this.props.router.location.query);
delete currentQuery.cat;
return (
<div>
@ -90,8 +90,14 @@ class CategoriesList extends Component {
title={<FormattedMessage id="categories"/>}
id="categories-dropdown"
>
<div className="category-menu-item dropdown-item"><MenuItem href={Router.path("posts.list")} eventKey={0}><FormattedMessage id="categories.all"/></MenuItem></div>
{categories && categories.length > 0 ? categories.map((category, index) => <Telescope.components.Category key={index} category={category} index={index} currentCategorySlug={currentCategorySlug} openModal={_.partial(this.openCategoryEditModal, index)}/>) : null}
<div className="category-menu-item dropdown-item">
<LinkContainer to={{pathname:"/", query: currentQuery}} activeClassName="category-active">
<MenuItem eventKey={0}>
<FormattedMessage id="categories.all"/>
</MenuItem>
</LinkContainer>
</div>
{categories && categories.length > 0 ? categories.map((category, index) => <Telescope.components.Category key={index} category={category} index={index} openModal={_.partial(this.openCategoryEditModal, index)}/>) : null}
{Users.is.admin(this.context.currentUser) ? this.renderCategoryNewButton() : null}
</DropdownButton>
<div>
@ -111,8 +117,8 @@ CategoriesList.propTypes = {
CategoriesList.contextTypes = {
currentUser: React.PropTypes.object,
currentRoute: React.PropTypes.object
messages: React.PropTypes.object,
};
module.exports = CategoriesList;
export default CategoriesList;
module.exports = withRouter(CategoriesList);
export default withRouter(CategoriesList);

View file

@ -1,8 +1,5 @@
import React, { PropTypes, Component } from 'react';
import Router from '../router.js'
import { Messages } from "meteor/nova:core";
//import { Messages } from "meteor/nova:core";
import NovaForm from "meteor/nova:forms";
const CategoriesNewForm = (props, context) => {
@ -14,7 +11,7 @@ const CategoriesNewForm = (props, context) => {
currentUser={context.currentUser}
methodName="categories.new"
successCallback={(category)=>{
Messages.flash("Category created.", "success");
context.messages.flash("Category created.", "success");
}}
/>
</div>
@ -24,7 +21,8 @@ const CategoriesNewForm = (props, context) => {
CategoriesNewForm.displayName = "CategoriesNewForm";
CategoriesNewForm.contextTypes = {
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
messages: React.PropTypes.object
};
module.exports = CategoriesNewForm;

View file

@ -1,9 +1,9 @@
import React, { PropTypes, Component } from 'react';
import Actions from "../actions.js"
import Router from "../router.js"
import { Button, DropdownButton, MenuItem } from 'react-bootstrap';
import classNames from "classnames";
import { Messages, ModalTrigger } from "meteor/nova:core";
//import { Messages, ModalTrigger } from 'meteor/nova:core';
import { LinkContainer } from 'react-router-bootstrap';
import { withRouter } from 'react-router'
class Category extends Component {
@ -18,20 +18,24 @@ class Category extends Component {
render() {
const {category, index, currentCategorySlug} = this.props;
const {category, index, router} = this.props;
const categoryClass = classNames("category-menu-item", "dropdown-item", {"category-active": currentCategorySlug === category.slug});
const currentQuery = router.location.query;
const currentCategorySlug = router.location.query.cat;
const newQuery = _.clone(router.location.query);
newQuery.cat = category.slug;
return (
<div className={categoryClass}>
<MenuItem
href={Router.extendPathWithQueryParams("posts.list", {}, {cat: category.slug})}
eventKey={index+1}
key={category._id}
>
{currentCategorySlug === category.slug ? <Telescope.components.Icon name="voted"/> : null}
{category.name}
</MenuItem>
<div className="category-menu-item dropdown-item">
<LinkContainer to={{pathname:"/", query: newQuery}} activeClassName="category-active">
<MenuItem
eventKey={index+1}
key={category._id}
>
{currentCategorySlug === category.slug ? <Telescope.components.Icon name="voted"/> : null}
{category.name}
</MenuItem>
</LinkContainer>
{Users.is.admin(this.context.currentUser) ? this.renderEdit() : null}
</div>
)
@ -49,5 +53,5 @@ Category.contextTypes = {
currentUser: React.PropTypes.object
};
module.exports = Category;
export default Category;
module.exports = withRouter(Category);
export default withRouter(Category);

View file

@ -1,5 +1,4 @@
import React, { PropTypes, Component } from 'react';
import Actions from '../actions.js';
import NovaForm from "meteor/nova:forms";
class CommentsEdit extends Component {

View file

@ -49,8 +49,8 @@ class CommentsItem extends Component{
if (window.confirm(deleteConfirmMessage)) {
Meteor.call('comments.deleteById', comment._id, (error, result) => {
Messages.flash(deleteSuccessMessage, "success");
Events.track("comment deleted", {'_id': comment._id});
this.context.messages.flash(deleteSuccessMessage, "success");
this.context.events.track("comment deleted", {'_id': comment._id});
});
}
@ -121,6 +121,8 @@ CommentsItem.propTypes = {
}
CommentsItem.contextTypes = {
messages: React.PropTypes.object,
events: React.PropTypes.object,
intl: intlShape
}

View file

@ -1,5 +1,4 @@
import React, { PropTypes, Component } from 'react';
import Actions from '../actions.js';
import NovaForm from "meteor/nova:forms";
class CommentsNew extends Component {

View file

@ -17,22 +17,23 @@ class App extends Component {
return {
currentUser: this.props.currentUser,
currentRoute: this.props.currentRoute,
actions: this.props.actions,
events: this.props.events,
messages: this.props.messages,
intl: intl
};
}
render() {
if (this.props.ready) {
return (
<IntlProvider locale={this.getLocale()} messages={Telescope.strings[this.getLocale()]}>
<Telescope.components.Layout currentUser={this.props.currentUser}>{this.props.content}</Telescope.components.Layout>
</IntlProvider>
)
} else {
return <Telescope.components.AppLoading />
}
return (
<IntlProvider locale={this.getLocale()} messages={Telescope.strings[this.getLocale()]}>
{
this.props.ready ?
<Telescope.components.Layout currentUser={this.props.currentUser}>{this.props.children}</Telescope.components.Layout>
: <Telescope.components.AppLoading />
}
</IntlProvider>
)
}
}
@ -40,12 +41,16 @@ class App extends Component {
App.propTypes = {
ready: React.PropTypes.bool,
currentUser: React.PropTypes.object,
currentRoute: React.PropTypes.object
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object,
}
App.childContextTypes = {
currentUser: React.PropTypes.object,
currentRoute: React.PropTypes.object,
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object,
intl: intlShape
}

View file

@ -1,7 +1,6 @@
import React, { PropTypes, Component } from 'react';
import { Alert } from 'react-bootstrap';
import { Messages } from "meteor/nova:core";
//import { Messages } from "meteor/nova:core";
class Flash extends Component{
@ -11,12 +10,12 @@ class Flash extends Component{
}
componentDidMount() {
Messages.markAsSeen(this.props.message._id);
this.context.messages.markAsSeen(this.props.message._id);
}
dismissFlash(e) {
e.preventDefault();
Messages.clear(this.props.message._id);
this.context.messages.clear(this.props.message._id);
}
render() {
@ -36,4 +35,8 @@ Flash.propTypes = {
message: React.PropTypes.object.isRequired
}
Flash.contextTypes = {
messages: React.PropTypes.object.isRequired
}
module.exports = Flash;

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Messages } from "meteor/nova:core";
//import { Messages } from "meteor/nova:core";
const Header = ({currentUser}) => {

View file

@ -1,17 +1,20 @@
import React from 'react';
import { IndexLink } from 'react-router';
const Logo = ({logoUrl, siteTitle}) => {
if (logoUrl) {
return (
<h1 className="logo-image ">
<a href="/">
<IndexLink to={{pathname: "/"}}>
<img src={logoUrl} alt={siteTitle} style={{maxWidth: "100px", maxHeight: "100px"}} />
</a>
</IndexLink>
</h1>
)
} else {
return (
<h1 className="logo-text"><a href="/">{siteTitle}</a></h1>
<h1 className="logo-text">
<IndexLink to={{pathname: "/"}}>{siteTitle}</IndexLink>
</h1>
)
}
}

View file

@ -2,11 +2,10 @@ import React, { PropTypes, Component } from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import Formsy from 'formsy-react';
import { Input } from 'formsy-react-components';
import Actions from "../actions.js";
//import Actions from "../actions.js";
import { Button } from 'react-bootstrap';
import Cookie from 'react-cookie';
import { Messages } from "meteor/nova:core";
//import { Messages } from "meteor/nova:core";
class Newsletter extends Component {
@ -37,10 +36,10 @@ class Newsletter extends Component {
}
subscribeEmail(data) {
Actions.call("newsletter.addEmail", data.email, (error, result) => {
this.context.actions.call("newsletter.addEmail", data.email, (error, result) => {
if (error) {
console.log(error);
Messages.flash(error.message, "error");
this.context.messages.flash(error.message, "error");
} else {
this.successCallbackSubscription(result);
}
@ -48,7 +47,7 @@ class Newsletter extends Component {
}
successCallbackSubscription(result) {
Messages.flash(this.context.intl.formatMessage({id: "newsletter.success_message"}), "success");
this.context.messages.flash(this.context.intl.formatMessage({id: "newsletter.success_message"}), "success");
this.dismissBanner();
}
@ -99,6 +98,8 @@ class Newsletter extends Component {
Newsletter.contextTypes = {
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
messages: React.PropTypes.object,
intl: intlShape
};

View file

@ -16,7 +16,7 @@ class NewsletterButton extends Component {
Meteor.call(action, this.context.currentUser, (error, result) => {
if (error) {
console.log(error);
Messages.flash(error.message, "error");
this.context.messages.flash(error.message, "error");
} else {
this.props.successCallback(result);
}
@ -42,10 +42,9 @@ NewsletterButton.propTypes = {
successCallback: React.PropTypes.func.isRequired
};
NewsletterButton.contextTypes = {
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
messages: React.PropTypes.object
}
module.exports = NewsletterButton;

View file

@ -1,8 +1,8 @@
import React, { PropTypes, Component } from 'react';
import { intlShape } from 'react-intl';
import Router from '../router.js'
import Formsy from 'formsy-react';
import FRC from 'formsy-react-components';
import { withRouter } from 'react-router'
const Input = FRC.Input;
@ -17,37 +17,38 @@ const delay = (function(){
class SearchForm extends Component{
constructor() {
super();
constructor(props) {
super(props);
this.search = this.search.bind(this);
this.state = {
search: props.router.location.query.query || ''
}
}
componentWillReceiveProps(nextProps) {
this.setState({
search: this.props.router.location.query.query || ''
});
}
search(data) {
if (Router.getRouteName() !== "posts.list") {
Router.go("posts.list");
}
if (data.searchQuery === '') {
data.searchQuery = null;
}
const router = this.props.router;
const query = data.searchQuery === '' ? {} : {query: data.searchQuery};
delay(function(){
Router.setQueryParams({query: data.searchQuery});
delay(() => {
router.push({pathname: "/", query: query});
}, 700 );
}
render() {
const currentQuery = this.context.currentRoute.queryParams.query;
return (
<div className="search-form">
<Formsy.Form onChange={this.search}>
<Input
name="searchQuery"
value={currentQuery}
value={this.state.search}
placeholder={this.context.intl.formatMessage({id: "posts.search"})}
type="text"
layout="elementOnly"
@ -64,5 +65,5 @@ SearchForm.contextTypes = {
intl: intlShape
}
module.exports = SearchForm;
export default SearchForm;
module.exports = withRouter(SearchForm);
export default withRouter(SearchForm);

View file

@ -2,7 +2,7 @@ import React, { PropTypes, Component } from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import NovaForm from "meteor/nova:forms";
import { DocumentContainer } from "meteor/utilities:react-list-container";
import { Messages } from "meteor/nova:core";
//import { Messages } from "meteor/nova:core";
class SettingsEditForm extends Component{
@ -23,7 +23,7 @@ class SettingsEditForm extends Component{
currentUser: this.context.currentUser,
methodName: "settings.edit",
successCallback: (category) => {
Messages.flash(this.context.intl.formatMessage({id: "settings.edited"}), "success");
this.context.messages.flash(this.context.intl.formatMessage({id: "settings.edited"}), "success");
}
}}
/>
@ -34,6 +34,7 @@ class SettingsEditForm extends Component{
SettingsEditForm.contextTypes = {
currentUser: React.PropTypes.object,
messages: React.PropTypes.object,
intl: intlShape
};

View file

@ -1,6 +1,6 @@
import React, { PropTypes, Component } from 'react';
import Actions from "../actions.js";
import { Messages } from "meteor/nova:core";
//import Actions from "../actions.js";
//import { Messages } from "meteor/nova:core";
import classNames from 'classnames';
class Vote extends Component {
@ -17,14 +17,14 @@ class Vote extends Component {
const user = this.context.currentUser;
if(!user){
Messages.flash("Please log in first");
this.context.messages.flash("Please log in first");
} else if (user.hasUpvoted(post)) {
Actions.call('posts.cancelUpvote', post._id, function(){
Events.track("post upvote cancelled", {'_id': post._id});
this.context.actions.call('posts.cancelUpvote', post._id, function(){
this.context.events.track("post upvote cancelled", {'_id': post._id});
});
} else {
Actions.call('posts.upvote', post._id, function(){
Events.track("post upvoted", {'_id': post._id});
this.context.actions.call('posts.upvote', post._id, function(){
this.context.events.track("post upvoted", {'_id': post._id});
});
}
@ -63,7 +63,10 @@ Vote.propTypes = {
}
Vote.contextTypes = {
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object
};
module.exports = Vote;

View file

@ -20,6 +20,8 @@ Telescope.registerComponent("SettingsEditForm", require('./common/SettingsEd
// posts
Telescope.registerComponent("PostsHome", require('./posts/PostsHome.jsx'));
Telescope.registerComponent("PostsSingle", require('./posts/PostsSingle.jsx'));
Telescope.registerComponent("PostsNewButton", require('./posts/PostsNewButton.jsx'));
Telescope.registerComponent("PostsLoadMore", require('./posts/PostsLoadMore.jsx'));
Telescope.registerComponent("PostsNoMore", require('./posts/PostsNoMore.jsx'));
@ -66,6 +68,8 @@ Telescope.registerComponent("CanEditUser", require('./permissions/CanEd
// users
Telescope.registerComponent("UsersSingle", require('./users/UsersSingle.jsx'));
Telescope.registerComponent("UsersAccount", require('./users/UsersAccount.jsx'));
Telescope.registerComponent("UsersEdit", require('./users/UsersEdit.jsx'));
Telescope.registerComponent("UsersProfile", require('./users/UsersProfile.jsx'));
Telescope.registerComponent("UsersProfileCheck", require('./users/UsersProfileCheck.jsx'));

View file

@ -1,10 +1,12 @@
import React from 'react';
import Router from '../router.js';
import { Link } from 'react-router';
const PostsCategories = ({post}) => {
return (
<div className="posts-categories">
{post.categoriesArray.map(category => <a className="posts-category" key={category._id} href={Router.path("posts.list", {}, {cat: category.slug})}>{category.name}</a>)}
{post.categoriesArray.map(category =>
<Link className="posts-category" key={category._id} to={{pathname: "/", query: {cat: category.slug}}}>{category.name}</Link>
)}
</div>
)
};

View file

@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router';
const PostsCommenters = ({post}) => {
return (
@ -7,11 +8,11 @@ const PostsCommenters = ({post}) => {
{post.commentersArray.map(user => <Telescope.components.UsersAvatar key={user._id} user={user}/>)}
</div>
<div className="posts-commenters-discuss">
<a href={Posts.getPageUrl(post)}>
<Link to={`/posts/${post._id}/${post.slug}/`}>
<Telescope.components.Icon name="comment" />
<span className="posts-commenters-comments-count">{post.commentCount}</span>
<span className="sr-only">Comments</span>
</a>
</Link>
</div>
</div>
)

View file

@ -2,8 +2,8 @@ import React, { PropTypes, Component } from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import NovaForm from "meteor/nova:forms";
import { DocumentContainer } from "meteor/utilities:react-list-container";
import { Messages } from "meteor/nova:core";
import Actions from "../actions.js";
//import { Messages } from "meteor/nova:core";
//import Actions from "../actions.js";
class PostsEditForm extends Component{
@ -18,9 +18,9 @@ class PostsEditForm extends Component{
const deletePostSuccess = this.context.intl.formatMessage({id: "posts.delete_success"}, {title: post.title});
if (window.confirm(deletePostConfirm)) {
Actions.call('posts.deleteById', post._id, (error, result) => {
Messages.flash(deletePostSuccess, "success");
Events.track("post deleted", {'_id': post._id});
this.context.actions.call('posts.deleteById', post._id, (error, result) => {
this.context.messages.flash(deletePostSuccess, "success");
this.context.events.track("post deleted", {'_id': post._id});
});
}
}
@ -51,7 +51,10 @@ class PostsEditForm extends Component{
// note: the document prop will be passed from DocumentContainer
collection: Posts,
currentUser: this.context.currentUser,
methodName: "posts.edit"
methodName: "posts.edit",
successCallback: (post) => {
this.context.messages.flash(this.context.intl.formatMessage({id: "posts.edit_success"}, {title: post.title}), 'success')
}
}}
/>
<hr/>
@ -67,6 +70,9 @@ PostsEditForm.propTypes = {
PostsEditForm.contextTypes = {
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object,
intl: intlShape
}

View file

@ -0,0 +1,30 @@
import React from 'react';
import { ListContainer, DocumentContainer } from "meteor/utilities:react-list-container";
const PostsHome = (props, context) => {
const params = _.isEmpty(props.location.query) ? {view: 'top'} : _.clone(props.location.query);
params.listId = "posts.list.main";
const {selector, options} = Posts.parameters.get(params);
return (
<ListContainer
collection={Posts}
publication="posts.list"
selector={selector}
options={options}
terms={params}
joins={Posts.getJoins()}
component={Telescope.components.PostsList}
cacheSubscription={false}
listId={params.listId}
limit={Telescope.settings.get("postsPerPage", 10)}
/>
)
};
PostsHome.displayName = "PostsHome";
module.exports = PostsHome;

View file

@ -3,6 +3,7 @@ import { FormattedMessage, FormattedRelative } from 'react-intl';
import { Button } from 'react-bootstrap';
import moment from 'moment';
import { ModalTrigger } from "meteor/nova:core";
import { Link } from 'react-router';
class PostsItem extends Component {
@ -51,15 +52,20 @@ class PostsItem extends Component {
<div className="posts-item-content">
<h3 className="posts-item-title">
<a className="posts-item-title-link" href={Posts.getLink(post)} target={Posts.getLinkTarget(post)}>{post.title}</a>
<Link to={`posts/${post._id}/${post.slug}/`} /*to={{name: "posts.single", params: {_id: post._id, slug: post.slug}}}*/ className="posts-item-title-link" target={Posts.getLinkTarget(post)}>
{post.title}
</Link>
{this.renderCategories()}
</h3>
<div className="posts-item-meta">
{post.user? <div className="posts-item-user"><Telescope.components.UsersAvatar user={post.user} size="small"/><Telescope.components.UsersName user={post.user}/></div> : null}
<div className="posts-item-date"><FormattedRelative value={post.postedAt}/></div>
<div className="posts-item-comments"><a href={Posts.getPageUrl(post)}><FormattedMessage id="comments.count" values={{count: post.commentCount}}/></a></div>
<div className="posts-item-comments">
<Link to={`posts/${post._id}/${post.slug}/`} /*to={{name: "posts.single", params: {_id: post._id, slug: post.slug}}}*/>
<FormattedMessage id="comments.count" values={{count: post.commentCount}}/>
</Link>
</div>
{(this.context.currentUser && this.context.currentUser.isAdmin) ?<Telescope.components.PostsStats post={post} />:null}
{this.renderActions()}
</div>

View file

@ -18,6 +18,7 @@ PostsNewButton.displayName = "PostsNewButton";
PostsNewButton.contextTypes = {
currentUser: React.PropTypes.object,
messages: React.PropTypes.object,
intl: intlShape
}

View file

@ -1,12 +1,11 @@
import React, { PropTypes, Component } from 'react';
import { intlShape } from 'react-intl';
import Router from '../router.js'
import { Messages } from "meteor/nova:core";
import NovaForm from "meteor/nova:forms";
import { withRouter } from 'react-router'
const PostsNewForm = (props, context) => {
const router = props.router;
return (
<Telescope.components.CanCreatePost>
@ -16,8 +15,8 @@ const PostsNewForm = (props, context) => {
currentUser={context.currentUser}
methodName="posts.new"
successCallback={(post)=>{
Messages.flash(context.intl.formatMessage({id: "posts.created_message"}), "success");
Router.go('posts.single', post);
context.messages.flash(context.intl.formatMessage({id: "posts.created_message"}), "success");
router.push({pathname: Posts.getPageUrl(post)});
}}
/>
</div>
@ -27,10 +26,11 @@ const PostsNewForm = (props, context) => {
PostsNewForm.contextTypes = {
currentUser: React.PropTypes.object,
messages: React.PropTypes.object,
intl: intlShape
};
PostsNewForm.displayName = "PostsNewForm";
module.exports = PostsNewForm;
export default PostsNewForm;
module.exports = withRouter(PostsNewForm);
export default withRouter(PostsNewForm);

View file

@ -0,0 +1,19 @@
import React from 'react';
import { DocumentContainer } from "meteor/utilities:react-list-container";
const PostsSingle = (props, context) => {
return (
<DocumentContainer
collection={Posts}
publication="posts.single"
selector={{_id: props.params._id}}
terms={props.params}
joins={Posts.getJoins()}
component={Telescope.components.PostsPage}
/>
)
};
PostsSingle.displayName = "PostsSingle";
module.exports = PostsSingle;

View file

@ -1,7 +1,8 @@
import React, { PropTypes, Component } from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import Router from '../router.js';
import { Button, ButtonGroup, DropdownButton, MenuItem } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { withRouter } from 'react-router'
const PostsViews = (props, context) => {
@ -12,18 +13,10 @@ const PostsViews = (props, context) => {
views = views.concat(adminViews);
}
const currentRoute = context.currentRoute;
const currentView = currentRoute.queryParams.view || props.defaultView;
// console.log(currentRoute);
const query = _.clone(props.router.location.query);
return (
<div className="posts-views">
{/*
<ButtonGroup>
{views.map(view => <Button className={currentRoute.route.name === "posts.list" && currentView === view ? "post-view-active" : "post-view-inactive"} bsStyle="default" key={view} href={Router.extendPathWithQueryParams("posts.list", {}, {view: view})}>{Telescope.utils.capitalise(view)}</Button>)}
</ButtonGroup>
<Button bsStyle="default" href={Router.path("posts.daily")} className={currentRoute.route.name === "posts.daily" ? "post-view-active" : "post-view-inactive"} >Daily</Button>
*/}
<DropdownButton
bsStyle="default"
className="views btn-secondary"
@ -31,11 +24,17 @@ const PostsViews = (props, context) => {
id="views-dropdown"
>
{views.map(view =>
<MenuItem key={view} href={Router.extendPathWithQueryParams("posts.list", {}, {view: view})} className={currentRoute.route.name === "posts.list" && currentView === view ? "dropdown-item post-view-active" : "dropdown-item post-view-inactive"}>
<FormattedMessage id={"posts."+view}/>
</MenuItem>
<LinkContainer key={view} to={{pathname: "/", query: {...query, view: view}}} /*to={}*/ className="dropdown-item" activeClassName="posts-view-active">
<MenuItem>
<FormattedMessage id={"posts."+view}/>
</MenuItem>
</LinkContainer>
)}
<MenuItem href={Router.path("posts.daily")} className={currentRoute.route.name === "posts.daily" ? "dropdown-item post-view-active" : "dropdown-item post-view-inactive"} ><FormattedMessage id="posts.daily"/></MenuItem>
<LinkContainer to={"/daily"} /*to={{name: "posts.daily"}}*/ className="dropdown-item" activeClassName="posts-view-active">
<MenuItem className={"bar"}>
<FormattedMessage id="posts.daily"/>
</MenuItem>
</LinkContainer>
</DropdownButton>
</div>
)
@ -57,4 +56,4 @@ PostsViews.contextTypes = {
PostsViews.displayName = "PostsViews";
module.exports = PostsViews;
module.exports = withRouter(PostsViews);

View file

@ -1,3 +0,0 @@
const Router = FlowRouter;
export default Router;

View file

@ -0,0 +1,23 @@
import React from 'react';
import { DocumentContainer } from "meteor/utilities:react-list-container";
const UsersAccount = (props, context) => {
const params = props.params.slug ? props.params : {_id: context.currentUser._id};
return (
<DocumentContainer
collection={Users}
publication="users.single"
selector={params}
terms={params}
component={Telescope.components.UsersEdit}
/>
)
};
UsersAccount.contextTypes = {
currentUser: React.PropTypes.object
}
UsersAccount.displayName = "PostsSingle";
module.exports = UsersAccount;

View file

@ -1,7 +1,5 @@
import React, { PropTypes, Component } from 'react';
import Router from '../router.js';
import { Button, FormControl } from 'react-bootstrap';
import { Accounts } from 'meteor/std:accounts-ui';
const UsersAccountForm = () => {

View file

@ -1,12 +1,10 @@
import React, { PropTypes, Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Row, Col } from 'react-bootstrap';
import NovaForm from "meteor/nova:forms";
//import { Messages } from "meteor/nova:core";
import { Messages } from "meteor/nova:core";
const UsersEdit = ({document, currentUser}) => {
const UsersEdit = ({document, currentUser}, context) => {
const user = document;
//const label = `Edit profile for ${Users.getDisplayName(user)}`;
@ -21,7 +19,7 @@ const UsersEdit = ({document, currentUser}) => {
document={user}
methodName="users.edit"
successCallback={(user)=>{
Messages.flash("User updated.", "success");
context.messages.flash("User updated.", "success");
}}
/>
</div>
@ -35,6 +33,10 @@ UsersEdit.propTypes = {
currentUser: React.PropTypes.object.isRequired
};
UsersEdit.contextTypes = {
messages: React.PropTypes.object
};
UsersEdit.displayName = "UsersEdit";
module.exports = UsersEdit;

View file

@ -2,9 +2,9 @@ import React, { PropTypes, Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/std:accounts-ui';
import Router from '../router.js';
import { Modal, Dropdown, MenuItem } from 'react-bootstrap';
import { ContextPasser } from "meteor/nova:core";
import { LinkContainer } from 'react-router-bootstrap';
class UsersMenu extends Component {
@ -33,7 +33,7 @@ class UsersMenu extends Component {
<Modal.Title><FormattedMessage id="settings.edit"/></Modal.Title>
</Modal.Header>
<Modal.Body>
<ContextPasser currentUser={this.props.user} closeCallback={this.closeModal}>
<ContextPasser currentUser={this.props.user} messages={this.context.messages} closeCallback={this.closeModal}>
<SettingsEditForm/>
</ContextPasser>
</Modal.Body>
@ -53,8 +53,12 @@ class UsersMenu extends Component {
<div>{Users.getDisplayName(user)}</div>
</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem className="dropdown-item" eventKey="1" href={Router.path("users.single", {slug: user.telescope.slug})}><FormattedMessage id="users.profile"/></MenuItem>
<MenuItem className="dropdown-item" eventKey="2" href={Router.path("account")}><FormattedMessage id="users.edit_account"/></MenuItem>
<LinkContainer to={`/users/${user.telescope.slug}`} /*to={{name: "users.single", params: {slug: user.telescope.slug}}}*/>
<MenuItem className="dropdown-item" eventKey="1"><FormattedMessage id="users.profile"/></MenuItem>
</LinkContainer>
<LinkContainer to={`/account`} /*to={{name: "account"}}*/>
<MenuItem className="dropdown-item" eventKey="2"><FormattedMessage id="users.edit_account"/></MenuItem>
</LinkContainer>
{Users.is.admin(user) ? <MenuItem className="dropdown-item" eventKey="3" onClick={this.openModal}><FormattedMessage id="settings"/></MenuItem> : null}
<MenuItem className="dropdown-item" eventKey="4" onClick={() => Meteor.logout(Accounts.ui._options.onSignedOutHook())}><FormattedMessage id="users.log_out"/></MenuItem>
</Dropdown.Menu>
@ -70,5 +74,9 @@ UsersMenu.propTypes = {
user: React.PropTypes.object
}
UsersMenu.contextTypes = {
messages: React.PropTypes.object
}
module.exports = UsersMenu;
export default UsersMenu;

View file

@ -1,10 +1,10 @@
import React, { PropTypes, Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal } from 'react-bootstrap';
import Router from '../router.js';
import NovaForm from "meteor/nova:forms";
import { withRouter } from 'react-router'
const UsersProfileCheckModal = ({currentUser, show}) => {
const UsersProfileCheckModal = ({currentUser, show, router}) => {
// return fields that are required by the schema but haven't been filled out yet
const schema = Users.simpleSchema()._schema;
@ -29,7 +29,7 @@ const UsersProfileCheckModal = ({currentUser, show}) => {
/>
</Modal.Body>
<Modal.Footer>
<FormattedMessage id="app.or"/> <a className="complete-profile-logout" onClick={ () => Meteor.logout(() => Router.go('/')) }><FormattedMessage id="users.log_out"/></a>
<FormattedMessage id="app.or"/> <a className="complete-profile-logout" onClick={ () => Meteor.logout(() => router.push({pathname: '/'})) }><FormattedMessage id="users.log_out"/></a>
</Modal.Footer>
</Modal>
)
@ -48,5 +48,5 @@ UsersProfileCheck.contextTypes = {
UsersProfileCheck.displayName = "UsersProfileCheck";
module.exports = UsersProfileCheck;
export default UsersProfileCheck;
module.exports = withRouter(UsersProfileCheck);
export default withRouter(UsersProfileCheck);

View file

@ -0,0 +1,19 @@
import React from 'react';
import { DocumentContainer } from "meteor/utilities:react-list-container";
const UsersSingle = (props, context) => {
return (
<DocumentContainer
collection={Users}
publication="users.single"
selector={{'telescope.slug': props.params.slug}}
terms={{'telescope.slug': props.params.slug}}
component={Telescope.components.UsersProfile}
documentPropName="user"
/>
)
};
UsersSingle.displayName = "PostsSingle";
module.exports = UsersSingle;

View file

@ -1,132 +1,53 @@
import React from 'react';
import {mount} from 'react-mounter';
import { IndexRoute, Route, useRouterHistory, browserHistory, createMemoryHistory } from 'react-router';
import { ReactRouterSSR } from 'meteor/reactrouter:react-router-ssr';
import { ListContainer, DocumentContainer } from "meteor/utilities:react-list-container";
import useNamedRoutes from 'use-named-routes';
import createBrowserHistory from 'history/lib/createBrowserHistory';
Telescope.routes = {};
// // ------------------------------------- Other -------------------------------- //
Telescope.addRoutes = (newRoutes) => {
Telescope.routes = Object.assign(Telescope.routes, newRoutes);
}
// FlowRouter.notFound = {
// action() {
// ({App, Error404} = Telescope.components);
// mount(App, {content: <Error404/>});
// }
// };
Telescope.addRoutes({
Meteor.startup(() => {
// ------------------------------------- Posts -------------------------------- //
Telescope.routes.add([
{name:"posts.daily", path:"daily", component:Telescope.components.PostsDaily},
{name:"posts.single", path:"posts/:_id(/:slug)", component:Telescope.components.PostsSingle},
{name:"users.single", path:"users/:slug", component:Telescope.components.UsersSingle},
{name:"users.account", path:"account", component:Telescope.components.UsersAccount},
{name:"users.edit", path:"users/:slug/edit", component:Telescope.components.UsersAccount}
]);
"posts.list": {
path: "/",
action(params, queryParams) {
({App, PostsList} = Telescope.components);
queryParams = _.isEmpty(queryParams) ? {view: 'new'} : _.clone(queryParams);
queryParams.listId = "posts.list.main";
({selector, options} = Posts.parameters.get(queryParams));
const postsPerPage = Telescope.settings.get("postsPerPage", 10);
mount(App, {content:
<ListContainer
collection={Posts}
publication="posts.list"
selector={selector}
options={options}
terms={queryParams}
joins={Posts.getJoins()}
component={PostsList}
cacheSubscription={false}
listId={queryParams.listId}
limit={postsPerPage}
/>})
}
},
"posts.daily": {
path: "/daily/:days?",
action(params, queryParams) {
({App, PostsDaily} = Telescope.components);
mount(App, {content: <PostsDaily days={params.days}/>})
}
},
"posts.single": {
path: "/posts/:_id/:slug?",
action(params, queryParams) {
({App, PostsPage} = Telescope.components);
mount(App, {content:
<DocumentContainer
collection={Posts}
publication="posts.single"
selector={{_id: params._id}}
terms={params}
joins={Posts.getJoins()}
component={PostsPage}
/>});
}
},
// ------------------------------------- Users -------------------------------- //
"users.single": {
path: "/users/:slug",
action(params, queryParams) {
({App, UsersProfile} = Telescope.components);
mount(App, {content:
<DocumentContainer
collection={Users}
publication="users.single"
selector={{'telescope.slug': params.slug}}
terms={{'telescope.slug': params.slug}}
component={UsersProfile}
documentPropName="user"
/>});
}
},
"account": {
path: "/account",
action(params, queryParams) {
({App, UsersEdit} = Telescope.components);
mount(App, {content:
<DocumentContainer
collection={Users}
publication="users.single"
selector={{_id: Meteor.userId()}}
terms={{_id: Meteor.userId()}}
component={UsersEdit}
/>});
}
},
"users.edit": {
path: "/users/:slug/edit",
action(params, queryParams) {
({App, UsersEdit} = Telescope.components);
mount(App, {content:
<DocumentContainer
collection={Users}
publication="users.single"
selector={params}
terms={params}
component={UsersEdit}
/>});
}
}
});
_.forEach(Telescope.routes, (route, routeName) => {
FlowRouter.route(route.path, {
name: routeName,
action: route.action
});
});
// ------------------------------------- Other -------------------------------- //
FlowRouter.notFound = {
action() {
({App, Error404} = Telescope.components);
mount(App, {content: <Error404/>});
const AppRoutes = {
path: '/',
component: Telescope.components.App,
indexRoute: { name: "posts.list", component: Telescope.components.PostsHome },
childRoutes: Telescope.routes.routes
}
};
let history;
const clientOptions = {}, serverOptions = {};
if (Meteor.isClient) {
history = useNamedRoutes(useRouterHistory(createBrowserHistory))({ routes: AppRoutes });
}
if (Meteor.isServer) {
history = useNamedRoutes(useRouterHistory(createMemoryHistory))({ routes: AppRoutes });
}
clientOptions.props = {onUpdate: Events.analyticsRequest};
// ReactRouterSSR.Run(AppRoutes, {historyHook: () => history}, {historyHook: () => history});
ReactRouterSSR.Run(AppRoutes, clientOptions, serverOptions);
});

View file

@ -30,4 +30,10 @@
text-transform: uppercase;
font-size: $smaller-font;
color: $red;
}
.dropdown-item.active{
a{
color: $white;
}
}

View file

@ -42,10 +42,10 @@
// @include border-radius;
// padding: 3px 5px;
// margin-right: 5px;
&.post-view-inactive{
&.posts-view-inactive{
@include activeHover;
}
&.post-view-active{
&.posts-view-active{
background: $light-blue;
}
}

View file

@ -54,7 +54,7 @@ Categories.getUrl = function (category, isAbsolute) {
var isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
var prefix = isAbsolute ? Telescope.utils.getSiteUrl().slice(0,-1) : "";
// return prefix + FlowRouter.path("postsCategory", category);
return prefix + FlowRouter.path("posts.list", {}, {cat: [category.slug]});
return `${prefix}/?cat=${category.slug}`;
};
Categories.helpers({getUrl: function () {return Categories.getUrl(this);}});

View file

@ -9,7 +9,7 @@
Comments.getPageUrl = function(comment, isAbsolute){
var isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
var prefix = isAbsolute ? Telescope.utils.getSiteUrl().slice(0,-1) : "";
return prefix + FlowRouter.path("posts.single", {_id: comment.postId}) + "#"+comment._id;
return prefix + "foo" + "#"+comment._id;
};
Comments.helpers({getPageUrl: function () {return Comments.getPageUrl(this);}});

View file

@ -5,7 +5,10 @@ class ContextPasser extends Component {
getChildContext() {
return {
closeCallback: this.props.closeCallback,
currentUser: this.props.currentUser // pass on currentUser
currentUser: this.props.currentUser, // pass on currentUser,
actions: this.props.actions,
events: this.props.events,
messages: this.props.messages,
};
}
@ -16,12 +19,18 @@ class ContextPasser extends Component {
ContextPasser.propTypes = {
closeCallback: React.PropTypes.func,
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object,
};
ContextPasser.childContextTypes = {
closeCallback: React.PropTypes.func,
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object,
};
export default ContextPasser;

View file

@ -48,7 +48,13 @@ class ModalTrigger extends Component {
<Modal bsSize={this.props.size} show={this.state.modalIsOpen} onHide={this.closeModal}>
{this.props.title ? this.renderHeader() : null}
<Modal.Body>
<ContextPasser currentUser={this.context.currentUser} closeCallback={this.closeModal}>
<ContextPasser
currentUser={this.context.currentUser}
actions={this.context.actions}
events={this.context.events}
messages={this.context.messages}
closeCallback={this.closeModal}
>
{this.props.children}
</ContextPasser>
</Modal.Body>
@ -68,7 +74,10 @@ ModalTrigger.defaultProps = {
}
ModalTrigger.contextTypes = {
currentUser: React.PropTypes.object
currentUser: React.PropTypes.object,
actions: React.PropTypes.object,
events: React.PropTypes.object,
messages: React.PropTypes.object
};
// ModalTrigger.childContextTypes = {

View file

@ -1,14 +1,15 @@
import { composeWithTracker } from 'react-komposer';
import Messages from '../messages.js';
function composer(props, onData) {
const subscriptions = Telescope.subscriptions.map((sub) => Meteor.subscribe(sub.name, sub.arguments));
FlowRouter.watchPathChange();
const data = {
currentUser: Meteor.user(),
currentRoute: FlowRouter.current()
actions: {call: Meteor.call},
events: Events,
messages: Messages
}
Meteor.call("settings.getJSON", (error, result) => {

View file

@ -1,53 +1,53 @@
import Messages from "./messages.js";
// import Messages from "./messages.js";
FlowRouter.extendPathWithQueryParams = (path, params, newQueryParams) => {
const current = FlowRouter.current();
const currentQueryParams = _.clone(current.queryParams);
return FlowRouter.path(path, params, _.extend(currentQueryParams, newQueryParams));
};
FlowRouter.triggers.exit([() => Messages.clearSeen()]);
FlowRouter.addToQueryArray = function (key, value) {
var keyArray = FlowRouter.getQueryParam(key) || [];
keyArray.push(value);
var params = {};
params[key] = keyArray;
FlowRouter.setQueryParams(params);
}
FlowRouter.removeFromQueryArray = function (key, value) {
var keyArray = FlowRouter.getQueryParam(key);
keyArray = _.without(keyArray, value);
var params = {};
params[key] = keyArray;
FlowRouter.setQueryParams(params);
}
if(Meteor.isServer) {
var timeInMillis = 1000 * 30; // 30 secs
FlowRouter.setPageCacheTimeout(timeInMillis);
FlowRouter.setDeferScriptLoading(true);
}
// FlowRouter.notFound = {
// action: function() {
// if (Meteor.isClient) {
// DocHead.addMeta({
// name: "name",
// property: "prerender-status-code",
// content: "404"
// });
// DocHead.addMeta({
// name: "name",
// property: "robots",
// content: "noindex, nofollow"
// });
// }
// BlazeLayout.render("layout", {main: "not_found"});
// }
// FlowRouter.extendPathWithQueryParams = (path, params, newQueryParams) => {
// const current = FlowRouter.current();
// const currentQueryParams = _.clone(current.queryParams);
// return FlowRouter.path(path, params, _.extend(currentQueryParams, newQueryParams));
// };
if (typeof Events !== "undefined" && Meteor.isClient) {
FlowRouter.triggers.enter([function () {Events.analyticsRequest()}]);
}
// FlowRouter.triggers.exit([() => Messages.clearSeen()]);
// FlowRouter.addToQueryArray = function (key, value) {
// var keyArray = FlowRouter.getQueryParam(key) || [];
// keyArray.push(value);
// var params = {};
// params[key] = keyArray;
// FlowRouter.setQueryParams(params);
// }
// FlowRouter.removeFromQueryArray = function (key, value) {
// var keyArray = FlowRouter.getQueryParam(key);
// keyArray = _.without(keyArray, value);
// var params = {};
// params[key] = keyArray;
// FlowRouter.setQueryParams(params);
// }
// if(Meteor.isServer) {
// var timeInMillis = 1000 * 30; // 30 secs
// FlowRouter.setPageCacheTimeout(timeInMillis);
// FlowRouter.setDeferScriptLoading(true);
// }
// // FlowRouter.notFound = {
// // action: function() {
// // if (Meteor.isClient) {
// // DocHead.addMeta({
// // name: "name",
// // property: "prerender-status-code",
// // content: "404"
// // });
// // DocHead.addMeta({
// // name: "name",
// // property: "robots",
// // content: "noindex, nofollow"
// // });
// // }
// // BlazeLayout.render("layout", {main: "not_found"});
// // }
// // };
// if (typeof Events !== "undefined" && Meteor.isClient) {
// FlowRouter.triggers.enter([function () {Events.analyticsRequest()}]);
// }

View file

@ -1,3 +0,0 @@
const Router = FlowRouter;
export default Router;

View file

@ -1,28 +1,9 @@
import React from 'react';
import Router from './router.js'
import {mount} from 'react-mounter';
import Cheatsheet from './components/Cheatsheet.jsx';
import Settings from './components/Settings.jsx';
import Emails from './components/Emails.jsx';
Router.route('/cheatsheet', {
name: 'cheatsheet',
action() {
mount(Telescope.components.App, {content: <Cheatsheet/>});
}
});
Router.route('/settings', {
name: 'settings',
action() {
mount(Telescope.components.App, {content: <Settings/>});
}
});
Router.route('/emails', {
name: 'emails',
action() {
mount(Telescope.components.App, {content: <Emails/>});
}
});
Telescope.routes.add([
{name: "cheatsheet", path: "/cheatsheet", component: Cheatsheet},
{name: "settings", path: "/settings", component: Settings},
{name: "emails", path: "/emails", component: Emails},
]);

View file

@ -2,6 +2,7 @@ import React, { PropTypes, Component } from 'react';
import {mount} from 'react-mounter';
import MoviesWrapper from './demo-components.jsx';
import Core from 'meteor/nova:core';
import { Route } from 'react-router';
//////////////////////////////////////////////////////
// Collection & Schema //
@ -56,12 +57,8 @@ Movies.attachSchema(schema);
// Route //
//////////////////////////////////////////////////////
FlowRouter.route('/demo', {
name: 'demo',
action() {
mount(MoviesWrapper);
}
});
// Telescope.routes.add(<Route name="demo" path="/demo" component={MoviesWrapper} />);
Telescope.routes.add({name:"demo", path:"/demo", component:MoviesWrapper});
//////////////////////////////////////////////////////
// Methods //

View file

@ -2,7 +2,7 @@ Events.analyticsRequest = function() {
// Google Analytics
if (typeof window.ga !== 'undefined'){
window.ga('send', 'pageview', {
'page': FlowRouter.current().path
'page': window.location.pathname
});
}
};

View file

@ -20,6 +20,14 @@ This package can generate new document and edit document forms from a [SimpleSch
- Support for custom form controls.
- Submission to Meteor methods.
### NPM Dependencies
```
react react-intl formsy-react react-bootstrap formsy-react-components
```
You also need to load Bootstrap's CSS separately.
### Usage
Example schema:
@ -97,6 +105,10 @@ Edit document form:
The collection in which to edit or insert a document.
###### `schema`
If you prefer, you can also specify a schema instead of a collection.
###### `document`
If present, the document to edit. If not present, the form will be a “new document” form.
@ -187,7 +199,7 @@ An object containing optional autofilled properties.
###### `addToAutofilledValues({name: value})`
A function that takes a property, and adds it to the `prefilledValues` object.
A function that takes a property, and adds it to the `autofilledValues` object.
###### `throwError({content, type})`
@ -202,3 +214,17 @@ The component handles three different layers of input values:
- An “autofilled” value, typically provided by an *other* form element (i.e. autofilling the post title from its URL).
The highest-priority value is the user input. If there is no user input, we default to the database value provided by the `props`. And if that one is empty too, we then look for autofilled values.
### i18n
This package uses [React Intl](https://github.com/yahoo/react-intl/) to automatically translate all labels. In order to do so it expects an `intl` object ot be passed as part of its context. For example, in a parent component:
```
getChildContext() {
const intlProvider = new IntlProvider({locale: myLocale}, myMessages);
const {intl} = intlProvider.getChildContext();
return {
intl: intl
};
}
```

View file

@ -0,0 +1,20 @@
import React, { PropTypes, Component } from 'react';
import { Alert } from 'react-bootstrap';
const Flash = () => {
let type = this.props.message.type;
type = type === "error" ? "danger" : type; // if type is "error", use "danger" instead
return (
<Alert className="flash-message" bsStyle={type}>
{this.props.message.content}
</Alert>
)
}
Flash.propTypes = {
message: React.PropTypes.object.isRequired
}
export default Flash;

View file

@ -48,7 +48,7 @@ class FormComponent extends Component {
return <Checkbox {...properties} />;
// note: checkboxgroup cause React refs error
case "checkboxgroup":
return <CheckboxGroup {...properties} />;
return <CheckboxGroup {...properties} />;
case "radiogroup":
return <RadioGroup {...properties} />;
case "select":

View file

@ -2,9 +2,9 @@ import React, { PropTypes, Component } from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import Formsy from 'formsy-react';
import { Button } from 'react-bootstrap';
import Flash from "./Flash.jsx";
import FormGroup from "./FormGroup.jsx";
import Utils from './utils.js';
import { flatten, deepValue, getEditableFields, getInsertableFields } from './utils.js';
/*
@ -48,9 +48,14 @@ class NovaForm extends Component{
// ------------------------------- Helpers ----------------------------- //
// --------------------------------------------------------------------- //
// return the current schema based on either the schema or collection prop
getSchema() {
return this.props.schema ? this.props.schema : this.props.collection.simpleSchema()._schema;
}
getFieldGroups() {
const schema = this.props.collection.simpleSchema()._schema;
const schema = this.getSchema();
// build fields array by iterating over the list of field names
let fields = this.getFieldNames().map(fieldName => {
@ -73,7 +78,7 @@ class NovaForm extends Component{
field.label = (typeof this.props.labelFunction === "function") ? this.props.labelFunction(intlFieldName) : intlFieldName,
// add value
field.value = this.getDocument() && Utils.deepValue(this.getDocument(), fieldName) ? Utils.deepValue(this.getDocument(), fieldName) : "";
field.value = this.getDocument() && deepValue(this.getDocument(), fieldName) ? deepValue(this.getDocument(), fieldName) : "";
// replace value by prefilled value if value is empty
if (fieldSchema.autoform && fieldSchema.autoform.prefill) {
@ -144,10 +149,10 @@ class NovaForm extends Component{
// get relevant fields
getFieldNames() {
const { collection, fields } = this.props;
const fields = this.props.fields;
// get all editable/insertable fields (depending on current form type)
let relevantFields = this.getFormType() === "edit" ? collection.getEditableFields(this.props.currentUser, this.getDocument()) : collection.getInsertableFields(this.props.currentUser);
let relevantFields = this.getFormType() === "edit" ? getEditableFields(this.getSchema(), this.props.currentUser, this.getDocument()) : getInsertableFields(this.getSchema(), this.props.currentUser);
// if "fields" prop is specified, restrict list of fields to it
if (typeof fields !== "undefined" && fields.length > 0) {
@ -207,7 +212,6 @@ class NovaForm extends Component{
// render errors
renderErrors() {
Flash = Telescope.components.Flash;
return <div className="form-errors">{this.state.errors.map(message => <Flash key={message} message={message}/>)}</div>
}
@ -288,7 +292,6 @@ class NovaForm extends Component{
this.setState({disabled: true});
const fields = this.getFieldNames();
const collection = this.props.collection;
// if there's a submit callback, run it
if (this.props.submitCallback) this.props.submitCallback();
@ -296,7 +299,7 @@ class NovaForm extends Component{
if (this.getFormType() === "new") { // new document form
// remove any empty properties
let document = _.compactObject(Utils.flatten(data));
let document = _.compactObject(flatten(data));
// add prefilled properties
if (this.props.prefilledProps) {
@ -311,7 +314,7 @@ class NovaForm extends Component{
const document = this.getDocument();
// put all keys with data on $set
const set = _.compactObject(Utils.flatten(data));
const set = _.compactObject(flatten(data));
// put all keys without data on $unset
const unsetKeys = _.difference(fields, _.keys(set));
@ -354,7 +357,8 @@ class NovaForm extends Component{
}
NovaForm.propTypes = {
collection: React.PropTypes.object.isRequired,
collection: React.PropTypes.object,
schema: React.PropTypes.object,
document: React.PropTypes.object, // if a document is passed, this will be an edit form
currentUser: React.PropTypes.object,
submitCallback: React.PropTypes.func,

View file

@ -1,24 +1,13 @@
// import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';
// checkNpmVersions({
// "formsy-react": "^0.18.0",
// "formsy-react-components": "^0.7.1",
// "react-bootstrap": "^0.29.0"
// // 'rebass': '^0.2.4',
// });
if (typeof SimpleSchema !== "undefined") {
SimpleSchema.extendOptions({
control: Match.Optional(Match.Any), // NovaForm control (String or React component)
order: Match.Optional(Number), // order in the form
group: Match.Optional(Object), // form fieldset group
insertableIf: Match.Optional(Function),
editableIf: Match.Optional(Function)
});
}
SimpleSchema.extendOptions({
control: Match.Optional(Match.Any), // NovaForm control (String or React component)
order: Match.Optional(Number), // order in the form
group: Match.Optional(Object) // form fieldset group
});
// import NewDocument from "./NewDocument.jsx";
// import EditDocument from "./EditDocument.jsx";
import NovaForm from "./NovaForm.jsx";
SimpleSchema.extendOptions({
insertableIf: Match.Optional(Function),
editableIf: Match.Optional(Function)
});
export default NovaForm;

View file

@ -1,7 +1,5 @@
const Utils = {};
// add support for nested properties
Utils.deepValue = function(obj, path){
const deepValue = function(obj, path){
for (var i=0, path=path.split('.'), len=path.length; i<len; i++){
obj = obj[path[i]];
};
@ -9,7 +7,7 @@ Utils.deepValue = function(obj, path){
};
// see http://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects
Utils.flatten = function(data) {
const flatten = function(data) {
var result = {};
function recurse (cur, prop) {
@ -34,4 +32,30 @@ Utils.flatten = function(data) {
return result;
}
export default Utils;
/**
* @method Mongo.Collection.getInsertableFields
* Get an array of all fields editable by a specific user for a given collection
* @param {Object} user the user for which to check field permissions
*/
const getInsertableFields = function (schema, user) {
const fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return field.insertableIf && field.insertableIf(user);
});
return fields;
};
/**
* @method Mongo.Collection.getEditableFields
* Get an array of all fields editable by a specific user for a given collection
* @param {Object} user the user for which to check field permissions
*/
const getEditableFields = function (schema, user, document) {
const fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return field.editableIf && field.editableIf(user, document);
});
return fields;
};
export { flatten, deepValue, getInsertableFields, getEditableFields };

View file

@ -12,10 +12,8 @@ Package.onUse(function(api) {
api.use([
'ecmascript',
'check',
'tmeasday:check-npm-versions@0.3.1',
'aldeed:simple-schema@1.5.3',
'aldeed:collection2@2.8.0',
'utilities:smart-methods@0.1.4',
'fourseven:scss@3.4.1'
]);

View file

@ -4,6 +4,7 @@ Telescope.strings.en = {
"posts.new_post": "New Post",
"posts.edit": "Edit",
"posts.edit_success": "Post “{title}” edited.",
"posts.delete": "Delete",
"posts.delete_confirm": "Delete post “{title}”?",
"posts.delete_success": "Post “{title}” deleted.",
@ -102,6 +103,10 @@ Telescope.strings.en = {
"settings.facebookPage": "Facebook Page",
"settings.googleAnalyticsId": "Google Analytics ID",
"settings.locale": "Locale",
"settings.requireViewInvite": "Require View Invite",
"settings.requirePostInvite": "Require Post Invite",
"settings.requirePostsApproval": "Require Posts Approval",
"settings.scoreUpdateInterval": "Score Update Interval",
"app.loading": "Loading…",
"app.404": "Sorry, we couldn't find what you were looking for.",

View file

@ -15,6 +15,7 @@ Telescope.VERSION = '0.26.3-nova';
*/
Telescope.config = {};
// ------------------------------------- Schemas -------------------------------- //
SimpleSchema.extendOptions({
@ -58,4 +59,16 @@ Telescope.subscriptions.preload = function (subscription, args) {
Telescope.subscriptions.push({name: subscription, arguments: args});
};
// ------------------------------------- Strings -------------------------------- //
Telescope.strings = {};
// ------------------------------------- Routes -------------------------------- //
Telescope.routes = {
routes: [],
add(routeOrRouteArray) {
const addedRoutes = Array.isArray(routeOrRouteArray) ? routeOrRouteArray : [routeOrRouteArray];
this.routes = this.routes.concat(addedRoutes);
}
}

View file

@ -1,7 +1,7 @@
var areIntlLocalesSupported = require('intl-locales-supported');
var localesMyAppSupports = [
Telescope.settings.get("locale")
Telescope.settings.get("locale", "en")
];
if (global.Intl) {

View file

@ -38,7 +38,8 @@ Package.onUse(function (api) {
'jparker:gravatar@0.4.1',
'tmeasday:publish-counts@0.7.3',
'meteorhacks:unblock@1.1.0',
'kadira:flow-router-ssr@3.13.0',
// 'kadira:flow-router-ssr@3.13.0',
"reactrouter:react-router-ssr@3.1.3",
// 'kadira:flow-router@2.12.1',
'utilities:smart-publications@0.1.4',
'utilities:smart-methods@0.1.4',
@ -57,12 +58,12 @@ Package.onUse(function (api) {
'lib/settings.js',
'lib/collections.js',
'lib/deep.js',
'lib/deep_extend.js'
'lib/deep_extend.js',
'lib/intl-polyfill.js'
], ['client', 'server']);
api.addFiles([
'lib/server/server-config.js',
'lib/server/intl-polyfill.js'
'lib/server/server-config.js'
], ['server']);
api.export([

View file

@ -1,6 +1,8 @@
import MailChimpList from './mailchimp.js';
function subscribeUserOnProfileCompletion (user) {
if (!!Telescope.settings.get('autoSubscribe') && !!Users.getEmail(user)) {
addToMailChimpList(user, false, function (error, result) {
MailChimpList.add(user, false, function (error, result) {
console.log(error);
console.log(result);
});

View file

@ -132,7 +132,7 @@ MailChimpList.add = function(userOrEmail, confirm, done){
// mark user as subscribed
if (!!user) {
Users.setSetting(user, 'newsletter_subscribeToNewsletter', true);
Users.methods.setSetting(user._id, 'newsletter_subscribeToNewsletter', true);
}
console.log("// User subscribed");
@ -175,7 +175,7 @@ MailChimpList.remove = (user) => {
var subscribe = api.call('lists', 'unsubscribe', subscribeOptions);
// mark user as unsubscribed
Users.setSetting(user, 'newsletter_subscribeToNewsletter', false);
Users.methods.setSetting(user._id, 'newsletter_subscribeToNewsletter', false);
console.log("// User unsubscribed");

View file

@ -20,7 +20,6 @@ Package.onUse(function (api) {
api.addFiles([
// 'package-tap.i18n',
// 'lib/collection.js',
'lib/callbacks.js',
'lib/custom_fields.js',
'lib/emails.js'
], ['client', 'server']);
@ -29,7 +28,8 @@ Package.onUse(function (api) {
'lib/server/cron.js',
'lib/server/emails.js',
'lib/server/methods.js',
'lib/server/mailchimp_api.js'
'lib/server/mailchimp_api.js',
'lib/server/callbacks.js'
], ['server']);
api.mainModule('lib/server.js', 'server');

View file

@ -1,3 +1,4 @@
import Posts from './config'
import marked from 'marked';
//////////////////////////////////////////////////////

View file

@ -1,3 +1,5 @@
import Posts from './config'
const adminGroup = {
name: "admin",
order: 2
@ -7,7 +9,7 @@ const adminGroup = {
* @summary Posts schema
* @type {SimpleSchema}
*/
Posts.schema = new SimpleSchema({
Posts.schemaJSON = {
/**
ID
*/
@ -226,15 +228,9 @@ Posts.schema = new SimpleSchema({
collection: () => Meteor.users
}
}
});
};
// schema transforms
// Meteor.startup(function(){
// // needs to happen after every fields were added
// Posts.internationalize();
// });
/**
* @summary Attach schema to Posts collection
*/
Posts.attachSchema(Posts.schema);
if (typeof SimpleSchema !== "undefined") {
Posts.schema = new SimpleSchema(Posts.schemaJSON);
Posts.attachSchema(Posts.schema);
}

View file

@ -1,8 +1,17 @@
// import { Mongo } from './imports.js';
/**
* @summary The global namespace/collection for Posts.
* @namespace Posts
*/
Posts = new Mongo.Collection("posts");
const PostsStub = {
helpers: x => x
}
/* we need to handle two scenarios: when the package is called as a Meteor package,
and when it's called as a NPM package */
Posts = typeof Mongo !== "undefined" ? new Mongo.Collection("posts") : PostsStub;
/**
* @summary Posts config namespace
@ -10,7 +19,6 @@ Posts = new Mongo.Collection("posts");
*/
Posts.config = {};
/**
* @summary Post Statuses
*/
@ -41,4 +49,6 @@ Posts.config.STATUS_PENDING = 1;
Posts.config.STATUS_APPROVED = 2;
Posts.config.STATUS_REJECTED = 3;
Posts.config.STATUS_SPAM = 4;
Posts.config.STATUS_DELETED = 5;
Posts.config.STATUS_DELETED = 5;
export default Posts;

View file

@ -1,4 +1,5 @@
import moment from 'moment';
import Posts from './config';
//////////////////
// Link Helpers //
@ -36,23 +37,12 @@ Posts.helpers({getLinkTarget: function () {return Posts.getLinkTarget(this);}});
* @param {Object} post
*/
Posts.getPageUrl = function(post, isAbsolute){
var isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
var prefix = isAbsolute ? Telescope.utils.getSiteUrl().slice(0,-1) : "";
return prefix + FlowRouter.path("posts.single", post);
return `${prefix}/posts/${post._id}/${post.slug}`;
};
Posts.helpers({getPageUrl: function (isAbsolute) {return Posts.getPageUrl(this, isAbsolute);}});
/**
* @summary Get post edit page URL.
* @param {String} id
*/
Posts.getEditUrl = function(post, isAbsolute){
var isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
var prefix = isAbsolute ? Telescope.utils.getSiteUrl().slice(0,-1) : "";
return prefix + FlowRouter.path("posts.edit", post);
};
Posts.helpers({getEditUrl: function (isAbsolute) {return Posts.getEditUrl(this, isAbsolute);}});
///////////////////
// Other Helpers //
///////////////////
@ -114,7 +104,7 @@ Posts.checkForSameUrl = function (url) {
* @summary When on a post page, return the current post
*/
Posts.current = function () {
return Posts.findOne(FlowRouter.getParam("_id"));
return Posts.findOne("foo");
};
/**

View file

@ -1,3 +1,5 @@
import Posts from './config'
/**
*
* Post Methods

View file

@ -1,3 +1,5 @@
import Posts from './config'
Posts.getNotificationProperties = function (data) {
const post = data.post;
const postAuthor = Meteor.users.findOne(post.userId);

View file

@ -1,3 +1,4 @@
import Posts from './config'
import moment from 'moment';
/**

View file

@ -1,3 +1,4 @@
import Posts from './config'
import PublicationsUtils from 'meteor/utilities:smart-publications';
Posts.publishedFields = {};

View file

@ -1,3 +1,5 @@
import Posts from './config'
/**
* @summary Post views are filters used for subscribing to and viewing posts
* @namespace Posts.views

View file

@ -1,3 +1,4 @@
import Users from './namespace.js';
import marked from 'marked';
//////////////////////////////////////////////////////

View file

@ -1,3 +1,5 @@
import Users from './namespace.js';
const adminGroup = {
name: "admin",
order: 10

View file

@ -1,3 +1,4 @@
import Users from './namespace.js';
import moment from 'moment';
////////////////////
@ -67,7 +68,7 @@ Users.getProfileUrl = function (user, isAbsolute) {
isAbsolute = typeof isAbsolute === "undefined" ? false : isAbsolute; // default to false
var prefix = isAbsolute ? Telescope.utils.getSiteUrl().slice(0,-1) : "";
if (user.telescope && user.telescope.slug) {
return prefix + FlowRouter.path("users.single", {slug: user.telescope.slug});
return `${prefix}/users/${user.telescope.slug}`;
} else {
return "";
}
@ -169,19 +170,6 @@ Users.getSetting = function (user, settingName, defaultValue) {
};
Users.helpers({getSetting: function (settingName, defaultValue) {return Users.getSetting(this, settingName, defaultValue);}});
/**
* @summary Set a user setting
* @param {Object} user
* @param {String} settingName
* @param {Object} defaultValue
*/
Users.setSetting = function (user, settingName, value) {
if (user) {
Meteor.call("users.setSetting", user._id, settingName, value);
}
};
Users.helpers({setSetting: function () {return Users.setSetting(this);}});
/**
* @summary Check if a user has upvoted a post
* @param {Object} user

View file

@ -1,3 +1,5 @@
import Users from './namespace.js';
var completeUserProfile = function (userId, modifier, user) {
Users.update(userId, modifier);
@ -33,6 +35,16 @@ Users.methods.edit = (userId, modifier, user) => {
}
Users.methods.setSetting = (userId, settingName, value) => {
// all settings should be in the user.telescope namespace, so add "telescope." if needed
var field = settingName.slice(0,10) === "telescope." ? settingName : "telescope." + settingName;
var modifier = {$set: {}};
modifier.$set[field] = value;
Users.update(userId, modifier);
}
Meteor.methods({
'users.compleProfile'(modifier, userId) {
@ -152,13 +164,7 @@ Meteor.methods({
throw new Meteor.Error(601, __('sorry_you_cannot_edit_this_user'));
}
// all settings should be in the user.telescope namespace, so add "telescope." if needed
var field = settingName.slice(0,10) === "telescope." ? settingName : "telescope." + settingName;
var modifier = {$set: {}};
modifier.$set[field] = value;
Users.update(userId, modifier);
Users.methods.setSetting(userId, settingName, value);
}

View file

@ -3,3 +3,5 @@
* @namespace Users
*/
Users = Meteor.users;
export default Users;

View file

@ -1,3 +1,5 @@
import Users from './namespace.js';
Users.getNotificationProperties = function (user) {
const properties = {
profileUrl: Users.getProfileUrl(user),

View file

@ -1,3 +1,5 @@
import Users from './namespace.js';
// note: using collection helpers here is probably a bad idea,
// because they'll throw an error when the user is undefined

View file

@ -1,3 +1,4 @@
import Users from './namespace.js';
import PublicationsUtils from 'meteor/utilities:smart-publications';
Users.publishedFields = {};

View file

@ -1,3 +1,5 @@
import Users from './namespace.js';
/**
* @summary Telescope roles
* @namespace Users.is