diff --git a/app/components/nav/nav.js b/app/components/nav/nav.js new file mode 100644 index 0000000..b273da5 --- /dev/null +++ b/app/components/nav/nav.js @@ -0,0 +1,82 @@ +var React = require('react/addons'); +var $ = require('jquery'); + +var Nav = React.createClass({ + getInitialState: function () { + return { + hideMenu: true, + isLogin: false, + user: null + } + }, + componentDidMount: function () { + var self = this; + $.get('/api/login/getStatus') + .then(function (result) { + self.setState({ + isLogin: result.isLogin, + user: result.user + }); + }); + }, + handleToggleMenu: function () { + this.setState({ + hideMenu: !this.state.hideMenu + }) + }, + render: function () { + var cx = React.addons.classSet; + var logoClass = cx({ + 'nav-logo': true, + 'hide': !this.state.hideMenu + }); + var menuClass = cx({ + 'nav-menu': true, + 'hide': this.state.hideMenu + }); + + return ( + + ); + } + }) + ; + +module.exports = Nav; \ No newline at end of file diff --git a/app/main.js b/app/main.js index 67ce86e..40b1e7c 100644 --- a/app/main.js +++ b/app/main.js @@ -2,9 +2,13 @@ var React = require('react'); // Related Control -var PostBox = require('./components/post/postbox'); +var PostBox = require('./components/post/PostBox'); +var Nav = require('./components/nav/nav'); React.render( - , - document.getElementById('content') +
+
+ , document.getElementById('content') ); diff --git a/data/seed.js b/data/seed.js index 861a8eb..73dc196 100644 --- a/data/seed.js +++ b/data/seed.js @@ -7,7 +7,10 @@ Models.sequelize.sync({force: true}).then(function () { // Add new User Models.User.create({ name: 'keanyc', - nickname: 'KeaNy' + nickname: 'KeaNy', + provider: 'Facebook', + loginId: '10204525184038018', + photo: 'https://graph.facebook.com/keanyc/picture?width=120&height=120' }) .then(function (user) { diff --git a/models/user.js b/models/user.js index c588f12..9e455be 100644 --- a/models/user.js +++ b/models/user.js @@ -3,7 +3,10 @@ module.exports = function (sequelize, DataTypes) { "User", { name: DataTypes.STRING, - nickname: DataTypes.STRING + nickname: DataTypes.STRING, + provider: DataTypes.ENUM('Local', 'Facebook'), + loginId: DataTypes.STRING, + photo: DataTypes.STRING }, { classMethods: { diff --git a/package.json b/package.json index aaeec05..5212b3f 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,12 @@ "body-parser": "~1.0.1", "connect-browserify": "^4.0.0", "express": "~4.0.0", + "express-session": "^1.11.2", "jquery": "^2.1.4", "mysql": "git://github.com/felixge/node-mysql.git", "node-jsx": "^0.13.3", + "passport": "^0.2.2", + "passport-facebook": "^2.0.0", "react": "^0.13.3", "react-ago-component": "^0.6.1", "reactify": "^1.1.1", diff --git a/public/css/base.css b/public/css/base.css index e1b4a97..59b8899 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -1,5 +1,5 @@ body { - font-family: "Microsoft Jhenghei", "ff-tisa-web-pro",Georgia,Cambria,"Times New Roman",Times,serif; + font-family: "Microsoft Jhenghei", "ff-tisa-web-pro", Georgia, Cambria, "Times New Roman", Times, serif; } a { @@ -8,84 +8,194 @@ a { } h1 { - font-family: Georgia,Cambria,"Times New Roman",Times,serif; - font-size: 50px; - text-shadow: 0 1px 3px rgba(0,0,0,0.3); - margin-bottom: 30px; - margin-top: 12px; - text-align: center; + font-family: Georgia, Cambria, "Times New Roman", Times, serif; + font-size: 50px; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + margin-bottom: 30px; + margin-top: 12px; + text-align: center; } h2 { - padding: 0; - margin-top: 16px; - margin-bottom: 4px; + padding: 0; + margin-top: 16px; + margin-bottom: 4px; } h3 { - padding: 0; - margin: 0; - color: rgba(0,0,0,0.3); - font-weight: normal; + padding: 0; + margin: 0; + color: rgba(0, 0, 0, 0.3); + font-weight: normal; +} + +p, ul { + margin: 0; } #content { - width: 100%; - max-width: 700px; - overflow: hidden; - margin: 0 auto; + width: 100%; + max-width: 700px; + overflow: hidden; + margin: 0 auto; } img { - max-width: 100%; + max-width: 100%; +} + +.show{ + display: block; } +.hide { + display: none; +} + +/* Post */ .post { - border-bottom: 1px solid #ccc; - margin-bottom: 50px; - padding-bottom: 50px; + border-bottom: 1px solid #ccc; + margin-bottom: 50px; + padding-bottom: 50px; } .postAuthor { - margin-top: 4px; + margin-top: 4px; } .postTitle { - font-size: 42px; + font-size: 42px; } .postContent { - /*display: none;*/ - font-size: 18px; - line-height: 30px; - margin-top: 30px; - overflow: hidden; - height: 90px; + /*display: none;*/ + font-size: 18px; + line-height: 30px; + margin-top: 30px; + overflow: hidden; + height: 90px; } .postMetaInline-avatar { - width: 36px; - height: 36px; - display: table-cell; + width: 36px; + height: 36px; + display: table-cell; } .avatar-image { - width: 100%; - height: 100%; - border-radius: 100%; + width: 100%; + height: 100%; + border-radius: 100%; } .postMetaInline-feedSummary { - display: table-cell; - vertical-align: middle; - font-size: 14px; - line-height: 1.4; - padding-left: 10px; + display: table-cell; + vertical-align: middle; + font-size: 14px; + line-height: 1.4; + padding-left: 10px; } .postMetaInline--supplemental { - display: block; - color: rgba(0,0,0,0.3); - font-size: 12px; - line-height: 1.1; + display: block; + color: rgba(0, 0, 0, 0.3); + font-size: 12px; + line-height: 1.1; +} + +/* Nav */ +.nav { + position: absolute; + top: 0; + left: 0; + bottom: 0; + outline: 0; + background-color: #232322; + z-index: 300; +} + +.nav-logo { + box-sizing: initial; + cursor: pointer; + position: absolute; + top: 10px; + left: 10px; + padding: 8px; + height: 26px; + width: 26px; + z-index: 700; + background-color: #333332; + text-align: center; + border: 0; + -webkit-border-radius: 100%; + -moz-border-radius: 100%; + border-radius: 100%; +} + +.nav-logo h1 { + font-family: Georgia, Cambria, "Times New Roman", Times, serif; + margin: 0; + padding: 0; + line-height: 26px; + font-size: 22px; + color: #FFF; +} + +.nav-menu { + text-align: left; + background: #ccc; + width: 280px; + height: 100%; +} + +.nav-menu-list { + list-style-type: none; + padding-top: 10px; + padding-left: 20px; +} + +.nav-menu-item { + padding: 5px; + line-height: 24px; + color: #555; + font-size: 14px; + font-weight: bold; +} + +.nav-menu-title { + line-height: 24px; + vertical-align: text-bottom; +} + +/* Icon */ +.icon { + font-style: normal; + display: inline-block; + vertical-align: baseline; + width: 24px; + height: 24px; + margin-right: 15px; } + +.icons-search { + background-image: url(../img/search.png); + background-size: 18px 18px !important; + background-repeat: no-repeat; +} + +.icons-keanux { + color: #000; + font-family: Georgia, Cambria, "Times New Roman", Times, serif; + font-size: 22px; + font-weight: bold; +} + +.icons-avatar { + color: #FFF; +} + +.icons-avatar img { + -webkit-border-radius: 100%; + -moz-border-radius: 100%; + border-radius: 100%; +} \ No newline at end of file diff --git a/routes/api.js b/routes/api.js index 933e39c..81a549d 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1,6 +1,62 @@ // Express Related Library var express = require('express'); +// Passport Library +var passport = require('passport'); +var FacebookStrategy = require('passport-facebook').Strategy; + +var FACEBOOK_APP_ID = "FACEBOOK_APP_ID"; +var FACEBOOK_APP_SECRET = "FACEBOOK_APP_SECRET"; + +// Passport session setup. +// To support persistent login sessions, Passport needs to be able to +// serialize users into and deserialize users out of the session. Typically, +// this will be as simple as storing the user ID when serializing, and finding +// the user by ID when deserializing. However, since this example does not +// have a database of user records, the complete Facebook profile is serialized +// and deserialized. +passport.serializeUser(function (user, done) { + done(null, user); +}); + +passport.deserializeUser(function (obj, done) { + done(null, obj); +}); + +// Use the FacebookStrategy within Passport. +// Strategies in Passport require a `verify` function, which accept +// credentials (in this case, an accessToken, refreshToken, and Facebook +// profile), and invoke a callback with a user object. +passport.use(new FacebookStrategy({ + clientID: FACEBOOK_APP_ID, + clientSecret: FACEBOOK_APP_SECRET, + callbackURL: "http://localhost:8080/api/login/facebook/callback", + profileFields: ['id', 'name', 'displayName', 'photos'] + }, + function (accessToken, refreshToken, profile, done) { + // asynchronous verification, for effect... + process.nextTick(function () { + Models.User + .findOrCreate({ + where: { + provider: 'Facebook', + loginId: profile.id + }, + defaults: { + name: profile.name.familyName + profile.name.givenName, + nickname: profile.displayName, + provider: 'Facebook', + loginId: profile.id, + photo: profile.photos[0].value + } + }) + .spread(function (user, created) { + return done(null, user); + }); + }); + } +)); + // Models var Models = require('../models'); @@ -15,21 +71,52 @@ router.get('/', function (req, res) { // To get all user data router.get('/users', function (req, res) { Models.User.findAll() - .then(function(users){ + .then(function (users) { res.json(users); - }, function(err){ + }, function (err) { throw err; }) }); // To get all posts router.get('/posts', function (req, res) { - Models.Post.findAll({ include: [{ model: Models.User, required: true}]}) - .then(function(posts){ - res.json({ data: posts}); - }, function(err){ + Models.Post.findAll({include: [{model: Models.User, required: true}]}) + .then(function (posts) { + res.json({data: posts}); + }, function (err) { throw err; }); }); +// GET /login/facebook +// Use passport.authenticate() as route middleware to authenticate the +// request. The first step in Facebook authentication will involve +// redirecting the user to facebook.com. After authorization, Facebook will +// redirect the user back to this application at /login/facebook/callback +router.get('/login/facebook', + passport.authenticate('facebook'), + function (req, res) { + // The request will be redirected to Facebook for authentication, so this + // function will not be called. + }); + +// GET /login/facebook/callback +// Use passport.authenticate() as route middleware to authenticate the +// request. If authentication fails, the user will be redirected back to the +// login page. Otherwise, the primary route function function will be called, +// which, in this example, will redirect the user to the home page. +router.get('/login/facebook/callback', + passport.authenticate('facebook', {failureRedirect: '/login'}), + function (req, res) { + res.redirect('/'); + }); + +// GET /login/getStatus +router.get('/login/getStatus', function (req, res) { + res.json({ + isLogin: req.isAuthenticated(), + user: req.user + }) +}); + module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js index 23c9acf..8261b37 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,12 @@ // Express Related Library var express = require('express'); +var session = require('express-session'); var bodyParser = require('body-parser'); var browserify = require('connect-browserify'); +// Passport Library +var passport = require('passport'); + // React Related Library var reactify = require('reactify'); var React = require('react'); @@ -14,17 +18,22 @@ nodeJsx.install({extension: '.jsx'}); var app = express(); app.use(bodyParser.urlencoded({extended: true})); app.use(bodyParser.json()); +app.use(session({secret: 'keyboard cat'})); +// Initialize Passport! Also use passport.session() middleware, to support +// persistent login sessions (recommended). +app.use(passport.initialize()); +app.use(passport.session()); app.use(express.static('public')); // Register Route and Bundle.js var apiRoute = require('./routes/api'); app.use('/api', apiRoute) - .use('/bundle.js', browserify.serve({ - entry: __dirname + '/app/main', - debug: true, - watch: true, - transforms: [reactify] - })); + .use('/bundle.js', browserify.serve({ + entry: __dirname + '/app/main', + debug: true, + watch: true, + transforms: [reactify] + })); // Start application var port = process.env.PORT || 8080;