Quickstart
A quick dive into getting started with Lore
A quick dive into getting started with Lore
In this step we'll finish our support for filtering and add a component to view the user's tweets.
You can view the finished code for this step by checking out the
filtering.2
branch of the completed project.
First we need a component to display the user's tweets. The behavior of this component is identical to the Feed, with one exception; we only want to display tweets the current user created. So start off by copying the Feed
component and renaming it UserTweets
.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
export default createReactClass({
displayName: 'UserTweets',
getInitialState() {
return {
timestamp: new Date().toISOString()
};
},
render() {
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
}
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', _.defaultsDeep({
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, page.query));
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, lastPage.query));
}}
/>
</div>
);
}
});
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
class Feed extends React.Component {
constructor(props) {
super(props);
this.state = {
timestamp: new Date().toISOString()
};
}
render() {
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
}
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', _.defaultsDeep({
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, page.query));
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
/>
</div>
);
}
}
export default Feed;
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
class Feed extends React.Component {
constructor(props) {
super(props);
this.state = {
timestamp: new Date().toISOString()
};
}
render() {
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
}
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', _.defaultsDeep({
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, page.query));
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
/>
</div>
);
}
}
export default Feed;
Then update the render()
method to include the changes below:
// src/components/UserTweets.js
render() {
const { params } = this.props;
const { timestamp } = this.state;
return (
<div className="feed">
// ...
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
},
user: Number(params.userId)
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
...
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
const isByUser = tweet.data.user === Number(params.userId);
return (isOptimistic || isNew) && isByUser;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
/>
</div>
);
}
Here we've extracted params
from props
, which is automatically provided by react-router
, so that we can get the userId
provided in the URL.
For the selectOther()
callback, we've added an isByUser
variable, to detect whether the tweet is by the user with the userId
. This will mean that only tweets created by the current user will show up on this page.
For the select()
callback, we did something similar, added the user
to the query parameters, so that the API will only return tweets created by that user. That means the Feed on the "My Tweets" page will consist only of tweets created by a single user.
Next, open routes.js
and import the UserTweets
component. Then register a new route for /users/:userId
that will display that component.
// routes.js
...
import UserTweets from './src/components/UserTweets';
export default (
<Route>
<Route path="/login" component={Login} />
<Route path="/logout" component={Logout} />
<Route path="/auth/callback" component={AuthCallback} />
<Route component={UserIsAuthenticated(Master)}>
<Route path="/" component={Layout}>
<IndexRoute component={Feed} />
<Route path="users/:userId" component={UserTweets} />
</Route>
</Route>
</Route>
);
With that change in place, refresh the browser and you'll now be able to view all the tweets or just the tweets created by the current user.
If everything went well, your application should now look like this when you select "My Tweets":
Below is a list of files modified during this step.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
export default createReactClass({
displayName: 'UserTweets',
getInitialState() {
return {
timestamp: new Date().toISOString()
};
},
render() {
const { params } = this.props;
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
},
user: Number(params.userId)
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', _.defaultsDeep({
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, page.query));
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
const isByUser = tweet.data.user === Number(params.userId);
return (isOptimistic || isNew) && isByUser;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
/>
</div>
);
}
});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
class UserTweets extends React.Component {
constructor(props) {
super(props);
this.state = {
timestamp: new Date().toISOString()
};
}
render() {
const { params } = this.props;
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
},
user: Number(params.userId)
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', _.defaultsDeep({
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, page.query));
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
const isByUser = tweet.data.user === Number(params.userId);
return (isOptimistic || isNew) && isByUser;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
/>
</div>
);
}
}
export default UserTweets;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import PayloadStates from '../constants/PayloadStates';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
class UserTweets extends React.Component {
constructor(props) {
super(props);
this.state = {
timestamp: new Date().toISOString()
};
}
render() {
const { params } = this.props;
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
},
user: Number(params.userId)
}
},
pagination: {
sort: 'createdAt DESC',
page: 1,
populate: 'user'
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', _.defaultsDeep({
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, page.query));
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
const isByUser = tweet.data.user === Number(params.userId);
return (isOptimistic || isNew) && isByUser;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
},
exclude: function(tweet) {
return tweet.state === PayloadStates.DELETED;
}
});
}}
/>
</div>
);
}
}
export default UserTweets;
import React from 'react';
import { Route, IndexRoute, Redirect } from 'react-router';
/**
* Wrapping the Master component with this decorator provides an easy way
* to redirect the user to a login experience if we don't know who they are.
*/
import UserIsAuthenticated from './src/decorators/UserIsAuthenticated';
/**
* Routes are used to declare your view hierarchy
* See: https://github.com/ReactTraining/react-router/blob/v3/docs/API.md
*/
import Master from './src/components/Master';
import Layout from './src/components/Layout';
import Feed from './src/components/Feed';
import Login from './src/components/Login';
import AuthCallback from './src/components/AuthCallback';
import Logout from './src/components/Logout';
import UserTweets from './src/components/UserTweets';
export default (
<Route>
<Route path="/login" component={Login} />
<Route path="/logout" component={Logout} />
<Route path="/auth/callback" component={AuthCallback} />
<Route component={UserIsAuthenticated(Master)}>
<Route path="/" component={Layout}>
<IndexRoute component={Feed} />
<Route path="users/:userId" component={UserTweets} />
</Route>
</Route>
</Route>
);
In the next section we'll we'll add support for WebSockets.