├── .gitignore ├── README.md ├── api ├── .gitignore ├── config.js ├── index.js ├── lib │ ├── cors │ │ └── index.js │ └── stream_utils │ │ └── index.js ├── package.json ├── processes.json └── routes │ ├── active.js │ ├── comments.js │ ├── contributions.js │ ├── explore.js │ ├── followers.js │ ├── following-activity.js │ ├── incoming-activity.js │ ├── index.js │ ├── likes.js │ ├── locations.js │ ├── searches.js │ ├── stats.js │ ├── trending.js │ ├── uploads.js │ └── users.js ├── app ├── .babelrc ├── .gitignore ├── LICENSE ├── app.js ├── bin │ └── www ├── config.js ├── docs │ ├── ast │ │ └── source │ │ │ ├── App.js.json │ │ │ ├── actions │ │ │ ├── Activity.js.json │ │ │ ├── App.js.json │ │ │ ├── Comments.js.json │ │ │ ├── Contributions.js.json │ │ │ ├── Explore.js.json │ │ │ ├── Header.js.json │ │ │ ├── Like.js.json │ │ │ ├── Location.js.json │ │ │ ├── Photo.js.json │ │ │ ├── Photos.js.json │ │ │ ├── Profile.js.json │ │ │ ├── Search.js.json │ │ │ ├── Stats.js.json │ │ │ ├── Trending.js.json │ │ │ ├── User.js.json │ │ │ └── index.js.json │ │ │ ├── components │ │ │ ├── Activity │ │ │ │ ├── Actor.js.json │ │ │ │ ├── Commented.js.json │ │ │ │ ├── Following.js.json │ │ │ │ ├── Liked.js.json │ │ │ │ └── index.js.json │ │ │ ├── Avatar │ │ │ │ └── index.js.json │ │ │ ├── BackButton │ │ │ │ └── index.js.json │ │ │ ├── Comment │ │ │ │ └── index.js.json │ │ │ ├── Header │ │ │ │ └── index.js.json │ │ │ ├── LikeButton │ │ │ │ └── index.js.json │ │ │ ├── Nav │ │ │ │ └── index.js.json │ │ │ ├── PhotoList │ │ │ │ ├── PhotoFooter.js.json │ │ │ │ ├── PhotoItem.js.json │ │ │ │ └── index.js.json │ │ │ ├── Tabs │ │ │ │ └── index.js.json │ │ │ ├── TimeAgo │ │ │ │ └── index.js.json │ │ │ └── index.js.json │ │ │ ├── main.js.json │ │ │ ├── reducers │ │ │ ├── Activity.js.json │ │ │ ├── App.js.json │ │ │ ├── Comments.js.json │ │ │ ├── Contributions.js.json │ │ │ ├── Explore.js.json │ │ │ ├── Header.js.json │ │ │ ├── Likes.js.json │ │ │ ├── Location.js.json │ │ │ ├── Pagination.js.json │ │ │ ├── Photo.js.json │ │ │ ├── Photos.js.json │ │ │ ├── Profile.js.json │ │ │ ├── Search.js.json │ │ │ ├── Stats.js.json │ │ │ ├── Tokens.js.json │ │ │ ├── Trending.js.json │ │ │ ├── User.js.json │ │ │ └── index.js.json │ │ │ ├── routes │ │ │ ├── Contributions │ │ │ │ ├── Contributions.js.json │ │ │ │ └── index.js.json │ │ │ ├── Explore │ │ │ │ ├── Explore.js.json │ │ │ │ └── index.js.json │ │ │ ├── FollowingActivity │ │ │ │ ├── FollowingActivity.js.json │ │ │ │ └── index.js.json │ │ │ ├── Home │ │ │ │ ├── Home.js.json │ │ │ │ ├── index.js.json │ │ │ │ └── routes │ │ │ │ │ └── Photo │ │ │ │ │ ├── Photo.js.json │ │ │ │ │ ├── components │ │ │ │ │ ├── PhotoComments.js.json │ │ │ │ │ └── PhotoMetadata.js.json │ │ │ │ │ └── index.js.json │ │ │ ├── Landing │ │ │ │ ├── Landing.js.json │ │ │ │ └── index.js.json │ │ │ ├── Location │ │ │ │ ├── Location.js.json │ │ │ │ └── index.js.json │ │ │ ├── Notifications │ │ │ │ ├── Notifications.js.json │ │ │ │ └── index.js.json │ │ │ ├── Profile │ │ │ │ ├── Profile.js.json │ │ │ │ ├── contributions │ │ │ │ │ └── index.js.json │ │ │ │ ├── index.js.json │ │ │ │ └── navigation │ │ │ │ │ └── index.js.json │ │ │ ├── Search │ │ │ │ ├── Search.js.json │ │ │ │ ├── components │ │ │ │ │ ├── Filters │ │ │ │ │ │ └── index.js.json │ │ │ │ │ └── index.js.json │ │ │ │ └── index.js.json │ │ │ ├── SearchResults │ │ │ │ ├── SearchResults.js.json │ │ │ │ └── index.js.json │ │ │ ├── Stats │ │ │ │ ├── Stats.js.json │ │ │ │ └── index.js.json │ │ │ ├── Trending │ │ │ │ ├── Trending.js.json │ │ │ │ └── index.js.json │ │ │ └── Upload │ │ │ │ ├── Upload.js.json │ │ │ │ └── index.js.json │ │ │ └── utils │ │ │ └── analytics.js.json │ ├── badge.svg │ ├── class │ │ └── modules │ │ │ ├── components │ │ │ ├── Activity │ │ │ │ ├── Actor.js~Actor.html │ │ │ │ ├── Commented.js~Commented.html │ │ │ │ ├── Commented.js~Following.html │ │ │ │ ├── Following.js~Following.html │ │ │ │ ├── Liked.js~Liked.html │ │ │ │ └── index.js~Item.html │ │ │ ├── Avatar │ │ │ │ └── index.js~Avatar.html │ │ │ ├── BackButton │ │ │ │ └── index.js~BackButton.html │ │ │ ├── Comment │ │ │ │ └── index.js~Comment.html │ │ │ ├── Header │ │ │ │ └── index.js~Header.html │ │ │ ├── LikeButton │ │ │ │ └── index.js~LikeButton.html │ │ │ ├── Nav │ │ │ │ └── index.js~Nav.html │ │ │ ├── PhotoList │ │ │ │ ├── PhotoFooter.js~PhotoFooter.html │ │ │ │ ├── PhotoItem.js~PhotoItem.html │ │ │ │ └── index.js~PhotoList.html │ │ │ ├── Tabs │ │ │ │ ├── index.js~Tab.html │ │ │ │ └── index.js~Tabs.html │ │ │ └── TimeAgo │ │ │ │ └── index.js~TimeAgo.html │ │ │ └── routes │ │ │ ├── Home │ │ │ └── routes │ │ │ │ └── Photo │ │ │ │ └── components │ │ │ │ ├── PhotoComments.js~PhotoPage.html │ │ │ │ └── PhotoMetadata.js~PhotoMetadata.html │ │ │ ├── Profile │ │ │ └── contributions │ │ │ │ └── index.js~Contributions.html │ │ │ └── Search │ │ │ └── components │ │ │ └── Filters │ │ │ └── index.js~Filters.html │ ├── coverage.json │ ├── css │ │ ├── prettify-tomorrow.css │ │ └── style.css │ ├── dump.json │ ├── file │ │ └── modules │ │ │ ├── App.js.html │ │ │ ├── actions │ │ │ ├── Activity.js.html │ │ │ ├── App.js.html │ │ │ ├── Comments.js.html │ │ │ ├── Contributions.js.html │ │ │ ├── Explore.js.html │ │ │ ├── Header.js.html │ │ │ ├── Like.js.html │ │ │ ├── Location.js.html │ │ │ ├── Photo.js.html │ │ │ ├── Photos.js.html │ │ │ ├── Profile.js.html │ │ │ ├── Search.js.html │ │ │ ├── Stats.js.html │ │ │ ├── Trending.js.html │ │ │ ├── User.js.html │ │ │ └── index.js.html │ │ │ ├── components │ │ │ ├── Activity │ │ │ │ ├── Actor.js.html │ │ │ │ ├── Commented.js.html │ │ │ │ ├── Following.js.html │ │ │ │ ├── Liked.js.html │ │ │ │ └── index.js.html │ │ │ ├── Avatar │ │ │ │ └── index.js.html │ │ │ ├── BackButton │ │ │ │ └── index.js.html │ │ │ ├── Comment │ │ │ │ └── index.js.html │ │ │ ├── Header │ │ │ │ └── index.js.html │ │ │ ├── LikeButton │ │ │ │ └── index.js.html │ │ │ ├── Nav │ │ │ │ └── index.js.html │ │ │ ├── PhotoList │ │ │ │ ├── PhotoFooter.js.html │ │ │ │ ├── PhotoItem.js.html │ │ │ │ └── index.js.html │ │ │ ├── Tabs │ │ │ │ └── index.js.html │ │ │ ├── TimeAgo │ │ │ │ └── index.js.html │ │ │ └── index.js.html │ │ │ ├── main.js.html │ │ │ ├── reducers │ │ │ ├── Activity.js.html │ │ │ ├── App.js.html │ │ │ ├── Comments.js.html │ │ │ ├── Contributions.js.html │ │ │ ├── Explore.js.html │ │ │ ├── Header.js.html │ │ │ ├── Likes.js.html │ │ │ ├── Location.js.html │ │ │ ├── Pagination.js.html │ │ │ ├── Photo.js.html │ │ │ ├── Photos.js.html │ │ │ ├── Profile.js.html │ │ │ ├── Search.js.html │ │ │ ├── Stats.js.html │ │ │ ├── Tokens.js.html │ │ │ ├── Trending.js.html │ │ │ ├── User.js.html │ │ │ └── index.js.html │ │ │ ├── routes │ │ │ ├── Contributions │ │ │ │ ├── Contributions.js.html │ │ │ │ └── index.js.html │ │ │ ├── Explore │ │ │ │ ├── Explore.js.html │ │ │ │ └── index.js.html │ │ │ ├── FollowingActivity │ │ │ │ ├── FollowingActivity.js.html │ │ │ │ └── index.js.html │ │ │ ├── Home │ │ │ │ ├── Home.js.html │ │ │ │ ├── index.js.html │ │ │ │ └── routes │ │ │ │ │ └── Photo │ │ │ │ │ ├── Photo.js.html │ │ │ │ │ ├── components │ │ │ │ │ ├── PhotoComments.js.html │ │ │ │ │ └── PhotoMetadata.js.html │ │ │ │ │ └── index.js.html │ │ │ ├── Landing │ │ │ │ ├── Landing.js.html │ │ │ │ └── index.js.html │ │ │ ├── Location │ │ │ │ ├── Location.js.html │ │ │ │ └── index.js.html │ │ │ ├── Notifications │ │ │ │ ├── Notifications.js.html │ │ │ │ └── index.js.html │ │ │ ├── Profile │ │ │ │ ├── Profile.js.html │ │ │ │ ├── contributions │ │ │ │ │ └── index.js.html │ │ │ │ ├── index.js.html │ │ │ │ └── navigation │ │ │ │ │ └── index.js.html │ │ │ ├── Search │ │ │ │ ├── Search.js.html │ │ │ │ ├── components │ │ │ │ │ ├── Filters │ │ │ │ │ │ └── index.js.html │ │ │ │ │ └── index.js.html │ │ │ │ └── index.js.html │ │ │ ├── SearchResults │ │ │ │ ├── SearchResults.js.html │ │ │ │ └── index.js.html │ │ │ ├── Stats │ │ │ │ ├── Stats.js.html │ │ │ │ └── index.js.html │ │ │ ├── Trending │ │ │ │ ├── Trending.js.html │ │ │ │ └── index.js.html │ │ │ └── Upload │ │ │ │ ├── Upload.js.html │ │ │ │ └── index.js.html │ │ │ └── utils │ │ │ └── analytics.js.html │ ├── function │ │ └── index.html │ ├── identifiers.html │ ├── image │ │ ├── badge.svg │ │ ├── github.png │ │ └── search.png │ ├── index.html │ ├── package.json │ ├── script │ │ ├── inherited-summary.js │ │ ├── inner-link.js │ │ ├── manual.js │ │ ├── patch-for-local.js │ │ ├── prettify │ │ │ ├── Apache-License-2.0.txt │ │ │ └── prettify.js │ │ ├── pretty-print.js │ │ ├── search.js │ │ ├── search_index.js │ │ └── test-summary.js │ ├── source.html │ └── variable │ │ └── index.html ├── esdoc.json ├── modules │ ├── App.js │ ├── actions │ │ ├── App.js │ │ ├── Comments.js │ │ ├── Contributions.js │ │ ├── Explore.js │ │ ├── FollowingActivity.js │ │ ├── Header.js │ │ ├── IncomingActivity.js │ │ ├── Like.js │ │ ├── Location.js │ │ ├── Photo.js │ │ ├── Photos.js │ │ ├── Profile.js │ │ ├── Search.js │ │ ├── Stats.js │ │ ├── Stream.js │ │ ├── Trending.js │ │ ├── User.js │ │ └── index.js │ ├── components │ │ ├── Activity │ │ │ ├── Actor.js │ │ │ ├── Commented.js │ │ │ ├── Following.js │ │ │ ├── Liked.js │ │ │ └── index.js │ │ ├── Avatar │ │ │ └── index.js │ │ ├── BackButton │ │ │ └── index.js │ │ ├── Comment │ │ │ └── index.js │ │ ├── Header │ │ │ └── index.js │ │ ├── LikeButton │ │ │ └── index.js │ │ ├── Nav │ │ │ └── index.js │ │ ├── PhotoList │ │ │ ├── PhotoFooter.js │ │ │ ├── PhotoItem.js │ │ │ └── index.js │ │ ├── Tabs │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── TimeAgo │ │ │ └── index.js │ │ └── index.js │ ├── main.js │ ├── reducers │ │ ├── App.js │ │ ├── Comments.js │ │ ├── Contributions.js │ │ ├── Explore.js │ │ ├── FollowingActivity.js │ │ ├── Header.js │ │ ├── IncomingActivity.js │ │ ├── Likes.js │ │ ├── Location.js │ │ ├── Navigation.js │ │ ├── Onboarding.js │ │ ├── Pagination.js │ │ ├── Photo.js │ │ ├── Photos.js │ │ ├── Profile.js │ │ ├── Search.js │ │ ├── Stats.js │ │ ├── Stream.js │ │ ├── Tokens.js │ │ ├── Trending.js │ │ ├── User.js │ │ └── index.js │ ├── routes │ │ ├── Contributions │ │ │ ├── Contributions.js │ │ │ └── index.js │ │ ├── Explore │ │ │ ├── Explore.js │ │ │ └── index.js │ │ ├── FollowingActivity │ │ │ ├── FollowingActivity.js │ │ │ └── index.js │ │ ├── Home │ │ │ ├── Home.js │ │ │ ├── index.js │ │ │ └── routes │ │ │ │ └── Photo │ │ │ │ ├── Photo.js │ │ │ │ ├── components │ │ │ │ ├── PhotoComments.js │ │ │ │ └── PhotoMetadata.js │ │ │ │ └── index.js │ │ ├── Landing │ │ │ ├── Landing.js │ │ │ └── index.js │ │ ├── Location │ │ │ ├── Location.js │ │ │ └── index.js │ │ ├── Notifications │ │ │ ├── Notifications.js │ │ │ └── index.js │ │ ├── Profile │ │ │ ├── Profile.js │ │ │ ├── contributions │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── navigation │ │ │ │ └── index.js │ │ ├── Search │ │ │ ├── Search.js │ │ │ ├── components │ │ │ │ ├── Filters │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── SearchResults │ │ │ ├── SearchResults.js │ │ │ └── index.js │ │ ├── Stats │ │ │ ├── Stats.js │ │ │ └── index.js │ │ ├── Trending │ │ │ ├── Trending.js │ │ │ └── index.js │ │ └── Upload │ │ │ ├── Upload.js │ │ │ └── index.js │ ├── style.css │ └── utils │ │ └── analytics.js ├── package.json ├── processes.json ├── public │ ├── css │ │ ├── styles.css │ │ └── styles.min.css │ ├── favicon.ico │ └── img │ │ ├── Background.png │ │ ├── add.svg │ │ ├── bell.svg │ │ ├── bg.png │ │ ├── landing.png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo@2x.png │ │ ├── map.svg │ │ ├── nav_icon.svg │ │ ├── point.png │ │ ├── point.svg │ │ └── search.svg ├── routes │ └── index.js ├── views │ ├── app.ejs │ ├── error.ejs │ └── index.ejs └── webpack.config.js ├── db └── cabin.sql ├── env.sh ├── install.md ├── terraform └── do │ └── cabin │ ├── files │ ├── cabin-web-nginx.conf │ ├── cabin_mysql_init.sh │ ├── index.html │ ├── motd │ └── motd.sh │ ├── main.tf │ ├── outputs.tf │ ├── templates │ ├── env.tpl │ ├── processes.tpl │ └── web.tpl │ └── variables.tf └── www ├── app.js ├── bin └── www ├── config.js ├── package.json ├── processes.json ├── public ├── css │ ├── animate.css │ ├── core.css │ ├── demo.css │ ├── kube.css │ └── style.css ├── favicon.ico ├── img │ ├── Logo.svg │ ├── algolialogo.svg │ ├── body-bg-angle.svg │ ├── bonus.png │ ├── bonus.svg │ ├── chevron.svg │ ├── digitaloceanlogo.svg │ ├── end-bg.png │ ├── footer-bg.svg │ ├── frame.png │ ├── github.svg │ ├── gplus.svg │ ├── hero-diagonal.png │ ├── hero-radial.svg │ ├── hero-react-redux.svg │ ├── hero_bg.svg │ ├── hero_cabin.png │ ├── imgix-small.svg │ ├── imgix_small_logo.png │ ├── imgix_small_logo.svg │ ├── imgixlogo.png │ ├── imgixlogo.svg │ ├── imgixlogosmall.png │ ├── keenlogo.svg │ ├── label-bg.svg │ ├── li-bg.svg │ ├── mapboxlogo.svg │ ├── og_cabin.png │ ├── partners-bg.svg │ ├── partners-flare.png │ ├── phone.png │ ├── reactlogo.svg │ ├── reduxlogo.svg │ ├── sketchapp.png │ ├── streamlogo.svg │ ├── topic-art-2.png │ ├── topic-art-3.png │ ├── topics-art-1.png │ ├── topo.jpg │ ├── twitter.svg │ ├── twittercta.svg │ └── wrap-bg.jpg └── js │ ├── cabin.js │ ├── power.js │ └── scotchPanels.js ├── routes └── index.js └── views ├── demo.ejs ├── error.ejs └── index.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | node_modules 27 | 28 | # Optional npm cache directory 29 | .npm 30 | 31 | # Optional REPL history 32 | .node_repl_history 33 | 34 | # OS 35 | .DS_Store 36 | 37 | env 38 | 39 | app/public/js/bundle.js 40 | app/public/js/app.js 41 | 42 | # idea 43 | .idea/ 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN-Project 2 | 3 | This is a MERN project using the following technologies: 4 | - [React](https://facebook.github.io/react/) and [React Router](https://reacttraining.com/react-router/) for the frontend 5 | - [Express](http://expressjs.com/) and [Mongoose](http://mongoosejs.com/) for the backend 6 | - [Sass](http://sass-lang.com/) for styles (using the SCSS syntax) 7 | - [Webpack](https://webpack.github.io/) for compilation 8 | 9 | 10 | ## Requirements 11 | 12 | - [Node.js](https://nodejs.org/en/) 6+ 13 | 14 | ```shell 15 | npm install 16 | ``` 17 | 18 | 19 | ## Running 20 | 21 | Make sure to add a `config.js` file in the `config` folder. See the example there for more details. 22 | 23 | Production mode: 24 | 25 | ```shell 26 | npm start 27 | ``` 28 | 29 | Development (Webpack dev server) mode: 30 | 31 | ```shell 32 | npm run start:dev 33 | ``` 34 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchoeup/reactjs-redux-nodejs/4978db5c405e378c0a2433f2e121f8891daa4372/api/.gitignore -------------------------------------------------------------------------------- /api/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Config 5 | */ 6 | module.exports = { 7 | name: 'GetStream.io - React Example App', 8 | version: '1.0.0', 9 | env: process.env.NODE_ENV || 'development', 10 | port: process.env.PORT || 8000, 11 | jwt: { 12 | secret: process.env.JWT_SECRET, 13 | }, 14 | db: { 15 | name: 'cabin', 16 | username: process.env.DB_USERNAME, 17 | password: process.env.DB_PASSWORD, 18 | host: process.env.DB_HOST, 19 | port: process.env.DB_PORT, 20 | }, 21 | mapbox: { 22 | accessToken: process.env.MAPBOX_ACCESS_TOKEN, 23 | }, 24 | s3: { 25 | key: process.env.S3_KEY, 26 | secret: process.env.S3_SECRET, 27 | bucket: process.env.S3_BUCKET, 28 | }, 29 | stream: { 30 | appId: process.env.STREAM_APP_ID, 31 | key: process.env.STREAM_KEY, 32 | secret: process.env.STREAM_SECRET, 33 | }, 34 | algolia: { 35 | appId: process.env.ALGOLIA_APP_ID, 36 | searchOnlyKey: process.env.ALGOLIA_SEARCH_ONLY_KEY, 37 | apiKey: process.env.ALGOLIA_API_KEY, 38 | }, 39 | keen: { 40 | projectId: process.env.KEEN_PROJECT_ID, 41 | writeKey: process.env.KEEN_WRITE_KEY, 42 | readKey: process.env.KEEN_READ_KEY, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module Dependencies 5 | */ 6 | var config = require('./config'), 7 | bunyan = require('bunyan'), 8 | winston = require('winston'), 9 | bunyanWinston = require('bunyan-winston-adapter'), 10 | mysql = require('mysql'), 11 | jwt = require('restify-jwt'), 12 | Mail = require('winston-mail').Mail, 13 | Sentry = require('winston-sentry'); 14 | 15 | /** 16 | * Global Dependencies 17 | */ 18 | global.__base = __dirname + '/'; 19 | global.config = require('./config.js'); 20 | global.restify = require('restify'); 21 | 22 | /** 23 | * Transports (Logging) 24 | */ 25 | var transports = [ 26 | new winston.transports.Console({ 27 | level: 'info', 28 | timestamp: function() { 29 | return new Date().toString(); 30 | }, 31 | json: true 32 | }) 33 | ]; 34 | 35 | /** 36 | * Sentry Transport (Logging) 37 | */ 38 | if (process.env.SENTRY) { 39 | new winston.transports.Console({ level: 'silly' }), 40 | new Sentry({ 41 | patchGlobal: true, 42 | dsn: process.env.SENTRY, 43 | }) 44 | } 45 | 46 | /** 47 | * Logging 48 | */ 49 | global.log = new winston.Logger({ 50 | transports: transports 51 | }); 52 | 53 | /** 54 | * Initialize Server 55 | */ 56 | global.server = restify.createServer({ 57 | name : config.name, 58 | version : config.version, 59 | log : bunyanWinston.createAdapter(log), 60 | }); 61 | 62 | /** 63 | * Middleware 64 | */ 65 | server.use(restify.bodyParser()); 66 | server.use(restify.acceptParser(server.acceptable)); 67 | server.use(restify.authorizationParser()); 68 | server.use(restify.queryParser({ mapParams: true })); 69 | server.pre(require('./lib/cors')()); 70 | server.use(restify.fullResponse()); 71 | server.use(jwt({ secret: config.jwt.secret }).unless({ 72 | path: ['/users'] 73 | })); 74 | 75 | /** 76 | * Initialize MySQL Connection 77 | */ 78 | global.db = mysql.createConnection({ 79 | host : config.db.host, 80 | user : config.db.username, 81 | password : config.db.password, 82 | database : config.db.name, 83 | timezone: 'UTC' 84 | }); 85 | db.connect(); 86 | 87 | db.query(` 88 | SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" 89 | `) 90 | 91 | /** 92 | * Boot 93 | */ 94 | server.listen(config.port, function () { 95 | require('./routes'); 96 | log.info( 97 | '%s v%s ready to accept connections on port listening on port %s in %s environment', 98 | server.name, 99 | config.version, 100 | config.port, 101 | config.env 102 | ); 103 | }); 104 | -------------------------------------------------------------------------------- /api/lib/cors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function cors(options) { 4 | 5 | const defaultAllowHeaders = ['Authorization', 'Content-Type']; 6 | const defaultAllowMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]; 7 | 8 | const opts = Object.assign({}, { 9 | allowHeaders: defaultAllowHeaders, 10 | allowMethods: defaultAllowMethods, 11 | allowOrigins: null, 12 | allowCreds: true, 13 | }, options || {}); 14 | 15 | const setHeader = (req, res, methods) => { 16 | const origin = req.headers.origin; 17 | const requestMethod = req.headers['access-control-request-method']; 18 | const requestHeaders = req.headers['access-control-request-headers']; 19 | 20 | res.once('header', () => { 21 | if (opts.allowCreds) res.header('Access-Control-Allow-Credentials', 'true'); 22 | 23 | if (opts.allowOrigins) { 24 | res.header('Access-Control-Allow-Origin', 25 | (Array.isArray(opts.allowOrigins)) ? opts.allowOrigins.join(', ') : opts.allowOrigins); 26 | } else { 27 | res.header('Access-Control-Allow-Origin', origin); 28 | } 29 | 30 | res.header('Access-Control-Allow-Methods', opts.allowMethods.join(', ')); 31 | res.header('Access-Control-Allow-Headers', opts.allowHeaders.map(h => h.toUpperCase()).join(', ')); 32 | }); 33 | }; 34 | 35 | return (req, res, next) => { 36 | setHeader(req, res); 37 | if (req.method == 'OPTIONS') return res.send(200); 38 | return next(); 39 | }; 40 | }; 41 | 42 | module.exports = cors; 43 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "algoliasearch": "^3.15.0", 8 | "async": "^2.0.0-rc.6", 9 | "bunyan": "^1.8.1", 10 | "bunyan-winston-adapter": "^0.2.0", 11 | "fb": "^1.1.1", 12 | "getstream": "^3.2.0", 13 | "jsonwebtoken": "^7.0.0", 14 | "keen.io": "^0.1.3", 15 | "knox": "^0.9.2", 16 | "mapbox-geocoding": "^0.1.4", 17 | "mysql": "^2.11.1", 18 | "node-uuid": "^1.4.7", 19 | "raven": "^1.2.0", 20 | "restify": "^4.1.0", 21 | "restify-jwt": "^0.4.0", 22 | "stack-trace": "^0.0.9", 23 | "winston": "^2.2.0", 24 | "winston-mail": "^1.2.0", 25 | "winston-sentry": "^0.1.4" 26 | }, 27 | "devDependencies": {}, 28 | "scripts": { 29 | "test": "echo \"Error: no test specified\" && exit 1" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/GetStream/stream-react-example.git" 34 | }, 35 | "keywords": [ 36 | "GetStream" 37 | ], 38 | "author": "Nick Parsons ", 39 | "license": "ISC", 40 | "bugs": { 41 | "url": "https://github.com/GetStream/stream-react-example/issues" 42 | }, 43 | "homepage": "https://github.com/GetStream/stream-react-example#readme" 44 | } 45 | -------------------------------------------------------------------------------- /api/processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps" : [{ 3 | "name" : "api", 4 | "script" : "node index.js", 5 | "watch" : "../", 6 | "log_date_format" : "YYYY-MM-DD HH:mm Z", 7 | }] 8 | } 9 | -------------------------------------------------------------------------------- /api/routes/active.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | server.get('/active', function(req, res, next) { 4 | 5 | // extract query params 6 | var params = req.params || {}; 7 | 8 | var sql = ` 9 | SELECT 10 | users.id AS id, 11 | users.first_name AS first_name, 12 | users.last_name AS last_name, 13 | MD5(users.email) AS email_md5, 14 | users.created_at AS created_at, 15 | users.modified_at AS modified_at, 16 | COUNT(uploads.user_id) as posts, 17 | 18 | (SELECT id FROM followers WHERE followers.user_id = ? AND followers.follower_id = users.id) AS following 19 | 20 | FROM users 21 | LEFT JOIN 22 | uploads 23 | ON uploads.user_id = users.id 24 | 25 | WHERE users.id != ? 26 | 27 | GROUP BY uploads.user_id, users.id 28 | HAVING posts > 0 AND following IS NULL 29 | ORDER BY posts DESC 30 | LIMIT 3 31 | `; 32 | 33 | db.query(sql, [params.user_id, params.user_id], function(err, result) { 34 | 35 | // catch all errors 36 | if (err) { 37 | 38 | // use global logger to log to console 39 | log.error(err); 40 | 41 | // return error message to client 42 | return next(new restify.InternalError(err.message)); 43 | 44 | } 45 | 46 | // send response to client 47 | res.send(200, result); 48 | return next(); 49 | 50 | }); 51 | 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /api/routes/contributions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get contributions for a specific user 5 | * URL: /contributions 6 | * Method: GET 7 | * Auth Required: Yes 8 | * @param {string} user_id This optional query param specifies an user id to query by 9 | * @returns {array} Returns a 200 status code with an array of upload (aka contribution) objects 10 | */ 11 | server.get('/contributions', function(req, res, next) { 12 | 13 | // extract query params 14 | var params = req.params || {}; 15 | 16 | // execute query 17 | db.query('SELECT * FROM uploads WHERE user_id = ? ORDER BY created_at DESC', [params.user_id], function(err, result) { 18 | 19 | // catch all errors 20 | if (err) { 21 | 22 | // use global logger to log to console 23 | log.error(err); 24 | 25 | // return error message to client 26 | return next(new restify.InternalError(err.message)); 27 | 28 | } 29 | 30 | // send response to client 31 | res.send(200, result) 32 | return next(); 33 | 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /api/routes/explore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get images for explore page for a specific user 5 | * URL: /explore 6 | * Method: GET 7 | * Auth Required: Yes 8 | * @param {string} user_id This optional query param specifies an user id to query by 9 | * @returns {array} Returns a 200 status code with an array of upload (aka explore) objects 10 | */ 11 | server.get('/explore', function(req, res, next) { 12 | 13 | // extract query params 14 | var params = req.params || {}; 15 | 16 | // build sql query 17 | var sql = ` 18 | SELECT 19 | uploads.*, 20 | COUNT(likes.id) AS likeTotal 21 | FROM uploads 22 | LEFT JOIN likes 23 | ON likes.upload_id = uploads.id 24 | WHERE uploads.user_id != ? 25 | GROUP BY uploads.id 26 | ORDER BY COUNT(likes.id) ASC 27 | LIMIT 15 28 | `; 29 | 30 | // execute query 31 | db.query(sql, [params.user_id], function(err, result) { 32 | 33 | // catch all errors 34 | if (err) { 35 | 36 | // use global logger to log to console 37 | log.error(err); 38 | 39 | // return error message to client 40 | return next(new restify.InternalError(err.message)); 41 | 42 | } 43 | 44 | // send response to client 45 | res.send(200, result) 46 | return next(); 47 | 48 | }); 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /api/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Catch All 5 | */ 6 | server.opts(/\.*/, function (req, res, next) { 7 | res.send(200); 8 | next(); 9 | }); 10 | 11 | /** 12 | * Routes 13 | */ 14 | require('./active'); 15 | require('./comments'); 16 | require('./followers'); 17 | require('./likes'); 18 | require('./searches'); 19 | require('./uploads'); 20 | require('./users'); 21 | require('./explore'); 22 | require('./trending'); 23 | require('./locations'); 24 | require('./contributions'); 25 | require('./stats'); 26 | require('./following-activity'); 27 | require('./incoming-activity'); 28 | -------------------------------------------------------------------------------- /api/routes/locations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get uploads based on location 5 | * URL: /locations 6 | * Method: GET 7 | * Auth Required: Yes 8 | * @param {string} location This required param specifies the location to filter by 9 | * @returns {object} Returns a 200 status code with an array of search objects 10 | */ 11 | server.get('/locations', function(req, res, next) { 12 | 13 | // extract query params 14 | var params = req.params || {}; 15 | 16 | // if params don't exist, respond with empty object 17 | if (!params.q) { 18 | res.send(200, []); 19 | return next(); 20 | } 21 | 22 | // build sql query 23 | var sql = ` 24 | SELECT * 25 | FROM uploads 26 | WHERE location LIKE ? 27 | ORDER BY created_at DESC 28 | `; 29 | 30 | // build query 31 | db.query(sql, [params.q], function(err, result) { 32 | 33 | // catch all errors 34 | if (err) { 35 | 36 | // use global logger to log to console 37 | log.error(err); 38 | 39 | // return error message to client 40 | return next(new restify.InternalError(err.message)); 41 | 42 | } 43 | 44 | // send response to client 45 | res.send(200, result) 46 | return next(); 47 | 48 | }); 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /api/routes/searches.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get all searches performed by a user (aka search history) 5 | * URL: /searches 6 | * Method: GET 7 | * Auth Required: Yes 8 | * @param {string} user_id This required param specifies the user id to filter by 9 | * @returns {object} Returns a 200 status code with an array of search objects 10 | */ 11 | server.get('/searches', function(req, res, next) { 12 | 13 | // extract query params 14 | var params = req.params || {}; 15 | 16 | // build sql query 17 | var sql = ` 18 | SELECT 19 | search, 20 | created_at 21 | FROM searches 22 | WHERE searches.user_id = ? 23 | GROUP BY searches.search 24 | ORDER BY created_at DESC 25 | LIMIT 10 26 | `; 27 | 28 | // execute query 29 | db.query(sql, [ params.user_id ], function(err, results) { 30 | 31 | // catch all errors 32 | if (err) { 33 | 34 | // use global logger to log to console 35 | log.error(err); 36 | 37 | // return error message to client 38 | return next(new restify.InternalError(err.message)); 39 | 40 | } 41 | 42 | // send response to client 43 | res.send(200, results); 44 | return next(); 45 | 46 | }); 47 | 48 | }); 49 | 50 | /** 51 | * Create a search record for history lookup 52 | * URL: /searches 53 | * Method: POST 54 | * Auth Required: Yes 55 | * @param {string} user_id This required param specifies the user id to associate search with 56 | * @param {string} search This required param specifies the search value 57 | * @returns {object} Returns a 200 status code with an array of search objects 58 | */ 59 | server.post('/searches', function(req, res, next) { 60 | 61 | // extract params from body 62 | var data = req.body || {}; 63 | 64 | // execute query using data from body 65 | db.query('INSERT INTO searches SET ?', data, function(err, result) { 66 | 67 | if (err) { 68 | log.error(err); 69 | return next(new restify.InternalError(err.message)); 70 | } 71 | 72 | // user object.assign to inject new record id 73 | result = Object.assign({ id: result.insertId }, data); 74 | 75 | // send response to client 76 | res.send(201, result); 77 | return next(); 78 | 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /api/routes/trending.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get trending uploads based on like count 5 | * URL: /trending 6 | * Method: GET 7 | * Auth Required: Yes 8 | * @param {string} user_id This required param specifies the user id to filter by 9 | * @returns {object} Returns a 200 status code with an array of upload objects 10 | */ 11 | server.get('/trending', function(req, res, next) { 12 | 13 | // extract params 14 | var params = req.params || {}; 15 | 16 | // build sql query 17 | var sql = ` 18 | SELECT 19 | uploads.*, 20 | COUNT(likes.id) AS likeTotal 21 | FROM uploads 22 | LEFT JOIN likes 23 | ON likes.upload_id = uploads.id 24 | WHERE uploads.user_id != ? 25 | GROUP BY uploads.id 26 | ORDER BY COUNT(likes.id) DESC 27 | `; 28 | 29 | // execute query 30 | db.query(sql, [params.user_id], function(err, result) { 31 | 32 | // catch all errors 33 | if (err) { 34 | 35 | // use global logger to log to console 36 | log.error(err); 37 | 38 | // return error message to client 39 | return next(new restify.InternalError(err.message)); 40 | 41 | } 42 | 43 | // send response to client 44 | res.send(200, result) 45 | return next(); 46 | 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime", 9 | "transform-decorators-legacy", 10 | "transform-es2015-block-scoping", 11 | "transform-es2015-constants" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # OS 36 | .DS_Store 37 | 38 | env 39 | -------------------------------------------------------------------------------- /app/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Stream 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | var express = require('express'), 5 | path = require('path'), 6 | favicon = require('serve-favicon'), 7 | logger = require('morgan'), 8 | cookieParser = require('cookie-parser'), 9 | bodyParser = require('body-parser'), 10 | routes = require('./routes/index'); 11 | 12 | var app = express(); 13 | 14 | /** 15 | * Middleware 16 | */ 17 | app.set('views', path.join(__dirname, 'views')); 18 | app.set('view engine', 'ejs'); 19 | 20 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 21 | app.use(logger('dev')); 22 | app.use(bodyParser.json()); 23 | app.use(bodyParser.urlencoded({ extended: false })); 24 | app.use(cookieParser()); 25 | app.use(express.static(path.join(__dirname, 'public'))); 26 | 27 | app.use('/', routes); 28 | 29 | app.use(function(req, res, next) { 30 | var err = new Error('Not Found'); 31 | err.status = 404; 32 | next(err); 33 | }); 34 | 35 | /** 36 | * Logging 37 | */ 38 | if (app.get('env') === 'development') { 39 | app.use(function(err, req, res, next) { 40 | res.status(err.status || 500); 41 | res.render('error', { 42 | message: err.message, 43 | error: err 44 | }); 45 | }); 46 | } 47 | 48 | app.use(function(err, req, res, next) { 49 | res.status(err.status || 500); 50 | res.render('error', { 51 | message: err.message, 52 | error: {} 53 | }); 54 | }); 55 | 56 | module.exports = app; 57 | -------------------------------------------------------------------------------- /app/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('react:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Config 5 | */ 6 | module.exports = { 7 | name: 'GetStream.io - React Example App', 8 | version: '1.0.0', 9 | env: process.env.NODE_ENV || 'DEVELOPMENT', 10 | mapbox: { 11 | accessToken: process.env.MAPBOX_ACCESS_TOKEN, 12 | }, 13 | stream: { 14 | appId: process.env.STREAM_APP_ID, 15 | key: process.env.STREAM_KEY, 16 | }, 17 | api: { 18 | baseUrl: process.env.API_URL, 19 | }, 20 | imgix: { 21 | baseUrl: process.env.IMGIX_BASE_URL, 22 | }, 23 | algolia: { 24 | appId: process.env.ALGOLIA_APP_ID, 25 | searchOnlyKey: process.env.ALGOLIA_SEARCH_ONLY_KEY, 26 | }, 27 | keen: { 28 | projectId: process.env.KEEN_PROJECT_ID, 29 | writeKey: process.env.KEEN_WRITE_KEY, 30 | readKey: process.env.KEEN_READ_KEY, 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/docs/ast/source/routes/Search/components/index.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "start": 0, 4 | "end": 32, 5 | "loc": { 6 | "start": { 7 | "line": 1, 8 | "column": 0 9 | }, 10 | "end": { 11 | "line": 2, 12 | "column": 0 13 | } 14 | }, 15 | "sourceType": "module", 16 | "body": [ 17 | { 18 | "type": "ExportNamedDeclaration", 19 | "start": 0, 20 | "end": 31, 21 | "loc": { 22 | "start": { 23 | "line": 1, 24 | "column": 0 25 | }, 26 | "end": { 27 | "line": 1, 28 | "column": 31 29 | } 30 | }, 31 | "specifiers": [ 32 | { 33 | "type": "Identifier", 34 | "start": 7, 35 | "end": 14, 36 | "loc": { 37 | "start": { 38 | "line": 1, 39 | "column": 7 40 | }, 41 | "end": { 42 | "line": 1, 43 | "column": 14 44 | } 45 | }, 46 | "exported": { 47 | "type": "Identifier", 48 | "start": 7, 49 | "end": 14, 50 | "loc": { 51 | "start": { 52 | "line": 1, 53 | "column": 7 54 | }, 55 | "end": { 56 | "line": 1, 57 | "column": 14 58 | } 59 | }, 60 | "name": "Filters" 61 | } 62 | } 63 | ], 64 | "source": { 65 | "type": "Literal", 66 | "start": 20, 67 | "end": 31, 68 | "loc": { 69 | "start": { 70 | "line": 1, 71 | "column": 20 72 | }, 73 | "end": { 74 | "line": 1, 75 | "column": 31 76 | } 77 | }, 78 | "value": "./Filters", 79 | "rawValue": "./Filters", 80 | "raw": "'./Filters'" 81 | } 82 | } 83 | ] 84 | } -------------------------------------------------------------------------------- /app/docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | 100% 15 | 100% 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/docs/css/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /app/docs/image/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | @ratio@ 15 | @ratio@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/docs/image/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchoeup/reactjs-redux-nodejs/4978db5c405e378c0a2433f2e121f8891daa4372/app/docs/image/github.png -------------------------------------------------------------------------------- /app/docs/image/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchoeup/reactjs-redux-nodejs/4978db5c405e378c0a2433f2e121f8891daa4372/app/docs/image/search.png -------------------------------------------------------------------------------- /app/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "algoliasearch": "^3.14.1", 10 | "axios": "^0.11.0", 11 | "cookie-parser": "~1.4.1", 12 | "ejs": "~2.4.1", 13 | "express": "~4.13.4", 14 | "getstream": "^3.2.0", 15 | "mapbox-geocoding": "^0.1.4", 16 | "morgan": "~1.7.0", 17 | "numeral": "^1.5.3", 18 | "react-geocoder": "^2.5.0", 19 | "serve-favicon": "~2.3.0" 20 | }, 21 | "devDependencies": { 22 | "babel-cli": "^6.8.0", 23 | "babel-core": "^6.8.0", 24 | "babel-loader": "^6.2.4", 25 | "babel-plugin-transform-class-properties": "^6.8.0", 26 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 27 | "babel-plugin-transform-es2015-block-scoping": "^6.8.0", 28 | "babel-plugin-transform-es2015-constants": "^6.1.4", 29 | "babel-plugin-transform-runtime": "^6.8.0", 30 | "babel-polyfill": "^6.8.0", 31 | "babel-preset-es2015": "^6.6.0", 32 | "babel-preset-es2017": "^1.4.0", 33 | "babel-preset-react": "^6.5.0", 34 | "babel-preset-stage-0": "^6.5.0", 35 | "body-parser": "~1.15.1", 36 | "css-loader": "^0.23.1", 37 | "esdoc": "^0.4.7", 38 | "esdoc-es7-plugin": "0.0.3", 39 | "extract-text-webpack-plugin": "^1.0.1", 40 | "humps": "^1.1.0", 41 | "jsx": "^0.9.89", 42 | "jsx-loader": "^0.13.2", 43 | "md5": "^2.1.0", 44 | "moment": "^2.13.0", 45 | "node-sass": "^3.7.0", 46 | "react": "~15.0.2", 47 | "react-dom": "^15.0.2", 48 | "react-image-component": "^1.0.0", 49 | "react-ink": "^5.1.0", 50 | "react-redux": "^4.4.5", 51 | "react-router": "^2.4.0", 52 | "react-router-redux": "^4.0.4", 53 | "react-tap-event-plugin": "^1.0.0", 54 | "redux": "^3.5.2", 55 | "redux-router": "^1.0.0-beta8", 56 | "redux-thunk": "^2.1.0", 57 | "sass-loader": "^3.2.0", 58 | "webpack": "^1.13.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/docs/script/inherited-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TABLE' && parent.classList.contains('summary')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var tbody = parent.querySelector('tbody'); 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | tbody.style.display = 'none'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | tbody.style.display = 'block'; 21 | } 22 | } 23 | 24 | var buttons = document.querySelectorAll('.inherited-summary thead .toggle'); 25 | for (var i = 0; i < buttons.length; i++) { 26 | buttons[i].addEventListener('click', toggle); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /app/docs/script/inner-link.js: -------------------------------------------------------------------------------- 1 | // inner link(#foo) can not correctly scroll, because page has fixed header, 2 | // so, I manually scroll. 3 | (function(){ 4 | var matched = location.hash.match(/errorLines=([\d,]+)/); 5 | if (matched) return; 6 | 7 | function adjust() { 8 | window.scrollBy(0, -55); 9 | var el = document.querySelector('.inner-link-active'); 10 | if (el) el.classList.remove('inner-link-active'); 11 | 12 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 13 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 14 | var el = document.querySelector(id); 15 | if (el) el.classList.add('inner-link-active'); 16 | } 17 | 18 | window.addEventListener('hashchange', adjust); 19 | 20 | if (location.hash) { 21 | setTimeout(adjust, 0); 22 | } 23 | })(); 24 | 25 | (function(){ 26 | var els = document.querySelectorAll('[href^="#"]'); 27 | for (var i = 0; i < els.length; i++) { 28 | var el = els[i]; 29 | el.href = location.href + el.getAttribute('href'); // because el.href is absolute path 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /app/docs/script/manual.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var matched = location.pathname.match(/([^/]*)\.html$/); 3 | if (!matched) return; 4 | 5 | var currentName = matched[1]; 6 | var cssClass = '.navigation [data-toc-name="' + currentName + '"]'; 7 | var styleText = cssClass + ' .manual-toc { display: block; }\n'; 8 | styleText += cssClass + ' .manual-toc-title { background-color: #039BE5; }\n'; 9 | styleText += cssClass + ' .manual-toc-title a { color: white; }\n'; 10 | var style = document.createElement('style'); 11 | style.textContent = styleText; 12 | document.querySelector('head').appendChild(style); 13 | })(); 14 | -------------------------------------------------------------------------------- /app/docs/script/patch-for-local.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (location.protocol === 'file:') { 3 | var elms = document.querySelectorAll('a[href="./"]'); 4 | for (var i = 0; i < elms.length; i++) { 5 | elms[i].href = './index.html'; 6 | } 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /app/docs/script/pretty-print.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | prettyPrint(); 3 | var lines = document.querySelectorAll('.prettyprint.linenums li[class^="L"]'); 4 | for (var i = 0; i < lines.length; i++) { 5 | lines[i].id = 'lineNumber' + (i + 1); 6 | } 7 | 8 | var matched = location.hash.match(/errorLines=([\d,]+)/); 9 | if (matched) { 10 | var lines = matched[1].split(','); 11 | for (var i = 0; i < lines.length; i++) { 12 | var id = '#lineNumber' + lines[i]; 13 | var el = document.querySelector(id); 14 | el.classList.add('error-line'); 15 | } 16 | return; 17 | } 18 | 19 | if (location.hash) { 20 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 21 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 22 | var line = document.querySelector(id); 23 | if (line) line.classList.add('active'); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /app/docs/script/test-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TR' && parent.classList.contains('test-describe')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var direction; 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | direction = 'closed'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | direction = 'opened'; 21 | } 22 | 23 | var targetDepth = parseInt(parent.dataset.testDepth, 10) + 1; 24 | var nextElement = parent.nextElementSibling; 25 | while (nextElement) { 26 | var depth = parseInt(nextElement.dataset.testDepth, 10); 27 | if (depth >= targetDepth) { 28 | if (direction === 'opened') { 29 | if (depth === targetDepth) nextElement.style.display = ''; 30 | } else if (direction === 'closed') { 31 | nextElement.style.display = 'none'; 32 | var innerButton = nextElement.querySelector('.toggle'); 33 | if (innerButton && innerButton.classList.contains('opened')) { 34 | innerButton.classList.remove('opened'); 35 | innerButton.classList.add('closed'); 36 | } 37 | } 38 | } else { 39 | break; 40 | } 41 | nextElement = nextElement.nextElementSibling; 42 | } 43 | } 44 | 45 | var buttons = document.querySelectorAll('.test-summary tr.test-describe .toggle'); 46 | for (var i = 0; i < buttons.length; i++) { 47 | buttons[i].addEventListener('click', toggle); 48 | } 49 | 50 | var topDescribes = document.querySelectorAll('.test-summary tr[data-test-depth="0"]'); 51 | for (var i = 0; i < topDescribes.length; i++) { 52 | topDescribes[i].style.display = ''; 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /app/esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./modules", 3 | "destination": "./docs", 4 | "plugins": [ 5 | {"name": "esdoc-es7-plugin"} 6 | ] 7 | } -------------------------------------------------------------------------------- /app/modules/actions/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | Photos as PhotoActions, 3 | IncomingActivity as IncomingActivityActions, 4 | FollowingActivity as FollowingActivityActions, 5 | } from './' 6 | 7 | /** 8 | * INIT 9 | * @type {string} 10 | */ 11 | export const INIT = 'APP_INIT' 12 | 13 | /** 14 | * init 15 | * loads photos and activities 16 | * Redux Action 17 | * Reference: http://redux.js.org/docs/basics/Actions.html 18 | * @returns {Function} 19 | */ 20 | export function init() { 21 | return dispatch => { 22 | Promise.all([ 23 | dispatch(PhotoActions.load()), 24 | dispatch(IncomingActivityActions.load()), 25 | dispatch(FollowingActivityActions.load()), 26 | ]).then(() => { 27 | dispatch(initDone()) 28 | }) 29 | } 30 | } 31 | 32 | /** 33 | * INIT_DONE 34 | * @type {string} 35 | */ 36 | export const INIT_DONE = 'APP_INIT_DONE' 37 | 38 | /** 39 | * initDone 40 | * @returns {{type: string}} 41 | */ 42 | export function initDone() { 43 | return { 44 | type: INIT_DONE 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/modules/actions/Comments.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * ADD_COMMENT 6 | * @type {string} 7 | */ 8 | export const ADD_COMMENT = 'PHOTOS_ADD_COMMENT' 9 | 10 | /** 11 | * _addCommentRequest 12 | * @param id 13 | * @private 14 | */ 15 | export const _addCommentRequest = (id) => ({ type: ADD_COMMENT, id, }) 16 | 17 | /** 18 | * _addCommentResponse 19 | * @param id 20 | * @param comment 21 | * @param user 22 | * @private 23 | */ 24 | export const _addCommentResponse = (id, comment, user) => ({ type: ADD_COMMENT, id, comment, user, }) 25 | 26 | /** 27 | * addComment 28 | * Posts comment data to API 29 | * Redux Action 30 | * Reference: http://redux.js.org/docs/basics/Actions.html 31 | * @param id 32 | * @param text 33 | * @returns {Function} 34 | */ 35 | export function addComment(id, text) { 36 | return (dispatch, getState) => { 37 | const user = getState().User 38 | dispatch(_addCommentRequest(id)) 39 | const data = { 40 | user_id: user.id, 41 | upload_id: id, 42 | comment: text, 43 | } 44 | axios.post(`${config.api.baseUrl}/comments`, data, { 45 | headers: { 46 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 47 | }, 48 | }) 49 | .then(res => { 50 | dispatch(_addCommentResponse(id, res.data, user)) 51 | }) 52 | } 53 | } 54 | 55 | /** 56 | * LOAD_COMMENTS 57 | * @type {string} 58 | */ 59 | export const LOAD_COMMENTS = 'PHOTOS_LOAD_COMMENT' 60 | 61 | /** 62 | * _loadCommentsRequest 63 | * @param postID 64 | * @private 65 | */ 66 | export const _loadCommentsRequest = (postID) => ({ type: LOAD_COMMENTS, postID, }) 67 | 68 | /** 69 | * _loadCommentsResponse 70 | * @param postID 71 | * @param comments 72 | * @private 73 | */ 74 | export const _loadCommentsResponse = (postID, comments) => ({ type: LOAD_COMMENTS, postID, comments, }) 75 | 76 | /** 77 | * load 78 | * Gets comments from API based on upload id 79 | * Redux Action 80 | * Reference: http://redux.js.org/docs/basics/Actions.html 81 | * @param postID 82 | * @returns {Function} 83 | */ 84 | export function load(postID) { 85 | return dispatch => { 86 | dispatch(_loadCommentsRequest(postID)) 87 | axios.get(`${config.api.baseUrl}/comments?upload_id=${postID}`, { 88 | headers: { 89 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 90 | }, 91 | }) 92 | .then(res => { 93 | dispatch(_loadCommentsResponse(postID, res.data)) 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/modules/actions/Contributions.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * LOAD 6 | * @type {string} 7 | */ 8 | export const LOAD = 'CONTRIBUTIONS_LOAD' 9 | 10 | /** 11 | * _loadRequest 12 | * @param userID 13 | * @private 14 | */ 15 | export const _loadRequest = (userID) => ({ type: LOAD, userID, }) 16 | 17 | /** 18 | * _loadResponse 19 | * @param userID 20 | * @param response 21 | * @private 22 | */ 23 | export const _loadResponse = (userID, response) => ({ type: LOAD, userID, response, }) 24 | 25 | /** 26 | * load 27 | * Get contributions from API by user id 28 | * Redux Action 29 | * Reference: http://redux.js.org/docs/basics/Actions.html 30 | * @param userID user id 31 | * @returns {Function} 32 | */ 33 | export function load(userID) { 34 | return (dispatch, getState) => { 35 | dispatch(_loadRequest(userID)) 36 | const user = getState().User 37 | axios.get(`${config.api.baseUrl}/contributions?user_id=${userID || user.id}`, { 38 | headers: { 39 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 40 | }, 41 | }) 42 | .then(res => { 43 | dispatch(_loadResponse(userID, res.data)) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/modules/actions/Explore.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * LOAD 6 | * @type {string} 7 | */ 8 | export const LOAD = 'EXPLORE_LOAD' 9 | 10 | /** 11 | * _loadRequest 12 | * @private 13 | */ 14 | const _loadRequest = () => ({ type: LOAD, }) 15 | 16 | /** 17 | * _loadResponse 18 | * @param response 19 | * @private 20 | */ 21 | const _loadResponse = (response) => ({ type: LOAD, response}) 22 | 23 | /** 24 | * load 25 | * Gets explore data from API for user 26 | * Redux Action 27 | * Reference: http://redux.js.org/docs/basics/Actions.html 28 | * @returns {Function} 29 | */ 30 | export function load() { 31 | return (dispatch, getState) => { 32 | dispatch(_loadRequest()) 33 | const user = getState().User 34 | axios.get(`${config.api.baseUrl}/explore?user_id=${user.id}`, { 35 | headers: { 36 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 37 | }, 38 | }) 39 | .then(res => { 40 | dispatch(_loadResponse(res.data)) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/modules/actions/FollowingActivity.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * LOAD 6 | * @type {string} 7 | */ 8 | export const LOAD = 'FOLLOWING_ACTIVITY_LOAD' 9 | 10 | /** 11 | * _loadRequest 12 | * @private 13 | */ 14 | export const _loadRequest = () => ({ type: LOAD, }) 15 | 16 | /** 17 | * _loadResponse 18 | * @param response 19 | * @private 20 | */ 21 | export const _loadResponse = (response) => ({ type: LOAD, response, }) 22 | 23 | /** 24 | * load 25 | * Get notifications from API for user 26 | * Redux Action 27 | * Reference: http://redux.js.org/docs/basics/Actions.html 28 | * @returns {Function} 29 | */ 30 | export function load() { 31 | return (dispatch, getState) => { 32 | return new Promise((resolve => { 33 | dispatch(_loadRequest()) 34 | axios.get(`${config.api.baseUrl}/following-activity?user_id=${getState().User.id}`, { 35 | headers: { 36 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 37 | }, 38 | }) 39 | .then(res => { 40 | dispatch(_loadResponse(res.data)) 41 | resolve() 42 | }) 43 | })) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/modules/actions/Header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LEFT 3 | * @type {string} 4 | */ 5 | export const LEFT = 'HEADER_LEFT' 6 | 7 | /** 8 | * left 9 | * @param component 10 | * @returns {{type: string, component: *}} 11 | */ 12 | export function left(component) { 13 | return { 14 | type: LEFT, 15 | component, 16 | } 17 | } 18 | 19 | /** 20 | * MIDDLE 21 | * @type {string} 22 | */ 23 | export const MIDDLE = 'HEADER_MIDDLE' 24 | 25 | 26 | /** 27 | * middle 28 | * @param component 29 | * @returns {{type: string, component: *}} 30 | */ 31 | export function middle(component) { 32 | return { 33 | type: MIDDLE, 34 | component, 35 | } 36 | } 37 | 38 | /** 39 | * RIGHT 40 | * @type {string} 41 | */ 42 | export const RIGHT = 'HEADER_RIGHT' 43 | 44 | /** 45 | * right 46 | * @param component 47 | * @returns {{type: string, component: *}} 48 | */ 49 | export function right(component) { 50 | return { 51 | type: RIGHT, 52 | component, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/modules/actions/IncomingActivity.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * LOAD 6 | * @type {string} 7 | */ 8 | export const LOAD = 'INCOMING_ACTIVITY_LOAD' 9 | 10 | /** 11 | * _loadRequest 12 | * @private 13 | */ 14 | export const _loadRequest = () => ({ type: LOAD, }) 15 | 16 | /** 17 | * _loadResponse 18 | * @param response 19 | * @private 20 | */ 21 | export const _loadResponse = (response) => ({ type: LOAD, response, }) 22 | 23 | 24 | /** 25 | * load 26 | * Get notifications from API for user 27 | * Redux Action 28 | * Reference: http://redux.js.org/docs/basics/Actions.html 29 | * @returns {Function} 30 | */ 31 | export function load() { 32 | return (dispatch, getState) => { 33 | return new Promise((resolve => { 34 | dispatch(_loadRequest()) 35 | axios.get(`${config.api.baseUrl}/incoming-activity?user_id=${getState().User.id}`, { 36 | headers: { 37 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 38 | }, 39 | }) 40 | .then(res => { 41 | dispatch(_loadResponse(res.data)) 42 | resolve() 43 | }) 44 | })) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/modules/actions/Location.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * LOAD 6 | * @type {string} 7 | */ 8 | export const LOAD = 'LOCATION_LOAD' 9 | 10 | /** 11 | * _loadRequest 12 | * @param location 13 | * @private 14 | */ 15 | export const _loadRequest = (location) => ({ type: LOAD, location, }) 16 | 17 | /** 18 | * _loadResponse 19 | * @param location 20 | * @param response 21 | * @private 22 | */ 23 | export const _loadResponse = (location, response) => ({ type: LOAD, location, response, }) 24 | 25 | /** 26 | * load 27 | * Gets data from API for a given location 28 | * Redux Action 29 | * Reference: http://redux.js.org/docs/basics/Actions.html 30 | * @param location 31 | * @returns {Function} 32 | */ 33 | export function load(location) { 34 | return (dispatch) => { 35 | dispatch(_loadRequest(location)) 36 | axios.get(`${config.api.baseUrl}/locations?q=${location}`, { 37 | headers: { 38 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 39 | }, 40 | }) 41 | .then(res => { 42 | dispatch(_loadResponse(location, res.data)) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/modules/actions/Photo.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | import { 5 | Like as LikeActions, 6 | Comments as CommentActions, 7 | } from 'actions' 8 | 9 | /** 10 | * LOAD 11 | * @type {string} 12 | */ 13 | export const LOAD = 'PHOTO_LOAD' 14 | 15 | /** 16 | * _loadRequest 17 | * @private 18 | */ 19 | const _loadRequest = () => ({ type: LOAD, }) 20 | 21 | /** 22 | * _loadResponse 23 | * @param response 24 | * @private 25 | */ 26 | const _loadResponse = (response) => ({ type: LOAD, response, }) 27 | 28 | /** 29 | * load 30 | * Gets single photo upload from API based on upload id 31 | * Redux Action 32 | * Reference: http://redux.js.org/docs/basics/Actions.html 33 | * @param id upload id 34 | * @returns {Function} 35 | */ 36 | export function load(id) { 37 | return dispatch => { 38 | return new Promise((resolve) => { 39 | dispatch(_loadRequest()) 40 | axios.get(`${config.api.baseUrl}/upload?id=${id}`, { 41 | headers: { 42 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 43 | }, 44 | }) 45 | .then(res => { 46 | dispatch(_loadResponse(res.data)) 47 | dispatch(CommentActions.load(id)) 48 | dispatch(LikeActions.load(id)) 49 | return resolve() 50 | }) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/modules/actions/Stream.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | import { 5 | Photos as PhotosActions, 6 | IncomingActivity as IncomingActivityActions, 7 | } from 'actions' 8 | 9 | export const CLEAR = 'STREAM_CLEAR' 10 | export function clear() { 11 | return { 12 | type: CLEAR, 13 | } 14 | } 15 | 16 | export function timeline(data) { 17 | return dispatch => { 18 | 19 | Promise.all(data.new.map(p => { 20 | 21 | const id = p.object.split(':')[1] 22 | 23 | return ( 24 | axios.get(`${config.api.baseUrl}/upload?id=${id}`, { 25 | headers: { 26 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 27 | }, 28 | }) 29 | ) 30 | 31 | })).then(results => { 32 | dispatch(PhotosActions.inject(results.map(r => r.data))) 33 | }) 34 | } 35 | } 36 | 37 | 38 | export const EVENT = 'STREAM_EVENT' 39 | 40 | const _newEvent = data => ({ type: EVENT, count: data.new.length,}) 41 | export function event(data) { 42 | return dispatch => { 43 | dispatch(_newEvent(data)) 44 | dispatch(IncomingActivityActions.load()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/modules/actions/Trending.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * LOAD 6 | * @type {string} 7 | */ 8 | export const LOAD = 'TRENDING_LOAD' 9 | 10 | /** 11 | * _loadRequest 12 | * @private 13 | */ 14 | const _loadRequest = () => ({ type: LOAD, }) 15 | 16 | /** 17 | * _loadResponse 18 | * @param response 19 | * @private 20 | */ 21 | const _loadResponse = (response) => ({ type: LOAD, response, }) 22 | 23 | /** 24 | * trending 25 | * Gets 'trending' data from API 26 | * Redux Action 27 | * Reference: http://redux.js.org/docs/basics/Actions.html 28 | * @returns {Function} 29 | */ 30 | export function load() { 31 | return (dispatch, getState) => { 32 | _loadRequest() 33 | const user = getState().User 34 | axios.get(`${config.api.baseUrl}/trending?user_id=${user.id}`, { 35 | headers: { 36 | Authorization: `Bearer ${localStorage.getItem('jwt')}` 37 | }, 38 | }) 39 | .then(res => { 40 | dispatch(_loadResponse(res.data)) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/modules/actions/User.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | import config from 'config' 3 | 4 | /** 5 | * FB_LOGIN 6 | * @type {string} 7 | */ 8 | export const FB_LOGIN = 'USER_FB_LOGIN' 9 | 10 | /** 11 | * _fbLoginInitial 12 | * @param initial 13 | * @private 14 | */ 15 | const _fbLoginInitial = (initial) => ({ type: FB_LOGIN, initial, }) 16 | 17 | /** 18 | * fbLogin 19 | * Performs Facebook login, and on success posts return data to API 20 | * Redux Action 21 | * Reference: http://redux.js.org/docs/basics/Actions.html 22 | * @param response {Object} 23 | * @returns {Function} 24 | */ 25 | export function fbLogin(response) { 26 | var token = response.authResponse.accessToken; 27 | var userID = response.authResponse.userID; 28 | return dispatch => { 29 | axios.post(`${config.api.baseUrl}/users`, { 30 | token : token, 31 | fb_user_id : userID 32 | }) 33 | .then(function(res) { 34 | localStorage.setItem('jwt', res.data.jwt); 35 | dispatch(_fbLoginInitial(res.data)) 36 | }) 37 | .catch(function(res) { 38 | window.location.reload() 39 | dispatch(_fbLoginInitial(res.data)) 40 | }); 41 | } 42 | } 43 | 44 | /** 45 | * LOGOUT 46 | * @type {string} 47 | */ 48 | export const LOGOUT = 'USER_LOGOUT' 49 | 50 | /** 51 | * _logoutRequest 52 | * @private 53 | */ 54 | export const _logoutRequest = () => ({ type: LOGOUT, }) 55 | 56 | /** 57 | * _logoutResponse 58 | * @param response 59 | * @private 60 | */ 61 | export const _logoutResponse = (response) => ({ type: LOGOUT, response, }) 62 | 63 | /** 64 | * logout 65 | * Performs Facebook logout for a given user 66 | * Redux Action 67 | * Reference: http://redux.js.org/docs/basics/Actions.html 68 | * @returns {Function} 69 | */ 70 | export function logout() { 71 | return dispatch => { 72 | dispatch(_logoutRequest()) 73 | FB.logout(response => { 74 | dispatch(_logoutResponse(response)) 75 | }) 76 | } 77 | } 78 | 79 | /** 80 | * FOLLOW 81 | * @type {string} 82 | */ 83 | export const FOLLOW = 'USER_FOLLOW' 84 | 85 | /** 86 | * follow 87 | * @param user 88 | * @returns {{type: string, user: *}} 89 | */ 90 | export function follow(user) { 91 | return { 92 | type: FOLLOW, 93 | user, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/modules/actions/index.js: -------------------------------------------------------------------------------- 1 | export * as App from './App' 2 | export * as User from './User' 3 | export * as Photos from './Photos' 4 | export * as Comments from './Comments' 5 | export * as Like from './Like' 6 | export * as Photo from './Photo' 7 | export * as Stats from './Stats' 8 | export * as Explore from './Explore' 9 | export * as Trending from './Trending' 10 | export * as Search from './Search' 11 | export * as Location from './Location' 12 | export * as Profile from './Profile' 13 | export * as Header from './Header' 14 | export * as Contributions from './Contributions' 15 | export * as IncomingActivity from './IncomingActivity' 16 | export * as FollowingActivity from './FollowingActivity' 17 | export * as Stream from './Stream' 18 | -------------------------------------------------------------------------------- /app/modules/components/Activity/Actor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Avatar } from 'components' 3 | import { Link } from 'react-router' 4 | /** 5 | * Actor component 6 | */ 7 | export default class Actor extends Component { 8 | 9 | /** 10 | * defaultProps 11 | * @type {{avatar: string, email: string, firstName: string, lastName: string}} 12 | */ 13 | 14 | static defaultProps = { 15 | avatar: '', 16 | first_name: '', 17 | last_name: '', 18 | } 19 | 20 | /** 21 | * render 22 | * @returns markup 23 | */ 24 | render() { 25 | return ( 26 | 27 |
28 |
29 | 30 |
31 |
{this.props.first_name}
{this.props.last_name && this.props.last_name.charAt(0) + '.'}
32 |
33 | 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/modules/components/Activity/Commented.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Actor from './Actor' 4 | import { Link } from 'react-router' 5 | import config from 'config' 6 | 7 | /** 8 | * Commented component 9 | */ 10 | export default class Commented extends Component { 11 | 12 | /** 13 | * defaultProps 14 | * @type {{actor: {}, user: {}, timestamp: null, timeSince: string}} 15 | */ 16 | static defaultProps = { 17 | actor: {}, 18 | user: {}, 19 | time: null, 20 | timeSince: '', 21 | } 22 | 23 | /** 24 | * render 25 | * @returns markup 26 | */ 27 | render() { 28 | return ( 29 |
30 |
31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | " 39 |

{this.props.comment}

40 |
41 |
42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/modules/components/Activity/Following.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Actor from './Actor' 4 | 5 | /** 6 | * Following component 7 | */ 8 | export default class Following extends Component { 9 | 10 | /** 11 | * 12 | * @type {{actor: {}, user: {}, timestamp: null, timeSince: string, following: boolean, onFollowBack: Following.defaultProps.onFollowBack, onUnfollow: Following.defaultProps.onUnfollow}} 13 | */ 14 | static defaultProps = { 15 | actor: {}, 16 | user: {}, 17 | time: null, 18 | timeSince: '', 19 | following: false, 20 | 21 | onFollowBack: () => { 22 | }, 23 | onUnfollow: () => { 24 | }, 25 | } 26 | 27 | /** 28 | * handleFollowBack 29 | * @param e 30 | */ 31 | handleFollowBack = (e) => { 32 | e.preventDefault() 33 | 34 | this.props.onFollowBack(e, this.props.actor) 35 | } 36 | 37 | /** 38 | * handleUnfollow 39 | * @param e 40 | */ 41 | handleUnfollow = (e) => { 42 | e.preventDefault() 43 | 44 | this.props.onUnfollow(e, this.props.actor) 45 | } 46 | 47 | /** 48 | * renderFollowButton 49 | * @returns markup 50 | */ 51 | renderFollowButton = () => { 52 | 53 | if (this.props.following) { 54 | return ( 55 |
56 | 57 |
58 | ) 59 | } 60 | 61 | return ( 62 |
63 | 64 |
65 | ) 66 | 67 | } 68 | 69 | /** 70 | * render 71 | * @returns markup 72 | */ 73 | render() { 74 | return ( 75 |
76 | 77 | {this.renderFollowButton()} 78 |
79 | 80 |
81 |
82 |

Followed, {this.props.timeSince}

83 |
84 |
85 | 86 |
87 | 88 |
89 |
90 |
91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/modules/components/Activity/Liked.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | import config from 'config' 4 | 5 | import Actor from './Actor' 6 | 7 | const Picture = props => ( 8 |
9 | 10 | 11 | 12 |
13 | ) 14 | 15 | /** 16 | * Liked component 17 | */ 18 | export default class Liked extends Component { 19 | 20 | /** 21 | * defaultProps 22 | * @type {{pictures: Array}} 23 | */ 24 | static defaultProps = { 25 | activities: [], 26 | } 27 | 28 | /** 29 | * renderMessage 30 | * @returns {*} 31 | */ 32 | renderMessage = () => { 33 | if (this.props.activity_count === 1) return 'Liked your picture' 34 | return `Liked ${this.props.activity_count} pictures` 35 | } 36 | 37 | /** 38 | * render 39 | * @returns markup 40 | */ 41 | render() { 42 | return ( 43 |
44 | 45 |
46 | 47 |
48 |
49 |

{this.renderMessage()}, {this.props.timeSince}

50 |
51 |
52 |
53 |
54 | {this.props.activities.slice(0,6).map((p, i) => )} 55 |
56 |
57 |
58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/modules/components/Activity/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, cloneElement } from 'react' 2 | 3 | export Actor from './Actor' 4 | export Following from './Following' 5 | export Liked from './Liked' 6 | export Commented from './Commented' 7 | 8 | import moment from 'moment' 9 | 10 | /** 11 | * Activity index component 12 | */ 13 | export class Item extends Component { 14 | 15 | /** 16 | * defaultProps 17 | * @type {{type: null, timestamp: null, actor: {}}} 18 | */ 19 | static defaultProps = { 20 | verb: null, 21 | time: null, 22 | actor: {}, 23 | } 24 | 25 | /** 26 | * render 27 | * @returns markup 28 | */ 29 | render() { 30 | return ( 31 |
  • 32 | {cloneElement(this.props.children, { 33 | timestamp: this.props.time, 34 | actor: this.props.actor, 35 | timeSince: moment.utc(this.props.time).fromNow(), 36 | })} 37 |
  • 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/modules/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | const styles = { 4 | root: { 5 | backgroundColor: '#fafafa', 6 | display: 'inline-block', 7 | } 8 | } 9 | 10 | /** 11 | * Avatar index component 12 | */ 13 | class Avatar extends Component { 14 | 15 | /** 16 | * defaultProps 17 | * @type {{email: null, height: number, imgHeight: number}} 18 | */ 19 | static defaultProps = { 20 | email_md5: null, 21 | height: 155, 22 | imgHeight: 400, 23 | } 24 | 25 | /** 26 | * render 27 | * @returns markup 28 | */ 29 | render() { 30 | 31 | const placeHolder = Object.assign({}, styles.root, { 32 | height: this.props.height, 33 | width: this.props.height, 34 | }) 35 | 36 | if (!this.props.emailHash) return
    37 | 38 | return 40 | 41 | } 42 | } 43 | 44 | export default Avatar 45 | -------------------------------------------------------------------------------- /app/modules/components/BackButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | 5 | /** 6 | * BackButton index component 7 | */ 8 | class BackButton extends Component { 9 | 10 | /** 11 | * defaultProps 12 | * @type {{to: string, icon: XML, label: string}} 13 | */ 14 | static defaultProps = { 15 | to: '/', 16 | icon: , 17 | label: 'Back', 18 | } 19 | 20 | /** 21 | * render 22 | * @returns markup 23 | */ 24 | render() { 25 | 26 | const content = {this.props.icon}{this.props.label} 27 | 28 | if (typeof this.props.to == 'string') { 29 | return ( 30 | {content} 31 | ) 32 | } 33 | 34 | return {content} 35 | 36 | } 37 | 38 | } 39 | 40 | export default BackButton 41 | -------------------------------------------------------------------------------- /app/modules/components/Comment/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | import TimeAgo from '../TimeAgo' 4 | 5 | /** 6 | * Comment index component 7 | */ 8 | class Comment extends Component { 9 | 10 | /** 11 | * 12 | * @returns markup 13 | */ 14 | render() { 15 | return ( 16 |
    17 | 18 |
    19 | {this.props.firstName} {this.props.lastName.charAt(0) + '.'} 20 |
    21 | 22 |
    23 | 24 |
    25 |
    26 | {this.props.comment} 27 |
    28 |
    29 | ) 30 | } 31 | 32 | } 33 | 34 | export default Comment 35 | -------------------------------------------------------------------------------- /app/modules/components/LikeButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { likePhoto } from 'utils/analytics' 4 | 5 | import { 6 | User as UserActions, 7 | } from 'actions' 8 | 9 | /** 10 | * LikeButton index component 11 | */ 12 | class LikeButton extends Component { 13 | 14 | /** 15 | * defaultProps 16 | * @type {{id: string, onLike: LikeButton.defaultProps.onLike}} 17 | */ 18 | static defaultProps = { 19 | id: '', 20 | onLike: () => {}, 21 | } 22 | 23 | /** 24 | * handleClick 25 | * @param e event 26 | */ 27 | handleClick = (e) => { 28 | this.props.onLike({id: this.props.id, liked: !this.props.liked}) 29 | if (!this.props.liked) { 30 | likePhoto(this.props.user.id, this.props.id) 31 | } 32 | } 33 | 34 | /** 35 | * render 36 | * @returns markup 37 | */ 38 | render() { 39 | let classes = ['item'] 40 | if (this.props.liked) classes.push('ion-ios-heart') 41 | if (!this.props.liked) classes.push('ion-ios-heart-outline') 42 | 43 | return 44 | } 45 | 46 | } 47 | 48 | export default connect(state => ({ 49 | user: state.User, 50 | }))(LikeButton) 51 | -------------------------------------------------------------------------------- /app/modules/components/PhotoList/PhotoFooter.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import LikeButton from 'components/LikeButton' 5 | 6 | /** 7 | * PhotoFooter component 8 | */ 9 | class PhotoFooter extends Component { 10 | 11 | static defaultProps = { 12 | showLike: false, 13 | } 14 | 15 | /** 16 | * render 17 | * @returns markup 18 | */ 19 | render() { 20 | const tags = this.props.hashtags.split(' ') 21 | 22 | return ( 23 |
    24 |
    25 |

    26 | 27 | {this.props.caption} in 28 |   29 | {this.props.location} 30 | 31 | 32 | 33 | {this.props.showLike 34 | ? 35 | : null} 36 |

    37 |
      38 | {tags.map(tag => 39 |
    • {tag}
    • 40 | )} 41 |
    42 |
    43 | ) 44 | } 45 | 46 | } 47 | 48 | export default PhotoFooter 49 | -------------------------------------------------------------------------------- /app/modules/components/PhotoList/PhotoItem.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import { Link } from 'react-router' 3 | import config from 'config' 4 | 5 | import { 6 | LikeButton, 7 | Avatar, 8 | } from 'components' 9 | 10 | import PhotoFooter from './PhotoFooter' 11 | 12 | class Actor extends Component { 13 | render() { 14 | return ( 15 |
    16 | 17 | 20 | 21 | 22 | {this.props.first_name} {this.props.last_name && this.props.last_name.charAt(0) + '.'} 23 | 24 |
    25 | ) 26 | } 27 | } 28 | 29 | /** 30 | * PhotoItem component 31 | */ 32 | class PhotoItem extends Component { 33 | 34 | /** 35 | * render 36 | * @returns markup 37 | */ 38 | render() { 39 | 40 | return ( 41 |
  • 42 |
    43 |
    44 | 45 |
    46 |
    47 | 48 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 60 | 63 | 64 |
    65 | 66 |
    67 |
  • 68 | ) 69 | } 70 | } 71 | 72 | export default PhotoItem 73 | -------------------------------------------------------------------------------- /app/modules/components/PhotoList/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | 3 | import PhotoItem from './PhotoItem' 4 | 5 | /** 6 | * PhotoList index component 7 | */ 8 | class PhotoList extends Component { 9 | 10 | /** 11 | * renderItems 12 | * @returns {*} 13 | */ 14 | renderItems = () => { 15 | return this.props.photos.map(photo => 16 | 17 | ) 18 | } 19 | 20 | /** 21 | * render 22 | * @returns markup 23 | */ 24 | render() { 25 | return ( 26 |
      27 | {this.renderItems()} 28 |
    29 | ) 30 | } 31 | } 32 | 33 | export PhotoFooter from './PhotoFooter' 34 | export default PhotoList 35 | -------------------------------------------------------------------------------- /app/modules/components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | require('./styles.css') 4 | 5 | /** 6 | * Tab component 7 | */ 8 | export class Tab extends Component { 9 | 10 | /** 11 | * defaultProps 12 | * @type {{active: boolean}} 13 | */ 14 | static defaultProps = { 15 | active: false, 16 | } 17 | 18 | /** 19 | * render 20 | * @returns markup or {null} 21 | */ 22 | render() { 23 | return this.props.active ? this.props.children : null 24 | } 25 | } 26 | 27 | /** 28 | * Tabs index component 29 | */ 30 | export class Tabs extends Component { 31 | 32 | /** 33 | * state 34 | * @type {{active: number}} 35 | */ 36 | state = { 37 | active: 0, 38 | } 39 | 40 | /** 41 | * handleClick 42 | * @param e 43 | * @param i 44 | * @param items 45 | */ 46 | handleClick = (e, i, items) => { 47 | this.setState({ 48 | active: i, 49 | }) 50 | } 51 | 52 | /** 53 | * render 54 | * @returns markup 55 | */ 56 | render() { 57 | 58 | const indicator = { 59 | left: (100 / 4) * this.state.active + '%', 60 | width: (100 / 4) + '%', 61 | } 62 | 63 | const children = React.Children.toArray(this.props.children) 64 | 65 | const items = children.map((c, i) => { 66 | return React.cloneElement(c, { 67 | active: i == this.state.active 68 | }) 69 | }) 70 | 71 | return ( 72 |
    73 |
    74 |
    75 | {children.map((tab, i) => { 76 | const active = (this.state.active == i) 77 | return ( 78 | 82 | ) 83 | })} 84 |
    85 |
    86 |
    87 | 88 | {items.filter(item => item.props.active)[0]} 89 | 90 |
    91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/modules/components/Tabs/styles.css: -------------------------------------------------------------------------------- 1 | .tabs header { 2 | position: relative; 3 | background: none; 4 | border-bottom: 1px solid #d8d8d8; 5 | width: 100%; 6 | margin-top: -40px; 7 | } 8 | 9 | .tabs header .items { 10 | display: flex; 11 | } 12 | 13 | .tabs header button { 14 | background: none; 15 | outline: none; 16 | border: 0; 17 | padding: 0; 18 | color: black; 19 | opacity: 0.6; 20 | border-radius: 0; 21 | padding-bottom: 15px; 22 | } 23 | 24 | .tabs header button.active { 25 | opacity: 1; 26 | /*border-bottom: 2px solid black; 27 | margin-bottom: -1px;*/ 28 | } 29 | 30 | .tabs header .indicator { 31 | height: 2px; 32 | background-color: black; 33 | position: absolute; 34 | bottom: 0; 35 | transition: 0.5s left ease-in-out; 36 | } 37 | -------------------------------------------------------------------------------- /app/modules/components/TimeAgo/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import moment from 'moment' 4 | 5 | /** 6 | * TimeAgo index component 7 | */ 8 | class TimeAgo extends Component { 9 | 10 | /** 11 | * defaultProps 12 | * @type {{updateDuration: number, timestamp: null}} 13 | */ 14 | static defaultProps = { 15 | updateDuration: 30000, 16 | timestamp: null, 17 | } 18 | 19 | /** 20 | * state 21 | * @type {{i: number}} 22 | */ 23 | state = {i: 0,} 24 | 25 | /** 26 | * componentDidMount 27 | */ 28 | componentDidMount() { 29 | /** 30 | * this.$i 31 | * @type {number|*} 32 | */ 33 | this.$i = setInterval(() => { 34 | this.setState({i: (this.state.i + 1)}) 35 | }, this.props.updateDuration) 36 | } 37 | 38 | /** 39 | * componentWillUnmount 40 | */ 41 | componentWillUnmount() { 42 | clearInterval(this.$i) 43 | } 44 | 45 | /** 46 | * render 47 | * @returns markup 48 | */ 49 | render() { 50 | return {moment.utc(this.props.timestamp).fromNow()} 51 | } 52 | 53 | } 54 | 55 | export default TimeAgo 56 | -------------------------------------------------------------------------------- /app/modules/components/index.js: -------------------------------------------------------------------------------- 1 | export Header from './Header'; 2 | export LikeButton from './LikeButton'; 3 | export PhotoList from './PhotoList'; 4 | export BackButton from './BackButton'; 5 | export * as Activity from './Activity' 6 | export Comment from './Comment' 7 | export Avatar from './Avatar' 8 | export TimeAgo from './TimeAgo' 9 | export { Tabs, Tab } from './Tabs' 10 | export Nav from './Nav' 11 | -------------------------------------------------------------------------------- /app/modules/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { Router, Route, browserHistory } from 'react-router' 5 | import { syncHistoryWithStore, routerReducer } from 'react-router-redux' 6 | import { createStore, combineReducers, applyMiddleware } from 'redux' 7 | import thunk from 'redux-thunk' 8 | import config from 'config' 9 | 10 | /** 11 | * window.keenClient 12 | * Instantiate new Keen client 13 | * @type {Keen} 14 | */ 15 | window.keenClient = new Keen({ 16 | projectId: config.keen.projectId, 17 | writeKey: config.keen.writeKey, 18 | readKey: config.keen.readKey 19 | }); 20 | 21 | require('./style.css') 22 | 23 | /** 24 | * rootRoute 25 | * @type {{path: string, getComponent: (function(*, *)), getChildRoutes: (function(*, *))}} 26 | */ 27 | const rootRoute = { 28 | path: '/', 29 | 30 | /** 31 | * getComponent 32 | * @param location 33 | * @param cb {Function} callback 34 | */ 35 | getComponent(location, cb) { 36 | cb(null, require('./App').default) 37 | }, 38 | 39 | /** 40 | * getChildRoutes 41 | * @param location 42 | * @param cb {Function} callback 43 | */ 44 | getChildRoutes(location, cb) { 45 | cb(null, [ 46 | require('./routes/Landing'), 47 | require('./routes/Home'), 48 | require('./routes/Upload'), 49 | require('./routes/Search'), 50 | require('./routes/SearchResults'), 51 | require('./routes/Notifications'), 52 | require('./routes/Explore'), 53 | require('./routes/Trending'), 54 | require('./routes/Profile'), 55 | require('./routes/FollowingActivity'), 56 | require('./routes/Stats'), 57 | require('./routes/Contributions'), 58 | require('./routes/Location'), 59 | ]) 60 | }, 61 | 62 | } 63 | 64 | /** 65 | * appElm 66 | * @type {Nullable.|Element} 67 | */ 68 | const appElm = document.getElementById('app') 69 | 70 | import * as reducers from './reducers' 71 | 72 | /** 73 | * store 74 | */ 75 | export const store = createStore( 76 | combineReducers({ 77 | ...reducers, 78 | 79 | routing: routerReducer 80 | }), 81 | applyMiddleware(thunk), 82 | ) 83 | 84 | /** 85 | * history 86 | */ 87 | const history = syncHistoryWithStore(browserHistory, store) 88 | 89 | /** 90 | * render Provider 91 | */ 92 | render(( 93 | 94 | appElm.scrollIntoView()} 97 | routes={rootRoute} /> 98 | 99 | ), appElm) 100 | -------------------------------------------------------------------------------- /app/modules/reducers/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | App as AppActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{loading: boolean}} 8 | */ 9 | const initialState = { loading: true, } 10 | 11 | /** 12 | * App 13 | * Redux Reducer for App action 14 | * Reference: http://redux.js.org/docs/basics/Reducers.html 15 | * @param state 16 | * @param action 17 | * @returns {*} 18 | * @constructor 19 | */ 20 | function App(state = initialState, action) { 21 | 22 | switch (action.type) { 23 | 24 | case AppActions.INIT_DONE: 25 | return Object.assign({}, state, { loading: false, }) 26 | } 27 | 28 | return state 29 | } 30 | 31 | export default App 32 | -------------------------------------------------------------------------------- /app/modules/reducers/Comments.js: -------------------------------------------------------------------------------- 1 | import { camelizeKeys, } from 'humps' 2 | 3 | import { 4 | Comments as CommentActions, 5 | } from 'actions' 6 | 7 | /** 8 | * initialState 9 | * @type {{comments: Array, uploadID: null}} 10 | */ 11 | const initialState = { 12 | comments: [], 13 | uploadID: null, 14 | } 15 | 16 | /** 17 | * Redux Reducer for Comments action 18 | * Reference: http://redux.js.org/docs/basics/Reducers.html 19 | * @param state 20 | * @param action 21 | * @returns {*} 22 | * @constructor 23 | */ 24 | function Comments(state = initialState, action) { 25 | switch (action.type) { 26 | 27 | case CommentActions.LOAD_COMMENTS: 28 | if (action.comments) { 29 | return Object.assign({}, state, { 30 | comments: [...action.comments.map(c => camelizeKeys(c))], 31 | uploadID: action.postID, 32 | }) 33 | } 34 | 35 | return initialState 36 | 37 | case CommentActions.ADD_COMMENT: 38 | if (action.comment) { 39 | const user = camelizeKeys(action.user) 40 | return Object.assign({}, state, { 41 | comments: [ 42 | Object.assign({}, camelizeKeys(action.comment), { 43 | firstName: user.firstName, 44 | lastName: user.lastName, 45 | createdAt: new Date(), 46 | }), 47 | ...state.comments, 48 | ] 49 | }) 50 | 51 | } 52 | return state 53 | } 54 | 55 | return state 56 | } 57 | 58 | export default Comments 59 | -------------------------------------------------------------------------------- /app/modules/reducers/Contributions.js: -------------------------------------------------------------------------------- 1 | import { 2 | Contributions as ContributionActions 3 | } from 'actions' 4 | 5 | /** 6 | * Contributions 7 | * Redux Reducer for Contributions action 8 | * Reference: http://redux.js.org/docs/basics/Reducers.html 9 | * @param state 10 | * @param action 11 | * @returns {*} 12 | * @constructor 13 | */ 14 | function Contributions(state = [], action) { 15 | 16 | switch (action.type) { 17 | case ContributionActions.LOAD: 18 | if (action.response) { 19 | return [ 20 | ...action.response, 21 | ] 22 | } 23 | return [] 24 | } 25 | 26 | return state 27 | } 28 | 29 | export default Contributions 30 | -------------------------------------------------------------------------------- /app/modules/reducers/Explore.js: -------------------------------------------------------------------------------- 1 | import { 2 | Explore as ExploreActions 3 | } from 'actions' 4 | 5 | /** 6 | * Explore 7 | * Redux Reducer for Explore action 8 | * Reference: http://redux.js.org/docs/basics/Reducers.html 9 | * @param state 10 | * @param action 11 | * @returns {*} 12 | * @constructor 13 | */ 14 | function Explore(state = [], action) { 15 | 16 | switch (action.type) { 17 | case ExploreActions.LOAD: 18 | if (action.response) { 19 | return [ 20 | ...action.response, 21 | ] 22 | } 23 | return [] 24 | } 25 | 26 | return state 27 | } 28 | 29 | export default Explore 30 | -------------------------------------------------------------------------------- /app/modules/reducers/FollowingActivity.js: -------------------------------------------------------------------------------- 1 | import { 2 | FollowingActivity as FollowingActivityActions, 3 | User as UserActions, 4 | Profile as ProfileActions, 5 | } from 'actions' 6 | 7 | /** 8 | * Following Activity 9 | * Redux Reducer for Activity action 10 | * Reference: http://redux.js.org/docs/basics/Reducers.html 11 | * @param state 12 | * @param action 13 | * @returns {*} 14 | * @constructor 15 | */ 16 | function FollowingActivity(state = [], action) { 17 | 18 | switch (action.type) { 19 | case FollowingActivityActions.LOAD: 20 | if (action.response) { 21 | return [ 22 | ...action.response, 23 | ] 24 | } 25 | return state 26 | 27 | case ProfileActions.UNFOLLOW: 28 | return state.map((item) => { 29 | if (item.activities[0].actor.id === action.userID && item.verb == 'follow') { 30 | const newItem = {...item} 31 | newItem.activities[0].actor.following = 0 32 | return newItem 33 | } 34 | return item 35 | }) 36 | 37 | case ProfileActions.FOLLOW: 38 | if (action.response) { 39 | return state.map((item) => { 40 | if (item.activities[0].actor.id === action.userID && item.verb == 'follow') { 41 | const newItem = {...item} 42 | newItem.activities[0].actor.following = 1 43 | return newItem 44 | } 45 | return item 46 | }) 47 | } 48 | return state 49 | } 50 | 51 | return state 52 | } 53 | 54 | export default FollowingActivity 55 | -------------------------------------------------------------------------------- /app/modules/reducers/Header.js: -------------------------------------------------------------------------------- 1 | import { 2 | Header as HeaderActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{left: null, middle: null, right: null}} 8 | */ 9 | const initialState = { 10 | left: null, 11 | middle: null, 12 | right: null, 13 | } 14 | 15 | /** 16 | * Header 17 | * Redux Reducer for Header action 18 | * Reference: http://redux.js.org/docs/basics/Reducers.html 19 | * @param state 20 | * @param action 21 | * @returns {*} 22 | * @constructor 23 | */ 24 | function Header(state = initialState, action) { 25 | 26 | switch (action.type) { 27 | 28 | case HeaderActions.LEFT: 29 | return Object.assign({}, state, { 30 | left: action.component, 31 | }) 32 | 33 | case HeaderActions.MIDDLE: 34 | return Object.assign({}, state, { 35 | middle: action.component, 36 | }) 37 | 38 | case HeaderActions.RIGHT: 39 | return Object.assign({}, state, { 40 | right: action.component, 41 | }) 42 | } 43 | 44 | return state 45 | } 46 | 47 | export default Header 48 | -------------------------------------------------------------------------------- /app/modules/reducers/IncomingActivity.js: -------------------------------------------------------------------------------- 1 | import { 2 | IncomingActivity as IncomingActivityActions, 3 | User as UserActions, 4 | Profile as ProfileActions, 5 | } from 'actions' 6 | 7 | /** 8 | * Activity 9 | * Redux Reducer for Activity action 10 | * Reference: http://redux.js.org/docs/basics/Reducers.html 11 | * @param state 12 | * @param action 13 | * @returns {*} 14 | * @constructor 15 | */ 16 | function IncomingActivity(state = [], action) { 17 | 18 | switch (action.type) { 19 | 20 | case IncomingActivityActions.LOAD: 21 | if (action.response) { 22 | return [ 23 | ...action.response, 24 | ] 25 | } 26 | return state 27 | 28 | case ProfileActions.UNFOLLOW: 29 | return state.map((item) => { 30 | if (item.activities[0].actor.id === action.userID && item.verb == 'follow') { 31 | const newItem = {...item} 32 | newItem.activities[0].actor.following = 0 33 | return newItem 34 | } 35 | return item 36 | }) 37 | 38 | case ProfileActions.FOLLOW: 39 | if (action.response) { 40 | return state.map((item) => { 41 | if (item.activities[0].actor.id === action.userID && item.verb == 'follow') { 42 | const newItem = {...item} 43 | newItem.activities[0].actor.following = 1 44 | return newItem 45 | } 46 | return item 47 | }) 48 | } 49 | return state 50 | } 51 | 52 | return state 53 | } 54 | 55 | export default IncomingActivity 56 | -------------------------------------------------------------------------------- /app/modules/reducers/Likes.js: -------------------------------------------------------------------------------- 1 | import { 2 | Like as LikeActions, 3 | } from 'actions' 4 | 5 | /** 6 | * 7 | * @type {{likes: number, liked: boolean}} 8 | */ 9 | const initialState = { 10 | likes: 0, 11 | liked: false, 12 | } 13 | 14 | /** 15 | * Likes 16 | * Redux Reducer for Likes action 17 | * Reference: http://redux.js.org/docs/basics/Reducers.html 18 | * @param state 19 | * @param action 20 | * @returns {*} 21 | * @constructor 22 | */ 23 | function Likes(state = initialState, action) { 24 | 25 | switch (action.type) { 26 | 27 | case LikeActions.LOAD: 28 | if (action.response) { 29 | return Object.assign({}, state, { 30 | likes: action.response.likes, 31 | liked: action.response.liked, 32 | }) 33 | } 34 | 35 | return initialState 36 | 37 | case LikeActions.ADD_LIKE: 38 | if (action.response) { 39 | return Object.assign({}, state, { liked: true, likes: action.response.likes }) 40 | } 41 | break 42 | 43 | case LikeActions.DELETE_LIKE: 44 | if (action.response) { 45 | return Object.assign({}, state, { liked: false, likes: action.response.likes }) 46 | } 47 | break 48 | 49 | } 50 | 51 | return state 52 | 53 | } 54 | 55 | export default Likes 56 | -------------------------------------------------------------------------------- /app/modules/reducers/Location.js: -------------------------------------------------------------------------------- 1 | import { 2 | Location as LocationActions 3 | } from 'actions' 4 | 5 | /** 6 | * Location 7 | * Redux Reducer for Location action 8 | * Reference: http://redux.js.org/docs/basics/Reducers.html 9 | * @param state 10 | * @param action 11 | * @returns {*} 12 | * @constructor 13 | */ 14 | function Location(state = [], action) { 15 | 16 | switch (action.type) { 17 | case LocationActions.LOAD: 18 | if (action.response) { 19 | return [ 20 | ...action.response, 21 | ] 22 | } 23 | return [] 24 | } 25 | 26 | return state 27 | } 28 | 29 | export default Location 30 | -------------------------------------------------------------------------------- /app/modules/reducers/Navigation.js: -------------------------------------------------------------------------------- 1 | export default function Navigation(state = {}, action) { 2 | switch (action.type) { 3 | case '@@router/LOCATION_CHANGE': 4 | return Object.assign({}, state, action.payload) 5 | } 6 | return state 7 | } 8 | -------------------------------------------------------------------------------- /app/modules/reducers/Onboarding.js: -------------------------------------------------------------------------------- 1 | import * as PhotosActions from 'actions/Photos' 2 | import * as ProfileActions from 'actions/Profile' 3 | 4 | function Onboarding(state = [], action) { 5 | switch (action.type) { 6 | case PhotosActions.ONBOARDING: 7 | return [...action.response] 8 | 9 | case ProfileActions.FOLLOW: 10 | return state.filter(profile => profile.id != action.userID) 11 | } 12 | 13 | return state 14 | } 15 | 16 | export default Onboarding 17 | -------------------------------------------------------------------------------- /app/modules/reducers/Pagination.js: -------------------------------------------------------------------------------- 1 | import { 2 | Photos as PhotoActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{lastId: null, fetching: boolean}} 8 | */ 9 | const initialState = { 10 | lastId: null, 11 | fetching: false, 12 | } 13 | 14 | /** 15 | * Pagination 16 | * Redux Reducer for Pagination action 17 | * Reference: http://redux.js.org/docs/basics/Reducers.html 18 | * @param state 19 | * @param action 20 | * @returns {*} 21 | * @constructor 22 | */ 23 | function Pagination(state = initialState, action) { 24 | 25 | switch (action.type) { 26 | 27 | case PhotoActions.LOAD: 28 | case PhotoActions.PAGINATE: 29 | if (action.response) { 30 | const lastItem = action.response.slice(-1) 31 | const obj = Object.assign({}, state, { 32 | fetching: false, 33 | }) 34 | if (lastItem[0] && lastItem[0].id) { 35 | Object.assign(obj, { lastId: lastItem[0].id, }) 36 | } 37 | return obj 38 | } 39 | return Object.assign({}, state, { fetching: true, }) 40 | } 41 | 42 | return state 43 | } 44 | 45 | export default Pagination 46 | -------------------------------------------------------------------------------- /app/modules/reducers/Photo.js: -------------------------------------------------------------------------------- 1 | import { 2 | Photo as PhotoActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{id: string, caption: string, created_at: string, email: string, fb_uid: string, filename: string, first_name: string, hashtags: string, last_name: string, latitude: string, liked: string, location: string, longitude: string, modified_at: string, user_id: string, loading: boolean}} 8 | */ 9 | const initialState = { 10 | id: '', 11 | caption: '', 12 | created_at: '', 13 | email_md5: '', 14 | fb_uid: '', 15 | filename: '', 16 | first_name: '', 17 | hashtags: '', 18 | last_name: '', 19 | latitude: '', 20 | liked: '', 21 | location: '', 22 | longitude: '', 23 | modified_at: '', 24 | user_id: '', 25 | 26 | loading: false, 27 | } 28 | 29 | /** 30 | * Photo 31 | * Redux Reducer for Photo action 32 | * Reference: http://redux.js.org/docs/basics/Reducers.html 33 | * @param state 34 | * @param action 35 | * @returns {*} 36 | * @constructor 37 | */ 38 | function Photo(state = initialState, action) { 39 | 40 | switch (action.type) { 41 | case PhotoActions.LOAD: 42 | if (action.response) { 43 | const res = action.response 44 | return Object.assign({}, state, { 45 | id: res.id, 46 | caption: res.caption, 47 | created_at: res.created_at, 48 | email: res.email, 49 | email_md5: res.email_md5, 50 | fb_uid: res.fb_uid, 51 | filename: res.filename, 52 | first_name: res.first_name, 53 | hashtags: res.hashtags, 54 | last_name: res.last_name, 55 | latitude: res.latitude, 56 | liked: res.liked, 57 | location: res.location, 58 | longitude: res.longitude, 59 | modified_at: res.modified_at, 60 | user_id: res.user_id, 61 | 62 | loading: false, 63 | }) 64 | } 65 | 66 | return Object.assign({}, state, { loading: true }) 67 | } 68 | 69 | return state 70 | } 71 | 72 | export default Photo 73 | -------------------------------------------------------------------------------- /app/modules/reducers/Photos.js: -------------------------------------------------------------------------------- 1 | import { 2 | Photos as PhotoActions, 3 | } from 'actions' 4 | 5 | /** 6 | * Photos 7 | * Redux Reducer for Photos action 8 | * Reference: http://redux.js.org/docs/basics/Reducers.html 9 | * @param state 10 | * @param action 11 | * @returns {*} 12 | * @constructor 13 | */ 14 | function Photos(state = [], action) { 15 | 16 | switch (action.type) { 17 | 18 | case PhotoActions.INJECT: 19 | if (action.posts) { 20 | const s = [...action.posts.map(r => ({...r, hidden: true}))] 21 | state.forEach(r => s.push(r)) 22 | return s 23 | } 24 | return state 25 | 26 | case PhotoActions.LOAD_HIDDEN: 27 | return state.map(p => ({...p, hidden: false })) 28 | 29 | case PhotoActions.PAGINATE: 30 | if (action.response) { 31 | const s = [...state] 32 | action.response.forEach(r => s.push(r)) 33 | return s 34 | } 35 | return state 36 | 37 | case PhotoActions.LOAD: 38 | if (action.response) { 39 | return [ 40 | ...action.response, 41 | ] 42 | } 43 | return [] 44 | 45 | case PhotoActions.RELOAD: 46 | return [...action.response] 47 | 48 | case PhotoActions.LIKE: 49 | if (action.response) { 50 | return state.map(item => { 51 | 52 | if (item.object.id == action.postID) { 53 | const newItem = {...item} 54 | newItem.object.liked = true 55 | return newItem 56 | } 57 | 58 | return item 59 | }) 60 | } 61 | 62 | case PhotoActions.UNLIKE: 63 | if (action.response) { 64 | return state.map(item => { 65 | 66 | if (item.object.id == action.postID) { 67 | const newItem = {...item} 68 | newItem.object.liked = false 69 | return newItem 70 | } 71 | 72 | return item 73 | }) 74 | } 75 | 76 | 77 | } 78 | 79 | return state 80 | } 81 | 82 | export default Photos 83 | -------------------------------------------------------------------------------- /app/modules/reducers/Profile.js: -------------------------------------------------------------------------------- 1 | import { 2 | Profile as ProfileActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{id: string, fb_uid: string, first_name: string, last_name: string, email: string, follower: boolean, following: boolean, follower_count: number, following_count: number, created_at: string, modified_at: string}} 8 | */ 9 | const initialState = { 10 | id: '', 11 | fb_uid: '', 12 | first_name: '', 13 | last_name: '', 14 | email_md5: '', 15 | follower: false, 16 | following: false, 17 | follower_count: 0, 18 | following_count: 0, 19 | created_at: '', 20 | modified_at: '', 21 | } 22 | 23 | /** 24 | * Profile 25 | * Redux Reducer for Profile action 26 | * Reference: http://redux.js.org/docs/basics/Reducers.html 27 | * @param state 28 | * @param action 29 | * @returns {*} 30 | * @constructor 31 | */ 32 | function Profile(state = initialState, action) { 33 | 34 | switch (action.type) { 35 | 36 | case ProfileActions.LOAD: 37 | if (action.response) { 38 | return Object.assign({}, state, { 39 | id: action.response.id, 40 | fb_uid: action.response.fb_id, 41 | first_name: action.response.first_name, 42 | last_name: action.response.last_name, 43 | email_md5: action.response.email_md5, 44 | follower: action.response.follower, 45 | following: action.response.following, 46 | follower_count: action.response.follower_count, 47 | following_count: action.response.following_count, 48 | created_at: action.response.created_at, 49 | modified_at: action.response.modified_at, 50 | }) 51 | } 52 | 53 | return initialState 54 | 55 | case ProfileActions.FOLLOW: 56 | if (action.response) { 57 | return Object.assign({}, state, { 58 | following: true, 59 | follower_count: state.follower_count + 1, 60 | }) 61 | } 62 | 63 | return state 64 | 65 | case ProfileActions.UNFOLLOW: 66 | if (typeof action.response != 'undefined') { 67 | return Object.assign({}, state, { 68 | following: false, 69 | follower_count: state.follower_count - 1, 70 | }) 71 | } 72 | 73 | } 74 | 75 | return state 76 | 77 | } 78 | 79 | export default Profile 80 | -------------------------------------------------------------------------------- /app/modules/reducers/Stats.js: -------------------------------------------------------------------------------- 1 | import { 2 | Stats as StatsActions, 3 | } from 'actions' 4 | 5 | /** 6 | * iniitalState 7 | * @type {{profileViews: {count: number, increment: number, color: string}, itemViews: {count: number, increment: number, color: string}, following: {count: number, increment: string, color: string}, followers: {count: number, increment: string, color: string}, mostViewed: Array, geoViews: Array}} 8 | */ 9 | const initialState = { 10 | profileViews: { 11 | 'count': 0, 12 | 'increment': 24, 13 | 'color': 'green' 14 | }, 15 | itemViews: { 16 | 'count': 0, 17 | 'increment': 24, 18 | 'color': 19 | 'green' 20 | }, 21 | following: { 22 | 'count': 0, 23 | 'increment': '', 24 | 'color': '' 25 | }, 26 | followers: { 27 | 'count': 0, 28 | 'increment': '', 29 | 'color': '' 30 | }, 31 | mostViewed: [ 32 | 33 | ], 34 | geoViews: [] 35 | } 36 | 37 | /** 38 | * Stats 39 | * Redux Reducer for Stats action 40 | * Reference: http://redux.js.org/docs/basics/Reducers.html 41 | * @param state 42 | * @param action 43 | * @returns {*} 44 | * @constructor 45 | */ 46 | function Stats(state = initialState, action) { 47 | switch (action.type) { 48 | 49 | case StatsActions.LOAD: 50 | if (action.response) { 51 | if (action.response.itemViews >= 0) { 52 | var newState = Object.assign({}, state, { 53 | itemViews: {'count': action.response.itemViews}, 54 | }) 55 | } else if (action.response.profileViews >= 0) { 56 | var newState = Object.assign({}, state, { 57 | profileViews: {'count': action.response.profileViews}, 58 | }) 59 | } else if (action.response.mostViewed) { 60 | var newState = Object.assign({}, state, { 61 | mostViewed: action.response.mostViewed, 62 | }) 63 | } else if (action.response.geoViews) { 64 | var newState = Object.assign({}, state, { 65 | geoViews: action.response.geoViews, 66 | }) 67 | } else if (action.response.newFollowers) { 68 | var newState = Object.assign({}, state) 69 | var increment = action.response.newFollowers.result 70 | newState['followers']['increment'] = increment 71 | newState['followers']['color'] = (increment) ? 'green' : 'red' 72 | } 73 | 74 | return newState 75 | } 76 | 77 | return initialState 78 | 79 | } 80 | 81 | return state 82 | 83 | } 84 | 85 | export default Stats 86 | -------------------------------------------------------------------------------- /app/modules/reducers/Stream.js: -------------------------------------------------------------------------------- 1 | import { 2 | Stream as StreamActions, 3 | } from 'actions' 4 | 5 | function Stream(state = 0, action) { 6 | switch (action.type) { 7 | case StreamActions.EVENT: 8 | return state + 1 9 | 10 | case StreamActions.CLEAR: 11 | return 0 12 | } 13 | 14 | return state 15 | } 16 | 17 | export default Stream 18 | -------------------------------------------------------------------------------- /app/modules/reducers/Tokens.js: -------------------------------------------------------------------------------- 1 | import { 2 | User as UserActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{notification: string, timelineAgg: string, timelineFlat: string}} 8 | */ 9 | const initialState = { 10 | notification: '', 11 | timelineAgg: '', 12 | timelineFlat: '', 13 | } 14 | 15 | /** 16 | * Tokens 17 | * Redux Reducer for Tokens action 18 | * Reference: http://redux.js.org/docs/basics/Reducers.html 19 | * @param state 20 | * @param action 21 | * @returns {*} 22 | * @constructor 23 | */ 24 | function Tokens(state = initialState, action) { 25 | switch (action.type) { 26 | 27 | case UserActions.FB_LOGIN: 28 | if (action.initial) { 29 | let newState = { 30 | notification: action.initial.tokens.notification, 31 | timelineAgg: action.initial.tokens.timeline.aggregated, 32 | timelineFlat: action.initial.tokens.timeline.flat, 33 | } 34 | return Object.assign({}, state, newState) 35 | } 36 | return state 37 | } 38 | 39 | return state 40 | } 41 | 42 | export default Tokens 43 | -------------------------------------------------------------------------------- /app/modules/reducers/Trending.js: -------------------------------------------------------------------------------- 1 | import { 2 | Trending as TrendingActions 3 | } from 'actions' 4 | 5 | /** 6 | * Trending 7 | * Redux Reducer for Trending action 8 | * Reference: http://redux.js.org/docs/basics/Reducers.html 9 | * @param state 10 | * @param action 11 | * @returns {*} 12 | * @constructor 13 | */ 14 | function Trending(state = [], action) { 15 | 16 | switch (action.type) { 17 | case TrendingActions.LOAD: 18 | if (action.response) { 19 | return [ 20 | ...action.response, 21 | ] 22 | } 23 | return [] 24 | } 25 | 26 | return state 27 | } 28 | 29 | export default Trending 30 | -------------------------------------------------------------------------------- /app/modules/reducers/User.js: -------------------------------------------------------------------------------- 1 | import { 2 | User as UserActions, 3 | } from 'actions' 4 | 5 | /** 6 | * initialState 7 | * @type {{id: string, fb_uid: string, first_name: string, last_name: string, email: string}} 8 | */ 9 | const initialState = { 10 | id: '', 11 | fb_uid: '', 12 | first_name: '', 13 | last_name: '', 14 | email_md5: '', 15 | } 16 | 17 | /** 18 | * User 19 | * Redux Reducer for User action 20 | * Reference: http://redux.js.org/docs/basics/Reducers.html 21 | * @param state 22 | * @param action 23 | * @returns {*} 24 | * @constructor 25 | */ 26 | function User(state = initialState, action) { 27 | 28 | switch (action.type) { 29 | case UserActions.LOGIN: 30 | return Object.assign({}, state, action.response) 31 | 32 | case UserActions.FB_LOGIN: 33 | if (action.initial) { 34 | return Object.assign({}, state, { 35 | id : action.initial.id, 36 | fb_uid : action.initial.fb_uid, 37 | first_name : action.initial.first_name, 38 | last_name : action.initial.last_name, 39 | email_md5 : action.initial.email_md5, 40 | }) 41 | } 42 | return state 43 | 44 | case UserActions.LOGOUT: 45 | return Object.assign({}, state, initialState) 46 | 47 | } 48 | 49 | return state 50 | } 51 | 52 | export default User 53 | -------------------------------------------------------------------------------- /app/modules/reducers/index.js: -------------------------------------------------------------------------------- 1 | export User from './User' 2 | export Photos from './Photos' 3 | export Comments from './Comments' 4 | export Likes from './Likes' 5 | export Photo from './Photo' 6 | export Stats from './Stats' 7 | export Explore from './Explore' 8 | export Trending from './Trending' 9 | export Search from './Search' 10 | export Location from './Location' 11 | export Profile from './Profile' 12 | export Header from './Header' 13 | export Contributions from './Contributions' 14 | export Pagination from './Pagination' 15 | export Tokens from './Tokens' 16 | export App from './App' 17 | export IncomingActivity from './IncomingActivity' 18 | export FollowingActivity from './FollowingActivity' 19 | export Stream from './Stream' 20 | export Navigation from './Navigation' 21 | export Onboarding from './Onboarding' 22 | -------------------------------------------------------------------------------- /app/modules/routes/Contributions/Contributions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router' 4 | import config from 'config' 5 | 6 | import { 7 | Contributions as ContributionActions 8 | } from 'actions' 9 | 10 | /** 11 | * Contributions 12 | * '/profile/:id/contributions' 13 | * React Route - Documentation: https://github.com/reactjs/react-router/tree/master/docs 14 | */ 15 | class Contributions extends Component { 16 | 17 | /** 18 | * componentDidMount 19 | */ 20 | componentDidMount() { 21 | this.props.dispatch(ContributionActions.load(this.props.params.id)) 22 | } 23 | 24 | /** 25 | * renderFeedOrMessage 26 | * @returns markup 27 | */ 28 | renderFeedOrMessage = () => { 29 | if (!this.props.contributions.length) { 30 | return ( 31 |
    32 |
    Upload your first image to get started!
    33 |
    34 | ) 35 | } 36 | return ( 37 |
    38 |
    39 | {this.props.contributions.map(item => 40 |
    41 | 42 | 44 | 45 |
    46 | )} 47 |
    48 |
    49 | ) 50 | 51 | } 52 | 53 | /** 54 | * render 55 | * @returns markup 56 | */ 57 | render() { 58 | return ( 59 |
    60 | {this.renderFeedOrMessage()} 61 |
    62 | ) 63 | } 64 | 65 | } 66 | 67 | /** 68 | * connect 69 | * Connects React component to a Redux store 70 | * Documentation: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options 71 | */ 72 | export default connect(state => ({ 73 | contributions: state.Contributions, 74 | }))(Contributions) 75 | -------------------------------------------------------------------------------- /app/modules/routes/Contributions/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { browserHistory } from 'react-router' 3 | import { BackButton } from 'components' 4 | 5 | module.exports = { 6 | path: '/profile/:id/contributions', 7 | 8 | /** 9 | * getComponent 10 | * @param location 11 | * @param cb {Function} callback 12 | */ 13 | getComponent(location, cb) { 14 | cb(null, require('./Contributions').default) 15 | }, 16 | 17 | /** 18 | * getHeaderLeft 19 | * @param location 20 | * @param cb {Function} callback 21 | */ 22 | getHeaderLeft(location, cb) { 23 | cb(null, browserHistory.goBack()}/>) 24 | }, 25 | 26 | /** 27 | * getHeaderMiddle 28 | * @param location 29 | * @param cb {Function} callback 30 | */ 31 | getHeaderMiddle(location, cb) { 32 | cb(null, ) 33 | }, 34 | 35 | /** 36 | * getHeaderRight 37 | * @param location 38 | * @param cb {Function} callback 39 | */ 40 | getHeaderRight(location, cb) { 41 | cb(null, ) 42 | }, 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/modules/routes/Explore/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router' 4 | import config from 'config' 5 | 6 | import { 7 | Explore as ExploreActions 8 | } from 'actions' 9 | 10 | 11 | /** 12 | * Explore 13 | * '/explore' 14 | * React Route - Documentation: https://github.com/reactjs/react-router/tree/master/docs 15 | */ 16 | class Explore extends Component { 17 | 18 | /** 19 | * componentDidMount 20 | */ 21 | componentDidMount() { 22 | this.props.dispatch(ExploreActions.load()) 23 | } 24 | 25 | /** 26 | * render 27 | * @returns markup 28 | */ 29 | render() { 30 | return ( 31 |
    32 | 33 |
    34 | See All≫ 35 |
    36 | 37 |
    38 |

    Trending

    39 |
    40 | 42 |
    43 |
    44 | 46 |
    47 |
    48 | 50 |
    51 |
    52 |
    53 |

    New

    54 |
    55 | {this.props.explore.map(item => 56 |
    57 | 58 | 60 | 61 |
    62 | )} 63 |
    64 |
    65 |
    66 | ) 67 | } 68 | 69 | } 70 | 71 | /** 72 | * connect 73 | * Connects React component to a Redux store 74 | * Documentation: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options 75 | */ 76 | export default connect(state => ({ 77 | explore: state.Explore, 78 | }))(Explore) 79 | -------------------------------------------------------------------------------- /app/modules/routes/Explore/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Nav } from 'components' 4 | 5 | module.exports = { 6 | path: '/explore', 7 | 8 | /** 9 | * getComponent 10 | * @param location 11 | * @param cb {Function} callback 12 | */ 13 | getComponent(location, cb) { 14 | cb(null, require('./Explore').default) 15 | }, 16 | 17 | /** 18 | * getHeaderMiddle 19 | * @param location 20 | * @param cb {Function} callback 21 | */ 22 | getHeaderMiddle(location, cb) { 23 | cb(null,