#### Repository
https://github.com/jrawsthorne/review.app
### New Features
- ~~User authentication system using SteemConnect~~ (uh-oh)
- ~~Database to store ids of posts~~
- ~~Posts need to be displayed with the linked metadata~~
- ~~Custom API to allow for more complex querying of posts~~
### #1 Database
The database needed to store a variety of information about a post including a way to reference each post. The common way to query the steemit api is by author and the permanent link so these fields were mandatory. On the front end I also wanted to be able to filter by the type of post, for example a review, or just a discussion. The database will also store the metadata relating to each post. For a review of a TV episode this would include the MovieDB ID, season number and episode number. To reduce the number of api requests to TheMovieDB I decided to also store the title, which would be the name of the show in this case, as well as the paths to relevant images. There will also be a rating field. In the future I will add this to a user model so all users can rate something.
This is the schema ([GitHub Link](https://github.com/jrawsthorne/review.app/blob/804400323a6c981978841f523469d11182c38776/src/apis/models/Post.js#L3-L55))
```javascript
const schema = new mongoose.Schema(
{
author: {
type: String,
required: true,
},
permlink: {
type: String,
required: true,
unique: true,
},
postType: {
type: String,
required: true,
},
mediaType: {
type: String,
required: true,
},
tmdbid: {
type: Number,
required: true,
},
title: {
type: String,
required: true,
},
posterPath: {
type: String,
},
backdropPath: {
type: String,
},
episodePath: {
type: String,
},
seasonPath: {
type: String,
},
seasonNum: {
type: Number,
},
episodeNum: {
type: Number,
},
rating: {
type: Number,
},
},
{
timestamps: true,
},
);
```
### #2 API routes
Retrieve post by author and permlink with related metadata ([GitHub Link](https://github.com/jrawsthorne/review.app/blob/804400323a6c981978841f523469d11182c38776/src/apis/routes/posts.js#L8-L17))
```javascript
router.get('/@:author/:permlink', (req, res) => {
const { author, permlink } = req.params;
Post.findOne({ author, permlink })
.then((post) => {
if (!post) {
return res.status(404).json({ error: "Post couldn't be found" });
}
return res.json(post);
});
});
```
Front end makes the request and merges with the data from the steemit api ([GitHub Link](https://github.com/jrawsthorne/review.app/blob/804400323a6c981978841f523469d11182c38776/src/actions/postActions.js#L52-L65))
```javascript
export const fetchPost = (author, permlink) => ({
type: FETCH_POST,
payload: axios.get(`/api/posts/@${author}/${permlink}`)
.then(res => res.data)
.then(post => steemAPI.getContentAsync(post.author, post.permlink)
.then(steemPost => getPostData(steemPost, post)))
.then(steemPost => steemPost.id !== 0 && steemPost),
meta: {
author,
permlink,
globalError: 'Sorry, there was an error fetching the post',
},
});
```
Retrieve posts matching certain criteria ([GitHub Link](https://github.com/jrawsthorne/review.app/blob/804400323a6c981978841f523469d11182c38776/src/apis/routes/posts.js#L19-L40))
```javascript
router.get('/', (req, res) => {
const {
postType = 'all', mediaType, tmdbid, seasonNum, episodeNum, rating, limit = 20,
} = req.query;
const query = {};
if (mediaType) query.mediaType = mediaType;
if (tmdbid) query.tmdbid = tmdbid;
if (seasonNum) query.seasonNum = seasonNum;
if (episodeNum) query.episodeNum = episodeNum;
if (rating) query.rating = rating;
if (postType !== 'all') {
query.postType = postType;
}
Post.count(query)
.then((count) => {
Post.find(query).limit(limit)
.then(posts => res.json({
count,
results: posts,
}));
});
});
```
Front end makes the request and calls the steemit api for every post it finds that isn't already stored locally ([GitHub Link](https://github.com/jrawsthorne/review.app/blob/804400323a6c981978841f523469d11182c38776/src/actions/postActions.js#L29-L50))
```javascript
export const fetchPosts = (posts, {
postType = 'all', mediaType = null, tmdbid = null, seasonNum = null, episodeNum = null, rating = null,
}) => ({
type: FETCH_POSTS,
payload: axios.get('/api/posts', {
params: {
postType: postType || 'all',
mediaType: mediaType || undefined,
tmdbid: tmdbid || undefined,
seasonNum: seasonNum || undefined,
episodeNum: seasonNum && episodeNum ? episodeNum : undefined,
rating: rating || undefined,
},
})
.then(res =>
Promise.all(res.data.results.filter(post => !_.get(posts, `@${post.author}/${post.permlink}`)).map(post => steemAPI.getContentAsync(post.author, post.permlink)
.then(steemPost => getPostData(steemPost, post))))
.then(steemPosts => arrayToObject(steemPosts.filter(post => post.id !== 0), 'url'))),
meta: {
globalError: 'Sorry, there was an error fetching posts',
},
});
```
Ability to update the metadata associated with a post if an image path changes for some reason ([GitHub Link](https://github.com/jrawsthorne/review.app/blob/804400323a6c981978841f523469d11182c38776/src/apis/routes/posts.js#L44-L95))
```javascript
router.post('/update-metadata', (req, res) => {
/* checking whether necessary values exist */
return Post.findOne({
author, permlink, postType, mediaType, tmdbid, seasonNum, episodeNum,
})
.then((post) => {
if (!post) return res.status(404).json({ error: "Post couldn't be found" });
if (mediaType === 'movie') {
theMovieDBAPI.movieInfo(tmdbid)
.then(movie => Post.findOneAndUpdate({
author, permlink, postType, mediaType, tmdbid,
}, { posterPath: movie.poster_path, backdropPath: movie.backdrop_path }, { new: true })
.then(newPost => res.json(newPost)));
}
/* checking other media types */
return post;
});
});
```
### #3 Display posts
Home page shows all posts, can filter by media type
![Home page](https://i.imgur.com/cRfWp71.jpg)
![Filtered home page](https://i.imgur.com/yLfeQdJ.png)
Every show/movie page shows the posts for it
![Movie page](https://i.imgur.com/4i5Lnt0.jpg)
![Show page](https://i.imgur.com/EpVzQvZ.jpg)
![Episode page](https://i.imgur.com/F0cgSjE.png)
Single post page shows metadata, post title and formatted body
![Movie post](https://i.imgur.com/ehTihba.png)
![Show post](https://i.imgur.com/mh32Nat.jpg)
![Episode post](https://i.imgur.com/bOOSOgb.png)
### #4 Mobile styling
Until I come up with a final design I added some mobile styling so content wouldn't expand out of the page. On the home page, a landscape image is shown to use the limited space better.
![Post page](https://i.imgur.com/TJYFBEP.jpg)
![Home page](https://i.imgur.com/eEQA2kX.jpg)
### #5 Login with SteemConnect
I fixed the SteemConnect login redirecting to localhost and now when you login it shows your avatar in the top right. The access token is only stored client side so there is no possibility of a leak if the server is hacked. I also only request an online token and not an offline one which can be used to generate unlimited tokens. Logout revokes the token so it can't ever be used again.
### Roadmap
- Better filters for pages - store list of posts matching filter in redux store
- Post formatting - youtube/dtube/more complex markdown/html
- Ability to actually write new posts
- Store all database info in JSON metadata of a post as backup
- User profile pages
- Allow all users to give star rating
- User actions (like, comment)
- Subscribe to certain show - get notifications when new episode airs
- Subscribed page showing all posts from subscribed shows/movies
#### Proof of Work Done
https://github.com/jrawsthorne