https://github.com/jrawsthorne/review.app
You can visit a user's profile page by going to /@username like with most steem apps. The profile page shows the name, username, website, location and bio at the top with the avatar to the left and cover in the background. Below that is posts by that user.
I decided that a user should be able to rate any media item on the site so I needed to create a place for ratings in the database.
This is the schema (GitHub Link)
const schema = new Schema(
{
user: {
type: Schema.Types.ObjectId,
ref: 'users',
required: true,
},
score: {
type: Number,
required: true,
},
mediaType: {
type: String,
required: true,
},
tmdbid: {
type: Number,
required: true,
},
seasonNum: {
type: Number,
},
episodeNum: {
type: Number,
},
},
{
timestamps: true,
},
);
Once logged in a user can go to any post and with the rest of the metadata at the top there are 5 stars. Clicking one will immediately add this rating to your account. In the future when there are sufficient ratings I hope to display an average as well. Clicking the same number of stars again will remove the rating so the minimum you can give is 1 star.
When a user sets the score to 0 it is completely deleted from the database. GitHub Link
if (parseInt(value, 10) === 0) {
return Rating
.findOne({
user,
mediaType,
tmdbid,
seasonNum,
episodeNum,
})
.remove()
.then(() => {
User.update({ _id: user.id }, { $pull: { ratings: rating.id } })
.then(() => res.json({}));
});
}
A user is also able to subscribe to certain shows or movies if they want to produce a feed with just posts from those shows/movies. I decided to implement this and will provide it as well as a general steem feed of content from users you follow.
This is the schema (GitHub Link)
const schema = new Schema(
{
user: {
type: Schema.Types.ObjectId,
ref: 'users',
required: true,
},
tmdbid: {
type: Number,
required: true,
},
type: {
type: String,
required: true,
},
},
);
To display all the posts for a subscription I loop through each one and find all the posts matching that criteria and add it to an array. (GithHub Link)
router.get('/subscriptions', passport.authenticate('jwt', { session: false }), (req, res) => {
const { user } = req;
let count = 0;
Subscription.find({ user })
.then((subscriptions) => {
Promise.all(subscriptions.map(subscription =>
Post.count({ type: subscription.type, tmdbid: subscription.tmdbid }).then(c =>
Post.find({ type: subscription.type, tmdbid: subscription.tmdbid })
.then((posts) => { count += c; return posts; }))))
.then(posts => res.json({
count,
results: orderBy(flatten(posts), 'createdAt', 'desc'),
}));
})
.catch(() => res.status(404).json({ error: 'Error fetching subscriptions' }));
});
To facilitate the previous two new features I added a new sign in flow so that I don't have to rely on steem authentication for every protected action. Having to access the steem api for every rating and subscription was very slow. As those two things don't rely on the steem blockchain it was unnecessary to authenticate using a steemconnect token so I now store a JSON web token with the users details in it. (GitHub Link)
router.post('/login', (req, res) => {
const { accessToken } = req.body;
if (!accessToken) res.status(400).json({ error: 'Not authenticated' });
steemConnectAPI.setAccessToken(accessToken);
steemConnectAPI.me()
.then((steemUser) => {
User.findOne({ username: steemUser.name }).populate({ path: 'ratings', select: 'score mediaType tmdbid seasonNum episodeNum' }).populate('subscriptions', 'tmdbid type')
.then((user) => {
if (user) {
const token = jwt.sign({
id: user.id,
username: user.username,
}, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({
user: {
...steemUser,
account: {
...steemUser.account,
ratings: {
scores: user.ratings,
},
subscriptions: {
items: user.subscriptions,
},
},
},
token,
});
} else {
new User({ username: steemUser.name }).save().then((newUser) => {
const token = jwt.sign({
id: newUser.id,
username: newUser.username,
}, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({
user: steemUser,
token,
});
});
}
});
})
.catch(() => res.status(400).json({ error: 'Not authenticated' }));
});
The interface for switching between episodes and seasons has been improved with a clickable tooltip. Although this doesn't provide as much information for the user, it saves a lot of space that was wasted before in my opinion. (GitHub Link)
{props.seasons &&
<div className="MediaHeader__info__selectors">
{props.seasons &&
<Popover
onVisibleChange={props.handleSeasonVisibleChange}
visible={props.showSeasons}
placement="bottom"
content={Object.values(props.seasons).map(season =>
(
<p
onClick={() => handleSeasonClick(season.season_number)}
className="Filter__option"
key={`season-${season.season_number}`}
>{season.name}
</p>
))}
trigger="click"
>
<span className="Filter__dropdown" style={{ marginLeft: 0 }}>
Seasons <Icon type="down" style={{ fontSize: 15 }} />
</span>
</Popover>
}
{props.episodes &&
<Popover
onVisibleChange={props.handleEpisodeVisibleChange}
visible={props.showEpisodes}
placement="bottom"
content={Object.values(props.episodes).map(episode =>
(
<p
onClick={() => handleEpisodeClick(episode.episode_number)}
className="Filter__option"
key={`season-${episode.episode_number}`}
>{episode.name}
</p>
))}
trigger="click"
>
<span className="Filter__dropdown">
Episodes <Icon type="down" style={{ fontSize: 15 }} />
</span>
</Popover>
}
</div>
}