├── .gitignore ├── LICENSE ├── README.md ├── chirps.js ├── gulpfile.js ├── login.js ├── package.json ├── public ├── lib │ └── skeleton │ │ ├── .bower.json │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── bower.json │ │ ├── css │ │ ├── normalize.css │ │ └── skeleton.css │ │ ├── images │ │ └── favicon.png │ │ └── index.html └── style.css ├── server.js ├── src ├── actions.js ├── api.js ├── components │ ├── App.js │ ├── ChirpBox.js │ ├── ChirpInput.js │ ├── ChirpList.js │ ├── FollowButton.js │ ├── Home.js │ ├── Navigation.js │ ├── UserList.js │ └── UserProfile.js ├── constants.js ├── dispatcher.js ├── main.js ├── stores │ ├── chirps.js │ ├── store.js │ └── users.js └── utils.js └── views ├── index.ejs └── login.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | .data 2 | node_modules 3 | .DS_Store 4 | public/main.js 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tuts+ 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Build a Microblogging App With Flux and React][published url] 2 | ## Instructor: [Andrew Burgess][instructor url] 3 | 4 | 5 | There’s a lot of talk about Flux these days. If you haven’t had a chance to try it yet, you might wonder what you’re missing! 6 | 7 | Flux is an architecture for React applications. It was developed by Facebook to complement the strengths of the React view framework. In this course, we’ll build a simple Twitter clone using the Flux architecture, with React powering the views. 8 | 9 | ## Source Files Description 10 | 11 | This repository contains the completed source code for the microblogging application. 12 | 13 | To install the NPM package dependencies, run `npm install` from the project directory. 14 | 15 | 16 | ------ 17 | 18 | These are source files for the Tuts+ course: [Build a Microblogging App With Flux and React][published url] 19 | 20 | Available on [Tuts+](https://tutsplus.com). Teaching skills to millions worldwide. 21 | 22 | [published url]: https://code.tutsplus.com/courses/build-a-microblogging-app-with-flux-and-react 23 | [instructor url]: https://tutsplus.com/authors/andrew-burgess 24 | -------------------------------------------------------------------------------- /chirps.js: -------------------------------------------------------------------------------- 1 | var router = module.exports = require('express').Router(); 2 | var login = require('./login'); 3 | 4 | var db = new (require('locallydb'))('./.data'); 5 | var chirps = db.collection('chirps'); 6 | 7 | router.route('/api/chirps') 8 | .all(login.required) 9 | .get(function (req, res) { 10 | res.json(chirps.toArray()); 11 | }) 12 | .post(function (req, res) { 13 | var chirp = req.body; 14 | chirp.userId = req.user.cid; 15 | 16 | // TO BE REMOVED 17 | chirp.username = req.user.username; 18 | chirp.fullname = req.user.fullname; 19 | chirp.email = req.user.email; 20 | 21 | var id = chirps.insert(chirp); 22 | res.json(chirps.get(id)); 23 | }); 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Note: The use of gulp-browserify has been refactored out, per the article at 2 | // https://medium.com/@sogko/gulp-browserify-the-gulp-y-way-bb359b3f9623 3 | // and https://github.com/substack/node-browserify/issues/1198 4 | var gulp = require('gulp'); 5 | var browserify = require('browserify'); 6 | var reactify = require('reactify'); 7 | var through2 = require('through2') 8 | var concat = require('gulp-concat'); 9 | var plumber = require('gulp-plumber'); 10 | 11 | 12 | gulp.task('browserify', function () { 13 | 14 | gulp.src('./src/main.js') 15 | .pipe(plumber()) 16 | //instead of using the blacklisted and unmaintained gulp-browserify, we'll run browserify using through2 17 | .pipe(through2.obj(function (file, enc, next){ 18 | browserify(file.path, {'debug': true}) 19 | .transform('reactify') 20 | .bundle(function(err, res){ 21 | file.contents = res; 22 | next(null, file); 23 | }); 24 | })) 25 | .pipe(concat('main.js')) 26 | .pipe(gulp.dest('public')) 27 | }); 28 | 29 | gulp.task('default', ['browserify']); 30 | 31 | gulp.task('watch', function () { 32 | gulp.watch('src/**/*.*', ['default']); 33 | }); 34 | -------------------------------------------------------------------------------- /login.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | var LocalStrategy = require('passport-local'); 3 | 4 | var LocallyDB = require('locallydb'); 5 | var db = new LocallyDB('./.data'); 6 | var users = db.collection('users'); 7 | 8 | var crypto = require('crypto'); 9 | 10 | function hash (password) { 11 | return crypto.createHash('sha512').update(password).digest('hex'); 12 | } 13 | 14 | passport.use(new LocalStrategy(function (username, password, done) { 15 | var user = users.where({ username: username, passwordHash: hash(password) }).items[0]; 16 | 17 | if (user) { 18 | done(null, user); 19 | } else { 20 | done(null, false); 21 | } 22 | })); 23 | 24 | passport.serializeUser(function (user, done) { 25 | done(null, user.cid); 26 | }); 27 | 28 | passport.deserializeUser(function (cid, done) { 29 | done(null, users.get(cid)); 30 | }); 31 | 32 | var router = require('express').Router(); 33 | var bodyParser = require('body-parser'); 34 | 35 | router.use(bodyParser.urlencoded({ extended: true })); // Login Page 36 | router.use(bodyParser.json()); // API 37 | router.use(require('cookie-parser')()); 38 | router.use(require('express-session')({ 39 | secret: 'p7r6uktdhmcgvho8o6e5ysrhxmcgjfkot7r6elu5dtjt7lirfyj', 40 | resave: false, 41 | saveUninitialized: true 42 | })); 43 | router.use(passport.initialize()); 44 | router.use(passport.session()); 45 | 46 | router.get('/login', function (req, res) { 47 | res.render('login'); 48 | }); 49 | 50 | router.post('/signup', function (req, res, next) { 51 | if (users.where({ username: req.body.username }).items.length === 0) { 52 | var user = { 53 | fullname: req.body.fullname, 54 | email: req.body.email, 55 | username: req.body.username, 56 | passwordHash: hash(req.body.password), 57 | following: [] 58 | }; 59 | 60 | var userId = users.insert(user); 61 | 62 | req.login(users.get(userId), function (err) { 63 | if (err) return next(err); 64 | res.redirect('/'); 65 | }); 66 | } else { 67 | res.redirect('/login'); 68 | } 69 | }); 70 | 71 | router.post('/login', passport.authenticate('local', { 72 | successRedirect: '/', 73 | failureRedirect: '/login' 74 | })); 75 | 76 | router.get('/logout', function (req, res) { 77 | req.logout(); 78 | res.redirect('/login'); 79 | }); 80 | 81 | function loginRequired (req, res, next) { 82 | if (req.isAuthenticated()) { 83 | next(); 84 | } else { 85 | res.redirect('/login'); 86 | } 87 | } 88 | 89 | function makeUserSafe (user) { 90 | var safeUser = {}; 91 | 92 | var safeKeys = ['cid', 'fullname', 'email', 'username', 'following']; 93 | 94 | safeKeys.forEach(function (key) { 95 | safeUser[key] = user[key]; 96 | }); 97 | return safeUser; 98 | } 99 | 100 | router.get('/api/users', function (req, res) { 101 | res.json(users.toArray().map(makeUserSafe)); 102 | }); 103 | 104 | router.post('/api/follow/:id', function (req, res) { 105 | var id = parseInt(req.params.id, 10); 106 | 107 | if (req.user.following.indexOf(id) < 0) { 108 | req.user.following.push(id); 109 | users.update(req.user.cid, req.user); 110 | } 111 | res.json(makeUserSafe(req.user)); 112 | }); 113 | 114 | router.post('/api/unfollow/:id', function (req, res) { 115 | var id = parseInt(req.params.id, 10); 116 | var pos = req.user.following.indexOf(id); 117 | if (pos > -1) { 118 | req.user.following.splice(pos, 1); 119 | users.update(req.user.cid, req.user); 120 | } 121 | res.json(makeUserSafe(req.user)); 122 | }); 123 | 124 | exports.routes = router; 125 | exports.required = loginRequired; 126 | exports.safe = makeUserSafe; 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chirper", 3 | "version": "1.0.0", 4 | "description": "A very simple twitter clone built with React and Flux", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "browserify": "^11.1.0", 13 | "gulp": "^3.9.0", 14 | "gulp-concat": "^2.6.0", 15 | "gulp-plumber": "^1.0.1", 16 | "reactify": "^1.1.1", 17 | "through2": "^2.0.0" 18 | }, 19 | "dependencies": { 20 | "body-parser": "^1.13.2", 21 | "cookie-parser": "^1.3.5", 22 | "ejs": "^2.3.2", 23 | "express": "^4.13.0", 24 | "express-session": "^1.11.3", 25 | "flux": "^2.0.3", 26 | "locallydb": "0.0.9", 27 | "moment": "^2.10.3", 28 | "object-assign": "^3.0.0", 29 | "passport": "^0.2.2", 30 | "passport-local": "^1.0.0", 31 | "react": "^0.13.3", 32 | "react-router": "^0.13.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/lib/skeleton/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skeleton", 3 | "version": "2.0.4", 4 | "homepage": "http://getskeleton.com/", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dhg/Skeleton" 8 | }, 9 | "authors": [ 10 | "Dave Gamache (http://davegamache.com/)" 11 | ], 12 | "description": "Skeleton is a dead-simple, responsive boilerplate to kickstart any responsive project.", 13 | "main": "css/skeleton.css", 14 | "keywords": [ 15 | "css", 16 | "skeleton", 17 | "responsive", 18 | "boilerplate" 19 | ], 20 | "license": "MIT", 21 | "_release": "2.0.4", 22 | "_resolution": { 23 | "type": "version", 24 | "tag": "2.0.4", 25 | "commit": "88f03612b05f093e3f235ced77cf89d3a8fcf846" 26 | }, 27 | "_source": "git://github.com/dhgamache/Skeleton.git", 28 | "_target": "~2.0.4", 29 | "_originalSource": "skeleton", 30 | "_direct": true 31 | } -------------------------------------------------------------------------------- /public/lib/skeleton/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /public/lib/skeleton/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2014 Dave Gamache 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /public/lib/skeleton/README.md: -------------------------------------------------------------------------------- 1 | # [Skeleton](http://getskeleton.com) 2 | Skeleton is a simple, responsive boilerplate to kickstart any responsive project. 3 | 4 | Check out for documentation and details. 5 | 6 | ## Getting started 7 | 8 | There are a couple ways to download Skeleton: 9 | - [Download the zip](https://github.com/dhg/Skeleton/releases/download/2.0.4/Skeleton-2.0.4.zip) 10 | - Clone the repo: `git clone https://github.com/dhg/Skeleton.git` (Note: this is under active development, so if you're looking for stable and safe, use the zipped download) 11 | 12 | 13 | ### What's in the download? 14 | 15 | The download includes Skeleton's CSS, Normalize CSS as a reset, a sample favicon, and an index.html as a starting point. 16 | 17 | ``` 18 | Skeleton/ 19 | ├── index.html 20 | ├── css/ 21 | │ ├── normalize.min.css 22 | │ └── skeleton.css 23 | └── images/ 24 | └── favicon.ico 25 | 26 | ``` 27 | 28 | ### Why it's awesome 29 | 30 | Skeleton is lightweight and simple. It styles only raw HTML elements (with a few exceptions) and provides a responsive grid. Nothing more. 31 | - Around 400 lines of CSS unminified and with comments 32 | - It's a starting point, not a UI framework 33 | - No compiling or installing...just vanilla CSS 34 | 35 | 36 | ## Browser support 37 | 38 | - Chrome latest 39 | - Firefox latest 40 | - Opera latest 41 | - Safari latest 42 | - IE latest 43 | 44 | The above list is non-exhaustive. Skeleton works perfectly with almost all older versions of the browsers above, though IE certainly has large degradation prior to IE9. 45 | 46 | 47 | ## License 48 | 49 | All parts of Skeleton are free to use and abuse under the [open-source MIT license](https://github.com/dhg/Skeleton/blob/master/LICENSE.md). 50 | 51 | 52 | ## Extensions 53 | 54 | The following are extensions to Skeleton built by the community. They are not officially supported, but all have been tested and are compatible with v2.0 (exact release noted): 55 | - [Skeleton on LESS](https://github.com/whatsnewsaes/Skeleton-less): Skeleton built with LESS for easier replacement of grid, color, and media queries. (Last update was to match v2.0.1) 56 | - [Skeleton on Sass](https://github.com/whatsnewsaes/Skeleton-Sass): Skeleton built with Sass for easier replacement of grid, color, and media queries. (Last update was to match v2.0.1) 57 | 58 | Have an extension you want to see here? Just shoot an email to hi@getskeleton.com with your extension! 59 | 60 | 61 | ## Colophon 62 | 63 | Skeleton was built using [Sublime Text 3](http://www.sublimetext.com/3) and designed with [Sketch](http://bohemiancoding.com/sketch). The typeface [Raleway](http://www.google.com/fonts/specimen/Raleway) was created by [Matt McInerney](http://matt.cc/) and [Pablo Impallari](http://www.impallari.com/). Code highlighting by Google's [Prettify library](https://code.google.com/p/google-code-prettify/). Icons in the header of the documentation are all derivative work of icons from [The Noun Project](http://thenounproject.com). [Feather](http://thenounproject.com/term/feather/22073) by Zach VanDeHey, [Pen](http://thenounproject.com/term/pen/21163) (with cap) by Ed Harrison, [Pen](http://thenounproject.com/term/pen/32847) (with clicker) by Matthew Hall, and [Watch](http://thenounproject.com/term/watch/48015) by Julien Deveaux. 64 | 65 | 66 | ## Acknowledgement 67 | 68 | Skeleton was created by [Dave Gamache](https://twitter.com/dhg) for a better web. 69 | -------------------------------------------------------------------------------- /public/lib/skeleton/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skeleton", 3 | "version": "2.0.4", 4 | "homepage": "http://getskeleton.com/", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dhg/Skeleton" 8 | }, 9 | "authors": [ 10 | "Dave Gamache (http://davegamache.com/)" 11 | ], 12 | "description": "Skeleton is a dead-simple, responsive boilerplate to kickstart any responsive project.", 13 | "main": "css/skeleton.css", 14 | "keywords": [ 15 | "css", 16 | "skeleton", 17 | "responsive", 18 | "boilerplate" 19 | ], 20 | "license": "MIT" 21 | } -------------------------------------------------------------------------------- /public/lib/skeleton/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /public/lib/skeleton/css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /public/lib/skeleton/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/build-a-microblogging-app-with-react-and-flux/266bfb84c3e1cb1710fc65f4f20c56341e3d7df3/public/lib/skeleton/images/favicon.png -------------------------------------------------------------------------------- /public/lib/skeleton/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Your page title here :) 9 | 10 | 11 | 12 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 34 |
35 |
36 |
37 |

