├── .eslintignore ├── .travis.yml ├── static ├── img │ ├── searchbg.jpg │ └── header-bk.png └── css │ └── app.css ├── index.js ├── client ├── index.js ├── index.prod.js └── index.dev.js ├── shared ├── redux │ ├── store │ │ ├── configureStore.js │ │ ├── configureStore.prod.js │ │ └── configureStore.dev.js │ ├── constants │ │ └── constants.js │ ├── reducers │ │ └── reducer.js │ └── actions │ │ └── actions.js ├── components │ ├── Footer │ │ └── Footer.jsx │ ├── Header │ │ └── Header.jsx │ └── TweetsBox │ │ └── TweetsBox.jsx ├── container │ ├── DevTools │ │ └── DevTools.js │ ├── testcontainer │ │ └── testcontainer.jsx │ ├── TweetsList │ │ └── TweetsList.js │ ├── App.js │ ├── Twitsection │ │ └── Twitsection.jsx │ └── Searchcontainer │ │ └── Searchcontainer.jsx ├── routes.js ├── modules │ └── GooglePlaces.js └── tests │ ├── reducer_test.spec.js │ └── components.spec.js ├── .editorconfig ├── mern.json ├── .gitignore ├── nodemon.json ├── .babelrc ├── server ├── models │ ├── post.js │ └── tweet.js ├── routes │ ├── post.routes.js │ └── twitter.routes.js ├── util │ ├── promiseUtils.js │ └── fetchData.js ├── config.js ├── controllers │ ├── post.controller.js │ └── twitter.controller.js ├── dummyData.js ├── server.js └── tests │ └── post.spec.js ├── webpack.config.dev.js ├── webpack.config.prod.js ├── LICENSE ├── README.md ├── .eslintrc └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.dev.js 2 | webpack.config.prod.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5' 4 | - '4' 5 | 6 | -------------------------------------------------------------------------------- /static/img/searchbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitTigerInst/ElasticSearch/HEAD/static/img/searchbg.jpg -------------------------------------------------------------------------------- /static/img/header-bk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitTigerInst/ElasticSearch/HEAD/static/img/header-bk.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | require('css-modules-require-hook'); 4 | require('./server/server'); 5 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./index.prod'); 3 | } else { 4 | module.exports = require('./index.dev'); 5 | } 6 | -------------------------------------------------------------------------------- /shared/redux/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod'); 3 | } else { 4 | module.exports = require('./configureStore.dev'); 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /mern.json: -------------------------------------------------------------------------------- 1 | { 2 | "container": "/shared/container", 3 | "dumb": "/shared/components", 4 | "functional": "/shared/components", 5 | "model": "/server/models", 6 | "route": "/server/routes", 7 | "controller": "/server/controllers" 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | .idea/ 5 | dump.rdb 6 | .vscode/ 7 | public/* 8 | static/dist 9 | static/css/app.min.css 10 | 11 | 12 | # Sublime editor 13 | # ============== 14 | .sublime-project 15 | *.sublime-project 16 | *.sublime-workspace -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules" 6 | ], 7 | "verbose": true, 8 | "watch": [ 9 | "server" 10 | ], 11 | "env": { 12 | "NODE_ENV": "development" 13 | }, 14 | "ext": "js json" 15 | } 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "env": { 4 | "production": { 5 | "plugins": [ 6 | "transform-react-remove-prop-types", 7 | "transform-react-constant-elements", 8 | "transform-react-inline-elements" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shared/redux/constants/constants.js: -------------------------------------------------------------------------------- 1 | export const ADD_POST = 'ADD_POST'; 2 | export const CHANGE_SELECTED_POST = 'CHANGE_SELECTED_POST'; 3 | export const ADD_POST_REQUEST = 'ADD_POST_REQUEST'; 4 | export const ADD_POSTS = 'ADD_POSTS'; 5 | export const ADD_SELECTED_POST = 'ADD_SELECTED_POST'; 6 | export const DELETE_POST = 'DELETE_POST'; 7 | -------------------------------------------------------------------------------- /shared/components/Footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Footer() { 4 | return ( 5 |
6 |

© 2016 ·ElasticSearch Team ·Bittiger

7 | 8 |
9 | ); 10 | } 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /shared/redux/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers/reducer'; 4 | 5 | export function configureStore(initialState = {}) { 6 | const enhancer = applyMiddleware(thunk); 7 | 8 | return createStore(rootReducer, initialState, enhancer); 9 | } 10 | -------------------------------------------------------------------------------- /shared/container/DevTools/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /shared/container/testcontainer/testcontainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class testContainer extends Component { 4 | constructor(props, context) { 5 | super(props, context); 6 | this.state = {}; 7 | } 8 | 9 | render() { 10 | return( 11 |
12 |

13 | Test Routes! 14 |

15 |
16 | ); 17 | } 18 | } 19 | 20 | export default testContainer; -------------------------------------------------------------------------------- /shared/container/TweetsList/TweetsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TweetsBox from '../../components/TweetsBox/TweetsBox'; 3 | 4 | const TweetsList = (props) => { 5 | const TweetsBoxes = props.tweets.map((tweet, index) => { 6 | return 7 | }); 8 | 9 | return ( 10 |
11 | {TweetsBoxes} 12 |
13 | ); 14 | 15 | }; 16 | 17 | export default TweetsList 18 | -------------------------------------------------------------------------------- /shared/container/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class App extends Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 | { this.props.children } 13 |
14 | ); 15 | } 16 | } 17 | 18 | App.propTypes = { 19 | children: PropTypes.object.isRequired, 20 | }; 21 | 22 | export default connect()(App); 23 | -------------------------------------------------------------------------------- /shared/routes.js: -------------------------------------------------------------------------------- 1 | import { Route, IndexRoute } from 'react-router'; 2 | import React from 'react'; 3 | import App from './container/App'; 4 | import Twitsection from './container/Twitsection/Twitsection'; 5 | import testcontainer from './container/testcontainer/testcontainer'; 6 | 7 | const routes = ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default routes; 15 | -------------------------------------------------------------------------------- /server/models/post.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const Schema = mongoose.Schema; 3 | 4 | const postSchema = new Schema({ 5 | name: { type: 'String', required: true }, 6 | title: { type: 'String', required: true }, 7 | content: { type: 'String', required: true }, 8 | slug: { type: 'String', required: true }, 9 | cuid: { type: 'String', required: true }, 10 | dateAdded: { type: 'Date', default: Date.now, required: true }, 11 | }); 12 | 13 | export default mongoose.model('Post', postSchema); 14 | -------------------------------------------------------------------------------- /server/routes/post.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as PostController from '../controllers/post.controller'; 3 | const router = new Router(); 4 | 5 | // Get all Posts 6 | router.route('/getPosts').get(PostController.getPosts); 7 | 8 | // Get one post by title 9 | router.route('/getPost').get(PostController.getPost); 10 | 11 | // Add a new Post 12 | router.route('/addPost').post(PostController.addPost); 13 | 14 | // Delete a Post 15 | router.route('/deletePost').post(PostController.deletePost); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /server/util/promiseUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Throw an array to it and a function which can generate promises 3 | * and it will call them sequentially, one after another 4 | */ 5 | export function sequence(items, consumer) { 6 | const results = []; 7 | const runner = () => { 8 | const item = items.shift(); 9 | if (item) { 10 | return consumer(item) 11 | .then((result) => { 12 | results.push(result); 13 | }) 14 | .then(runner); 15 | } 16 | 17 | return Promise.resolve(results); 18 | }; 19 | 20 | return runner(); 21 | } 22 | -------------------------------------------------------------------------------- /client/index.prod.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import routes from '../shared/routes'; 3 | import { render } from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { Router, browserHistory } from 'react-router'; 6 | import { configureStore } from '../shared/redux/store/configureStore'; 7 | 8 | const store = configureStore(window.__INITIAL_STATE__); 9 | const history = browserHistory; 10 | const dest = document.getElementById('root'); 11 | 12 | render( 13 | 14 | , dest); 15 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | mongoURL: process.env.MONGO_URL || 'mongodb://luke:test@jello.modulusmongo.net:27017/etyP4isy', 3 | port: process.env.PORT || 8000, 4 | // Change this to your customized Twitter API key or just use it 5 | consumer_key: 'yMJ4PyU6wzasCIa0wHKtrpVTn', 6 | consumer_secret: 'ZjLihdaSxg4wxc5csn8MPrCJ3CE2FiBMcZw9NQQQO82hPdse5C', 7 | access_token: '3330431387-iFa2TNFhZVpbGy0DdftL4xulrYKbOMAlpkdAHLv', 8 | access_token_secret: 'HjoNbWGaI41LjhjvAFtiYZfjkP8vE0R7h7iN9sAfNHyoU', 9 | // elasticsearch config 10 | indexName: 'logstash_twitter_dev', 11 | host: 'http://13.92.81.137:9200', 12 | log: 'info' 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /server/util/fetchData.js: -------------------------------------------------------------------------------- 1 | /* This was inspired from https://github.com/caljrimmer/isomorphic-redux-app/blob/73e6e7d43ccd41e2eb557a70be79cebc494ee54b/src/common/api/fetchComponentDataBeforeRender.js */ 2 | import { sequence } from './promiseUtils'; 3 | 4 | export function fetchComponentData(store, components, params) { 5 | const needs = components.reduce((prev, current) => { 6 | return (current.need || []) 7 | .concat((current.WrappedComponent && (current.WrappedComponent.need !== current.need) ? current.WrappedComponent.need : []) || []) 8 | .concat(prev); 9 | }, []); 10 | 11 | return sequence(needs, need => store.dispatch(need(params, store.getState()))); 12 | } 13 | -------------------------------------------------------------------------------- /server/routes/twitter.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as TwitterController from '../controllers/twitter.controller'; 3 | const router = new Router(); 4 | 5 | // test url with sample parameter 6 | router.route('/search').get(TwitterController.search); 7 | 8 | // searching query API 9 | router.route('/search/:query/:geolocalization/:count/:resultType/').get(TwitterController.searchWithParams); 10 | 11 | // Test ElasticSearch Connection 12 | router.route('/test_conn').get(TwitterController.testConn); 13 | 14 | // Search twittes by area 15 | router.route('/searchByAreaHashtag/:area/:hashtag').get(TwitterController.searchByAreaHashtag); 16 | 17 | // Search twittes by hashtag 18 | router.route('/searchByHashtag/:hashtag').get(TwitterController.searchByHashtag); 19 | 20 | 21 | 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /shared/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | function Header(props, context) { 5 | return ( 6 |
7 |
8 |

