Skip to content

Instantly share code, notes, and snippets.

@gate3
Last active April 26, 2019 10:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gate3/bc2c0a82447848f822bcf74cfc57966a to your computer and use it in GitHub Desktop.
Save gate3/bc2c0a82447848f822bcf74cfc57966a to your computer and use it in GitHub Desktop.
COOKPAD WEB INTERVIEW TEST

Picture Sharing App for Cookpad Programming Interview

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.

Scope of work


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

ScreenShots


Upload Page

Upload Page

Comments Page

Comment Page

Feed (Visitor)

Visitor Feed

Feed (User)

User Feed

Login

Login

Register

Register

Profile

Profile

Technologies Used


  • 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

Implementation


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

Implementation Analysis

MasterLayout


This layout component defines the structure of the page and seperates the entire page into three different parts. The Header, Content and Footer.

StaticPageLayout


This layout component does not contain the navigation bar. The homepage is the only page making use of this layout.

PageLayout


This layout component contains the navigation bar and is used by all other pages except the homepage.

BaseView


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.

NavBar


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

Homepage


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.

BaseFeedView


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.

Feeds


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.

Profile


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.

Upload


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.

Logout


This is the page that logs the user out and returns them to the Homepage.

BaseStore


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.

AuthStore


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 the userInfo 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

EmitterStore


This class just creates an emitter as a singleton so that all classes can use thesame emitter object.

ImageStore


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.

PostStore


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.

UploadStore


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.

StoreManager


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.

Constants


This file contains different constants used throughout the application. It contains the name of all the tables and other things.

Index


This contains routing information for all the pages.

MobxForms


This contain form configuration for Login and Registration.

Code I'm Proud Of

Feed and Profile

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 />))
      }
}

BaseView

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.

ImageCard

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

Other Information

  • 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
