A Medium clone that allows users to publicly publish articles and/or blogs that can be viewed and commented on by others.
Make sure to have brew installed. Brew install the following:
brew install rbenv
brew install postgresql@14
Once rbenv is installed, run rbenv install 3.1.0
to install the ruby version specified in the gemfile and rbenv local 3.1.0
to set it as the local ruby version. Verify that the ruby version is properly installed by running ruby -v
If rails is not yet installed on your machine, run gem install rails
then bundle install
the gems. You may need to update your bundler so in that case, run gem install bundler
before running bundle install
again.
Once installed the gems, run rails db:create
and rails db:migrate
to create and migrate the database.
Then you can start the server with rails s
.
- Heroku
- Ruby on Rails
- PostgreSQL
- ActiveRecord
- Webpack
- AWS S3
- React
- Redux
- SCSS
User auth is an important feature that I want to highlight here because requiring a user to be logged in before having access to any of the other functionality on the site is crucial for creating a true user experience. The login and signup portion of the site uses a modal and also allows for quick access between login and signup. There is also a easy access button for individuals who wish to demo the website before committing to their own account.
The following code snippet comes from my session_form
where I implemented the functionality of closing the modal upon processing a user's session form and then pushing that user to their feed page. Also shown below is how I was able to capture the values using event handling like e.currentTarget.value
as well as how errors were being rendered by the front end.
handleSubmit(e) {
e.preventDefault();
const user = {...this.state};
this.props.processForm(user)
.then(this.props.closeModal)
.then(() => this.props.history.push('/feed'));
}
update(field) {
return e => this.setState({ [field]: e.currentTarget.value })
}
renderErrors() {
const { errors } = this.props;
return (
<ul className='session-form-errors'>
{errors.map((error, idx) => (
<li key={`error-${idx}`}>{error}</li>
))}
</ul>
);
}
Stories are the bread and butter of this project. Any logged in user can publish, view, and delete stories. In particular, stories can be published with images. Also each user is greeted by a feed upon log in, which are stories that are randomly generated upon each refresh.
const mDTP = dispatch => ({
fetchAllStories: () => dispatch(fetchAllStories()),
shuffleStories: stories => {
let i = stories.length - 1;
for (; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[stories[i], stories[j]] = [stories[j], stories[i]];
}
return stories;
}
});
const shuffledStories = shuffleStories(stories);
let storyIndexTop = <StoryIndexTop stories={shuffledStories.slice(0, 5)} />
let storyIndexPopular = <StoryIndexPopular stories={shuffledStories.slice(5,9)} />
Clicking on a story, either on the text or image, will take the user to that stories page, where the full story is displayed. Other notable features of the feed include: A "Editor's Picks" which is akin to sending the user to a story that is featured. A sticky aside that moves with the user as they scroll up and down the page, deleting stories. A drop down menu for links to publishing new stories as well as viewing stories published by the current user.
Logged in users can also leave comments on stories. Each user can respond to other users and a user can delete their own comments. Comments can be nested, which was done by recursive rendering of the comments container component.
// comment_show.jsx
if (!comment) {
return null;
} else {
const nestedComments = (commentsByParent[comment.id] || []).map(comment => {
return (
<CommentShowContainer
key={comment.id}
story={story}
comment={comment}
commenters={commenters}
commentsByParent={commentsByParent} />
);
});
Logged in users can follow and unfollow other users. The users profile page will also display all the stories written by that user and clicking on any of the stories will take the user directly to the story show page.
Logged in users can like and unlike stories. A story displays the number of likes it has. Liking a story will change how the button looks depending on the story's liked status. The following code snippet demonstrates how I was able to achieve that:
handleLike(e) {
const { story, likeStory, unlikeStory } = this.props;
e.preventDefault();
if (story.likedByCurrentUser) {
unlikeStory(story.id);
} else {
likeStory(story.id);
}
}
status() {
const { story } = this.props;
return story.likedByCurrentUser ? "Unlike" : "Like";
}
This project had its ups and downs, and some of the downs were greater than others. One notable challenge that I came across was figuring out how to load the feed page more efficiently because it had to fetch a lot of information on each load of the page. To fix that issue, I had to resolve some N+1 queries in my controllers as well as resizing the images that were being sent to the frontend so that the frontend did not have to render big images before resizing it at the same time.
Below is a code snippet of how I accomplished that using .includes()
as well as using the Active Storage mini_magiick
gem.
def index
@stories = Story.includes(:author, "photo_attachment": :blob).all
render :index
end
Another notable challenge I encountered was figuring out how to properly render the comments React component. At first I ran into the issue of the comments not being rendered at all. But I quickly realized that I needed to use my Comments container component instead of the presentational component to get the props. In addition, I needed to figure out how to clear the comment form and how to automatically close the comments form when the user submits a comment. I was able to accomplish both of these by setting local state:
// Clearing comments form in create_comment_form.jsx
handleSubmit(e) {
e.preventDefault();
if (this.props.handleClick) {
this.props.processForm(this.state)
.then(() => this.props.handleClick(e))
.then(() => this.props.fetchStory(this.props.storyId));
} else {
this.props.processForm(this.state)
.then(() => this.props.fetchStory(this.props.storyId))
.then(() => this.setState({ body: ''}));
}
}
// Conditional rendering of comments form in comment_show.jsx
displayCommentBox() {
const { story, comment } = this.props;
if (this.state.toggled) {
return (
<CreateCommentFormContainer story={story} comment={comment} handleClick={this.handleClick} />
);
} else {
return null;
}
}
Some features to add to the project in the future: heroku run rails db:migrate
- Sort by categories
- Editing user profile