Picshare is a picture sharing app that was created for the Cookpad porgramming interview. It allows users upload images which both site visitors and registered users can see. Registered users can comment on and picture owners can delete.
The app was created using ReactJs and Mobx. It is backed by a cloud based backend, Parse. It was deisgned with modularity in mind and as such is split into several components each handling a single functionality. I also followed ReactJs best practice of passing down data from a single component (Source of truth), while the other components it is composed of are merely for rendering data.
There is also complete separation of concerns. All data fetching, manipulation and promise resolving are done in stores while Mobx and an Event Emitter acts as a transport mechanism for moving data to my views. All stores are also singleton classes, which means the amount of initialization is greatly reduced and this helps performance.
I also attempted to keep this as DRY as possible, this is evident in the use of a StoreManager and an ImageStore passing Stores and assets to my views.
The user story scope given to for the interview by cookpad is as follows:
- As a visitor, I can register an account and sign in
- As a signed in user, I can sign out
- As a signed in user, I can upload an image
- As both a signed in user and visitor, I can view uploaded images
- As a signed in user, I can delete an image that I uploaded
- As a signed in user, I can comment on an image
In addition to the scope given above, i also implemented the following:
- Picture Caption
- Profile Page where only my pages appear
- ReactJs
- Parse Server
- FbEmitter (Event Emitter by Facebook)
- React DropZone
- React Router
- Mobx
- Mobx Forms
- Timeago React
- antD (Frontend framework)
- Prop Types
- React-S-Alert
The application has three parent classes. One is for the stores while the other is for the views. All stores or views inherit from either one
- Base Class for stores (BaseStore.js)
- Base Component for views (BaseView.js)
- Base Feed View (BaseFeedView.js)
The application has four (4) pages:
- Homepage (App.js) (Also contains login and registration)
- Feeds (Feed.js)
- Upload (Upload.js)
- Profile (Profile.js)
- Logout (Logout.js)
There are six (6) stores/repositories for data:
- Authentication (AuthStore.js)
- Event Emitter (EmitterStore.js)
- Images (ImageStore.js)
- Posts (PostStore.js)
- Image Upload (UploadStore.js)
- Store Manager (StoreManager.js)
There are three layouts:
- MasterLayout
- PageLayout
- StaticPageLayout
There are three components
- CommentViewer
- ImageCard
- NavBar
A constants file
- Constants.js
The main container
- Index.js
This layout component defines the structure of the page and seperates the entire page into three different parts. The Header, Content and Footer.
This layout component does not contain the navigation bar. The homepage is the only page making use of this layout.
This layout component contains the navigation bar and is used by all other pages except the homepage.
This page is the base class from which other pages inherit. It is contains functionility common to all views. In this case, the common functionality is to refresh the current user and make sure its still up to date.
The Navigation bar component renders differently depending on if the user is logged in or not. For a logged in user, there are three menu items available:
- Feeds
- Upload
- User
- Profile
- Logout
The page contains a simple explanation of the app along with a button with which visitors can view the uploaded images. The page also contains a tab component for login and registration forms. The forms where split into two components namely:
- Login
- Register There is a button on the page that allows visitors explore the uploaded images.
The page extends the BaseView Class and makes use of the StaticPageLayout.
The BaseFeedView
contains common functionalities between the Feeds and Profile page. The Feed and profile are very similar and as such share some common elements that should be rendered. The BaseFeedView
renders this common elements and implements a getContent
function. Both Feeds
and Profile
now implement the getContent
method to display their own unique views and data.
The feeds page contains the uploaded images, caption, comments and a textarea to add new comments. The are two main components on this page is called the <ImageCard />
and <LoadingImageCard />
. This page inherits from the BaseFeedView
class.
It calls the fetchFeed
method from the PostStore
class before the page loads and displays the posts as soon as it is available.
-
ImageCard: The ImageCard component contains a complete feed item. A feed item contains Uploaded Image, Uploader Name, Image Caption, Comments, Upload Time, Comment Time. The ImageCard receieves the feed information from the Feeds page and distributes to three (3) other components what they are to display. The ImageCard is further subdivided into three more components:
-
ImageCardHeader: This component contains the name of the uploader and the time of the upload. The component receieves just these information from the ImageCard. It also contains a button to delete the particular feed. The delete button calls a function that was passed down to it from ImageCard component, so the deletion is actually done by ImageCard.
-
ImageCardContent: This component contains the uploaded image. It receives the image url from ImageCard
-
ImageCardFooter: This component contains a
<textarea>
for new comments, the caption of the uploaded image and It also contains a clickable link to view previous comments. The comments are opened in a dialog component called<CommentView>
.<CommentView>
displays all comments for a specific post.
-
-
LoadingImageCard: This component is used to display a loading card while the main content loads in the background.
This page is almost thesame as the Feeds page. The only difference is that it displays only the images uploaded by the currently logged in user that is viewing the page and the delete button is always visible.
It calls the fetchMyFeed
method from the PostStore
class before the page loads and displays the posts as soon as it is available.
The upload page is used to create a new post. It makes use of the react dropzone component for image selection. It also contains a textarea for image caption.
After the image has been selected, the user clicks the upload button which passes the selected image and caption to the PostStore
class which handles saving the post.
This is the page that logs the user out and returns them to the Homepage.
This class contains the initialization of the backend and also a general variable that contains the backend object. This variable is inherited by all the classes that extend this class.
This class contains authentication functionailty. It contains the following function
- setUserInfo: It stores the information of the currently logged in user in a variable called
userInfo
. Any page or component that wishes to get information on the currently logged in user gets it from theuserInfo
variable. - signUp: It contains implementation of the sign up process. All data has been checked by
Mobx-Forms
before being passed into this function by the Register Form component. - login: It contains login implementation.
- logout: It contains logout implementation
This class just creates an emitter as a singleton so that all classes can use thesame emitter object.
This class contains urls to the location of images. Since each component and page are in different locations relative to the folder containing the images. The ImageStore class acts as a one stop repository for all the images so components or pages don't have to resolve the url's themselves. It also helps keep the code from breaking incase an image location changes.
This class contains the implementation for the creation of new post, fetching of all posts, fetching of the logged in user's post, adding comment, fetching comments for a particular post and deleting post. The functions it contains are as follows:
- createPost: This function is used to create a new post. It delegates image upload to another class
UploadStore
which returns a promise back to it, the createPost method waits for this promise to resolve before it can create the post. - fetchFeed: This function fetches all the posts/feeds.
- fetchMyFeed: This function fetches all posts/feeds owned by the currently logged in user.
- fetchComments: This function fetches all comments for a particular post.
- addComment: This function adds a new comment for a particular post.
- deletePost: This function is used to delete a specific post.
This class contains the implementation for uploading a new image. The user's name is appended to the random string generated by the backend as the name of the file.
This class, just like the ImageStore is to help all pages and components deal with the location of Store/Repository files. It also helps reduce the amount of code written, because it exports all Stores as a single object and the user can pick from that page or component can pick from the object which store they require.
This file contains different constants used throughout the application. It contains the name of all the tables and other things.
This contains routing information for all the pages.
This contain form configuration for Login and Registration.
They both inherit from BaseFeedView and in their render method I called
render = ()=> super.render()
While they both implement their unique elements in getContent() function
//feed
getContent () {
super.getContent()
if (PostStore.isLoading) {
return <div className='text-center'>
<LoadingImageCard />
<h3> ...Please Wait </h3>
</div>
} else {
return (PostStore.feed.length < 1
? <h2>{Constants.placeholderStrings.feed}</h2>
: PostStore.feed.map((p, k) => <ImageCard post={p} key={k} />))
}
}
// profile
getContent () {
super.getContent()
if (PostStore.isLoading) {
return <div className='text-center'>
<LoadingImageCard />
<h3> ...Please Wait </h3>
</div>
} else {
return (PostStore.feed.length < 1
? <h2>{Constants.placeholderStrings.feed}</h2>
: PostStore.feed.map((p, k) =>
<ImageCard post={p} key={k} showDelete />))
}
}
The stores are singletons and as such the setUserInfo function gets called just once, when the user logs out the view doesn't get updated and the user is still assumed logged in.
constructor () {
super()
/* Authstore is a singleton and as such setUserInfo is being called just once. So the new user data isn't getting updated. Calling from this parent constructor which all views inherit makes sure the function gets called each time and the current user info is valid
*/
AuthStore.setUserInfo()
}
In the constructor of BaseView I called the setUserInfo method so that each time the view changes it also gets called.
I used es6 destructuring to extract data from the post item that was passed to ImageCard
const rawPost = this.props.post
const { user, createdAt, files, caption, comments } = rawPost.attributes
- The app uses React Router for routing, TimeAgo React for up to the second update of post Time, antD as frontend React Framework and PropTypes for prop information.
- FbEmitter is used to emit events from the store when the app needs to show and error or make a page change
- React-S-Alert is used to display notifications
- MobxForms is used for validation