9 | MERN Starter Blog 10 |

11 | { 12 | context.router.isActive('/', true) 13 | ? Add Post 14 | : null 15 | } 16 |
17 |
18 | ); 19 | } 20 | 21 | Header.contextTypes = { 22 | router: React.PropTypes.object, 23 | }; 24 | 25 | Header.propTypes = { 26 | // onClick: PropTypes.func.isRequired, 27 | // handleLogoClick: PropTypes.func, 28 | }; 29 | 30 | export default Header; 31 | -------------------------------------------------------------------------------- /shared/components/TweetsBox/TweetsBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TweetsBox = ({tweet}) => { 4 | //这里tweet已经传进来了 5 | const id = tweet.id; 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | {tweet.name} 17 |
18 |
19 | {tweet.time} 20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 | {tweet.content} 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | 35 | export default TweetsBox 36 | -------------------------------------------------------------------------------- /client/index.dev.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import routes from '../shared/routes'; 3 | import DevTools from '../shared/container/DevTools/DevTools'; 4 | import { render } from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | import { Router, browserHistory } from 'react-router'; 7 | import { configureStore } from '../shared/redux/store/configureStore'; 8 | 9 | const store = configureStore(window.__INITIAL_STATE__); 10 | const history = browserHistory; 11 | const dest = document.getElementById('root'); 12 | 13 | let toRender; 14 | 15 | if (process.env.CLIENT && !window.devToolsExtension) { 16 | toRender = ( 17 |
18 | 19 |
20 |
); 21 | } else { 22 | toRender = ( 23 | 24 | ); 25 | } 26 | 27 | render(toRender, dest); 28 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | devtool: 'cheap-module-eval-source-map', 5 | 6 | entry: ['webpack-hot-middleware/client', 7 | './client/index.js', 8 | ], 9 | 10 | output: { 11 | path: __dirname + '/dist/', 12 | filename: 'bundle.js', 13 | publicPath: '/dist/', 14 | }, 15 | 16 | resolve: { 17 | extensions: ['', '.js', '.jsx'], 18 | }, 19 | 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.css$/, 24 | loader: 'style!css?modules', 25 | }, 26 | { 27 | test: /\.jsx*$/, 28 | exclude: [/node_modules/, /.+\.config.js/], 29 | loader: 'babel', 30 | query: { 31 | presets: ['react-hmre'], 32 | }, 33 | }, 34 | ], 35 | }, 36 | 37 | plugins: [ 38 | new webpack.HotModuleReplacementPlugin(), 39 | new webpack.DefinePlugin({ 40 | 'process.env': { 41 | CLIENT: JSON.stringify(true) 42 | } 43 | }) 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | 7 | entry: __dirname + "/client/index.js", 8 | 9 | output: { 10 | path: __dirname + '/static/dist/', 11 | filename: 'bundle.js', 12 | }, 13 | 14 | resolve: { 15 | extensions: ['', '.js', '.jsx'], 16 | }, 17 | 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.css$/, 22 | loader: ExtractTextPlugin.extract('style','css?modules'), 23 | }, 24 | { 25 | test: /\.jsx*$/, 26 | exclude: /node_modules/, 27 | loader: 'babel', 28 | } 29 | ], 30 | }, 31 | 32 | plugins: [ 33 | new webpack.optimize.OccurenceOrderPlugin(), 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | 'NODE_ENV': JSON.stringify('production'), 37 | } 38 | }), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | compressor: { 41 | warnings: false, 42 | } 43 | }), 44 | new ExtractTextPlugin("app.css"), 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Hashnode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /shared/redux/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import DevTools from '../../container/DevTools/DevTools'; 4 | import rootReducer from '../reducers/reducer'; 5 | 6 | export function configureStore(initialState = {}) { 7 | let enhancerClient; 8 | if (process.env.CLIENT) { 9 | enhancerClient = compose( 10 | applyMiddleware(thunk), 11 | window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument() 12 | ); 13 | } 14 | 15 | 16 | const enhancerServer = applyMiddleware(thunk); 17 | 18 | let store; 19 | 20 | if (process.env.CLIENT) { 21 | store = createStore(rootReducer, initialState, enhancerClient); 22 | } else { 23 | store = createStore(rootReducer, initialState, enhancerServer); 24 | } 25 | 26 | if (module.hot) { 27 | // Enable Webpack hot module replacement for reducers 28 | module.hot.accept('../reducers/reducer', () => { 29 | const nextReducer = require('../reducers/reducer').default; 30 | store.replaceReducer(nextReducer); 31 | }); 32 | } 33 | 34 | return store; 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TwitterMap 2 | ### A web application based on MERN Stack (MongoDB replaced by `ElasticSearch`, `Express`, `Nodejs`, `Reactjs`), Twitter Streaming API to realize hot tweets searching and visualization. 3 | 4 | --- 5 | 6 | ## Feature (update) 7 | 8 | - Twitter API 9 | - Elastic Search 10 | 11 | --- 12 | 13 | ## Install 14 | 15 | - Install lastest verison of `Nodejs` 16 | 17 | - Start server 18 | ```bash 19 | $ npm start 20 | ``` 21 | - Test page with url `localhost:8000` 22 | 23 | 24 | --- 25 | ### How to sync with github 26 | For first time 27 | 28 | 1. add source remote to your local git (git remote add source git@github.com:BitTigerInst/ElasticSearch.git) 29 | 2. in your console, git checkout master (your branch will become your local/master) 30 | 3. git pull source master (merge source/master into local/master) 31 | 4. now your local/master (i.e. [master] is updated) 32 | 4.1 Often, there will be merge conflict (and console will which file, which line has merge conflict -> fix it in your editor) 33 | 4.2 Then git add, git commit, git push and make another pull request 34 | 5. Now you can work on new feature, e.g. git checkout -b new_feature123 35 | 36 | Later 37 | 1. On your local working branch (i.e. [new_feature123]), git pull source master 38 | 39 | 40 | -------------------------------------------------------------------------------- /shared/redux/reducers/reducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/constants'; 2 | 3 | const initialState = { posts: [], post: null }; 4 | 5 | const postReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case ActionTypes.ADD_POST : 8 | return { 9 | posts: [{ 10 | name: action.name, 11 | title: action.title, 12 | content: action.content, 13 | slug: action.slug, 14 | cuid: action.cuid, 15 | _id: action._id, 16 | }, ...state.posts], 17 | post: state.post }; 18 | 19 | case ActionTypes.CHANGE_SELECTED_POST : 20 | return { 21 | posts: state.posts, 22 | post: action.slug, 23 | }; 24 | 25 | case ActionTypes.ADD_POSTS : 26 | return { 27 | posts: action.posts, 28 | post: state.post, 29 | }; 30 | 31 | case ActionTypes.ADD_SELECTED_POST : 32 | return { 33 | post: action.post, 34 | posts: state.posts, 35 | }; 36 | 37 | case ActionTypes.DELETE_POST : 38 | return { 39 | posts: state.posts.filter((post) => post._id !== action.post._id), 40 | }; 41 | 42 | default: 43 | return state; 44 | } 45 | }; 46 | 47 | export default postReducer; 48 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "ecmaFeatures": { 10 | "jsx": true, 11 | "es6": true, 12 | "classes": true 13 | }, 14 | "rules": { 15 | "indent": [0, 2, {"SwitchCase": 1}], 16 | "comma-dangle": [1, "always-multiline"], 17 | "max-len": [1, 180, 4], 18 | "arrow-body-style": [0], 19 | 20 | ////////// Node.js ////////// 21 | 22 | 23 | ////////// Possible Errors ////////// 24 | "prefer-arrow-callback": 0, 25 | "func-names": 0, // require function expressions to have a name (off by default) 26 | "comma-dangle": 0, // disallow trailing commas in object literals 27 | "new-cap": 0, // require a capital letter for constructors 28 | "space-infix-ops": 0, // require spaces around operators 29 | "space-before-function-paren": 0, 30 | "object-shorthand": 0, 31 | 32 | ////////// Variables ////////// 33 | 34 | "no-unused-vars": 0, // disallow declaration of variables that are not used in the code 35 | 36 | ////////// ECMAScript 6 ////////// 37 | 38 | "no-var": 0 // require let or const instead of var (off by default) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/container/Twitsection/Twitsection.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Header from '../../components/Header/Header'; 4 | import Footer from '../../components/Footer/Footer'; 5 | import fetch from 'isomorphic-fetch'; 6 | import { initMap } from '../../modules/GooglePlaces'; 7 | import Searchcontainer from '../Searchcontainer/Searchcontainer'; 8 | import TweetsList from '../TweetsList/TweetsList'; 9 | import moment from 'moment'; 10 | 11 | class Twitsection extends Component { 12 | constructor(props, context) { 13 | super(props, context); 14 | this.state = {}; 15 | this.state.tweets = []; 16 | } 17 | 18 | fetchtweets(){ 19 | 20 | let url = "/TwitterAPI/search"; 21 | fetch(url, { 22 | method: 'get', 23 | headers: new Headers({ 24 | 'Content-Type': 'application/json', 25 | }), 26 | }).then((res) => res.json()).then((res) => this.setState({tweets:res})); 27 | } 28 | 29 | 30 | 31 | renderTweetsData(data) { 32 | this.setState({tweets:data}); 33 | console.log(this.state.tweets) 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export default Twitsection; 52 | -------------------------------------------------------------------------------- /server/controllers/post.controller.js: -------------------------------------------------------------------------------- 1 | import Post from '../models/post'; 2 | import cuid from 'cuid'; 3 | import slug from 'slug'; 4 | import sanitizeHtml from 'sanitize-html'; 5 | 6 | export function getPosts(req, res) { 7 | Post.find().sort('-dateAdded').exec((err, posts) => { 8 | if (err) { 9 | return res.status(500).send(err); 10 | } 11 | res.json({ posts }); 12 | }); 13 | } 14 | 15 | export function addPost(req, res) { 16 | if (!req.body.post.name || !req.body.post.title || !req.body.post.content) { 17 | return res.status(403).end(); 18 | } 19 | 20 | const newPost = new Post(req.body.post); 21 | 22 | // Let's sanitize inputs 23 | newPost.title = sanitizeHtml(newPost.title); 24 | newPost.name = sanitizeHtml(newPost.name); 25 | newPost.content = sanitizeHtml(newPost.content); 26 | 27 | newPost.slug = slug(newPost.title.toLowerCase(), { lowercase: true }); 28 | newPost.cuid = cuid(); 29 | newPost.save((err, saved) => { 30 | if (err) { 31 | return res.status(500).send(err); 32 | } 33 | return res.json({ post: saved }); 34 | }); 35 | } 36 | 37 | export function getPost(req, res) { 38 | const newSlug = req.query.slug.split('-'); 39 | const newCuid = newSlug[newSlug.length - 1]; 40 | Post.findOne({ cuid: newCuid }).exec((err, post) => { 41 | if (err) { 42 | return res.status(500).send(err); 43 | } 44 | res.json({ post }); 45 | }); 46 | } 47 | 48 | export function deletePost(req, res) { 49 | const postId = req.body.postId; 50 | Post.findById(postId).exec((err, post) => { 51 | if (err) { 52 | return res.status(500).send(err); 53 | } 54 | 55 | post.remove(() => { 56 | res.status(200).end(); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /shared/modules/GooglePlaces.js: -------------------------------------------------------------------------------- 1 | export function initMap(dataset) { 2 | //initial Site 3 | 4 | var initSite = dataset[0].googlePlace; 5 | 6 | //initialize the google map 7 | var infowindow; 8 | var map = new google.maps.Map(document.getElementById('map'), { 9 | center: initSite, 10 | zoom: 7 11 | }); 12 | 13 | //add the InfoWindow to map 14 | infowindow = new google.maps.InfoWindow(); 15 | 16 | SitePin(); 17 | function SitePin(){ 18 | var data = dataset; 19 | for( var i = 0; i < data.length; i++){ 20 | var marker = new google.maps.Marker({ 21 | map: map, 22 | position: data[i].googlePlace 23 | }); 24 | test(data[i]); 25 | } 26 | function test(dataset) { 27 | google.maps.event.addListener(marker, 'click', function() { 28 | var content = '
' + 29 | '
' + 30 | '
' + 31 | '
"'+ dataset.name +'"
' + 32 | '
'+ 33 | '
"'+ dataset.content +'"
' + 34 | '
'; 35 | infowindow.setContent(content); 36 | infowindow.open(map, this); 37 | }); 38 | } 39 | } 40 | } 41 | 42 | function attachData(marker, dataset) { 43 | var infowindow = new google.maps.InfoWindow({ 44 | content: '
' + 45 | '
' + 46 | '
' + 47 | '
"'+ dataset.name +'"
' + 48 | '
'+ 49 | '
"'+ dataset.content +'"
' + 50 | '
' 51 | }); 52 | marker.addListener(marker, 'click', function() { 53 | infowindow.open(map, this); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /shared/tests/reducer_test.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import postReducer from '../redux/reducers/reducer'; 3 | import deepFreeze from 'deep-freeze'; 4 | import * as ActionTypes from '../redux/constants/constants'; 5 | 6 | describe('reducer tests', () => { 7 | it('action ADD_POST is working', () => { 8 | const stateBefore = { posts: ['foo'], post: null }; 9 | const stateAfter = { posts: [{ 10 | name: 'prank', 11 | title: 'first post', 12 | content: 'Hello world!', 13 | _id: null, 14 | cuid: null, 15 | slug: 'first-post', 16 | }, 'foo'], post: null }; 17 | 18 | const action = { 19 | type: ActionTypes.ADD_POST, 20 | name: 'prank', 21 | title: 'first post', 22 | content: 'Hello world!', 23 | _id: null, 24 | cuid: null, 25 | slug: 'first-post', 26 | }; 27 | deepFreeze(stateBefore); 28 | deepFreeze(action); 29 | expect(stateAfter).toEqual(postReducer(stateBefore, action)); 30 | }); 31 | 32 | it('action ADD_SELECTED_POST is working', () => { 33 | const stateBefore = { 34 | posts: [{ 35 | name: 'prank', 36 | title: 'first post', 37 | content: 'Hello world!', 38 | _id: null, 39 | slug: 'first-post', 40 | 41 | }], 42 | selectedPost: null, 43 | }; 44 | 45 | const stateAfter = { 46 | posts: [{ 47 | name: 'prank', 48 | title: 'first post', 49 | content: 'Hello world!', 50 | _id: null, 51 | slug: 'first-post', 52 | }], 53 | post: { 54 | name: 'prank', 55 | title: 'first post', 56 | content: 'Hello world!', 57 | _id: null, 58 | slug: 'first-post', 59 | }, 60 | }; 61 | 62 | const action = { 63 | type: ActionTypes.ADD_SELECTED_POST, 64 | post: { 65 | name: 'prank', 66 | title: 'first post', 67 | content: 'Hello world!', 68 | _id: null, 69 | slug: 'first-post', 70 | }, 71 | }; 72 | 73 | deepFreeze(stateBefore); 74 | deepFreeze(action); 75 | expect(stateAfter).toEqual(postReducer(stateBefore, action)); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /shared/container/Searchcontainer/Searchcontainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { initMap } from '../../modules/GooglePlaces'; 4 | import moment from 'moment'; 5 | 6 | class Searchcontainer extends Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | this.state = {}; 10 | } 11 | 12 | _handleSubmit(event) { 13 | event.preventDefault(); 14 | let cleanData; 15 | let city = this._city.value; 16 | let query = this._query.value; 17 | let url = `/TwitterAPI/searchByAreaHashtag/${city}/${query}`; 18 | 19 | $.get(url).done(function(data) { 20 | cleanData = data.hits.hits.map(function(tweet) { 21 | let lng = tweet._source.place.bounding_box.coordinates[0][0][0]; 22 | let lat = tweet._source.place.bounding_box.coordinates[0][0][1]; 23 | if (tweet._source.coordinates !== null) { 24 | lat = tweet._source.coordinates.coordinates[1]; 25 | lng = tweet._source.coordinates.coordinates[0]; 26 | } 27 | let time = moment(tweet._source['@timestamp']).utc().format('MM-DD h:mm A'); 28 | return { 29 | img:tweet._source.user.profile_image_url, 30 | time:time, 31 | name:tweet._source.user.name, 32 | content:tweet._source.text, 33 | googlePlace:{ 34 | lat:lat, 35 | lng:lng 36 | } 37 | } 38 | }) 39 | initMap(cleanData); 40 | this.setState({tweets:data}); 41 | this.props.renderTweetsData(cleanData); 42 | }.bind(this)).fail(function() { 43 | alert('Error occured!'); 44 | }); 45 | } 46 | 47 | render() { 48 | return ( 49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | this._query = input}/> 57 | this._city = input} /> 58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 | ) 66 | } 67 | } 68 | 69 | export default Searchcontainer; -------------------------------------------------------------------------------- /shared/redux/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/constants'; 2 | import Config from '../../../server/config'; 3 | import fetch from 'isomorphic-fetch'; 4 | 5 | const baseURL = typeof window === 'undefined' ? process.env.BASE_URL || (`http://localhost:${Config.port}`) : ''; 6 | 7 | export function addPost(post) { 8 | return { 9 | type: ActionTypes.ADD_POST, 10 | name: post.name, 11 | title: post.title, 12 | content: post.content, 13 | slug: post.slug, 14 | cuid: post.cuid, 15 | _id: post._id, 16 | }; 17 | } 18 | 19 | export function changeSelectedPost(slug) { 20 | return { 21 | type: ActionTypes.CHANGE_SELECTED_POST, 22 | slug, 23 | }; 24 | } 25 | 26 | export function addPostRequest(post) { 27 | return (dispatch) => { 28 | fetch(`${baseURL}/api/addPost`, { 29 | method: 'post', 30 | body: JSON.stringify({ 31 | post: { 32 | name: post.name, 33 | title: post.title, 34 | content: post.content, 35 | }, 36 | }), 37 | headers: new Headers({ 38 | 'Content-Type': 'application/json', 39 | }), 40 | }).then((res) => res.json()).then(res => dispatch(addPost(res.post))); 41 | }; 42 | } 43 | 44 | export function addSelectedPost(post) { 45 | return { 46 | type: ActionTypes.ADD_SELECTED_POST, 47 | post, 48 | }; 49 | } 50 | 51 | export function getPostRequest(post) { 52 | return (dispatch) => { 53 | return fetch(`${baseURL}/api/getPost?slug=${post}`, { 54 | method: 'get', 55 | headers: new Headers({ 56 | 'Content-Type': 'application/json', 57 | }), 58 | }).then((response) => response.json()).then(res => dispatch(addSelectedPost(res.post))); 59 | }; 60 | } 61 | 62 | export function deletePost(post) { 63 | return { 64 | type: ActionTypes.DELETE_POST, 65 | post, 66 | }; 67 | } 68 | 69 | export function addPosts(posts) { 70 | return { 71 | type: ActionTypes.ADD_POSTS, 72 | posts, 73 | }; 74 | } 75 | 76 | export function fetchPosts() { 77 | return (dispatch) => { 78 | return fetch(`${baseURL}/api/getPosts`). 79 | then((response) => response.json()). 80 | then((response) => dispatch(addPosts(response.posts))); 81 | }; 82 | } 83 | 84 | export function deletePostRequest(post) { 85 | return (dispatch) => { 86 | fetch(`${baseURL}/api/deletePost`, { 87 | method: 'post', 88 | body: JSON.stringify({ 89 | postId: post._id, 90 | }), 91 | headers: new Headers({ 92 | 'Content-Type': 'application/json', 93 | }), 94 | }).then(() => dispatch(deletePost(post))); 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /server/dummyData.js: -------------------------------------------------------------------------------- 1 | import Post from './models/post'; 2 | 3 | export default function () { 4 | Post.count().exec((err, count) => { 5 | if (count > 0) { 6 | return; 7 | } 8 | 9 | const content1 = `Sed ut perspiciatis unde omnis iste natus error 10 | sit voluptatem accusantium doloremque laudantium, totam rem aperiam, 11 | eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae 12 | vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit 13 | aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos 14 | qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem 15 | ipsum quia dolor sit amet. Lorem ipsum dolor sit amet, consectetur adipiscing elit, 16 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 17 | enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 20 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 21 | est laborum`; 22 | 23 | const content2 = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, 24 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 25 | enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi 26 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit 27 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 28 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 29 | est laborum. Sed ut perspiciatis unde omnis iste natus error 30 | sit voluptatem accusantium doloremque laudantium, totam rem aperiam, 31 | eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae 32 | vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit 33 | aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos 34 | qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem 35 | ipsum quia dolor sit amet.`; 36 | 37 | const post1 = new Post({ name: 'Admin', title: 'Hello MERN', slug: 'hello-mern', cuid: 'cikqgkv4q01ck7453ualdn3hd', content: content1 }); 38 | const post2 = new Post({ name: 'Admin', title: 'Lorem Ipsum', slug: 'lorem-ipsum', cuid: 'cikqgkv4q01ck7453ualdn3hf', content: content2 }); 39 | 40 | Post.create([post1, post2], (error) => { 41 | if (!error) { 42 | // console.log('ready to go....'); 43 | } 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /server/controllers/twitter.controller.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | const router = Express.Router(); 3 | import Config from '../config'; 4 | import Elastic from '../models/tweet'; 5 | import Twit from 'twit'; 6 | const T = new Twit({ 7 | consumer_key: Config.consumer_key, 8 | consumer_secret: Config.consumer_secret, 9 | access_token: Config.access_token, 10 | access_token_secret: Config.access_token_secret, 11 | timeout_ms: 60*1000 // optional HTTP request timeout to apply to all requests. 12 | }); 13 | 14 | // test url with sample parameter 15 | export function search(req, res) { 16 | const query = 'a'; 17 | const count = 1; 18 | const resultType = 'recent'; 19 | const geoCode = [-22.912214, -43.230182, '1km']; 20 | T.get('search/tweets', { q: query, geocode: geoCode, count: count, result_type: resultType }, (err, data) => { 21 | if (err) { 22 | return res.status(500).send(err); 23 | } 24 | res.json(data); 25 | }); 26 | } 27 | 28 | // searching query API 29 | export function searchWithParams(req, res) { 30 | const query = req.params.query; 31 | const count = req.params.count; 32 | const resultType = req.params.resultType; 33 | const geoCode = req.params.geolocalization; 34 | T.get('search/tweets', { q: query, geocode: geoCode, count: count, result_type: resultType }, (err, data) => { 35 | if (err) { 36 | return res.status(500).send(err); 37 | } 38 | res.json(data); 39 | }); 40 | } 41 | 42 | export function testConn(req, res) { 43 | Elastic.testConnection().then((result) => { 44 | if (result) { 45 | res.json('All is well!'); 46 | } else { 47 | res.json('elasticsearch cluster is down!'); 48 | } 49 | }); 50 | } 51 | 52 | export function searchByAreaHashtag(req, res) { 53 | let area = req.params.area; 54 | let hashtag = req.params.hashtag; 55 | Elastic.searchByAreaHashtag(area, hashtag).then((result) => { 56 | if (result) { 57 | res.json(result); 58 | } else { 59 | res.json('search query failed!'); 60 | } 61 | }); 62 | } 63 | 64 | 65 | export function searchByHashtag(req,res) { 66 | let hashtag = req.params.hashtag; 67 | Elastic.searchByHashtag(hashtag).then((result) => { 68 | if (result) { 69 | res.json(result); 70 | } else { 71 | res.json('search query failed!'); 72 | } 73 | }); 74 | } 75 | 76 | export function searchByHashtagCityCount(req,res) { 77 | let hashtag = req.params.hashtag; 78 | let location = req.params.location; 79 | let count = req.params.count; 80 | Elastic.searchByHashtag(hashtag, location, count).then((result) => { 81 | if (result) { 82 | res.json(result); 83 | } else { 84 | res.json('search query failed!'); 85 | } 86 | }); 87 | } 88 | 89 | 90 | 91 | export default router; 92 | -------------------------------------------------------------------------------- /shared/tests/components.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import PostListItem from '../components/PostListItem/PostListItem'; 4 | import PostCreateView from '../components/PostCreateView/PostCreateView'; 5 | import React from 'react'; 6 | import expectJSX from 'expect-jsx'; 7 | import { Link } from 'react-router'; 8 | 9 | expect.extend(expectJSX); 10 | 11 | describe('component tests', () => { 12 | it('should render PostListItem properly', () => { 13 | const renderer = TestUtils.createRenderer(); 14 | const post = { 15 | name: 'Prank', 16 | title: 'first post', 17 | content: 'hello world!', 18 | slug: 'first-post', 19 | cuid: 'cikpdcdn60000zjxom3dmavzq', 20 | }; 21 | renderer.render( 22 | 29 | ); 30 | const output = renderer.getRenderOutput(); 31 | expect(output).toEqualJSX( 32 |
33 |

34 | 35 | {post.title} 36 | 37 |

38 |

By {post.name}

39 |

{post.content}

40 |

Delete Post

41 |
42 |
43 | ); 44 | }); 45 | 46 | it('should render PostCreateView properly', () => { 47 | const renderer = TestUtils.createRenderer(); 48 | renderer.render(); 49 | 50 | const output = renderer.getRenderOutput(); 51 | expect(output).toEqualJSX( 52 |
53 |
54 |

Create new post

55 | 56 | 57 | 58 | Submit 59 |
60 |
61 | ); 62 | }); 63 | 64 | it('should show post create form in PostCreateView if showAddPost is true', () => { 65 | const renderer = TestUtils.createRenderer(); 66 | renderer.render(); 67 | 68 | const output = renderer.getRenderOutput(); 69 | expect(output).toEqualJSX( 70 |
71 |
72 |

Create new post

73 | 74 | 75 | 76 | Submit 77 |
78 |
79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-starter", 3 | "version": "1.0.0", 4 | "description": "Boilerplate project for building Isomorphic apps using React and Redux", 5 | "scripts": { 6 | "test": "mocha shared/tests/*.spec.js --compilers js:babel-register", 7 | "test:server": "cross-env NODE_ENV=test PORT=8080 MONGO_URL=mongodb://localhost:27017/mern-test mocha --compilers js:babel-register --recursive server/tests/**/*.spec.js", 8 | "start": "cross-env NODE_ENV=development nodemon index.js", 9 | "start:prod": "cross-env NODE_ENV=production node index.js", 10 | "bs": "npm run clean && npm run build && npm run start:prod", 11 | "minify": "cleancss -o static/css/app.min.css static/css/app.css", 12 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js && npm run minify", 13 | "clean": "rimraf static/dist", 14 | "slate": "rimraf node_modules && npm install", 15 | "lint": "eslint client server shared" 16 | }, 17 | "pre-commit": [ 18 | "lint", 19 | "test", 20 | "test:server" 21 | ], 22 | "repository": { 23 | "type": "github", 24 | "url": "https://github.com/BitTigerInst/ElasticSearch" 25 | }, 26 | "bugs": { 27 | "url": "" 28 | }, 29 | "homepage": "", 30 | "author": "BTSearcher", 31 | "license": "MIT", 32 | "dependencies": { 33 | "babel-core": "^6.4.5", 34 | "body-parser": "^1.14.2", 35 | "cookie-parser": "~1.3.5", 36 | "cuid": "^1.3.8", 37 | "debug": "~2.2.0", 38 | "elasticsearch": "^11.0.1", 39 | "express": "^4.13.4", 40 | "history": "^1.17.0", 41 | "isomorphic-fetch": "^2.2.1", 42 | "moment": "^2.13.0", 43 | "mongoose": "^4.3.7", 44 | "morgan": "~1.6.1", 45 | "react": "^15.0.1", 46 | "react-addons-create-fragment": "^15.0.2", 47 | "react-dom": "^15.0.1", 48 | "react-redux": "^4.1.2", 49 | "react-router": "^2.0.0-rc5", 50 | "redux": "^3.5.2", 51 | "redux-thunk": "^1.0.3", 52 | "sanitize-html": "^1.11.3", 53 | "serve-favicon": "~2.3.0", 54 | "slug": "^0.9.1", 55 | "twit": "^2.2.4" 56 | }, 57 | "devDependencies": { 58 | "babel-eslint": "^5.0.0-beta6", 59 | "babel-loader": "^6.2.1", 60 | "babel-plugin-react-transform": "^2.0.0", 61 | "babel-plugin-transform-react-constant-elements": "6.5.0", 62 | "babel-plugin-transform-react-inline-elements": "6.6.5", 63 | "babel-plugin-transform-react-remove-prop-types": "0.2.4", 64 | "babel-polyfill": "^6.3.14", 65 | "babel-preset-es2015": "^6.3.13", 66 | "babel-preset-react": "^6.3.13", 67 | "babel-preset-react-hmre": "^1.1.0", 68 | "babel-register": "^6.7.2", 69 | "chai": "^3.5.0", 70 | "clean-css": "^3.4.9", 71 | "cross-env": "^1.0.7", 72 | "css-loader": "^0.23.1", 73 | "css-modules-require-hook": "^2.1.0", 74 | "deep-freeze": "0.0.1", 75 | "eslint": "^1.10.3", 76 | "eslint-config-airbnb": "^4.0.0", 77 | "eslint-plugin-react": "^3.16.1", 78 | "expect": "^1.13.4", 79 | "expect-jsx": "^2.2.2", 80 | "extract-text-webpack-plugin": "^1.0.1", 81 | "mocha": "^2.4.5", 82 | "nodemon": "^1.9.1", 83 | "pre-commit": "^1.1.2", 84 | "react-addons-test-utils": "^15.0.1", 85 | "react-transform-hmr": "^1.0.1", 86 | "redux-devtools": "^3.1.1", 87 | "redux-devtools-dock-monitor": "^1.1.0", 88 | "redux-devtools-log-monitor": "^1.0.4", 89 | "rimraf": "^2.5.1", 90 | "style-loader": "^0.13.0", 91 | "supertest": "^1.1.0", 92 | "webpack": "^1.12.12", 93 | "webpack-dev-middleware": "^1.5.1", 94 | "webpack-hot-middleware": "^2.6.4" 95 | }, 96 | "engines": { 97 | "node": ">=4" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /server/models/tweet.js: -------------------------------------------------------------------------------- 1 | import elasticsearch from 'elasticsearch'; 2 | import config from '../config'; 3 | const elasticClient = new elasticsearch.Client({ 4 | host: config.host, 5 | log: config.log 6 | }); 7 | 8 | /** 9 | * Test ElasticSearch Connection 10 | **/ 11 | 12 | function testConnection() { 13 | return elasticClient.ping({ 14 | // ping usually has a 3000ms timeout 15 | requestTimeout: Infinity, 16 | // undocumented params are appended to the query string 17 | hello: 'elasticsearch!' 18 | }); 19 | } 20 | 21 | exports.testConnection = testConnection; 22 | 23 | /** 24 | * search twittes by area with time range 25 | * ref see https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html 26 | **/ 27 | 28 | function searchByAreaWithinTimeRange(area,time_range) { 29 | time_range = "10m" 30 | return elasticClient.search({ 31 | index: config.indexName, 32 | body: 33 | { 34 | "query": { 35 | "bool": { 36 | "must": [ 37 | { 38 | "match": { 39 | "place.name": area, 40 | } 41 | }, 42 | { 43 | "range": { 44 | "@timestamp": { 45 | "gte": `now-${time_range}`, 46 | "lt": "now" 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | } 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * search twittes by area 59 | **/ 60 | 61 | function searchByAreaHashtag(area, hashtag) { 62 | return elasticClient.search({ 63 | index: config.indexName, 64 | // q: `place.name: ${area}` 65 | body: 66 | { 67 | "from": 0, 68 | "size": 30, 69 | "query": { 70 | "bool": { 71 | "must": [ 72 | { 73 | "match": { 74 | "place.name": area 75 | } 76 | }, 77 | { 78 | "match": { 79 | "entities.hashtags": hashtag 80 | } 81 | } 82 | ], 83 | "filter": { 84 | "exists": { 85 | "field": "coordinates" 86 | } 87 | } 88 | } 89 | } 90 | } 91 | }); 92 | } 93 | 94 | /** 95 | * search twittes by hashtag 96 | **/ 97 | function searchByHashtag(hashtag) { 98 | return elasticClient.search({ 99 | index: config.indexName, 100 | body: 101 | { 102 | "query": { 103 | "bool": { 104 | "must": [ 105 | { 106 | "match": { 107 | "entities.hashtags": hashtag 108 | } 109 | } 110 | ] 111 | } 112 | } 113 | } 114 | }); 115 | } 116 | 117 | 118 | /** 119 | * search twittes by hashtag with fuzziness 120 | **/ 121 | function searchByHashtagWithFuzziness(hashtag, fuzziness) { 122 | return elasticClient.search({ 123 | index: config.indexName, 124 | body: 125 | { 126 | "query": { 127 | "bool": { 128 | "must": [ 129 | { 130 | "fuzzy": { 131 | "entities.hashtags": { 132 | "value": `${hashtag}`, 133 | "boost": 1, 134 | "fuzziness": `${fuzziness}`, 135 | "prefix_length": 0, 136 | "max_expansions": 100 137 | } 138 | } 139 | } 140 | ] 141 | } 142 | } 143 | } 144 | }); 145 | } 146 | 147 | 148 | exports.searchByAreaHashtag = searchByAreaHashtag; 149 | 150 | exports.searchByHashtag = searchByHashtag; 151 | 152 | exports.searchByHashtagWithFuzziness = searchByHashtagWithFuzziness; 153 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import mongoose from 'mongoose'; 3 | import bodyParser from 'body-parser'; 4 | import path from 'path'; 5 | 6 | // Webpack Requirements 7 | import webpack from 'webpack'; 8 | import config from '../webpack.config.dev'; 9 | import webpackDevMiddleware from 'webpack-dev-middleware'; 10 | import webpackHotMiddleware from 'webpack-hot-middleware'; 11 | 12 | // Initialize the Express App 13 | const app = new Express(); 14 | 15 | if (process.env.NODE_ENV !== 'production') { 16 | const compiler = webpack(config); 17 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 18 | app.use(webpackHotMiddleware(compiler)); 19 | } 20 | 21 | // React And Redux Setup 22 | import { configureStore } from '../shared/redux/store/configureStore'; 23 | import { Provider } from 'react-redux'; 24 | import React from 'react'; 25 | import { renderToString } from 'react-dom/server'; 26 | import { match, RouterContext } from 'react-router'; 27 | 28 | // Import required modules 29 | import routes from '../shared/routes'; 30 | import { fetchComponentData } from './util/fetchData'; 31 | import posts from './routes/post.routes'; 32 | import tweets from './routes/twitter.routes'; 33 | import dummyData from './dummyData'; 34 | import serverConfig from './config'; 35 | 36 | // MongoDB Connection 37 | mongoose.connect(serverConfig.mongoURL, (error) => { 38 | if (error) { 39 | console.error('Please make sure Mongodb is installed and running!'); // eslint-disable-line no-console 40 | throw error; 41 | } 42 | 43 | // feed some dummy data in DB. 44 | dummyData(); 45 | }); 46 | 47 | // Apply body Parser and server public assets and routes 48 | app.use(bodyParser.json({ limit: '20mb' })); 49 | app.use(bodyParser.urlencoded({ limit: '20mb', extended: false })); 50 | app.use(Express.static(path.resolve(__dirname, '../static'))); 51 | app.use('/api', posts); 52 | app.use('/TwitterAPI', tweets); 53 | // Render Initial HTML 54 | const renderFullPage = (html, initialState) => { 55 | const cssPath = process.env.NODE_ENV === 'production' ? '/css/app.min.css' : '/css/app.css'; 56 | return ` 57 | 58 | 59 | 60 | 61 | 62 | 63 | MERN Starter - Blog App 64 | 65 | 66 | 67 | 68 | 69 | 70 |
${html}
71 | 74 | 75 | 76 | 77 | `; 78 | }; 79 | 80 | const renderError = err => { 81 | const softTab = ' '; 82 | const errTrace = process.env.NODE_ENV !== 'production' ? 83 | `:

${softTab}${err.stack.replace(/\n/g, `
${softTab}`)}
` : ''; 84 | return renderFullPage(`Server Error${errTrace}`, {}); 85 | }; 86 | 87 | // Server Side Rendering based on routes matched by React-router. 88 | app.use((req, res, next) => { 89 | match({ routes, location: req.url }, (err, redirectLocation, renderProps) => { 90 | if (err) { 91 | return res.status(500).end(renderError(err)); 92 | } 93 | 94 | if (redirectLocation) { 95 | return res.redirect(302, redirectLocation.pathname + redirectLocation.search); 96 | } 97 | 98 | if (!renderProps) { 99 | return next(); 100 | } 101 | 102 | const initialState = { tweets: [], tweet: {} }; 103 | 104 | const store = configureStore(initialState); 105 | 106 | return fetchComponentData(store, renderProps.components, renderProps.params) 107 | .then(() => { 108 | const initialView = renderToString( 109 | 110 | 111 | 112 | ); 113 | const finalState = store.getState(); 114 | 115 | res.status(200).end(renderFullPage(initialView, finalState)); 116 | }); 117 | }); 118 | }); 119 | 120 | // start app 121 | app.listen(serverConfig.port, (error) => { 122 | if (!error) { 123 | console.log(`MERN is running on port: ${serverConfig.port}! Build something amazing!`); // eslint-disable-line 124 | } 125 | }); 126 | 127 | export default app; 128 | -------------------------------------------------------------------------------- /server/tests/post.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import mocha from 'mocha'; 4 | import app from '../server'; 5 | import chai from 'chai'; 6 | import request from 'supertest'; 7 | import mongoose from 'mongoose'; 8 | import Post from '../models/post'; 9 | 10 | const expect = chai.expect; 11 | 12 | function connectDB(done) { 13 | if (mongoose.connection.name !== 'mern-test') { 14 | return done(); 15 | } 16 | 17 | mongoose.connect((process.env.MONGO_URL || 'mongodb://localhost:27017/mern-test'), function (err) { 18 | if (err) return done(err); 19 | done(); 20 | }); 21 | } 22 | 23 | function dropDB(done) { 24 | if (mongoose.connection.name !== 'mern-test') { 25 | return done(); 26 | } 27 | 28 | mongoose.connection.db.dropDatabase(function (err) { 29 | mongoose.connection.close(done); 30 | }); 31 | } 32 | 33 | describe('GET /api/getPosts', function () { 34 | 35 | beforeEach('connect and add two post entries', function (done) { 36 | 37 | connectDB(function () { 38 | var post1 = new Post({name: 'Prashant', title: 'Hello Mern', content: "All cats meow 'mern!'"}); 39 | var post2 = new Post({name: 'Mayank', title: 'Hi Mern', content: "All dogs bark 'mern!'"}); 40 | 41 | Post.create([post1, post2], function (err, saved) { 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | afterEach(function (done) { 48 | dropDB(done); 49 | }); 50 | 51 | // The code here means we have to run the program while committing code to github. Comment this temporarily. 52 | // it('Should correctly give number of Posts', function (done) { 53 | 54 | // request(app) 55 | // .get('/api/getPosts') 56 | // .set('Accept', 'application/json') 57 | // .end(function (err, res) { 58 | // Post.find().exec(function (err, posts) { 59 | // expect(posts.length).to.equal(res.body.posts.length); 60 | // done(); 61 | // }); 62 | // }); 63 | // }); 64 | }); 65 | 66 | describe('GET /api/getPost', function () { 67 | 68 | beforeEach('connect and add one Post entry', function(done){ 69 | 70 | connectDB(function () { 71 | var post = new Post({ name: 'Foo', title: 'bar', slug: 'bar', cuid: 'f34gb2bh24b24b2', content: 'Hello Mern says Foo' }); 72 | 73 | post.save(function (err, saved) { 74 | done(); 75 | }); 76 | }); 77 | }); 78 | 79 | afterEach(function (done) { 80 | dropDB(done); 81 | }); 82 | 83 | it('Should send correct data when queried against a title', function (done) { 84 | 85 | request(app) 86 | .get('/api/getPost?slug=bar-f34gb2bh24b24b2') 87 | .set('Accept', 'application/json') 88 | .end(function (err, res) { 89 | Post.findOne({ cuid: 'f34gb2bh24b24b2' }).exec(function (err, post) { 90 | expect(post.name).to.equal('Foo'); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | }); 97 | 98 | describe('POST /api/addPost', function () { 99 | 100 | beforeEach('connect and add a post', function (done) { 101 | 102 | connectDB(function () { 103 | done(); 104 | }); 105 | }); 106 | 107 | afterEach(function (done) { 108 | dropDB(done); 109 | }); 110 | 111 | it('Should send correctly add a post', function (done) { 112 | 113 | request(app) 114 | .post('/api/addPost') 115 | .send({ post: { name: 'Foo', title: 'bar', content: 'Hello Mern says Foo' } }) 116 | .set('Accept', 'application/json') 117 | .end(function (err, res) { 118 | Post.findOne({ title: 'bar' }).exec(function (err, post) { 119 | expect(post.name).to.equal('Foo'); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | 125 | }); 126 | 127 | describe('POST /api/deletePost', function () { 128 | var postId; 129 | 130 | beforeEach('connect and add one Post entry', function(done){ 131 | 132 | connectDB(function () { 133 | var post = new Post({ name: 'Foo', title: 'bar', slug: 'bar', cuid: 'f34gb2bh24b24b2', content: 'Hello Mern says Foo' }); 134 | 135 | post.save(function (err, saved) { 136 | postId = saved._id; 137 | done(); 138 | }); 139 | }); 140 | }); 141 | 142 | afterEach(function (done) { 143 | dropDB(done); 144 | }); 145 | 146 | it('Should connect and delete a post', function (done) { 147 | 148 | // Check if post is saved in DB 149 | Post.findById(postId).exec(function (err, post) { 150 | expect(post.name).to.equal('Foo') 151 | }); 152 | 153 | request(app) 154 | .post('/api/deletePost') 155 | .send({ postId: postId}) 156 | .set('Accept', 'application/json') 157 | .end(function () { 158 | 159 | // Check if post is removed from DB 160 | Post.findById(postId).exec(function (err, post) { 161 | expect(post).to.equal(null); 162 | done(); 163 | }); 164 | }); 165 | }) 166 | }); 167 | -------------------------------------------------------------------------------- /static/css/app.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Exo+2:400,300,500,600); 2 | *{ 3 | margin: 0; 4 | padding: 0; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | line-height: normal; 9 | } 10 | 11 | ::-webkit-input-placeholder { 12 | color:#aaa; 13 | font-weight: 300; 14 | } 15 | ::-moz-placeholder { 16 | color:#aaa; 17 | font-weight: 300; 18 | } 19 | :-ms-input-placeholder { 20 | color:#aaa; 21 | font-weight: 300; 22 | } 23 | input:-moz-placeholder { 24 | color:#aaa; 25 | font-weight: 300; 26 | } 27 | 28 | body { 29 | background: #fafafa; 30 | font-family: 'Lato', sans-serif; 31 | font-size: 16px; 32 | } 33 | 34 | button{ 35 | background: none; 36 | border: 1px solid black; 37 | } 38 | 39 | .subtitle { 40 | margin-top: 0; 41 | margin-left: 0; 42 | color: #9E9E9E; 43 | } 44 | 45 | .container { 46 | min-height: 600px; 47 | width: 100%; 48 | padding: 15px; 49 | margin: 0 auto; 50 | } 51 | 52 | .form { 53 | display: none; 54 | background: #FAFAFA; 55 | padding: 32px 0; 56 | border: 1px solid #eee; 57 | border-radius: 4px; 58 | } 59 | 60 | .form-content{ 61 | width: 100%; 62 | max-width: 600px; 63 | margin: auto; 64 | font-size: 14px; 65 | } 66 | 67 | .form .form-title{ 68 | font-size: 16px; 69 | font-weight: 700; 70 | margin-bottom: 16px; 71 | color: #757575; 72 | } 73 | 74 | .form .form-field{ 75 | width: 100%; 76 | margin-bottom: 16px; 77 | font-family: 'Lato', sans-serif; 78 | font-size: 16px; 79 | line-height: normal; 80 | padding: 12px 16px; 81 | border-radius: 4px; 82 | border: 1px solid #ddd; 83 | outline: none; 84 | color: #212121; 85 | } 86 | 87 | .form textarea.form-field{ 88 | min-height: 200px; 89 | } 90 | 91 | .form .post-submit-button{ 92 | display: inline-block; 93 | padding: 8px 16px; 94 | font-size: 18px; 95 | color: #FFF; 96 | background: #03A9F4; 97 | text-decoration: none; 98 | border-radius: 4px; 99 | } 100 | 101 | .form.appear { 102 | display: block; 103 | } 104 | 105 | .single-post { 106 | margin: 20px 0; 107 | padding: 15px; 108 | border-radius: 2px; 109 | } 110 | 111 | .single-post .post-title{ 112 | font-size: 28px; 113 | margin-bottom: 16px; 114 | font-weight: 400; 115 | color: #616161; 116 | } 117 | 118 | .single-post.post-detail .post-title{ 119 | font-size: 42px; 120 | color: #454545; 121 | } 122 | 123 | .single-post .post-title a{ 124 | text-decoration: none; 125 | color: #616161; 126 | } 127 | 128 | .single-post .author-name{ 129 | font-size: 16px; 130 | margin-bottom: 16px; 131 | color: #757575; 132 | } 133 | 134 | .single-post .post-desc{ 135 | font-size: 14px; 136 | color: #888; 137 | margin-bottom: 8px; 138 | } 139 | 140 | .single-post.post-detail .post-desc{ 141 | font-size: 16px; 142 | color: #555; 143 | } 144 | 145 | .single-post .post-action a{ 146 | color: #555; 147 | text-decoration: none; 148 | font-size: 14px; 149 | font-style: italic; 150 | } 151 | 152 | .single-post .post-action a:hover{ 153 | color: #EF5350; 154 | } 155 | 156 | .single-post .divider{ 157 | border: 0; 158 | height: 1px; 159 | background: #ccc; 160 | width: 250px; 161 | margin: 32px auto 0; 162 | } 163 | 164 | .header { 165 | background-color: #eee; 166 | background-image: url('/img/header-bk.png'); 167 | background-position: center; 168 | background-size: cover; 169 | border-bottom: 1px solid #ccc; 170 | } 171 | 172 | .header .header-content { 173 | width: 100%; 174 | max-width: 980px; 175 | margin: auto; 176 | padding: 64px 16px; 177 | overflow: auto; 178 | } 179 | 180 | .site-title { 181 | font-weight: 300; 182 | font-size: 42px; 183 | float: left; 184 | } 185 | 186 | .site-title a{ 187 | text-decoration: none; 188 | color: #FFF; 189 | } 190 | 191 | .add-post-button { 192 | display: inline-block; 193 | color: #FFF; 194 | background: #03A9F4; 195 | padding: 8px 16px; 196 | text-decoration: none; 197 | border-radius: 1000px; 198 | float: right; 199 | } 200 | 201 | .footer{ 202 | text-align: center; 203 | padding: 56px 0; 204 | background-color: #FFF; 205 | background-image: url('/img/header-bk.png'); 206 | background-position: center; 207 | background-size: cover; 208 | } 209 | 210 | .footer p{ 211 | margin: 0 0 8px 0; 212 | font-size: 18px; 213 | color: #FFF; 214 | } 215 | 216 | .footer a{ 217 | color: #FFF; 218 | text-decoration: none; 219 | font-weight: 700; 220 | } 221 | 222 | #map{ 223 | margin: 0 auto; 224 | width: 80%; 225 | min-height: 500px; 226 | } 227 | 228 | .SearchWrapper{ 229 | height: 450px; 230 | position: relative; 231 | } 232 | 233 | .SearchWidget{ 234 | width: 42%; 235 | margin: 0 auto; 236 | top: 50%; 237 | position: relative; 238 | } 239 | 240 | .tweet-form-fields{ 241 | height: 40px; 242 | } 243 | 244 | .tweet-form-fields input{ 245 | height: 100%; 246 | border: none; 247 | -webkit-box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.2); 248 | box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.2); 249 | background-color: rgba(255, 255, 255, 0.2); 250 | border-radius: 3px; 251 | padding: 20px; 252 | margin:0 5px; 253 | color:white; 254 | } 255 | 256 | .tweet-form-fields ::-webkit-input-placeholder{ /* WebKit, Blink, Edge */ 257 | color: rgba(255, 255, 255, 0.55); 258 | } 259 | 260 | .tweet-form-fields :-moz-placeholder{ /* WebKit, Blink, Edge */ 261 | color: rgba(255, 255, 255, 0.55); 262 | } 263 | 264 | .tweet-form-fields :-ms-input-placeholder{ /* WebKit, Blink, Edge */ 265 | color: rgba(255, 255, 255, 0.55); 266 | } 267 | 268 | .tweet-form-fields ::-moz-placeholder{ /* WebKit, Blink, Edge */ 269 | color: rgba(255, 255, 255, 0.55); 270 | } 271 | 272 | .tweet-form-fields input, 273 | .tweet-form-fields input::-webkit-input-placeholder { 274 | font-size: 20px; 275 | vertical-align: top; 276 | } 277 | 278 | .tweet-form-fields button{ 279 | margin-top: 20px; 280 | border-radius: 3px; 281 | vertical-align: super; 282 | height: 100%; 283 | width: 80px; 284 | font-size: 15px; 285 | background: #b34365; 286 | border: none; 287 | color: white; 288 | } 289 | 290 | .tweet-form-action{ 291 | display: inline-block; 292 | height: 100%; 293 | } 294 | 295 | .bgwrapper{ 296 | height: 400px; 297 | } 298 | 299 | .bgwrapper, .bgwrapper img{ 300 | background-size: cover; 301 | width: 100%; 302 | height: 100%; 303 | position: absolute; 304 | } 305 | 306 | /*css for TweetsList*/ 307 | 308 | .user-info{ 309 | padding: 14px; 310 | overflow: auto; 311 | } 312 | 313 | .tweet-avatar img{ 314 | border: 1px #ccc solid; 315 | border-radius: 50%; 316 | width: 50px; 317 | height: 50px; 318 | display: inline-block; 319 | float: left; 320 | } 321 | 322 | .tweet-id{ 323 | 324 | font-weight: bold; 325 | } 326 | 327 | .tweet-info{ 328 | padding-top: 5px; 329 | padding-left: 15px; 330 | float: left; 331 | } 332 | 333 | .tweet-time{ 334 | margin-top: 8.5px; 335 | font-size: .8em; 336 | } 337 | 338 | .tweets-text{ 339 | font-size: .9em; 340 | padding: 30px; 341 | line-height: 20px; 342 | } 343 | 344 | .tweet-wrapper{ 345 | display: inline-block; 346 | width: 33.3%; 347 | } 348 | 349 | .tweet-showbox{ 350 | font-weight: 300; 351 | background-color: #fff; 352 | margin: 20px; 353 | border: 1px #ccc solid; 354 | border-radius: 7px; 355 | } 356 | 357 | .pin-header{ 358 | overflow: auto; 359 | } 360 | 361 | .pin-header div{ 362 | display: inline-block; 363 | float: left; 364 | } 365 | 366 | .pin-avatar img{ 367 | width: 50px; 368 | height: 50px; 369 | border-radius: 50%; 370 | background-image: url(传过来数据的img); 371 | background-size: cover; 372 | } 373 | 374 | .pin-username{ 375 | padding: 16px; 376 | font-size: 16px; 377 | font-weight: 400; 378 | } 379 | 380 | .pin-content{ 381 | padding: 25px; 382 | } 383 | 384 | @media (max-width: 767px){ 385 | .add-post-button{ 386 | float: left; 387 | margin-top: 16px; 388 | } 389 | } 390 | --------------------------------------------------------------------------------