├── .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 |
' +
45 | ''
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 |
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 |
--------------------------------------------------------------------------------