├── Procfile ├── .gitignore ├── .babelrc ├── .DS_Store ├── test ├── mocha.opts ├── setup.js ├── client-page-test.js ├── DBhandler-test.js ├── client-lib-utils-test.js └── server-test.js ├── controller ├── .DS_Store ├── routes │ ├── view.js │ └── login.js ├── api.js └── DBhandler.js ├── views ├── index.ejs ├── error.ejs ├── footer.ejs └── header.ejs ├── dist ├── fonts │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── home.chunk.js ├── list.chunk.js ├── new.chunk.js ├── loading.css ├── vendor │ ├── react.min.js │ └── bootstrap.min.js └── vote.bundle.js ├── .travis.yml ├── client ├── components │ ├── loading.jsx │ ├── spning.jsx │ ├── footer.jsx │ ├── BottomLoader.js │ ├── transition.jsx │ └── header.jsx ├── index.jsx ├── list.jsx ├── home.jsx ├── lib │ └── utils.js ├── detail.jsx └── new.jsx ├── start.sh ├── index.js ├── serverConfig.js ├── package.json ├── webpack.config.js ├── .eslintrc └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | data 3 | coverage 4 | .DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-0", "react"] 3 | } -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elevenbeans/we-voting/HEAD/.DS_Store -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | --require ./test/setup.js -------------------------------------------------------------------------------- /controller/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elevenbeans/we-voting/HEAD/controller/.DS_Store -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header') -%> 2 |
3 | 4 | <%- include('footer') -%> 5 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header') -%> 2 | 3 |
4 | no this page! 5 | <%- include('footer') -%> 6 | -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elevenbeans/we-voting/HEAD/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elevenbeans/we-voting/HEAD/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elevenbeans/we-voting/HEAD/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | branches: 5 | only: 6 | - master 7 | install: 8 | - npm install 9 | script: 10 | - npm test 11 | after_success: 12 | - npm run coveralls 13 | services: mongodb -------------------------------------------------------------------------------- /views/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 2 | // console.log(dom.window.document.querySelector("p").textContent); // "Hello world" 3 | 4 | const jsdom = require("jsdom"); 5 | 6 | const { JSDOM } = jsdom; 7 | const dom = new JSDOM(``); 8 | 9 | const { window } = dom; 10 | 11 | // console.log(typeof document === 'undefined'); 12 | 13 | if (typeof document === 'undefined') { 14 | global.window = window; 15 | global.document = window.document; 16 | global.navigator = window.navigator; 17 | } -------------------------------------------------------------------------------- /client/components/loading.jsx: -------------------------------------------------------------------------------- 1 | const Loading = () => ( 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ) 20 | 21 | export default Loading; -------------------------------------------------------------------------------- /client/components/spning.jsx: -------------------------------------------------------------------------------- 1 | const Spning = () => ( 2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ) 32 | 33 | export default Spning; -------------------------------------------------------------------------------- /client/components/footer.jsx: -------------------------------------------------------------------------------- 1 | const Footer = () => ( 2 |
3 |
4 | 22 |
23 | ) 24 | 25 | export default Footer 26 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | # compile resource file 2 | echo '\033[0;32;1m######################\033[0m' 3 | echo '\033[0;32;1mcompiling resource ...\033[0m' 4 | echo '\033[0;32;1m######################\033[0m' 5 | 6 | npm run bundle 7 | 8 | # watch resource file 9 | echo '\033[0;32;1m#####################################################\033[0m' 10 | echo '\033[0;32;1mfinished compiling, now start server and watching ...\033[0m' 11 | echo '\033[0;32;1m#####################################################\033[0m' 12 | 13 | echo '\033[0;33;1msee it in http://localhost:8088\033[0m' 14 | 15 | npm run start & 16 | 17 | npm run watch -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var compression = require('compression'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var api = require('./controller/api'); 9 | var view = require('./controller/routes/view'); 10 | var login = require('./controller/routes/login'); 11 | 12 | app.set('port', (process.env.PORT || 5000)); 13 | 14 | // views is directory for all template files 15 | app.set('views', __dirname + '/views'); 16 | app.set('view engine', 'ejs'); 17 | 18 | app.use(compression()); 19 | 20 | app.use(express.static(__dirname + '/')); 21 | 22 | app.use(bodyParser.json()); 23 | app.use(bodyParser.urlencoded({ extended: false })); 24 | 25 | app.use('/api', api); 26 | app.use('/login', login); 27 | app.use('/', view); 28 | 29 | if (!process.env.NODE_ENV) { process.env.NODE_ENV = 'dev-HMR'; } 30 | 31 | module.exports = app.listen(app.get('port'), function() { 32 | console.log('Node app is running on port', app.get('port')); 33 | }); 34 | -------------------------------------------------------------------------------- /test/client-page-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import should from 'should'; 4 | import App from '../client/home'; 5 | 6 | // // debugger 7 | // if(process.env.NODE_ENV === 'dev-test'){ // for unit test 8 | // if (typeof userInfo === 'undefined') { 9 | // var userInfo = { 10 | // name:'Guest' 11 | // }; 12 | // } 13 | // } 14 | 15 | // function shallowRender(Component) { 16 | // const renderer = TestUtils.createRenderer(); 17 | // renderer.render(); 18 | // return renderer.getRenderOutput(); 19 | // } 20 | 21 | // describe('Shallow Rendering', function () { 22 | // it('App\'s title should be Todos', function () { 23 | // const app = shallowRender(App); 24 | // // component's shallow rendering has props.children 25 | // // expect(app.props.children[0].type).to.equal('h1'); 26 | // // expect(app.props.children[0].props.children).to.equal('Todos'); 27 | // should.equal('div', app.props.children[0].type); 28 | 29 | // }); 30 | // }); -------------------------------------------------------------------------------- /views/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Let's Vote 6 | 7 | 8 | 9 | 10 | 11 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /controller/routes/view.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var serverConf = require('../../serverConfig'); 5 | var config = serverConf.devConfig; 6 | 7 | console.log('process.env.NODE_ENV in view config::::', process.env.NODE_ENV); 8 | 9 | if (process.env.NODE_ENV === 'pre') config = serverConf.preConfig; 10 | if (process.env.NODE_ENV === 'production') config = serverConf.prdConfig; 11 | 12 | // 该路由使用的中间件 13 | router.use(function timeLog(req, res, next) { 14 | console.log('Time: ', Date.now()); 15 | next(); 16 | }); 17 | 18 | router.get('/detail/:id', throwToHome); 19 | router.get('/detail', throwToHome); 20 | router.get('/list/:name', throwToHome); 21 | router.get('/list', throwToHome); 22 | router.get('/new', throwToHome); 23 | 24 | router.get('/', throwToHome); 25 | 26 | router.get('*', throwToError); 27 | 28 | /** 29 | * filter array. 30 | * @param {obj} request The first number. 31 | * @param {obj} response The first number. 32 | */ 33 | function throwToHome(request, response) { 34 | response.render('index', 35 | { 36 | cdnUrl: config.CDN_URL 37 | } 38 | ); 39 | } 40 | 41 | /** 42 | * filter array. 43 | * @param {obj} request The first number. 44 | * @param {obj} response The first number. 45 | */ 46 | function throwToError(request, response) { 47 | response.render('error', 48 | { 49 | cdnUrl: config.CDN_URL 50 | } 51 | ); 52 | } 53 | 54 | module.exports = router; 55 | -------------------------------------------------------------------------------- /serverConfig.js: -------------------------------------------------------------------------------- 1 | const githubApi = { 2 | 'ACCESS_TOKEN': 'https://github.com/login/oauth/access_token', 3 | 'USER_INFO': 'https://api.github.com/user', 4 | 'AUTHORIZE': 'https://github.com/login/oauth/authorize' 5 | }; 6 | 7 | exports.devConfig = { 8 | 'HOST_URL': 'http://localhost:5000', 9 | 'CDN_URL': 'http://localhost:8088', 10 | 'REDIRECT_URI': 'http://localhost:5000/login/github/callback', 11 | 'CLIENT_ID': '411b91fde0088b2efa2a', 12 | 'CLIENT_SECRET': '1aef5524c34d0f11c2441e1dce2af8afa7d39ee1', 13 | 'GITHUB_API': githubApi, 14 | 'DB_URL': 'mongodb://localhost:27017/voting' 15 | }; 16 | 17 | exports.preConfig = { 18 | 'HOST_URL': 'http://localhost:5000', 19 | 'CDN_URL': 'http://localhost:5000', 20 | 'REDIRECT_URI': 'http://localhost:5000/login/github/callback', 21 | 'CLIENT_ID': '411b91fde0088b2efa2a', 22 | 'CLIENT_SECRET': '1aef5524c34d0f11c2441e1dce2af8afa7d39ee1', 23 | 'GITHUB_API': githubApi, 24 | 'DB_URL': 'mongodb://localhost:27017/voting' 25 | }; 26 | 27 | exports.prdConfig = { 28 | 'HOST_URL': 'https://we-voting-ele.herokuapp.com', 29 | 'CDN_URL': 'https://we-voting-ele.herokuapp.com', 30 | 'REDIRECT_URI': 'https://we-voting-ele.herokuapp.com/login/github/callback', 31 | 'CLIENT_ID': '7d6b761d11f8d943d54f', 32 | 'CLIENT_SECRET': '94e24dac049b6c7b3cdd754db23d6ba24a33e455', 33 | 'GITHUB_API': githubApi, 34 | 'DB_URL': 'mongodb://heroku_h9jz5wwb:nsh3jskaofuh5l4ah7i5cogipe@ds063536.mlab.com:63536/heroku_h9jz5wwb' 35 | }; 36 | -------------------------------------------------------------------------------- /client/components/BottomLoader.js: -------------------------------------------------------------------------------- 1 | class BottomLoader { 2 | constructor(isDebounce) { 3 | this.BUFF = 200; 4 | this.viewportHeight = window.screen.height; 5 | this.DIFF = this.viewportHeight; 6 | this.cbContent = {}; 7 | this.count = 1; 8 | this.last = 0; 9 | this.init(isDebounce); 10 | } 11 | init(isDeb) { 12 | var self = this; 13 | var timer = null; 14 | window.addEventListener('scroll', function() { 15 | if (isDeb) { 16 | if (typeof timer === 'number') { 17 | clearTimeout(timer); 18 | } 19 | timer = setTimeout(function() { 20 | // 添加onscroll事件处理 21 | self.detect(); 22 | }, self.BUFF); 23 | } else { 24 | self.throttle(200, self.detect); 25 | } 26 | }, false); 27 | } 28 | detect() { 29 | var self = this; 30 | var docHeight = document.body.clientHeight; 31 | var scrollTop = document.body.scrollTop; // scroll distance 32 | var elBottomPos = docHeight; 33 | var cbContent = self.cbContent; 34 | if ((self.viewportHeight + scrollTop + cbContent.diff >= elBottomPos)) { 35 | cbContent.callback && cbContent.callback(self.count); 36 | console.log('Loader ' + self.count + ' times'); 37 | self.count++; 38 | } 39 | } 40 | addCallback(callback, config) { 41 | var self = this; 42 | self.cbContent = { 43 | diff: config.diff || self.DIFF, 44 | callback: callback 45 | }; 46 | if (config.immediately) { 47 | self.detect(); 48 | } 49 | } 50 | throttle(delay, action) { 51 | var self = this; 52 | var curr = +new Date(); 53 | if (curr - self.last > delay) { 54 | // console.log('in'); 55 | action.apply(this, arguments); 56 | self.last = curr; 57 | } 58 | } 59 | } 60 | export default BottomLoader; 61 | -------------------------------------------------------------------------------- /controller/api.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express'); 3 | var router = express.Router(); 4 | 5 | // var request = require('request'); 6 | 7 | var serverConf = require('../serverConfig'); 8 | var config = serverConf.devConfig; 9 | 10 | var dbhandler = require('./DBhandler'); 11 | 12 | // console.log('process.env.NODE_ENV in login config::::', process.env.NODE_ENV); 13 | 14 | if (process.env.NODE_ENV === 'production') config = serverConf.prdConfig; 15 | 16 | // 该路由使用的中间件 17 | router.use(function timeLog(req, res, next) { 18 | console.log('Time: ', Date.now()); 19 | next(); 20 | }); 21 | 22 | /** 23 | * filter array 24 | * @param {array} dataArray The first number. 25 | * @return {obj} The sum of the two numbers. 26 | */ 27 | function filterOptions(dataArray) { 28 | return dataArray.map(function(item) { 29 | delete item['options']; 30 | return item; 31 | }); 32 | } 33 | 34 | router.post('/getPollList', function(req, resp) { 35 | if (req.body.userName) { 36 | dbhandler.queryPolls(req.body.userName, req.body.pageSize, req.body.pageNum, 37 | function(data) { 38 | resp.send(filterOptions(data)); 39 | }, 40 | function(err) { 41 | } 42 | ); 43 | } else { // 查询所有存在的 (不为空的) 44 | dbhandler.queryPolls({ $exists: true }, req.body.pageSize, req.body.pageNum, 45 | function(data) { 46 | resp.send(filterOptions(data)); 47 | }, 48 | function(err) { 49 | } 50 | ); 51 | } 52 | }); 53 | 54 | router.post('/getPollByID', function(req, resp) { 55 | dbhandler.queryPollByID(req.body.pollID, 56 | function(data) { 57 | resp.send(data); 58 | }, 59 | function(err) { 60 | } 61 | ); 62 | }); 63 | 64 | router.post('/insertPoll', function(req, resp) { 65 | dbhandler.insertPoll(req.body, 66 | function() { 67 | resp.send({ 68 | 'result': true 69 | }); 70 | } 71 | ); 72 | }); 73 | 74 | router.post('/upDatePollByID', function(req, resp) { 75 | dbhandler.upDatePollByID(req.body, 76 | function() { 77 | resp.send({ 78 | 'result': true 79 | }); 80 | } 81 | ); 82 | }); 83 | 84 | module.exports = router; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wevoting", 3 | "version": "1.0.0", 4 | "description": "A Voting App", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "export NODE_ENV=dev-test && mocha --opts ./test/mocha.opts test/*.js", 8 | "cover": "export NODE_ENV=dev-test && istanbul cover _mocha -- --opts ./test/mocha.opts test/*.js -R nyan", 9 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls", 10 | "start": "node index.js", 11 | "watch": "webpack-dev-server --port 8088 --hide-modules", 12 | "bundle": "webpack --progress --hide-modules", 13 | "build": "export NODE_ENV=production && webpack --progress --hide-modules" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/elevenBeans/WeVoting.git" 18 | }, 19 | "keywords": [ 20 | "voting" 21 | ], 22 | "author": "elevenbeans", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/elevenBeans/WeVoting/issues" 26 | }, 27 | "homepage": "https://github.com/elevenBeans/WeVoting#readme", 28 | "dependencies": { 29 | "body-parser": "^1.17.1", 30 | "compression": "^1.6.2", 31 | "ejs": "^2.5.6", 32 | "express": "^4.15.2", 33 | "mongodb": "^2.2.26", 34 | "react-d3-components": "^0.6.6", 35 | "react-redux": "^5.0.6", 36 | "redux": "^3.7.2", 37 | "request": "^2.81.0" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.21.0", 41 | "babel-eslint": "^7.2.3", 42 | "babel-loader": "^6.2.4", 43 | "babel-polyfill": "^6.20.0", 44 | "babel-preset-es2015": "^6.13.2", 45 | "babel-preset-react": "^6.11.1", 46 | "babel-preset-stage-0": "^6.24.1", 47 | "babel-preset-stage-1": "^6.13.0", 48 | "coveralls": "^2.13.1", 49 | "eslint": "^4.11.0", 50 | "eslint-loader": "^1.9.0", 51 | "eslint-plugin-react": "^7.5.1", 52 | "file-loader": "^0.11.1", 53 | "istanbul": "1.0.0-alpha.2", 54 | "jsdom": "^11.0.0", 55 | "jsx-loader": "^0.13.2", 56 | "mocha": "^3.4.1", 57 | "nodemon": "^1.8.1", 58 | "react": "^15.4.1", 59 | "react-addons-test-utils": "^15.5.1", 60 | "react-dom": "^15.4.1", 61 | "react-router": "^3.0.0", 62 | "react-router-transition": "0.0.6", 63 | "should": "^11.2.1", 64 | "supertest": "^3.0.0", 65 | "url-loader": "^0.5.7", 66 | "webpack": "^1.13.1", 67 | "webpack-dev-server": "^1.16.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Router, browserHistory } from 'react-router'; 5 | 6 | import { RouteTransition, presets } from 'react-router-transition'; 7 | 8 | import Header from './components/header'; 9 | 10 | // import Home from './home'; // Load on demand 11 | // import Detail from './detail'; 12 | // import List from './list'; 13 | // import New from './new'; 14 | 15 | if (module.hot && process.env.NODE_ENV === 'dev-HMR') module.hot.accept(); 16 | 17 | var styles = presets.slideLeft; 18 | 19 | const App = function({ children, location }) { 20 | styles = location.action === 'POP' ? presets.slideRight : presets.slideLeft; 21 | return ( 22 |
23 |
24 | 30 | {children} 31 | 32 |
33 | ); 34 | }; 35 | 36 | const rootRoute = { 37 | path: '/', 38 | component: App, 39 | indexRoute: { 40 | getComponent( nextState, callback ){ 41 | require.ensure([], require => { 42 | callback('', require('home').default); 43 | },'home'); 44 | } 45 | }, 46 | childRoutes: [ 47 | { 48 | path:'new', 49 | getComponent( nextState, callback ){ 50 | $('#globalTransition').css('display', 'block'); 51 | require.ensure([], require => { 52 | callback(null, require('new').default); 53 | }, 'new'); 54 | } 55 | }, 56 | { 57 | path:'list(/:name)', 58 | getComponent(nextState, callback){ 59 | $('#globalTransition').css('display', 'block'); 60 | require.ensure([], require => { 61 | callback(null, require('list').default); 62 | }, 'list'); 63 | } 64 | }, 65 | { 66 | path:'detail(/:id)', 67 | getComponent(nextState, callback){ 68 | $('#globalTransition').css('display', 'block'); 69 | require.ensure([],require => { 70 | callback(null, require('detail').default); 71 | }, 'detail'); 72 | } 73 | } 74 | ] 75 | }; 76 | 77 | render( 78 | ( 79 | 84 | 85 | ), document.getElementById('app') 86 | ); 87 | -------------------------------------------------------------------------------- /client/list.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Component } from 'react'; 4 | import { Link } from 'react-router'; 5 | 6 | import Loading from './components/loading'; 7 | 8 | import Footer from './components/footer'; 9 | 10 | import BottomLoader from './components/BottomLoader'; 11 | 12 | 13 | class List extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.props.params.pageNum=1; 18 | this.state = { 19 | loadingPop: true, 20 | pollList: [] 21 | }; 22 | } 23 | componentDidMount() { 24 | $('#globalTransition').css('display', 'none'); 25 | //滚动监听事件处理 26 | const that=this; 27 | const bl = new BottomLoader(this); 28 | let name=that.props.params.name; 29 | let pageNum=that.props.params.pageNum; 30 | bl.addCallback(function(){ // debouce 31 | if(name === userInfo.name){ 32 | that.fetchPollList(pageNum,name); 33 | }else{ 34 | that.fetchPollList(pageNum); 35 | } 36 | pageNum++; 37 | },{ 38 | diff:300, // 触发距离(距离底部) 39 | immediately:true 40 | }); 41 | } 42 | fetchPollList(pageNum,userName){ 43 | $.ajax({ 44 | type: "POST", 45 | url: '/api/getPollList', 46 | async: true, 47 | contentType: "application/json;charset=utf-8", 48 | data: JSON.stringify({'userName': userName,'pageSize': 5, 'pageNum':pageNum }), 49 | dataType: 'json', 50 | success: function (data) { 51 | if(data && data.length !== 0) { 52 | this.setState({ 53 | pollList: this.state.pollList.concat(data), 54 | loadingPop: false 55 | }); 56 | } else { 57 | this.setState({ 58 | loadingPop: false 59 | }); 60 | } 61 | }.bind(this) 62 | }); 63 | } 64 | 65 | render() { 66 | return ( 67 |
68 | {this.state.pollList.map((item)=>( 69 | 70 |
71 |
72 |

{item.title}

73 |
74 |
75 | {item.description} 76 |
77 |
Created by {item.ownerName}
78 |
79 | 80 | ))} 81 | {this.state.loadingPop && } 82 | {!this.state.loadingPop && 83 | this.state.pollList.length === 0 && 84 |
no result ~
85 | } 86 |
87 |
88 | ); 89 | } 90 | 91 | } 92 | 93 | export default List; 94 | -------------------------------------------------------------------------------- /controller/routes/login.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var request = require('request'); 5 | 6 | var serverConf = require('../../serverConfig'); 7 | var config = serverConf.devConfig; 8 | 9 | var dbhandler = require('../DBhandler'); 10 | 11 | console.log('process.env.NODE_ENV in login config::::', process.env.NODE_ENV); 12 | 13 | if (process.env.NODE_ENV === 'production') config = serverConf.prdConfig; 14 | 15 | // 该路由使用的中间件 16 | router.use(function timeLog(req, res, next) { 17 | console.log('Time: ', Date.now()); 18 | next(); 19 | }); 20 | 21 | var _currentPath = ''; 22 | 23 | router.get('/github/callback', function(req, resp) { 24 | request( 25 | { 26 | url: config.GITHUB_API.ACCESS_TOKEN, 27 | form: { 28 | client_id: config.CLIENT_ID, 29 | client_secret: config.CLIENT_SECRET, 30 | code: req.query.code, 31 | redirect_uri: config.REDIRECT_URI, 32 | state: req.query.state 33 | } 34 | }, 35 | function(err, response, body) { 36 | request( 37 | { 38 | url: config.GITHUB_API.USER_INFO + '?' + body, 39 | headers: { 40 | 'User-Agent': 'request' 41 | } 42 | }, 43 | function(error, res, data) { 44 | // get usrInfo 45 | data = JSON.parse(data); 46 | function toBase64(str) { 47 | if (typeof(str) !== 'string') str = str + ''; 48 | var _temp = new Buffer(str); 49 | return _temp.toString('base64'); 50 | } 51 | resp.cookie('id', toBase64(data.id), { path: '/' }); 52 | resp.cookie('name', toBase64(data.name), { path: '/' }); 53 | resp.cookie('email', toBase64(data.email), { path: '/' }); 54 | resp.cookie('avatar', toBase64(data.avatar_url), { path: '/' }); 55 | 56 | dbhandler.insertUser(data, 57 | function(data) { 58 | resp.redirect(_currentPath); 59 | }, 60 | function(err) { 61 | if (err === 'EXIST_USER') { 62 | resp.redirect(_currentPath); 63 | } else { 64 | resp.send(err); 65 | } 66 | } 67 | ); 68 | } 69 | ); 70 | } 71 | ); 72 | }); 73 | 74 | router.get('/github', function(req, resp) { 75 | var dataStr = (new Date()).valueOf(); 76 | var path = config.GITHUB_API.AUTHORIZE; 77 | path += '?client_id=' + config.CLIENT_ID; 78 | path += '&scope=repo,gist'; 79 | path += '&state=' + dataStr; 80 | path += '&state=' + dataStr; 81 | _currentPath = req.query.currentPath; 82 | resp.redirect(path); 83 | }); 84 | 85 | module.exports = router; 86 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | var CDN_URL = ''; 5 | var EXTERNALS = { // dev 这里应该不加 react 和 react-dom 的 external, build 要加 6 | 'react': 'window.React', 7 | 'react-dom': 'window.ReactDOM' 8 | }; 9 | 10 | console.log('process.env.NODE_ENV in webpack config::::', process.env.NODE_ENV); 11 | 12 | if (!process.env.NODE_ENV) process.env.NODE_ENV = 'dev-HMR'; 13 | 14 | if (process.env.NODE_ENV === 'dev-HMR') { 15 | CDN_URL = 'http://localhost:8088/'; 16 | } 17 | if (process.env.NODE_ENV === 'pre') CDN_URL = '//localhost:5000/'; 18 | if (process.env.NODE_ENV === 'production') CDN_URL = 'https://we-voting-ele.herokuapp.com/'; 19 | 20 | var config = { 21 | entry: { 22 | vote: [ 23 | './client/index.jsx' 24 | ], 25 | router: ['react-router'] // CommonsChunkPlugin 26 | }, 27 | output: { 28 | path: path.resolve(__dirname, './dist'), 29 | publicPath: CDN_URL + 'dist/', // 静态资源文件内的请求路径指向静态资源服务器 30 | filename: '[name].bundle.js', 31 | chunkFilename: '[name].chunk.js' 32 | }, 33 | externals: EXTERNALS, 34 | devtool: process.env.NODE_ENV === 'dev-HMR' ? 'eval-source-map' : '', 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$|\.jsx$/, 39 | exclude: /node_modules/, 40 | loader: 'babel-loader', 41 | query: { 42 | presets: ['react', 'es2015'] 43 | } 44 | }, 45 | { 46 | test: /\.js$|\.jsx$/, 47 | exclude: /node_modules/, 48 | loader: 'eslint-loader' 49 | } 50 | ] 51 | }, 52 | plugins: [ 53 | new webpack.optimize.OccurenceOrderPlugin(), // 比对id的使用频率和分布来得出最短的id分配给使用频率高的模块 54 | new webpack.HotModuleReplacementPlugin(), // 同命令行中的 --hot 55 | 56 | new webpack.optimize.CommonsChunkPlugin('router', '[name].bundle.js'), // CommonsChunk 57 | new webpack.optimize.UglifyJsPlugin({ 58 | compress: { 59 | warnings: false 60 | }, 61 | output: { 62 | comments: false // remove all comments 63 | } 64 | }), 65 | new webpack.DefinePlugin({ 66 | // The DefinePlugin allows you to create global constants which can be configured at compile time. 67 | 'process.env': { 68 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 69 | } 70 | }) 71 | ], 72 | resolve: { 73 | root: path.resolve(__dirname, './client'), 74 | fallback: [path.resolve(__dirname, './node_modules')], 75 | extensions: ['', '.js', '.jsx'] 76 | }, 77 | devServer: { 78 | hot: process.env.NODE_ENV === 'dev-HMR', 79 | inline: true, 80 | headers: { 'Access-Control-Allow-Origin': '*' } 81 | }, 82 | eslint: { 83 | configFile: './.eslintrc' 84 | } 85 | }; 86 | 87 | module.exports = config; 88 | -------------------------------------------------------------------------------- /client/components/transition.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | 4 | const Transition = () => ( 5 |
16 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | ) 52 | 53 | export default Transition; 54 | -------------------------------------------------------------------------------- /test/DBhandler-test.js: -------------------------------------------------------------------------------- 1 | // var app = require('../index'); 2 | // var request = require('supertest')(app); 3 | var DBhandler = require('../controller/DBhandler'); 4 | 5 | var should = require('should'); 6 | 7 | var sucCal = function(obj) { 8 | // console.info('safdasdf:::', obj); 9 | should.exist(obj); 10 | obj.should.be.Object(); 11 | }; 12 | var errCal = function(err) { 13 | // console.error('error::::',err); 14 | err.should.be.equal('EXIST_USER'); 15 | }; 16 | 17 | describe('DBhandler operator', function() { 18 | describe('insertUser', function() { 19 | it('should excute sucCal', function() { 20 | DBhandler.insertUser( 21 | { 22 | 'id': '12121', 23 | 'name': 'libin', 24 | 'email': 'test.ctrip.com' 25 | }, 26 | function(obj) { 27 | sucCal(obj); 28 | }, 29 | function(err) { 30 | errCal(err); 31 | } 32 | ); 33 | }); 34 | }); 35 | describe('insertPoll', function() { 36 | it('should excute sucCal', function() { 37 | DBhandler.insertPoll( 38 | { 39 | 'title': 'testcase', 40 | 'description': 'testcasetestcase', 41 | 'options': [ 42 | { 43 | option:'option1',count:0,index:0 44 | }, 45 | { 46 | option:'option2',count:0,index:1 47 | }, 48 | { 49 | option:'option3',count:0,index:2 50 | } 51 | ], 52 | 'ownerName': 'libin' 53 | }, 54 | function(obj) { 55 | sucCal(obj); 56 | }, 57 | function(err) { 58 | errCal(err); 59 | } 60 | ); 61 | }); 62 | }); 63 | describe('queryPolls', function() { 64 | it('should excute sucCal', function() { 65 | DBhandler.queryPolls( 66 | 'libin', 5, 1, 67 | function(docs) { 68 | docs.should.be.Array(); 69 | }, 70 | function(err) { 71 | should.not.exist(err); 72 | } 73 | ); 74 | }); 75 | it('should excute sucCal', function() { 76 | DBhandler.queryPolls( 77 | '', 78 | function(docs) { 79 | docs.should.be.Array(); 80 | }, 81 | function(err) { 82 | should.not.exist(err); 83 | } 84 | ); 85 | }); 86 | }); 87 | describe('queryPollByID', function() { 88 | it('should excute sucCal', function() { 89 | DBhandler.queryPollByID( 90 | '', 91 | function(docs) { 92 | docs.should.be.Array(); 93 | }, 94 | function(err) { 95 | should.not.exist(err); 96 | } 97 | ); 98 | }); 99 | }); 100 | describe('upDatePollByID', function() { 101 | it('should excute sucCal', function() { 102 | DBhandler.upDatePollByID( 103 | { 104 | 'pollID': '', 105 | 'index': 1, 106 | 'voter': 'libin' 107 | }, 108 | function(docs) { 109 | should.exist(docs); 110 | }, 111 | function(err) { 112 | should.not.exist(err); 113 | } 114 | ); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /client/home.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react'; 4 | import { Component } from 'react'; 5 | import { Link } from 'react-router'; 6 | 7 | import Footer from './components/footer'; 8 | 9 | // if(process.env.NODE_ENV === 'dev-test'){ // for react unit test 10 | // if (typeof userInfo === 'undefined') { 11 | // var userInfo = { 12 | // name:'Guest' 13 | // }; 14 | // } 15 | // } 16 | 17 | class Home extends Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | componentDidMount() { 22 | $('#globalTransition').css('display', 'none'); 23 | } 24 | render(){ 25 | return( 26 |
27 |
28 |
31 |
34 |

35 | {'Let\'s voting!'}
36 | 49 |

50 |

51 | This voting app is built by @elevenbeans, 52 | following the instructions of "Build a Voting App | Free Code Camp".
53 | Github Name: elevenBeans
54 |

55 | {!userInfo.name && 56 |

57 | >"))))),i.default.createElement("div",{className:"container"},i.default.createElement("div",{className:"row"},i.default.createElement("div",{className:"col-md-12"},i.default.createElement("h2",null,"View more polls?"),i.default.createElement("p",{className:"lead"},"You can select a poll to see the results and vote, or sign-in to make a new poll."),i.default.createElement("p",null,i.default.createElement(s.Link,{className:"btn btn-default",to:"/list",role:"button"},"View all polls »"))))),i.default.createElement(p.default,null))}}]),t}(c.Component);t.default=m}}); -------------------------------------------------------------------------------- /test/client-lib-utils-test.js: -------------------------------------------------------------------------------- 1 | // var assert = require('assert'); 2 | var should = require('should'); 3 | 4 | var utils = require('../client/lib/utils'); 5 | 6 | describe('utils', function() { 7 | describe('#getCookie()', function() { 8 | it('should return value when the item exist', function() { 9 | should.equal('', utils.getCookie('prop')); 10 | }); 11 | }); 12 | }); 13 | 14 | describe('utils', function() { 15 | describe('#setCookie()', function() { 16 | it('should return true when the cookie is setted', function() { 17 | should.equal(true, utils.setCookie('name','libin', { path: '/'})); 18 | }); 19 | }); 20 | }); 21 | 22 | describe('utils', function() { 23 | describe('#Base64()', function() { 24 | it('should return right decode value', function() { 25 | should.equal('elevenbeansf2e@gmail.com', utils.base64.decode('ZWxldmVuYmVhbnNmMmVAZ21haWwuY29t')); // email 26 | }); 27 | it('should return right decode value', function() { 28 | should.equal('elevenBeans', utils.base64.decode('ZWxldmVuQmVhbnM=')); // name 29 | }); 30 | it('should return decode value', function() { 31 | should.equal('6982813', utils.base64.decode('Njk4MjgxMw==')); // name 32 | }); 33 | // Njk4MjgxMw%3D%3D 34 | it('should return a string', function() { 35 | // should.equal(true, utils.base64.encode('elevenBeans')); 36 | utils.base64.encode('elevenBeans').should.be.String(); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('utils', function() { 42 | describe('#getDevice()', function() { 43 | it('should return bool', function() { 44 | should.equal(false, utils.getDevice('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36').mobile); 45 | should.equal(true, utils.getDevice('Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1').mobile); 46 | should.equal(true, utils.getDevice('Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1').ios); 47 | should.equal(true, utils.getDevice('Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1').iPhone); 48 | }); 49 | }); 50 | describe('#formatPercentage()', function() { 51 | it('should return percentageNum when the value is digital', function() { 52 | should.equal(33.3, utils.formatPercentage(0.3333)); 53 | should.equal(3.3, utils.formatPercentage(0.0333)); 54 | should.equal(25, utils.formatPercentage(0.25)); 55 | should.equal(100, utils.formatPercentage(1)); 56 | }); 57 | }); 58 | describe('#urlType()', function() { 59 | it('should return bool', function() { 60 | should.equal(true, utils.getPageType('/list/elevenBeans').specialListPage); 61 | should.equal(true, utils.getPageType('/list').listPage); 62 | should.equal(true, utils.getPageType('/detail/1494926562227').detailPage); 63 | should.equal(false, utils.getPageType('/detail2/1494926562227').detailPage); 64 | should.equal(true, utils.getPageType('/detail').detailPage); 65 | should.equal(true, utils.getPageType('/').homePage); 66 | should.equal(true, utils.getPageType('/new').newPage); 67 | should.equal(false, utils.getPageType('/list1/elevenBeans').specialListPage); 68 | should.equal(true, utils.getPageType('/list/124').specialListPage); 69 | should.equal(false, utils.getPageType('/detail/eee').detailPage); 70 | should.equal(false, utils.getPageType('/det').detailPage); 71 | should.equal(true, utils.getPageType('').homePage); 72 | should.equal(false, utils.getPageType('/new1').newPage); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /client/components/header.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // import React from 'react'; 4 | import { Component } from 'react'; 5 | import { Link } from 'react-router'; 6 | 7 | import Transition from './transition'; 8 | 9 | import { getCookie, setCookie, base64, getPageType} from '../lib/utils'; 10 | 11 | class Header extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | } 16 | componentWillMount() { 17 | userInfo.name = base64.decode(getCookie('name')); 18 | userInfo.avatar = base64.decode(getCookie('avatar')); 19 | // userInfo.id = base64.decode(getCookie('id')); 20 | } 21 | signOut() { 22 | setCookie('name','', { path: '/'}); 23 | setCookie('avatar','', { path: '/'}); 24 | setCookie('id','', { path: '/'}); 25 | setCookie('email','', { path: '/'}); 26 | location.reload(); 27 | } 28 | render() { 29 | var _imgUrl = userInfo.avatar; 30 | return ( 31 |

32 | 33 | 119 |
120 | ); 121 | } 122 | } 123 | export default Header; -------------------------------------------------------------------------------- /test/server-test.js: -------------------------------------------------------------------------------- 1 | var app = require('../index'); 2 | var request = require('supertest')(app); 3 | var should = require("should"); 4 | 5 | describe('ServerAPI', function() { 6 | describe('getPollList', function() { 7 | it('should return an Array', function(done) { 8 | request.post('/api/getPollList') 9 | .send({'pageSize': 5, 'pageNum':1 }) 10 | .expect(200, function(err, response) { 11 | should.not.exist(err); 12 | should.exist(response); 13 | response.body.should.be.Array(); 14 | done(); 15 | }); 16 | }); 17 | }); 18 | describe('getPollByID', function() { 19 | it('should return an Array', function(done) { 20 | request.post('/api/getPollByID') 21 | .send({pollID:"1494907457596"}) 22 | .expect(200, function(err, response) { 23 | should.not.exist(err); 24 | should.exist(response); 25 | response.body.should.be.Array(); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | describe('insertPoll', function() { 31 | it('should return an obj', function(done) { 32 | request.post('/api/insertPoll') 33 | .send({ 34 | description:"asdf", 35 | options:[{ 36 | option: "asdf", 37 | count: 0, 38 | index: 0 39 | }, { 40 | option: "sadf", 41 | count: 0, 42 | index: 1 43 | }], 44 | ownerName: "elevenBeans", 45 | title:"asdf", 46 | voterList:[] 47 | }) 48 | .expect(200, function(err, response) { 49 | should.not.exist(err); 50 | should.exist(response); 51 | response.body.should.be.Object(); 52 | response.body.result.should.be.Boolean(); 53 | done(); 54 | }); 55 | }); 56 | }); 57 | describe('upDatePollByID', function() { 58 | it('should return a obj', function(done) { 59 | request.post('/api/upDatePollByID') 60 | .send({ 61 | 'pollID': '1494907457596', 62 | 'index': 0, 63 | 'voter': 'unit test' 64 | }) 65 | .expect(200, function(err, response) { 66 | should.not.exist(err); 67 | should.exist(response); 68 | response.body.should.be.Object(); 69 | response.body.result.should.be.Boolean(); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('ServerView', function() { 77 | describe('getNewPage', function() { 78 | it('should return new page', function(done) { 79 | request.get('/new') 80 | .expect("Content-Type", "text/html; charset=utf-8") 81 | .expect(200, function(err, resp) { 82 | should.not.exist(err); 83 | should.exist(resp); 84 | done(); 85 | }); 86 | }); 87 | }); 88 | describe('getListPage', function() { 89 | it('should return list page', function(done) { 90 | request.get('/list') 91 | .expect("Content-Type", "text/html; charset=utf-8") 92 | .expect(200, function(err, response) { 93 | should.not.exist(err); 94 | should.exist(response); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | describe('getDetailPage', function() { 100 | it('should return detail page', function(done) { 101 | request.get('/detail') 102 | .expect("Content-Type", "text/html; charset=utf-8") 103 | .expect(200, function(err, response) { 104 | should.not.exist(err); 105 | should.exist(response); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | describe('getHomePage', function() { 111 | it('should return home page', function(done) { 112 | request.get('/') 113 | .expect("Content-Type", "text/html; charset=utf-8") 114 | .expect(200, function(err, response) { 115 | should.not.exist(err); 116 | should.exist(response); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | }); -------------------------------------------------------------------------------- /controller/DBhandler.js: -------------------------------------------------------------------------------- 1 | 2 | var mongo = require('mongodb').MongoClient; 3 | 4 | var serverConf = require('../serverConfig'); 5 | 6 | var dbUrl = serverConf.devConfig.DB_URL; 7 | 8 | if (process.env.NODE_ENV === 'production') dbUrl = serverConf.prdConfig.DB_URL; 9 | 10 | var DBhander = {}; 11 | 12 | /** 13 | * filter array. 14 | * @param {str} id The first number. 15 | * @param {function} sucCal The first number. 16 | * @param {function} errCal The first number. 17 | */ 18 | function queryUser(id, sucCal, errCal) { 19 | // 根据用户名查询用户信息, 回调中返回信息; 查询失败则回调返回 err 20 | mongo.connect(dbUrl, function(err, db) { 21 | var userList = db.collection('userList'); 22 | userList.find({ '_id': id }, { }).toArray(function(err, docs) { 23 | if (err) { 24 | db.close(); 25 | errCal(err); 26 | } else { 27 | db.close(); 28 | sucCal(docs); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | DBhander.insertUser = function(obj, sucCal, errCal) { 35 | // 插入一条新用户 成功返回 cb(obj) 失败返回 err 36 | mongo.connect(dbUrl, function(err, db) { 37 | if (err) { 38 | db.close(); 39 | errCal(err); 40 | } else { 41 | var userList = db.collection('userList'); 42 | // 插入开销大,先查询,为新用户再插入 43 | queryUser( // 先查询 44 | obj.id, 45 | function(data) { // 查询成功返回 46 | if (data.length === 0) { // 用户不存在 47 | var _timestamp = new Date().getTime(); 48 | userList.insertOne( // 插入 49 | { 50 | '_id': obj.id, 51 | 'name': obj.name, 52 | 'email': obj.email, 53 | 'timestamp': _timestamp 54 | }, 55 | function() { // 插入成功, end 56 | db.close(); 57 | sucCal(obj); 58 | } 59 | ); 60 | } else { // 用户已存在 61 | db.close(); 62 | errCal('EXIST_USER'); // 插入失败, end 63 | } 64 | }, 65 | function(err) { // 查询失败, end 66 | db.close(); 67 | errCal(err); 68 | } 69 | ); 70 | } 71 | }); 72 | }; 73 | 74 | DBhander.insertPoll = function(obj, sucCal) { 75 | // 插入一条新用户 成功返回 cb(obj) 失败返回 err 76 | mongo.connect(dbUrl, function(err, db) { 77 | var pullList = db.collection('pollList'); 78 | var _timestamp = new Date().getTime(); 79 | pullList.insertOne( // 插入 80 | { 81 | '_id': _timestamp, 82 | 'pollID': _timestamp + '', 83 | 'title': obj.title, 84 | 'description': obj.description, 85 | 'options': obj.options, 86 | 'ownerName': obj.ownerName 87 | }, 88 | function() { // 插入成功, end 89 | db.close(); 90 | sucCal(obj); 91 | } 92 | ); 93 | }); 94 | }; 95 | 96 | DBhander.queryPolls = function(ownerName, pageSize, pageNum, sucCal, errCal) { // 根据用户 name 查询 poll 97 | mongo.connect(dbUrl, function(err, db) { 98 | var pullList = db.collection('pollList'); 99 | pullList.find({ ownerName: ownerName }, { }).sort({ timestamp: -1 }).skip(pageSize * (pageNum - 1)).limit(pageSize).toArray(function(err, docs) { 100 | if (err) { 101 | db.close(); 102 | errCal(err); 103 | } else { 104 | db.close(); 105 | sucCal(docs); 106 | } 107 | }); 108 | }); 109 | }, 110 | 111 | DBhander.queryPollByID = function(id, sucCal, errCal) { // 根据 poll id 查询 poll 112 | mongo.connect(dbUrl, function(err, db) { 113 | var pullList = db.collection('pollList'); 114 | pullList.find({ pollID: id }, { }).sort({ timestamp: -1 }).toArray(function(err, docs) { 115 | if (err) { 116 | db.close(); 117 | errCal(err); 118 | } else { 119 | db.close(); 120 | sucCal(docs); 121 | } 122 | }); 123 | }); 124 | }, 125 | 126 | DBhander.upDatePollByID = function(obj, sucCal, errCal) { 127 | // 插入一条新用户 成功返回 cb(obj.id) 失败返回 err 128 | mongo.connect(dbUrl, function(err, db) { 129 | var pullList = db.collection('pollList'); 130 | if (err) { 131 | errCal(err); 132 | } 133 | pullList.updateOne( // update 134 | { 135 | 'pollID': obj.pollID + '', 136 | 'options.index': obj.index 137 | }, 138 | { 139 | $inc: { 'options.$.count': +1 }, 140 | $push: { 'voterList': obj.voter } 141 | }, 142 | function() { // update, end 143 | db.close(); 144 | sucCal(obj); 145 | } 146 | ); 147 | }); 148 | }; 149 | 150 | module.exports = DBhander; 151 | -------------------------------------------------------------------------------- /dist/list.chunk.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([2],{9:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){return React.createElement("div",{className:"container"},React.createElement("hr",null),React.createElement("footer",null,React.createElement("p",{style:{"font-size":"14px"}},"© ",React.createElement("br",null),React.createElement("iframe",{src:"https://ghbtns.com/github-btn.html?user=elevenbeans&type=follow&count=true",frameborder:"0",scrolling:"0",width:"170px",height:"20px",style:{border:"0"}}))))};t.default=n},25:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){return React.createElement("div",{className:"uil-ellipsis-css",style:{transform:"scale(0.25)",margin:"0 auto"}},React.createElement("div",{className:"ib"},React.createElement("div",{className:"circle"},React.createElement("div",null)),React.createElement("div",{className:"circle"},React.createElement("div",null)),React.createElement("div",{className:"circle"},React.createElement("div",null)),React.createElement("div",{className:"circle"},React.createElement("div",null))))};t.default=n},55:function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var n=0;n=a&&(l.callback&&l.callback(e.count),console.log("Loader "+e.count+" times"),e.count++)}},{key:"addCallback",value:function(e,t){var n=this;n.cbContent={diff:t.diff||n.DIFF,callback:e},t.immediately&&n.detect()}},{key:"throttle",value:function(e,t){var n=this,a=+new Date;a-n.last>e&&(t.apply(this,arguments),n.last=a)}}]),e}();t.default=l},61:function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function l(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function c(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n -1 || u.indexOf('Linux') > -1, //android终端 124 | iPhone: u.indexOf('iPhone') > -1, //是否为iPhone 125 | iPad: u.indexOf('iPad') > -1 //是否iPad 126 | }; 127 | } 128 | 129 | function formatPercentage(num){ 130 | return (Math.round(num*1000)).toFixed()/10; 131 | } 132 | 133 | var base64 = new Base64(); 134 | 135 | function getPageType(url){ 136 | if(!url) url = '/'; 137 | return { 138 | homePage: !!url.match(/^\/$/), 139 | listPage: !!url.match(/^\/list$/), 140 | specialListPage: !!url.match(/^\/list(\/\w+)$/), 141 | detailPage: !!url.match(/^\/detail$|^\/detail(\/\d+)$/), 142 | newPage: !!url.match(/^\/new$/) 143 | } 144 | } 145 | 146 | export { getCookie, setCookie, base64, getDevice, formatPercentage, getPageType } 147 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "amd": true, 7 | "es6": true, 8 | }, 9 | "globals": { 10 | "$": true, 11 | "define": true, 12 | "require": true, 13 | "restfulApi": true 14 | }, 15 | "extends": [ 16 | "eslint:recommended", 17 | "plugin:react/recommended" 18 | ], 19 | "parser": "babel-eslint", 20 | "parserOptions": { 21 | "ecmaVersion": 6, 22 | "ecmaFeatures": { 23 | "jsx": true, 24 | "experimentalObjectRestSpread": true, 25 | }, 26 | }, 27 | "plugins": [ 28 | "react" 29 | ], 30 | "root": true, 31 | "rules": { 32 | // 使用 2 个空格缩进 33 | "indent": [2, 2], 34 | // 使用驼峰命名或者 UPPERCASE_WITH_UNDERSCORES 35 | "camelcase": [2, {"properties": "never"}], 36 | // 使用单引号 37 | "quotes": [2, "single", {"allowTemplateLiterals": true}], 38 | // 使用分号 39 | "semi": [2, "always"], 40 | // 使用 === 和 !== 41 | "eqeqeq": [2, "always"], 42 | // 不省略 {} 43 | // "curly": [2, "all"], 44 | 45 | // 禁用不必要的布尔转换 46 | "no-extra-boolean-cast": 2, 47 | 48 | // disable rules from base configurations 49 | "no-console": "off", 50 | 51 | // code style 52 | // 数组中起始位置是否需要空格 53 | "array-bracket-spacing": [2, "never"], 54 | // 设置{}风格 55 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 56 | // 不允许在数组或对象最后一项使用逗号 57 | "comma-dangle": [2, "never"], 58 | // 在分号前不适用空格,分号后使用空格 59 | "comma-spacing": 2, 60 | // 分号风格,默认放在行尾 61 | "comma-style": 2, 62 | // 在变量属性中禁用空格 63 | "computed-property-spacing": 2, 64 | // 设置对 this 的引用 65 | "consistent-this": [2, "that"], 66 | // 文件以新行结尾 67 | "eol-last": 2, 68 | // 在函数标识和 () 之间禁用空格 69 | "func-call-spacing": 2, 70 | // JSX 使用双引号 71 | "jsx-quotes": [2, "prefer-double"], 72 | // 在 key 和 value 之间的空格一个空格 73 | "key-spacing": 2, 74 | // 在关键字前后使用空格 75 | "keyword-spacing": 2, 76 | // 行注释位置(上面 ?后面) 77 | // "line-comment-position": 2, 78 | // 最深嵌套 4 层 79 | "max-depth": [2, { "max": 4 }], 80 | // 最大行长 81 | "max-len": [2, { 82 | "code": 120, 83 | "ignoreUrls": true, 84 | // "ignoreStrings": true, 85 | "ignoreTemplateLiterals": true, 86 | "ignoreRegExpLiterals": true 87 | }], 88 | // 文件最大行数 89 | "max-lines": [2, 600], 90 | // 函数最多参数 91 | "max-params": [2, { "max": 4 }], 92 | // 每行最大语句条数为 2 93 | "max-statements-per-line": [2, { "max": 2 }], 94 | // 函数最大语句条数 95 | "max-statements": [1, { "max": 16 }], 96 | // 使用 new 实例化对象,类名大写字母开头 97 | "new-cap": 2, 98 | // 不使用 Array 构造函数 99 | "no-array-constructor": 2, 100 | // "no-lonely-if": 2, 101 | // "no-mixed-operators": 0, 102 | "no-mixed-spaces-and-tabs": 2, // eslint:recommended 103 | // 最大两行空白 104 | "no-multiple-empty-lines": [2, {"max": 2}], 105 | // 禁用 Object 构造函数 106 | "no-new-object": 2, 107 | // 禁用 tabs 108 | "no-tabs": 2, 109 | // 禁用行末额外的空白 110 | "no-trailing-spaces": 2, 111 | // 禁用不需要的三元表达式 112 | "no-unneeded-ternary": 2, 113 | // 属性前没有空白 114 | "no-whitespace-before-property": 2, 115 | // 在对象 {} 中使用空格 116 | "object-curly-spacing": [2, "always"], 117 | // 单独使用声明 118 | "one-var": [2, { 119 | "var": "never", 120 | "let": "never", 121 | "const": "never", 122 | }], 123 | // 在块中禁用大量空白填充 124 | "padded-blocks": [2, "never"], 125 | // 在需要使用引号时该对象所有属性都使用引号 126 | "quote-props": [2, "consistent"], 127 | // 需要使用 JSDoc 128 | "require-jsdoc": [2, { 129 | "require": { 130 | "FunctionDeclaration": true, 131 | "MethodDefinition": false, 132 | "ClassDeclaration": false, 133 | }, 134 | }], 135 | // 配置 JSDoc 规则 136 | "valid-jsdoc": [2, { 137 | "requireParamDescription": false, 138 | "requireReturnDescription": false, 139 | "requireReturn": false, 140 | "prefer": {"returns": "return"}, 141 | }], 142 | "semi-spacing": [2, { 143 | "before": false, 144 | "after": true 145 | }], 146 | // 在块语句前使用空格 147 | "space-before-blocks": 2, 148 | // 在函数声明后禁用空格 149 | "space-before-function-paren": [2, "never"], 150 | // 在中缀操作符之间需要空格 151 | "space-infix-ops": [2, {"int32Hint": false}], 152 | // 在一元运算符前后禁用空格 153 | "space-unary-ops": 2, 154 | // 在括号首尾禁用空格 155 | "space-in-parens": 2, 156 | // 在注释后使用空格 157 | "spaced-comment": [2, "always"], 158 | 159 | // react 160 | // 这里关掉属性类型校验 161 | "react/prop-types": "off", 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /dist/new.chunk.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([3],{9:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){return React.createElement("div",{className:"container"},React.createElement("hr",null),React.createElement("footer",null,React.createElement("p",{style:{"font-size":"14px"}},"© ",React.createElement("br",null),React.createElement("iframe",{src:"https://ghbtns.com/github-btn.html?user=elevenbeans&type=follow&count=true",frameborder:"0",scrolling:"0",width:"170px",height:"20px",style:{border:"0"}}))))};t.default=n},26:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){return React.createElement("div",{className:"uil-spin-css",style:{"-webkit-transform":"scale(0.1)"}},React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)),React.createElement("div",null,React.createElement("div",null)))};t.default=n},62:function(e,t,n){"use strict";function l(e){return e&&e.__esModule?e:{default:e}}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n 99 | 100 | ## Techstack overview 101 | 102 | #### Server: 103 | 104 | + Enviroment: Node 105 | + Framework: [Express](http://expressjs.com/) 106 | + Tools: Request, compression, body-praser 107 | + Template engine: Ejs 108 | + DataBase: [Mongodb](https://www.mongodb.com/) 109 | 110 | #### Front-end: 111 | 112 | + JS standard: ECMAScript 6 113 | + Framework: React + ReactDOM + React-Router 114 | + Module bundler and compiler: Webpack + Babel 115 | + Open source components: [react-d3-components](https://github.com/codesuki/react-d3-components) 116 | 117 | ## Pages 118 | 119 | + home page 120 | + router: `/` 121 | + example: `https://we-voting-ele.herokuapp.com/` 122 | + list page 123 | + router: `/list(/:name)` 124 | + example: `https://we-voting-ele.herokuapp.com/list` 125 | 126 | + detail page 127 | + router: `/detail(/:id)` 128 | + example: `https://we-voting-ele.herokuapp.com/detail/1494908221812` 129 | 130 | + new page 131 | + router: `/new` 132 | + example: `https://we-voting-ele.herokuapp.com/new` 133 | 134 | ## Directories 135 | 136 | ``` 137 | |-- client // front-end code 138 | |-- components // front-end components 139 | |-- footer.jsx // public footer 140 | |-- header.jsx // public header 141 | |-- loading.jsx // loading amination 142 | |-- spning.jsx // spning amination 143 | |-- lib // front-end library 144 | |-- utils.jsx 145 | |-- detail.jsx // detail page 146 | |-- home.jsx // home page 147 | |-- index.jsx // front-end intrance 148 | |-- list.jsx // list page 149 | |-- new.jsx // new page 150 | |-- controller // server-end controller 151 | |-- routes 152 | |-- login.js // login routes 153 | |-- view.js // view routes 154 | |-- api.js // api controller 155 | |-- DBhandler.js // DataBase CRUD 156 | |-- dist // compiled front-end code 157 | |-- vendor 158 | |-- jquery.min.js 159 | |-- bootstrap.min.js 160 | |-- bootstrap.min.css 161 | |-- react-dom.min.js // react-dom production version 162 | |-- react.min.js // react production version 163 | |-- loading.css 164 | |-- vote.bundle.js // bundled voteApp intrance file 165 | |-- router.bundle.js // bundled react-router 166 | |-- detail.chunk.js // splitted JS file in detail page 167 | |-- home.chunk.js // splitted JS file in home page 168 | |-- list.chunk.js // splitted JS file in list page 169 | |-- new.chunk.js // splitted JS file in new page 170 | |-- views // server-end views 171 | |-- error.ejs 172 | |-- footer.ejs 173 | |-- header.ejs 174 | |-- index.ejs 175 | |-- .gitignore 176 | |-- Procfile // heroku file 177 | |-- README.md 178 | |-- index.js // app intrance file 179 | |-- package.json 180 | |-- serverConfig.js // enviroment configuration 181 | |-- start.sh // start file for mac 182 | |-- webpack.config.js 183 | ``` 184 | 185 | ## LICENSE 186 | 187 | [MIT](https://mit-license.org/) 188 | -------------------------------------------------------------------------------- /client/detail.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react'; 4 | import { Component } from 'react'; 5 | 6 | import * as ReactD3 from 'react-d3-components'; 7 | 8 | import Loading from './components/loading'; 9 | import Spning from './components/spning'; 10 | 11 | import Footer from './components/footer'; 12 | 13 | import { getDevice, formatPercentage } from './lib/utils'; 14 | 15 | let PieChart = ReactD3.PieChart; 16 | 17 | let _width = 320, _height = 200; 18 | 19 | if(!getDevice(navigator.userAgent).mobile){ 20 | _width = 600; 21 | _height = 400; 22 | } 23 | 24 | class Detail extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | pollDetailData: [], 29 | loadingPop: true, 30 | isVoting: false, 31 | votingIndex: '', 32 | votable: true 33 | } 34 | } 35 | componentDidMount() { 36 | $('#globalTransition').css('display', 'none'); 37 | this.fetchPollDetail(this.props.params.id); 38 | } 39 | fetchPollDetail(id){ 40 | $.ajax({ 41 | type: 'POST', 42 | url: '/api/getPollByID', 43 | async: true, 44 | contentType: "application/json;charset=utf-8", 45 | data: JSON.stringify({'pollID': id}), 46 | dataType: 'json', 47 | success: function (data) { 48 | if(data && data.length !== 0) { 49 | this.setState({ 50 | pollDetailData: data, 51 | loadingPop: false 52 | }); 53 | data[0] && data[0].voterList && data[0].voterList.map(function(item){ 54 | if(item === userInfo.name){ 55 | this.setState({ 56 | votable: false 57 | }); 58 | } 59 | }.bind(this)); 60 | } else { 61 | this.setState({ 62 | loadingPop: false 63 | }); 64 | } 65 | }.bind(this) 66 | }); 67 | } 68 | voteOption(e){ 69 | if(!userInfo.name) { 70 | location.href = '/login/github' + '?currentPath=' + location.pathname; 71 | return false; 72 | } 73 | this.setState({ 74 | isVoting: true, 75 | votingIndex: $(e.target).attr('data-index') 76 | }); 77 | $.ajax({ 78 | 'type': "POST", 79 | 'url': '/api/upDatePollByID', 80 | 'async': true, 81 | 'contentType': "application/json;charset=utf-8", 82 | 'data': JSON.stringify({ 83 | 'pollID': this.props.params.id, 84 | 'index': ~~$(e.target).attr('data-index'), 85 | 'voter': userInfo.name 86 | }), 87 | 'dataType': 'json', 88 | success: function (data) { 89 | if(data && data.result) { 90 | this.fetchPollDetail(this.props.params.id); 91 | this.setState({ 92 | isVoting: false, 93 | votingIndex: '' 94 | }); 95 | } else { 96 | this.setState({ 97 | isVoting: false, 98 | votingIndex: '' 99 | }); 100 | } 101 | }.bind(this) 102 | }); 103 | } 104 | render() { 105 | let _result = this.state.pollDetailData[0]; 106 | let data = { 107 | 'label': '', 108 | 'values': [] 109 | }; 110 | let _countZero = 0; // 无人投票的选项数量 111 | let totalCounts = 0; // 该选项的投票总数 112 | 113 | _result && _result.options.map(function(item){ 114 | totalCounts += item.count; 115 | }); 116 | _result && _result.options.map(function(item){ 117 | data.label = item.option; 118 | if(!item.count){ 119 | item.count = 0; 120 | _countZero++; 121 | } 122 | data.values.push({ 123 | 'x': item.option + ' (' + formatPercentage(item.count/totalCounts) + '%)', 124 | 'y': item.count 125 | }); 126 | }); 127 | 128 | return ( 129 |
130 | {this.state.pollDetailData.length !== 0 && 131 |
132 |
133 |
134 |

138 | {_result.title} 139 |

140 |
141 |
142 | {_result.description} 143 |
144 |
145 |
    149 | {_result && _result.options.map( (item, index) => ( 150 |
  • 153 |
    154 | {item.option} 155 | 156 | 168 | 169 |
    170 |
  • 171 | ) 172 | )} 173 |
174 | 175 | { _result.options.length !== _countZero && 176 | 183 | } 184 |
185 | } 186 | {this.state.loadingPop && } 187 | {!this.state.loadingPop && 188 | this.state.pollDetailData.length === 0 && 189 |
no result ~
190 | } 191 |
192 |
193 | ); 194 | } 195 | 196 | } 197 | export default Detail; 198 | -------------------------------------------------------------------------------- /client/new.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react'; 4 | import { Component } from 'react'; 5 | import { browserHistory } from 'react-router'; 6 | 7 | import Spning from './components/spning'; 8 | import Footer from './components/footer'; 9 | 10 | class New extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | options: [], 16 | isSubmitting: false 17 | } 18 | } 19 | componentDidMount() { 20 | $('#globalTransition').css('display', 'none'); 21 | if(!userInfo.name){ 22 | // 不支持未登陆 landing, 滚去首页! 23 | location.href = location.origin; 24 | } 25 | } 26 | addOptions(e){ 27 | var _temp = this.state.options; 28 | 29 | if(!$('#poll-option').val()) return 30 | 31 | _temp.push({ 32 | 'option': $('#poll-option').val(), 33 | 'count': 0, 34 | 'index': this.state.options.length 35 | }); 36 | 37 | this.setState({ 38 | 'options': _temp 39 | }); 40 | $('#poll-option').val(''); 41 | } 42 | deleteOptions(e){ 43 | var _index = $(e.target).attr('data-index'); 44 | var _temp = this.state.options; 45 | _temp = _temp.filter(function(item, index){ 46 | return index !== ~~_index; 47 | }); 48 | 49 | 50 | _temp.map(function(item, index){ 51 | item.index = index; 52 | }); 53 | 54 | this.setState({ 55 | 'options': _temp 56 | }); 57 | 58 | } 59 | submitPollData(){ 60 | if(this.state.options.length < 2 ){ 61 | alert('At least two options'); 62 | return 63 | } 64 | if(!$('#poll-title').val()){ 65 | alert('Input your poll title!'); 66 | return 67 | } 68 | if(!$('#poll-description').val()){ 69 | alert('Input your poll description!'); 70 | return 71 | } 72 | var _request = { 73 | 'title': $('#poll-title').val() || 'Default title', 74 | 'description': $('#poll-description').val() || 'Default description', 75 | 'options': this.state.options, 76 | 'ownerName': userInfo.name, 77 | 'voterList': [] 78 | } 79 | this.setState({ 80 | 'isSubmitting': true 81 | }); 82 | $.ajax({ 83 | type: "POST", 84 | url: '/api/insertPoll', 85 | async: true, 86 | contentType: "application/json;charset=utf-8", 87 | data: JSON.stringify(_request), 88 | dataType: 'json', 89 | success: function (data) { 90 | if(data && data.result) { 91 | console.log(this.props); 92 | browserHistory.push('/list/' + userInfo.name); 93 | this.setState({ 94 | 'isSubmitting': false 95 | }); 96 | } 97 | }.bind(this) 98 | }); 99 | } 100 | render() { 101 | return ( 102 |
103 |

Create a new poll.

104 | 108 | Poll title: 109 | 110 |
114 | 122 |

123 | 127 | Description: 128 | 129 |
133 | 143 |

144 | 148 | Options: 149 | 150 |
    154 | {this.state.options.map( 155 | (item, index)=>( 156 |
  • 159 |
    160 | {item.option} 161 | 162 | 170 | 171 |
    172 |
  • 173 | ) 174 | )} 175 | {this.state.options.length === 0?
  • To be added ...
  • :''} 176 |
177 |
178 |
179 |
180 | 187 | 188 | 195 | 196 |
197 |
198 |

199 | 205 |
206 |
207 | ); 208 | } 209 | 210 | } 211 | export default New; 212 | -------------------------------------------------------------------------------- /dist/loading.css: -------------------------------------------------------------------------------- 1 | 2 | @-webkit-keyframes uil-ellipsis { 3 | 0% { 4 | -webkit-transform: scale(0); 5 | transform: scale(0); 6 | left: 0px; 7 | opacity: 1; 8 | } 9 | 12.5% { 10 | -webkit-transform: scale(1); 11 | transform: scale(1); 12 | } 13 | 25% { 14 | left: 0px; 15 | } 16 | 37.5% { 17 | left: 70px; 18 | } 19 | 50% { 20 | left: 70px; 21 | } 22 | 62.5% { 23 | left: 140px; 24 | } 25 | 75% { 26 | left: 140px; 27 | -webkit-transform: scale(1); 28 | transform: scale(1); 29 | } 30 | 87.5% { 31 | left: 140px; 32 | -webkit-transform: scale(0); 33 | transform: scale(0); 34 | opacity: 1; 35 | } 36 | 100% { 37 | left: 140px; 38 | opacity: 0; 39 | } 40 | } 41 | @keyframes uil-ellipsis { 42 | 0% { 43 | -webkit-transform: scale(0); 44 | transform: scale(0); 45 | left: 0px; 46 | opacity: 1; 47 | } 48 | 12.5% { 49 | -webkit-transform: scale(1); 50 | transform: scale(1); 51 | } 52 | 25% { 53 | left: 0px; 54 | } 55 | 37.5% { 56 | left: 70px; 57 | } 58 | 50% { 59 | left: 70px; 60 | } 61 | 62.5% { 62 | left: 140px; 63 | } 64 | 75% { 65 | left: 140px; 66 | -webkit-transform: scale(1); 67 | transform: scale(1); 68 | } 69 | 87.5% { 70 | left: 140px; 71 | -webkit-transform: scale(0); 72 | transform: scale(0); 73 | opacity: 1; 74 | } 75 | 100% { 76 | left: 140px; 77 | opacity: 0; 78 | } 79 | } 80 | .uil-ellipsis-css { 81 | background: none; 82 | position: relative; 83 | width: 200px; 84 | height: 200px; 85 | } 86 | .uil-ellipsis-css .ib { 87 | width: 100%; 88 | height: 100%; 89 | -webkit-transform: rotate(0deg); 90 | transform: rotate(0deg); 91 | } 92 | .uil-ellipsis-css .circle { 93 | width: 60px; 94 | height: 60px; 95 | position: absolute; 96 | top: 70px; 97 | opacity: 0; 98 | text-align: center; 99 | -webkit-animation: uil-ellipsis 1s linear infinite; 100 | animation: uil-ellipsis 1s linear infinite; 101 | } 102 | .uil-ellipsis-css .circle > div { 103 | width: 60px; 104 | height: 60px; 105 | border-radius: 30px; 106 | margin: 0px; 107 | } 108 | .uil-ellipsis-css .circle:nth-of-type(2n+1) > div { 109 | background: #403d3d; 110 | } 111 | .uil-ellipsis-css .circle:nth-of-type(2n) > div { 112 | background: #808a80; 113 | } 114 | .circle:nth-of-type(1) { 115 | -webkit-animation-delay: -1s; 116 | animation-delay: -1s; 117 | } 118 | .circle:nth-of-type(2) { 119 | -webkit-animation-delay: -0.75s; 120 | animation-delay: -0.75s; 121 | } 122 | .circle:nth-of-type(3) { 123 | -webkit-animation-delay: -0.5s; 124 | animation-delay: -0.5s; 125 | } 126 | .circle:nth-of-type(4) { 127 | -webkit-animation-delay: -0.25s; 128 | animation-delay: -0.25s; 129 | } 130 | 131 | .uil-spin-css { 132 | background: none; 133 | position: relative; 134 | width: 28px; 135 | height: 20px; 136 | right: 8px; 137 | bottom: 7px; 138 | } 139 | @-webkit-keyframes uil-spin-css { 140 | 0% { 141 | opacity: 1; 142 | -webkit-transform: scale(1.5); 143 | transform: scale(1.5); 144 | } 145 | 100% { 146 | opacity: 0.1; 147 | -webkit-transform: scale(1); 148 | transform: scale(1); 149 | } 150 | } 151 | @keyframes uil-spin-css { 152 | 0% { 153 | opacity: 1; 154 | -webkit-transform: scale(1.5); 155 | transform: scale(1.5); 156 | } 157 | 100% { 158 | opacity: 0.1; 159 | -webkit-transform: scale(1); 160 | transform: scale(1); 161 | } 162 | } 163 | .uil-spin-css > div { 164 | width: 24px; 165 | height: 24px; 166 | margin-left: 4px; 167 | margin-top: 4px; 168 | position: absolute; 169 | } 170 | .uil-spin-css > div > div { 171 | width: 100%; 172 | height: 100%; 173 | border-radius: 100px; 174 | background: #ffffff; 175 | } 176 | .uil-spin-css > div:nth-of-type(1) > div { 177 | -webkit-animation: uil-spin-css 1s linear infinite; 178 | animation: uil-spin-css 1s linear infinite; 179 | -webkit-animation-delay: -0.87s; 180 | animation-delay: -0.87s; 181 | } 182 | .uil-spin-css > div:nth-of-type(1) { 183 | -webkit-transform: translate(84px, 84px) rotate(45deg) translate(70px, 0); 184 | transform: translate(84px, 84px) rotate(45deg) translate(70px, 0); 185 | } 186 | .uil-spin-css > div:nth-of-type(2) > div { 187 | -webkit-animation: uil-spin-css 1s linear infinite; 188 | animation: uil-spin-css 1s linear infinite; 189 | -webkit-animation-delay: -0.75s; 190 | animation-delay: -0.75s; 191 | } 192 | .uil-spin-css > div:nth-of-type(2) { 193 | -webkit-transform: translate(84px, 84px) rotate(90deg) translate(70px, 0); 194 | transform: translate(84px, 84px) rotate(90deg) translate(70px, 0); 195 | } 196 | .uil-spin-css > div:nth-of-type(3) > div { 197 | -webkit-animation: uil-spin-css 1s linear infinite; 198 | animation: uil-spin-css 1s linear infinite; 199 | -webkit-animation-delay: -0.62s; 200 | animation-delay: -0.62s; 201 | } 202 | .uil-spin-css > div:nth-of-type(3) { 203 | -webkit-transform: translate(84px, 84px) rotate(135deg) translate(70px, 0); 204 | transform: translate(84px, 84px) rotate(135deg) translate(70px, 0); 205 | } 206 | .uil-spin-css > div:nth-of-type(4) > div { 207 | -webkit-animation: uil-spin-css 1s linear infinite; 208 | animation: uil-spin-css 1s linear infinite; 209 | -webkit-animation-delay: -0.5s; 210 | animation-delay: -0.5s; 211 | } 212 | .uil-spin-css > div:nth-of-type(4) { 213 | -webkit-transform: translate(84px, 84px) rotate(180deg) translate(70px, 0); 214 | transform: translate(84px, 84px) rotate(180deg) translate(70px, 0); 215 | } 216 | .uil-spin-css > div:nth-of-type(5) > div { 217 | -webkit-animation: uil-spin-css 1s linear infinite; 218 | animation: uil-spin-css 1s linear infinite; 219 | -webkit-animation-delay: -0.37s; 220 | animation-delay: -0.37s; 221 | } 222 | .uil-spin-css > div:nth-of-type(5) { 223 | -webkit-transform: translate(84px, 84px) rotate(225deg) translate(70px, 0); 224 | transform: translate(84px, 84px) rotate(225deg) translate(70px, 0); 225 | } 226 | .uil-spin-css > div:nth-of-type(6) > div { 227 | -webkit-animation: uil-spin-css 1s linear infinite; 228 | animation: uil-spin-css 1s linear infinite; 229 | -webkit-animation-delay: -0.25s; 230 | animation-delay: -0.25s; 231 | } 232 | .uil-spin-css > div:nth-of-type(6) { 233 | -webkit-transform: translate(84px, 84px) rotate(270deg) translate(70px, 0); 234 | transform: translate(84px, 84px) rotate(270deg) translate(70px, 0); 235 | } 236 | .uil-spin-css > div:nth-of-type(7) > div { 237 | -webkit-animation: uil-spin-css 1s linear infinite; 238 | animation: uil-spin-css 1s linear infinite; 239 | -webkit-animation-delay: -0.12s; 240 | animation-delay: -0.12s; 241 | } 242 | .uil-spin-css > div:nth-of-type(7) { 243 | -webkit-transform: translate(84px, 84px) rotate(315deg) translate(70px, 0); 244 | transform: translate(84px, 84px) rotate(315deg) translate(70px, 0); 245 | } 246 | .uil-spin-css > div:nth-of-type(8) > div { 247 | -webkit-animation: uil-spin-css 1s linear infinite; 248 | animation: uil-spin-css 1s linear infinite; 249 | -webkit-animation-delay: -0s; 250 | animation-delay: -0s; 251 | } 252 | .uil-spin-css > div:nth-of-type(8) { 253 | -webkit-transform: translate(84px, 84px) rotate(360deg) translate(70px, 0); 254 | transform: translate(84px, 84px) rotate(360deg) translate(70px, 0); 255 | } -------------------------------------------------------------------------------- /dist/vendor/react.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React v15.4.1 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.React=t()}}(function(){return function t(e,n,r){function o(u,a){if(!n[u]){if(!e[u]){var s="function"==typeof require&&require;if(!a&&s)return s(u,!0);if(i)return i(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var l=n[u]={exports:{}};e[u][0].call(l.exports,function(t){var n=e[u][1][t];return o(n?n:t)},l,l.exports,t,e,n,r)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u1){for(var h=Array(y),m=0;m1){for(var g=Array(b),E=0;E>"),O={array:u("array"),bool:u("boolean"),func:u("function"),number:u("number"),object:u("object"),string:u("string"),symbol:u("symbol"),any:a(),arrayOf:s,element:c(),instanceOf:l,node:v(),objectOf:p,oneOf:f,oneOfType:d,shape:y};o.prototype=Error.prototype,e.exports=O},{12:12,14:14,19:19,23:23,26:26,9:9}],14:[function(t,e,n){"use strict";var r="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";e.exports=r},{}],15:[function(t,e,n){"use strict";function r(t,e,n){this.props=t,this.context=e,this.refs=s,this.updater=n||a}function o(){}var i=t(27),u=t(6),a=t(11),s=t(24);o.prototype=u.prototype,r.prototype=new o,r.prototype.constructor=r,i(r.prototype,u.prototype),r.prototype.isPureReactComponent=!0,e.exports=r},{11:11,24:24,27:27,6:6}],16:[function(t,e,n){"use strict";var r=t(27),o=t(3),i=r({__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:{ReactCurrentOwner:t(7)}},o);e.exports=i},{27:27,3:3,7:7}],17:[function(t,e,n){"use strict";e.exports="15.4.1"},{}],18:[function(t,e,n){"use strict";var r=!1;e.exports=r},{}],19:[function(t,e,n){"use strict";function r(t){var e=t&&(o&&t[o]||t[i]);if("function"==typeof e)return e}var o="function"==typeof Symbol&&Symbol.iterator,i="@@iterator";e.exports=r},{}],20:[function(t,e,n){"use strict";function r(t){return i.isValidElement(t)?void 0:o("143"),t}var o=t(21),i=t(9);t(25);e.exports=r},{21:21,25:25,9:9}],21:[function(t,e,n){"use strict";function r(t){for(var e=arguments.length-1,n="Minified React error #"+t+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+t,r=0;r>2,o=(3&a)<<4|r>>4,s=(15&r)<<2|l>>6,c=63&l,isNaN(r)?s=c=64:isNaN(l)&&(c=64),u=u+e.charAt(i)+e.charAt(o)+e.charAt(s)+e.charAt(c);return u},this.decode=function(t){var a,r,l,i,o,s,c,u="",f=0;for(t=t.replace(/[^A-Za-z0-9\+\/\=]/g,"");f>4,r=(15&o)<<4|s>>2,l=(3&s)<<6|c,u+=String.fromCharCode(a),64!=s&&(u+=String.fromCharCode(r)),64!=c&&(u+=String.fromCharCode(l));return u=n(u)};var t=function(e){e=e.replace(/\r\n/g,"\n");for(var t="",n=0;n127&&a<2048?(t+=String.fromCharCode(a>>6|192),t+=String.fromCharCode(63&a|128)):(t+=String.fromCharCode(a>>12|224),t+=String.fromCharCode(a>>6&63|128),t+=String.fromCharCode(63&a|128))}return t},n=function(e){for(var t="",n=0,a=0,r=0,l=0;n191&&a<224?(r=e.charCodeAt(n+1),t+=String.fromCharCode((31&a)<<6|63&r),n+=2):(r=e.charCodeAt(n+1),l=e.charCodeAt(n+2),t+=String.fromCharCode((15&a)<<12|(63&r)<<6|63&l),n+=3);return t}}function l(e){return{mobile:!!e.match(/AppleWebKit.*Mobile.*/),ios:!!e.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),android:e.indexOf("Android")>-1||e.indexOf("Linux")>-1,iPhone:e.indexOf("iPhone")>-1,iPad:e.indexOf("iPad")>-1}}function i(e){return Math.round(1e3*e).toFixed()/10}function o(e){return e||(e="/"),{homePage:!!e.match(/^\/$/),listPage:!!e.match(/^\/list$/),specialListPage:!!e.match(/^\/list(\/\w+)$/),detailPage:!!e.match(/^\/detail$|^\/detail(\/\d+)$/),newPage:!!e.match(/^\/new$/)}}Object.defineProperty(t,"__esModule",{value:!0});var s=new r;t.getCookie=n,t.setCookie=a,t.base64=s,t.getDevice=l,t.formatPercentage=i,t.getPageType=o},56:function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function l(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;n10*P&&(e.accumulatedTime=0),0===e.accumulatedTime)return e.animationID=null,void e.startAnimationIfNecessary();var r=(e.accumulatedTime-Math.floor(e.accumulatedTime/P)*P)/P,l=Math.floor(e.accumulatedTime/P),i={},o={},s={},c={};for(var f in t)if(t.hasOwnProperty(f)){var d=t[f];if("number"==typeof d)s[f]=d,c[f]=0,i[f]=d,o[f]=0;else{for(var m=e.state.lastIdealStyle[f],y=e.state.lastIdealVelocity[f],v=0;v1)for(var n=1;n10*b&&(e.accumulatedTime=0),0===e.accumulatedTime)return e.animationID=null,void e.startAnimationIfNecessary();for(var l=(e.accumulatedTime-Math.floor(e.accumulatedTime/b)*b)/b,i=Math.floor(e.accumulatedTime/b),o=[],s=[],c=[],u=[],p=0;p10*I&&(e.accumulatedTime=0),0===e.accumulatedTime)return e.animationID=null,void e.startAnimationIfNecessary();for(var s=(e.accumulatedTime-Math.floor(e.accumulatedTime/I)*I)/I,c=Math.floor(e.accumulatedTime/I),u=i(e.props.willEnter,e.props.willLeave,e.state.mergedPropsStyles,n,e.state.currentStyles,e.state.currentVelocities,e.state.lastIdealStyles,e.state.lastIdealVelocities),f=u[0],p=u[1],m=u[2],y=u[3],h=u[4],g=0;ga[u])return-1;if(r>l[u]&&sa[u])return 1;if(i>l[u]&&o3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------