Basic Page

38 |

This index.html page is a placeholder with the CSS, font and favicon. It's just waiting for you to add some content! If you need some help hit up the Skeleton documentation.

39 |
40 |
41 |
42 | 43 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | .chirp { 2 | list-style-type: none; 3 | padding: 6px; 4 | border: 1px solid #d1d1d1; 5 | border-radius: 4px; 6 | } 7 | 8 | .chirp p { 9 | margin-bottom: 0; 10 | } 11 | 12 | .timestamp { 13 | color: #d1d1d1; 14 | } 15 | 16 | .chirp img { 17 | display: none; 18 | } 19 | 20 | @media (min-width: 550px) { 21 | .chirp img { 22 | display: block; 23 | border-radius: 4px; 24 | width: 100%; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var login = require('./login'); 3 | 4 | express() 5 | .set('view engine', 'ejs') 6 | .use(express.static('./public')) 7 | .use(login.routes) 8 | .use(require('./chirps')) 9 | .get('*', login.required, function (req, res) { 10 | res.render('index', { 11 | user: login.safe(req.user) 12 | }); 13 | }) 14 | .listen(3000); 15 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | var dispatcher = require('./dispatcher'); 2 | var constants = require('./constants'); 3 | 4 | Object.keys(constants).forEach(function (key) { 5 | 6 | var funcName = key.split('_').map(function (word, i) { 7 | if (i === 0) return word.toLowerCase(); 8 | return word[0] + word.slice(1).toLowerCase(); 9 | }).join(''); 10 | 11 | exports[funcName] = function (data) { 12 | dispatcher.dispatch({ 13 | actionType: constants[key], 14 | data: data 15 | }); 16 | }; 17 | }); 18 | 19 | /* 20 | exports.chirp = function (data) { 21 | dispatcher.dispatch({ 22 | actionType: constants.CHIRP, 23 | data: data 24 | }); 25 | }; 26 | */ 27 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | var actions = require('./actions'); 2 | var dispatcher = require('./dispatcher'); 3 | var constants = require('./constants'); 4 | 5 | var API = module.exports = { 6 | fetchChirps: function () { 7 | get('/api/chirps').then(actions.gotChirps.bind(actions)); 8 | }, 9 | fetchUsers: function () { 10 | get('/api/users').then(actions.gotUsers.bind(actions)); 11 | }, 12 | startFetchingChirps: function () { 13 | this.fetchChirps(); 14 | return setInterval(this.fetchChirps, 1000); 15 | }, 16 | startFetchingUsers: function (){ 17 | this.fetchUsers(); 18 | return setInterval(this.fetchUsers, 5000); 19 | }, 20 | saveChirp: function (text) { 21 | text = text.trim(); 22 | if(text === '') return; 23 | 24 | post('/api/chirps', { text: text }).then(actions.chirped.bind(actions)); 25 | }, 26 | follow: function (id) { 27 | post('/api/follow/' + id).then(actions.followed.bind(actions)); 28 | }, 29 | unfollow: function (id) { 30 | post('/api/unfollow/' + id).then(actions.unfollowed.bind(actions)); 31 | } 32 | }; 33 | 34 | 35 | function get(url) { 36 | return fetch(url, { 37 | credentials: 'same-origin' 38 | }).then(function (res) { 39 | return res.json(); 40 | }); 41 | } 42 | 43 | function post(url, body) { 44 | return fetch(url, { 45 | method: 'POST', 46 | credentials: 'include', 47 | body: JSON.stringify(body || {}), 48 | headers: { 49 | 'Content-Type' : 'application/json', 50 | 'Accept': 'application/json' 51 | } 52 | }).then(function (res) { 53 | return res.json(); 54 | }); 55 | } 56 | 57 | dispatcher.register(function (action) { 58 | switch (action.actionType) { 59 | case constants.CHIRP: 60 | API.saveChirp(action.data); 61 | break; 62 | case constants.FOLLOW: 63 | API.follow(action.data); 64 | break; 65 | case constants.UNFOLLOW: 66 | API.unfollow(action.data); 67 | } 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var RouteHandler = require('react-router').RouteHandler; 3 | 4 | var Navigation = require('./Navigation'); 5 | 6 | var App = React.createClass({ 7 | render: function () { 8 | return (
9 |
10 |

Chirper

11 |
12 | 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 |
); 23 | } 24 | }); 25 | 26 | module.exports = App; 27 | -------------------------------------------------------------------------------- /src/components/ChirpBox.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var utils = require('../utils'); 3 | var Link = require('react-router').Link; 4 | var moment = require('moment'); 5 | 6 | // user 7 | // timestamp 8 | // 9 | // text || buttons 10 | 11 | var ChirpBox = React.createClass({ 12 | render: function () { 13 | var user = this.props.user; 14 | 15 | var timestamp = this.props.timestamp ? 16 | ' ' + String.fromCharCode(8226) + ' ' + this.props.timestamp : 17 | ''; 18 | 19 | var id = (typeof user.userId === 'number') ? user.userId : user.cid; 20 | 21 | return (
  • 22 | 23 | 24 | 25 |
    26 |

    27 | {user.fullname} 28 | 29 | @{user.username} {timestamp} 30 | 31 |

    32 |

    {this.props.children}

    33 |
    34 |
  • ); 35 | } 36 | }); 37 | 38 | module.exports = ChirpBox; 39 | -------------------------------------------------------------------------------- /src/components/ChirpInput.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var ChirpInput = React.createClass({ 4 | getInitialState: function () { 5 | return { 6 | value: '' 7 | }; 8 | }, 9 | render: function () { 10 | return (
    11 |
    12 | 18 |
    19 |
    20 | 25 |
    26 |
    ); 27 | }, 28 | handleChange: function (evt) { 29 | this.setState({ 30 | value: evt.target.value 31 | }); 32 | }, 33 | handleClick: function (evt) { 34 | this.props.onSave(this.state.value); 35 | this.setState({ 36 | value: '' 37 | }); 38 | } 39 | }); 40 | 41 | module.exports = ChirpInput; 42 | -------------------------------------------------------------------------------- /src/components/ChirpList.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Box = require('./ChirpBox'); 3 | var moment = require('moment'); 4 | 5 | var UserStore = require('../stores/users'); 6 | 7 | var ChirpList = React.createClass({ 8 | render: function () { 9 | var items = this.props.chirps.map(function (chirp) { 10 | return 13 | {chirp.text} 14 | ; 15 | }); 16 | return
      {items}
    ; 17 | } 18 | }); 19 | 20 | module.exports = ChirpList; 21 | -------------------------------------------------------------------------------- /src/components/FollowButton.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var actions = require('../actions'); 3 | var UserStore = require('../stores/users'); 4 | 5 | var FollowButton = module.exports = React.createClass({ 6 | getInitialState: function () { 7 | return { 8 | id: UserStore.currentUser.cid, 9 | currentlyFollowing: UserStore.currentUser.following 10 | }; 11 | }, 12 | mixins: [UserStore.mixin()], 13 | render: function () { 14 | if (this.state.id === this.props.userId) return This is you! ; 15 | 16 | var text, action; 17 | 18 | if (this.state.currentlyFollowing.indexOf(this.props.userId) > -1) { 19 | text = 'Unfollow'; 20 | action = this.unfollow; 21 | } else { 22 | text = 'Follow'; 23 | action = this.follow; 24 | } 25 | 26 | return ; 27 | }, 28 | unfollow: function () { 29 | actions.unfollow(this.props.userId); 30 | }, 31 | follow: function () { 32 | actions.follow(this.props.userId); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var actions = require('../actions'); 3 | var ChirpInput = require('./ChirpInput'); 4 | var ChirpList = require('./ChirpList'); 5 | var ChirpStore = require('../stores/chirps'); 6 | 7 | var Home = React.createClass({ 8 | getInitialState: function () { 9 | return { 10 | chirps: ChirpStore.timeline() 11 | }; 12 | }, 13 | mixins: [ChirpStore.mixin()], 14 | render: function () { 15 | return
    16 | 17 | 18 |
    ; 19 | }, 20 | saveChirp: function (text) { 21 | actions.chirp(text); 22 | } 23 | }); 24 | 25 | module.exports = Home; 26 | -------------------------------------------------------------------------------- /src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Link = require('react-router').Link; 3 | var UserStore = require('../stores/users'); 4 | 5 | var Navigation = module.exports = React.createClass({ 6 | getInitialState: function () { 7 | return { 8 | username: UserStore.currentUser.username 9 | }; 10 | }, 11 | render: function () { 12 | return
      13 |
    • Timeline
    • 14 |
    • Users
    • 15 |
    • Logout ({this.state.username})
    • 16 |
    ; 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/UserList.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var UserStore = require('../stores/users'); 3 | var actions = require('../actions'); 4 | var Link = require('react-router').Link; 5 | 6 | var Box = require('./ChirpBox'); 7 | var FollowButton = require('./FollowButton'); 8 | 9 | var UserList = module.exports = React.createClass({ 10 | getInitialState: function () { 11 | return { 12 | users: UserStore.all(), 13 | user: UserStore.currentUser 14 | }; 15 | }, 16 | mixins: [UserStore.mixin()], 17 | render: function () { 18 | var items = this.state.users.filter(function (user) { 19 | return this.state.user.cid !== user.cid; 20 | }.bind(this)).map(function (user) { 21 | return 22 | 23 | ; 24 | }); 25 | 26 | return
      {items}
    ; 27 | 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/UserProfile.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var UserStore = require('../stores/users'); 3 | var ChirpStore = require('../stores/chirps'); 4 | var utils = require('../utils'); 5 | var FollowButton = require('./FollowButton'); 6 | 7 | var UserProfile = module.exports = React.createClass({ 8 | getInitialState: function () { 9 | var id = parseInt(this.props.params.id, 10); 10 | 11 | return { 12 | user: UserStore.get(id) || {}, 13 | chirps: ChirpStore.byUserId(id) 14 | }; 15 | }, 16 | mixins: [UserStore.mixin(), ChirpStore.mixin()], 17 | render: function () { 18 | console.log("UserProfile Rendering"); 19 | var chirps = this.state.chirps.map(function (chirp) { 20 | return
  • {chirp.text}
  • ; 21 | }); 22 | 23 | return (
    24 | 25 |
    26 | 27 |

    {this.state.user.fullname}

    28 |

    @{this.state.user.username}

    29 | 30 |

    31 | 32 |
      33 | {chirps} 34 |
    35 |
    36 |
    ); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CHIRP: 'CHIRP', 3 | CHIRPED: 'CHIRPED', 4 | GOT_CHIRPS: 'GOT_CHIRPS', 5 | 6 | GOT_CURRENT_USER: 'GOT_CURRENT_USER', 7 | GOT_USERS: 'GOT_USERS', 8 | 9 | FOLLOW: 'FOLLOW', 10 | FOLLOWED: 'FOLLOWED', 11 | 12 | UNFOLLOW: 'UNFOLLOW', 13 | UNFOLLOWED: 'UNFOLLOWED' 14 | }; 15 | -------------------------------------------------------------------------------- /src/dispatcher.js: -------------------------------------------------------------------------------- 1 | var flux = require('flux'); 2 | 3 | var dispatcher = module.exports = new flux.Dispatcher(); 4 | 5 | dispatcher.register(function (action) { 6 | console.log(action); 7 | }); 8 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactRouter = require('react-router'); 3 | var Route = ReactRouter.Route; 4 | var API = require("./api"); 5 | 6 | var routes = ( 7 | 8 | 9 | 10 | ); 11 | 12 | API.startFetchingChirps(); 13 | API.startFetchingUsers(); 14 | 15 | ReactRouter.run(routes, ReactRouter.HistoryLocation, function (Root) { 16 | React.render(, document.getElementById('app')); 17 | }); 18 | -------------------------------------------------------------------------------- /src/stores/chirps.js: -------------------------------------------------------------------------------- 1 | var constants = require('../constants'); 2 | var UserStore = require('./users'); 3 | 4 | var ChirpStore = module.exports = require('./store').extend({ 5 | init: function () { 6 | this.bind(constants.GOT_CHIRPS, this.set); 7 | this.bind(constants.CHIRPED, this.add); 8 | }, 9 | timeline: function () { 10 | var ids = [UserStore.currentUser.cid].concat(UserStore.currentUser.following); 11 | return this._data.filter(function (chirp) { 12 | return ids.indexOf(chirp.userId) > -1; 13 | }); 14 | }, 15 | byUserId: function (id) { 16 | return this._data.filter(function (chirp) { 17 | return chirp.userId === id; 18 | }); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/stores/store.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign'); 2 | var EventEmitterProto = require('events').EventEmitter.prototype; 3 | var CHANGE_EVENT = 'CHANGE'; 4 | 5 | var storeMethods = { 6 | init: function () {}, 7 | set: function (arr) { 8 | var currIds = this._data.map(function (m) { return m.cid; }); 9 | 10 | arr.filter(function (item) { 11 | return currIds.indexOf(item.cid) === -1; 12 | }).forEach(this.add.bind(this)); 13 | 14 | this.sort(); 15 | }, 16 | add: function (item) { 17 | this._data.push(item); 18 | this.sort(); 19 | }, 20 | sort: function () { 21 | this._data.sort(function (a, b) { 22 | return +new Date(b.$created) - +new Date(a.$created); 23 | }); 24 | }, 25 | all: function () { 26 | return this._data; 27 | }, 28 | get: function (id) { 29 | return this._data.filter(function (item) { 30 | return item.cid === id; 31 | })[0]; 32 | }, 33 | addChangeListener: function (fn) { 34 | this.on(CHANGE_EVENT, fn); 35 | }, 36 | removeChangeListener: function (fn) { 37 | this.removeListener(CHANGE_EVENT, fn); 38 | }, 39 | emitChange: function () { 40 | this.emit(CHANGE_EVENT); 41 | }, 42 | bind: function (actionType, actionFn) { 43 | if (this.actions[actionType]) { 44 | this.actions[actionType].push(actionFn); 45 | } else { 46 | this.actions[actionType] = [actionFn]; 47 | } 48 | } 49 | }; 50 | 51 | var num = 1; 52 | 53 | exports.extend = function (methods) { 54 | var store = { 55 | _data: [], 56 | actions: {}, 57 | mixin: function () { 58 | var n = num++; 59 | var obj = { 60 | componentDidMount: function () { 61 | store.addChangeListener(this['onChange' + n]); 62 | }, 63 | componentWillUnmount: function () { 64 | store.removeChangeListener(this['onChange' + n]); 65 | } 66 | }; 67 | 68 | obj['onChange' + n] = function () { 69 | this.setState(this.getInitialState()); 70 | }; 71 | return obj; 72 | } 73 | }; 74 | 75 | assign(store, EventEmitterProto, storeMethods, methods); 76 | 77 | store.init(); 78 | 79 | require('../dispatcher').register(function (action) { 80 | if (store.actions[action.actionType]) { 81 | store.actions[action.actionType].forEach(function (fn) { 82 | fn.call(store, action.data); 83 | store.emitChange(); 84 | }); 85 | } 86 | }); 87 | 88 | return store; 89 | }; 90 | -------------------------------------------------------------------------------- /src/stores/users.js: -------------------------------------------------------------------------------- 1 | var constants = require('../constants'); 2 | 3 | var UserStore = module.exports = require('./store').extend({ 4 | init: function () { 5 | this.bind(constants.GOT_USERS, this.set); 6 | this.bind(constants.FOLLOWED, this.updateUser); 7 | this.bind(constants.UNFOLLOWED, this.updateUser); 8 | }, 9 | currentUser: USER, 10 | updateUser: function (data) { 11 | this.currentUser = data; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | exports.avatar = function (email) { 4 | if (!email) return ''; 5 | 6 | email = crypto.createHash('md5').update(email).digest('hex'); 7 | 8 | return 'http://www.gravatar.com/avatar/' + email + '?s=92'; 9 | }; 10 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chirper App 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chirper App 5 | 6 | 7 | 8 | 9 | 10 |
    11 |
    12 |

    Chirper

    13 |
    14 | 15 |
    16 | 17 |
    18 |

    Log In

    19 | 20 |
    21 |

    22 |

    23 | 24 |

    25 |
    26 |
    27 | 28 |
    29 |

    Sign Up

    30 | 31 |
    32 |

    33 |

    34 |

    35 |

    36 | 37 |

    38 |
    39 |
    40 |
    41 |
    42 | 43 | 44 | --------------------------------------------------------------------------------