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;