@import '~antd/dist/antd.css';
html, body {
height: 100%;
font-size: inherit;
}
div.ant-layout-content{
background: #fff;
}
div.large-bg{
/*background: url(assets/bg/bg1.jpeg) no-repeat center center fixed;*/
background-repeat: no-repeat!important;
background-position: center center!important;
background-attachment: fixed!important;
-webkit-background-size: cover!important;
-moz-background-size: cover!important;
-o-background-size: cover!important;
background-size: cover!important;
height: 100vh;
}
.tab-container{
padding: 20px;
background: #fff;
position: absolute;
bottom: 30%;
}
.container{
padding: 80px;
}
.header{
background: #fff;
border-bottom: solid #f1f1f1;
padding-bottom: 20px;
height: 70px;
}
.large-icon{
font-size: 30px;
margin: 0px 10px;
}
.image-card-header{
border-bottom:solid thin #ccc;
padding: 15px;
}
.image-card{
max-width: 650px;
width: 650px!important;
font-size: 16px;
max-height: 1000px;
height: 760px;
margin-bottom: 50px;
}
.ant-card-body{
padding: 5px 0px!important;
}
.feed-container{
text-align: -webkit-center;
}
img.avatar{
height: 50px;
width: 50px;
border-radius: 50%;
}
.image-card-content{
height: 500px;
position: relative;
}
.image-card-footer{
padding: 20px;
}
.overlay-card-image{
position: absolute;
bottom: 0px;
background-color: rgba(0,0,0,0.7);
width:100%;
padding: 10px;
}
.overlay-card-image h3{
color: #fff;
}
.comment-textarea{
width: 100%;
height: 77px;
border:none;
outline: none;
padding: 10px;
border-top:solid thin #f1f1f1;
font-size: large;
font-weight: 700;
}
p.error{
color:red !important;
}
.ant-row.ant-form-item{
margin-bottom: 0px;
}
.pc-btn-lg{
width:100%!important;
}
.navMenu{
padding: 25px!important;
outline: none;
position: relative;
list-style-type: none;
padding: 0;
margin: 0;
text-align: left;
background-color: #fff;
border-radius: 4px;
-webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
background-clip: padding-box;
}
.navMenu a{
font-size: 20px;
padding: 25px;
width: 100%;
color: #000;
display: block;
}
.filepicker.dropzone.dz-clickable{
padding: 50px!important;
}
img.post-image{
max-width: 100%;
max-height: 100%;
min-width: 100%;
min-height: 100%;
}
.spinner{
position: absolute;
top: 50%;
left: 50%;
}
.time-text{
color: #ccc;
font-size: small;
}
.pc-tmp-btn{
background: #108ee9;
color: #fff;
padding: 12px;
border-radius: 5px;
}
.text-center{
text-align: center;
}
import React, { Component } from 'react'
import {StaticPageLayout} from './pages/layouts'
import Login from './pages/auth/Login'
import Register from './pages/auth/Register'
import Constants from './stores/Constants'
import EmitterStore from './stores/EmitterStore'
import PropTypes from 'prop-types'
import { Row, Col, Tabs } from 'antd'
import { NavLink } from 'react-router-dom'
import {AuthStore} from './StoreManager'
import BaseView from './BaseView'
const TabPane = Tabs.TabPane
class App extends BaseView {
constructor () {
super()
this.successToken = ''
}
/**
* Upon successful login or registration a success event is emitted and the current view is changed to display the feeds page.
*/
componentWillMount () {
this.successToken = EmitterStore.emitterObject.addListener(Constants.eventListeners.auth.success, (s) => {
this.context.router.history.push('/feed')
})
}
componentWillUnmount () {
if (this.successToken !== '') {
this.successToken.remove()
}
}
callback (k) { // tabs callback
}
/**
* This method is used to render the login/register tab depending on if the user is logged in or out.
* <Login /> is the component containing the login form
* <Register /> is the component containing the registration form
*/
renderViewOnLoginCondition () {
if (AuthStore.userInfo != null) {
return <div style={{ marginBottom: '10px', textAlign: 'center' }}>
<NavLink to='/feed' className='pc-tmp-btn'>
View My Feed
</NavLink>
</div>
} else {
return <div>
<div style={{ marginBottom: '10px', textAlign: 'center' }}>
<NavLink to='/feed' className='pc-tmp-btn'>
Take A Look Around
</NavLink>
</div>
<Tabs defaultActiveKey='1' onChange={this.callback.bind(this)}>
<TabPane tab='Login' key='1'>
<Login />
</TabPane>
<TabPane tab='Register' key='2'>
<Register />
</TabPane>
</Tabs>
</div>
}
}
render () {
return (
<StaticPageLayout>
<Row type='flex' align='middle'>
<Col span={6} offset={16} className='tab-container'>
<div style={{ textAlign: 'center' }}>
<h1>PicShare</h1>
<h3>Share pictures with thousands of people and see what they think.</h3>
<br />
</div>
{this.renderViewOnLoginCondition()}
</Col>
</Row>
</StaticPageLayout>
)
}
}
App.contextTypes = {
router: PropTypes.object
}
export default App
import BaseStore from './BaseStore'
import { action, extendObservable, runInAction } from 'mobx'
import Emitter from './EmitterStore'
import Constants from './Constants'
class AuthStore extends BaseStore {
constructor () {
super()
extendObservable(this, {
isLoading: false,
signUp: action(this.signUp),
login: action(this.login),
userInfo: {},
setUserInfo: action(this.setUserInfo)
})
this.setUserInfo()
}
setUserInfo () {
this.userInfo = this.parse.User.current()
}
signUp ({full_name, email, password}) {
this.isLoading = true
const userObject = new this.parse.User()
userObject.set('username', email)
userObject.set('password', password)
userObject.set('full_name', full_name)
userObject.set('email', email)
userObject.signUp().then(
(s) => {
runInAction(() => {
this.isLoading = false
Emitter.emitterObject.emit(Constants.eventListeners.auth.success, s)
})
},
(e) => {
runInAction(() => {
this.isLoading = false
Emitter.emitterObject.emit(Constants.eventListeners.auth.error, e)
})
}
)
}
login ({ email, password }) {
this.isLoading = true
this.parse.User.logIn('doyinolarewaju@gmail.com', 'holdme').then(
(s) => {
Emitter.emitterObject.emit(Constants.eventListeners.auth.success, s)
},
(e) => {
Emitter.emitterObject.emit(Constants.eventListeners.auth.error, e)
}
)
}
logout () {
this.parse.User.logOut().then(
(s) => {
Emitter.emitterObject.emit(Constants.eventListeners.logout.success, s)
},
(e) => {
Emitter.emitterObject.emit(Constants.eventListeners.logout.success, e)
}
)
}
}
export default new AuthStore()
import React, { Component } from 'react'
import {PageLayout} from './layouts'
import { Row, Col } from 'antd'
import BaseView from '../BaseView'
class BaseFeedView extends BaseView {
constructor (viewType) {
super()
}
getContent () {
}
render () {
return (
<PageLayout>
<Row type='flex' justify='center'>
<Col span={12}>
{this.getContent()}
</Col>
</Row>
</PageLayout>
)
}
}
export default BaseFeedView
import { useStrict } from 'mobx'
import Constants from './Constants'
import Parse from 'parse'
useStrict(true)
export default class BaseStore {
constructor () {
Parse.initialize(Constants.parseCredentials.appId, 'yD7jQ6XF6w6qngaEEVwTql0yWXuKH6ksfQzBxyXd')
Parse.serverURL = Constants.parseCredentials.parseUrl
this.parse = Parse
}
}
import {Component} from 'react'
import { AuthStore } from './StoreManager'
export default class BaseView extends Component {
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()
}
}
import React,{Component} from 'react';
import './CommentViewer.css';
import Rodal from 'rodal'
import 'rodal/lib/rodal.css'
import {PostStore} from '../../StoreManager'
import { observer } from 'mobx-react'
import { Spin } from 'antd'
import TimeAgo from 'timeago-react'
const CommentViewer = observer(
class CommentViewer extends Component {
constructor () {
super()
this.state = {
modalVisible: false
}
}
// I don't want the component fetching comments as soon as it gets loaded, so i will check if its visible or not
componentWillReceiveProps(nextProps) {
if (nextProps.modalVisible){
this.toggleModal()
PostStore.fetchComments(this.props.commentRelation)
}
}
// toggle modal state
toggleModal () {
this.setState({
modalVisible: !this.state.modalVisible
})
}
// Don't want a messy render method
getContent () {
if (PostStore.isCommentLoading) {// while it's loading show a nice spinner
return <div className='spinner'>
<Spin size="large" />
</div>
}else{// after its done loading
return <div style={{ padding:'20px' }}>
<h3>
{
(PostStore.comments.length < 1
? 'No Comments yet'
: PostStore.comments.map((d,k)=>{
const {user, comment} = d.attributes
return <p key={k}>
<b>{user.attributes.full_name}</b> {comment} &nbsp;
<small className='time-text' >
<TimeAgo
datetime={d.createdAt}
/>
</small>
</p>
}
) )
}
</h3>
</div>
}
}
render = () => (
<Rodal visible={this.state.modalVisible}
width={600} height={600}
onClose={this.toggleModal.bind(this)}
style={{ padding:'0px' }}>
{this.getContent()}
</Rodal>
)
}
)
export default CommentViewer
const placeholderStrings = {
feed: 'No new posts have been created!'
}
const parseCredentials = {
appId: 'Llzyx0LXvL2WsGsJC7q3j3RWdnDVoB5aXpO9dF2Y',
masterKey: 'RcfjAWE4FIov9clRuKd7wLuO0ZkKpHDLnNGZ6gJe',
parseUrl: 'https://parseapi.back4app.com/'
}
const eventListeners = {
auth: {
success: 'auth:success',
signUpError: 'auth:signup:error',
loginError: 'auth:login:error'
},
logout: {
success: 'logout:success',
error: 'logout:error'
},
upload: {
success: 'upload:success',
error: 'upload:error'
}
}
const tables = {
posts: 'posts',
comments: 'comments',
user: 'userInfo'
}
export default {
placeholderStrings,
parseCredentials,
eventListeners,
tables
}
import { EventEmitter } from 'fbemitter'
class EmitterStore {
constructor () {
this.emitterObject = new EventEmitter()
}
getEmitterObject () {
return this.emitterObject
}
}
var store = new EmitterStore()
export default store
import React from 'react'
import { ImageCard, LoadingImageCard } from '../components/ImageCard'
import { Constants, PostStore } from '../StoreManager'
import { observer } from 'mobx-react'
import BaseFeedView from './BaseFeedView'
const Feed = observer(
class Feed extends BaseFeedView {
componentWillMount () {
PostStore.fetchFeed()
}
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} />))
}
}
render = ()=> super.render()
}
)
export default Feed
import React, {Component} from 'react'
import './ImageCard.css'
import { Card } from 'antd'
import ImageCardHeader from './ImageCardHeader'
import ImageCardContent from './ImageCardContent'
import ImageCardFooter from './ImageCardFooter'
import {PostStore} from '../../StoreManager'
class ImageCard extends Component {
addCommentHandler (comment) {
PostStore.addComment(this.props.post, comment)
}
deletePost () {
PostStore.deletePost(this.props.post)
}
render () {
const rawPost = this.props.post
const { user, createdAt, files, caption, comments } = rawPost.attributes
return (
<Card className='image-card'>
<ImageCardHeader user={user.attributes}
time={createdAt}
showDelete={this.props.showDelete}
deleteFunc={this.deletePost.bind(this)} />
<ImageCardContent images={files} />
<ImageCardFooter commentRelation={comments} caption={caption}
createComment={this.addCommentHandler.bind(this)} />
</Card>
)
}
}
export default ImageCard
import React,{Component} from 'react'
import './ImageCard.css'
import PropTypes from 'prop-types'
class ImageCardContent extends Component {
constructor(){
super()
this.state = {
image: null
}
}
render = () => {
return <div className='image-card-content'>
<img className='post-image'
src={this.props.images[0]._url}
alt='Post'
/>
&nbsp;
</div>
}
}
ImageCardContent.PropTypes = {
image: PropTypes.string.isRequired
}
export default ImageCardContent
import React,{Component} from 'react'
import './ImageCard.css'
import CommentView from '../CommentViewer'
import { AuthStore } from '../../StoreManager'
import { observer } from 'mobx-react'
const ImageCardFooter = observer(
class ImageCardFooter extends Component {
constructor(){
super()
this.state = {
modalVisible:false
}
this.commentText = ''
}
saveComment (evt) { // on enter key press, send the comment to parent component to save
if(evt.key === 'Enter' && this.commentText.value !== ''){// on enter key press and the value isnt empty
this.props.createComment(this.commentText.value)
this.commentText.value = ''
this.commentText.blur()
}
}
showComments () { // control the modal
this.setState({
modalVisible:true
})
}
renderCommentTextBox () { // conditionally show comment textbox
if (AuthStore.userInfo != null){ // if a user is logged in show it
return <textarea placeholder='Add your comment'
ref={(input)=>this.commentText = input}
onKeyPress={this.saveComment.bind(this)}
className='comment-textarea'>
</textarea>
} else { // if no user is logged in don't show it
return null
}
}
render = () => (
<div>
<div className='image-card-footer'>
<a onClick={()=> this.showComments()}> View all comments </a>
<h3>{this.props.caption}</h3>
</div>
{this.renderCommentTextBox()}
<CommentView commentRelation={this.props.commentRelation}
modalVisible={this.state.modalVisible} />
</div>
)
}
)
export default ImageCardFooter
import React,{Component} from 'react'
import './ImageCard.css'
import { Row, Col, Icon } from 'antd'
import PropTypes from 'prop-types'
import { ImageStore } from '../../StoreManager'
import TimeAgo from 'timeago-react'
class ImageCardHeader extends Component {
renderDelete () {
if (this.props.showDelete){
return <a onClick={()=>this.props.deleteFunc()}>
<Icon type='delete' />
</a>
} else {
return null
}
}
render = () => {
const {displayImageUrl, full_name} = this.props.user
return <Row type='flex' align='middle' className='image-card-header'>
<Col span={12} push={1}>
<Row type='flex'>
<img src={ displayImageUrl || ImageStore.Avatar }
className='avatar' alt='user'/>
<Col>
<b>{full_name}</b>
<br />
<TimeAgo
datetime={this.props.time}
/>
</Col>
</Row>
</Col>
<Col span={2} push={10}>
{this.renderDelete()}
</Col>
</Row>
}
}
ImageCardHeader.PropTypes = {
user: PropTypes.object.isRequired,
time: PropTypes.string.isRequired
}
export default ImageCardHeader
import bg1 from '../assets/bg/bg1.jpeg'
import bg2 from '../assets/bg/bg2.jpeg'
import bg3 from '../assets/bg/bg3.jpeg'
import bg4 from '../assets/bg/bg4.jpeg'
import Avatar from '../assets/user_avatar.png'
export default {
bg1,
bg2,
bg3,
bg4,
Avatar
}
import React from 'react'
import ReactDOM from 'react-dom'
import './App.css'
import {
BrowserRouter as Router,
Route,
Switch
} from 'react-router-dom'
import App from './App'
import Logout from './Logout'
import Feed from './pages/Feed'
import Profile from './pages/Profile'
import Upload from './pages/Upload'
import registerServiceWorker from './registerServiceWorker'
import Alert from 'react-s-alert'
import 'react-s-alert/dist/s-alert-default.css'
import 'react-s-alert/dist/s-alert-css-effects/slide.css'
import createBrowserHistory from 'history/createBrowserHistory'
const history = createBrowserHistory()
ReactDOM.render(
<div>
<Alert stack={{limit: 3}} />
<Router history={history}>
<Switch>
<Route exact path='/' render={() => <App />} />
<Route exact path='/feed' render={() => <Feed />} />
<Route exact path='/upload' render={() => <Upload />} />
<Route exact path='/profile' render={() => <Profile />} />
<Route exact path='/logout' render={() => <Logout />} />
</Switch>
</Router>
</div>
,
document.getElementById('root')
)
registerServiceWorker()
import React, {Component} from 'react'
import './ImageCard.css'
import { Card } from 'antd'
class LoadingImageCard extends Component {
render () {
return (
<Card loading />
)
}
}
export default LoadingImageCard
import React, {Component} from 'react'
import './Login.css'
import { observer } from 'mobx-react'
import MobxForm from '../MobxForms/MobxLoginForm'
import { Form, Icon, Input, Button, Checkbox } from 'antd';
import { AuthStore } from '../../../StoreManager'
const FormItem = Form.Item;
const Login = (observer(
class Login extends Component {
render = () => {
const form = MobxForm
return <Form className="login-form" onSubmit={form.onSubmit}>
<FormItem>
<Input {...form.$('email').bind()}
prefix={<Icon type="mail" style={{ fontSize: 13 }} />}
placeholder="Email" />
<p className='error'>{form.$('email').error}</p>
</FormItem>
<FormItem>
<Input {...form.$('password').bind()}
prefix={<Icon type="lock" style={{ fontSize: 13 }} />} type="password" placeholder="Password" />
<p className='error'>{form.$('password').error}</p>
</FormItem>
<FormItem>
<Checkbox>Remember me</Checkbox>
<br />
<Button type="primary"
loading={AuthStore.isLoading}
htmlType="submit"
className="login-form-button pc-btn-lg">
Log in
</Button>
</FormItem>
</Form>
}
}
))
export default Login
import React,{Component} from 'react'
import AuthStore from './stores/AuthStore'
import PropTypes from 'prop-types'
import EmitterStore from './stores/EmitterStore'
import Constants from './stores/Constants'
export default class Logout extends Component{
componentWillMount() {
AuthStore.logout()
EmitterStore.getEmitterObject().addListener(Constants.eventListeners.logout.success,
(s)=>this.context.router.history.push('/')
)
}
render = () => (
<div className='spinner'>
<h1>PicShare</h1>
<h3>Please wait...</h3>
</div>
)
}
Logout.contextTypes = {
router: PropTypes.object
}
import React, { Component } from 'react'
import {NavMenu} from '../../components/NavBar'
import { Layout } from 'antd'
const { Header, Content, Footer } = Layout
class MasterLayout extends Component {
render () {
const renderHeader = () => {
if (this.props.isLoggedIn) {
return <Header className='header'>
<NavMenu />
</Header>
}
return null
}
const renderFooter = () => {
if (!this.props.isLoggedIn) {
return <Footer style={{ textAlign: 'center' }}>
PicShare © 2017
</Footer>
}
return null
}
return (
<Layout className='layout'>
{renderHeader()}
<Content>
{this.props.children}
</Content>
{renderFooter()}
</Layout>
)
}
}
export default MasterLayout
import React, {Component} from 'react'
import './NavBar.css'
import { NavLink } from 'react-router-dom'
import { Icon, Dropdown, Row, Col } from 'antd'
import { AuthStore } from '../../StoreManager'
import { observer } from 'mobx-react'
const NavMenu = observer(
class NavMenu extends Component {
getContent () {// if the user is loggedin
if (AuthStore.userInfo != null) {// the menu items
const menu = <div className='navMenu'>
<NavLink to='/profile' className='navLink'>Profile</NavLink>
<NavLink to='/logout' className='navLink'>Logout</NavLink>
</div>
return <div style={{ float: 'right', padding: '10px' }}>
<NavLink to='/feed'>
<Icon type="bars" className='large-icon' />
</NavLink>
<NavLink to='/upload'>
<Icon type='cloud-upload' className='large-icon' />
</NavLink>
<Dropdown overlay={menu} trigger={['click']}>
<a className='ant-dropdown-link'>
<Icon type='user' className='large-icon' />
</a>
</Dropdown>
</div>
}else{// if the user is logged out
return <div style={{ float: 'right', padding: '10px' }}>
<NavLink to='/' className='pc-tmp-btn'>
Get Started
</NavLink>
</div>
}
}
render = () => (
<Row type='flex' align='middle'>
<Col span={8} push={1}>
<NavLink to='/'> <h2> PicShare </h2> </NavLink>
</Col>
<Col span={4} push={12}>
{this.getContent()}
</Col>
</Row>
)
}
)
export default NavMenu
import Alert from 'react-s-alert'
class NotificationStore {
showInfo (msg) {
Alert.info(msg, {
effect: 'slide',
position: 'top-right',
timeout: 6000,
offset: 100
})
}
showError (msg) {
Alert.info(msg, {
effect: 'slide',
position: 'top-right',
timeout: 6000,
offset: 100
})
}
}
const notif = new NotificationStore()
export default notif
import React, { Component } from 'react'
import MasterLayout from './MasterLayout'
class PageLayout extends Component {
render () {
return (
<MasterLayout isLoggedIn>
<div className='container'>
{this.props.children}
</div>
</MasterLayout>
)
}
}
export default PageLayout
import BaseStore from './BaseStore'
import Emitter from './EmitterStore'
import UploadStore from './UploadStore'
import Constants from './Constants'
import AuthStore from './AuthStore'
import { action, extendObservable, runInAction } from 'mobx'
import NotificationStore from './NotificationStore'
class PostStore extends BaseStore {
constructor () {
super()
extendObservable(this, {
isLoading: false,
initialLoad: true,
isCommentLoading: false,
commentCount: 0,
comments: [],
feed: [],
createPost: action(this.createPost),
fetchFeed: action(this.fetchFeed),
fetchComments: action(this.fetchComments),
addComment: action(this.addComment),
deletePost: action(this.deletePost)
})
}
createPost ({files, caption}) {
this.isLoading = true
const {id} = AuthStore.userInfo
UploadStore.upload(id, files).then(
(s) => {
let Post = this.parse.Object.extend(Constants.tables.posts)
let post = new Post()
post.set('files', s)
post.set('user', AuthStore.userInfo)
post.set('caption', caption)
post.save().then(
(suc) => {
runInAction(() => {
this.isLoading = false
Emitter.emitterObject.emit(Constants.eventListeners.upload.success, suc)
})
})
})
.catch((e) => {
runInAction(() => {
this.isLoading = false
Emitter.emitterObject.emit(Constants.eventListeners.upload.error, e)
})
})
}
fetchFeed () {
this.isLoading = true
let Posts = this.parse.Object.extend(Constants.tables.posts)
let posts = new this.parse.Query(Posts)
posts.descending('updatedAt')
posts.include('user')
posts.find().then(
(p) => {
runInAction(() => {
this.isLoading = false
this.feed = p
})
},
(e) => {
runInAction(() => {
this.isLoading = false
})
}
)
}
fetchMyFeed () {
this.isLoading = true
let Posts = this.parse.Object.extend(Constants.tables.posts)
let posts = new this.parse.Query(Posts)
posts.descending('updatedAt')
posts.include('user')
posts.equalTo('user', this.parse.User.current())
posts.find().then(
(p) => {
runInAction(() => {
this.isLoading = false
this.feed = p
})
},
(e) => {
runInAction(() => {
this.isLoading = false
})
}
)
}
fetchComments (commentRelation) {
this.isCommentLoading = true
const query = commentRelation.query()
query.include('user')
query.descending('createdAt')
query.find().then(
(s) => {
runInAction(() => {
this.comments = s
this.isCommentLoading = false
})
},
(e) => console.log(e)
)
}
addComment (postObject, comment) {
if (AuthStore.userInfo != null && postObject != null) {
const commentRelation = postObject.relation('comments')
let Comments = this.parse.Object.extend(Constants.tables.comments)
let commentObject = new Comments()
commentObject.set('comment', comment)
commentObject.set('user', AuthStore.userInfo)
commentObject.set('postId', postObject.id)
commentObject.save().then(
(c) => {
commentRelation.add(c)
postObject.save().then(
(s) => {
console.log(s)
NotificationStore.showInfo('Comment Added!')
},
(e) => console.log(e)
)
}
).catch(e => console.log(e))
} else {
// log the error that user doesnt exist
}
}
deletePost (postObject) {
// this.isLoading = true
postObject.destroy().then(
(s) => {
console.log(s)
runInAction(() => this.fetchMyFeed())
},
(e) => {
console.log(e)
}
)
}
}
const store = new PostStore()
export default store
import React from 'react'
import { ImageCard, LoadingImageCard } from '../components/ImageCard'
import { Constants, PostStore } from '../StoreManager'
import { observer } from 'mobx-react'
import BaseFeedView from './BaseFeedView'
const Profile = observer(
class Profile extends BaseFeedView {
componentWillMount () {
PostStore.fetchMyFeed()
}
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 />))
}
}
render = () => super.render()
}
)
export default Profile
import React,{Component} from 'react';
import './Register.css';
import { observer } from 'mobx-react'
import MobxRegister from '../MobxForms/MobxRegistrationForm'
import { AuthStore, EmitterStore, Constants } from '../../../StoreManager'
import { Form, Icon, Input, Button, Checkbox } from 'antd';
const FormItem = Form.Item;
const Register = (observer(
class Register extends Component {
constructor () {
super()
this.errorToken = this.successToken = ''
this.state = {
error: false
}
}
handleSubmit () {
const validationHandlers = {
onSuccess (form) {
AuthStore.signUp(form.values())
},
onError (form) {
form.invalidate('This is a generic error message!')
}
}
MobxRegister.submit({
onSuccess: validationHandlers.onSuccess,
onError: validationHandlers.onError
})
}
componentWillMount() {
this.errorToken = EmitterStore.emitterObject.addListener(Constants.eventListeners.auth.signUpError, (e)=>{
this.setState({
error: true
})
})
}
componentWillUnmount() {
this.errorToken.remove()
}
render = () => {
const form = MobxRegister
const showError = () =>{
if (this.state.error){
return <p className='error'>Email already exists!</p>
}
return null
}
return <div>
{showError()}
<Form onSubmit={form.onSubmit} className="login-form" method='post'>
<FormItem>
<Input {...form.$('full_name').bind()}
prefix={<Icon type="user" style={{ fontSize: 13 }} />}
placeholder="Full Name"/>
<p className='error'>{form.$('full_name').error}</p>
</FormItem>
<FormItem>
<Input {...form.$('email').bind()}
prefix={<Icon type="mail" style={{ fontSize: 13 }} />}
placeholder="Email" />
<p className='error'>{form.$('email').error}</p>
</FormItem>
<FormItem>
<Input {...form.$('password').bind()}
prefix={<Icon type="lock" style={{ fontSize: 13 }} />}
type="password"
placeholder="Password" />
<p className='error'>{form.$('password').error}</p>
</FormItem>
<FormItem>
<Input {...form.$('passwordConfirm').bind()}
prefix={<Icon type="lock" style={{ fontSize: 13 }} />}
type="password"
placeholder="Confirm Password" />
<p className='error'>{form.$('passwordConfirm').error}</p>
</FormItem>
<FormItem>
<Checkbox>Remember me</Checkbox>
<br />
<Button onClick={this.handleSubmit.bind(this)}
type="primary"
htmlType="submit" size='large'
loading={AuthStore.isLoading}
className="login-form-button pc-btn-lg">
Register
</Button>
</FormItem>
</Form>
</div>
}
}
))
const WrappedRegistrationForm = Form.create()(Register);
export default WrappedRegistrationForm
import React, { Component } from 'react'
import MasterLayout from './MasterLayout'
import ImageStore from '../../stores/ImageStore'
class StaticPageLayout extends Component {
render () {
const rand = Math.floor(Math.random() * 3)
const imgArray = [ImageStore.bg1, ImageStore.bg2, ImageStore.bg3, ImageStore.bg4]
return (
<MasterLayout>
<div className='large-bg' style={{ background: 'url(' + imgArray[rand] + ')' }}>
{this.props.children}
</div>
</MasterLayout>
)
}
}
export default StaticPageLayout
import AuthStore from './stores/AuthStore'
import Constants from './stores/Constants'
import EmitterStore from './stores/EmitterStore'
import ImageStore from './stores/ImageStore'
import PostStore from './stores/PostStore'
import NotificationStore from './stores/NotificationStore'
export {
AuthStore,
Constants,
EmitterStore,
ImageStore,
PostStore,
NotificationStore
}
import React, {Component} from 'react'
import { PostStore, EmitterStore, Constants } from '../StoreManager'
import {PageLayout} from './layouts'
import { Row, Col, Button } from 'antd'
import DropzoneComponent from 'react-dropzone-component'
import 'react-dropzone-component/styles/filepicker.css'
import 'dropzone/dist/dropzone.css'
import PropTypes from 'prop-types'
import { observer } from 'mobx-react'
import Alert from 'react-s-alert'
import BaseView from '../BaseView'
const Upload = (observer(
class Upload extends BaseView {
constructor () {
super()
this.dropzoneObject = null
this.caption = ''
this.state = {
messageDisplay: false,
disabledUpload: true
}
this.successToken = this.errorToken = ''
}
componentWillMount () {
EmitterStore.emitterObject.addListener(Constants.eventListeners.upload.success, (s) => {
this.context.router.history.push('/feed')
})
EmitterStore.emitterObject.addListener(Constants.eventListeners.upload.error, (e) => {
console.log(e)
})
}
uploadImage (dropzoneObject) {
if (dropzoneObject.files.length > 0) {
PostStore.createPost({files: dropzoneObject.files, caption: this.caption.value})
}
}
fileAdded (file) {
this.setState({
disabledUpload: false
})
}
fileRemoved (file) {
this.setState({
disabledUpload: true
})
}
render () {
const componentConfig = {
iconFiletypes: ['.jpg', '.png'],
showFiletypeIcon: true,
postUrl: 'no-url'
}
let eventHandlers = {
init: (drp) => this.dropzoneObject = drp,
addedfile: this.fileAdded.bind(this),
removedfile: this.fileRemoved.bind(this)
}
const djsConfig = { autoProcessQueue: false,
uploadMultiple: true,
addRemoveLinks: true
}
return (
<PageLayout>
<Row type='flex' justify='center'>
<Col span={12}>
<DropzoneComponent config={componentConfig}
eventHandlers={eventHandlers}
djsConfig={djsConfig} />
<textarea
placeholder='Add image caption'
className='comment-textarea'
style={{ marginTop: '20px' }}
ref={(input) => { this.caption = input }}
/>
<Button type='primary' loading={PostStore.isLoading}
disabled={this.state.disabledUpload || PostStore.isLoading}
onClick={this.uploadImage.bind(this, this.dropzoneObject)}
htmlType='submit'
className='login-form-button pc-btn-lg'
style={{ height: '50px' }}>
Upload
</Button>
</Col>
</Row>
<Alert stack={{limit: 3}} />
</PageLayout>
)
}
}
))
Upload.contextTypes = {
router: PropTypes.object
}
export default Upload
import BaseStore from './BaseStore'
class UploadStore extends BaseStore {
upload (name, files) {
let promise = []
for (let f of files) {
let parseFile = new this.parse.File(name, f)
promise.push(parseFile.save())
}
return this.parse.Promise.when(promise)
}
}
const store = new UploadStore()
export default store
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment