├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── classifier.json
├── client
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
│ ├── components
│ ├── App.js
│ ├── Header.js
│ ├── Preloader.js
│ ├── Sidebar.js
│ ├── Tweet.js
│ └── styles
│ │ ├── App.css
│ │ ├── header.css
│ │ ├── index.css
│ │ ├── preloader.css
│ │ ├── sidebar.css
│ │ └── tweets.css
│ ├── containers
│ ├── categories.js
│ └── tweets.js
│ ├── index.js
│ ├── logo.svg
│ ├── registerServiceWorker.js
│ └── util
│ ├── categories.js
│ ├── config.js
│ └── fetch.js
├── controllers
├── __tests__
│ └── tweets.test.js
├── index.js
└── tweets.js
├── lib
├── __mocks__
│ └── redis.js
├── classifier.js
├── logger.js
└── redis.js
├── package-lock.json
├── package.json
├── server.js
├── start-client.js
├── template.html
├── training-data.json
└── training.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react"],
3 | "plugins": [
4 | "transform-async-to-generator"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | client/node_modules
3 | client/build/
4 | client/package.json
5 | client/package-lock.json
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "parserOptions": {
4 | "ecmaVersion": 6,
5 | "sourceType": "module",
6 | "ecmaFeatures": {
7 | "jsx": true
8 | }
9 | },
10 | "env": {
11 | "browser": true,
12 | "node": true,
13 | "es6": true,
14 | "jest": true
15 | },
16 | "plugins": [
17 | "react"
18 | ],
19 | "rules": {
20 | "jsx-quotes": ["error", "prefer-double"]
21 | },
22 | "extends": ["standard", "standard-react"]
23 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .env
3 | test.js
4 | pos.js
5 | search.js
6 | /client/build
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Oluwaseun Omoyajowo
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Job Tweets
2 | Find Real time job tweets!
3 |
4 | ## Getting Started
5 |
6 | ### Installion
7 | 1. Clone the repo
8 | 2. Install all server modules using `npm install`
9 | 3. For client modules, navigate to client directory and run `npm install`
10 | 4. Get your API credentials.
11 | To get Consumer Key & Consumer Secret, you have to create an app in Twitter via
12 | https://apps.twitter.com/app/new
13 | Then you'll be taken to a page containing Consumer Key & Consumer Secret.
14 |
15 | 5. In the main directory, Rename the .env-dev file to .env and enter your twitter api credentials.
16 | 6. To start the both servers (Backend and Client dev server), run `npm run dev` from the main server directory.
17 |
18 | ### Urgent ToDo
19 |
20 | 1. Cache tweets for 24 hrs
21 | 2. Filter tweets base on location
22 | 3. Filter tweets base on Job Spec
23 |
24 | All commit in development branch.
25 |
26 | Don't forget to star and contribute!
27 |
28 |
--------------------------------------------------------------------------------
/classifier.json:
--------------------------------------------------------------------------------
1 | {"classifier":{"classFeatures":{"SD":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"10":2,"11":2,"12":2,"13":2,"14":2,"15":2,"16":2,"17":2,"18":2,"19":2,"20":2,"21":2,"22":2,"23":2,"24":2,"25":2,"26":2,"27":2,"28":2,"29":2,"30":2,"31":2,"32":2,"33":2,"34":2,"35":2,"36":2,"37":2,"38":2,"39":2,"40":2,"41":2,"42":2,"43":2,"44":2,"45":2,"46":2,"47":2,"48":2,"49":2,"50":2,"51":2},"SM":{"52":2,"53":2,"54":2,"55":2,"56":2,"57":2,"58":2,"59":2,"60":2,"61":2,"62":2,"63":2,"64":2,"65":2,"66":2,"67":2},"BDM":{"51":2,"68":2,"69":2,"70":2,"71":2,"72":2,"73":2},"HC":{"74":2,"75":2,"76":2,"77":2,"78":2,"79":2,"80":2,"81":2},"IC":{"82":2,"83":2,"84":2,"85":2},"RS":{"57":2,"86":2,"87":2,"88":2,"89":2,"90":2},"HR":{"91":2,"92":2,"93":2,"94":2},"HT":{"95":2,"96":2,"97":2,"98":2,"99":2,"100":2,"101":2,"102":2,"103":2,"104":2,"105":2},"MP":{"69":2,"73":2,"106":2,"107":2,"108":2},"RE":{"109":2,"110":2,"111":2},"AF":{"107":2,"112":2,"113":2,"114":2,"115":2,"116":2,"117":2},"TL":{"118":2,"119":2,"120":2,"121":2,"122":2,"123":2,"124":2,"125":2,"126":2,"127":2,"128":2},"AD":{"50":2,"114":2,"129":2,"130":2,"131":2,"132":2,"133":2,"134":2,"135":2,"136":2,"137":2,"138":2,"139":2,"140":2},"CW":{"66":2,"140":2,"141":2,"142":2,"143":2,"144":2,"145":2,"146":2,"147":2,"148":2,"149":2},"SA":{"51":2,"73":2,"79":2,"150":2,"151":2,"152":2,"153":2,"154":2,"155":2,"156":2},"MU":{"93":2,"145":2,"157":2,"158":2,"159":2,"160":2,"161":2,"162":2,"163":2},"OF":{"164":2,"165":2,"166":2,"167":2,"168":2,"169":2,"170":2,"171":2},"CS":{"78":2,"172":2,"173":2,"174":2,"175":2},"RF":{"70":2,"165":2,"176":2,"177":2,"178":2,"179":2,"180":2,"181":2,"182":2,"183":2,"184":2,"185":2,"186":2,"187":2,"188":2,"189":2,"190":2},"BF":{"70":2,"72":2,"79":2,"171":2,"172":2,"191":2,"192":2,"193":2,"194":2,"195":2,"196":2,"197":2,"198":2,"199":2,"200":2},"LG":{"165":2,"166":2,"198":2,"201":2,"202":2,"203":2,"204":2,"205":2,"206":2},"EG":{"48":2,"207":2,"208":2,"209":2,"210":2,"211":2,"212":2}},"classTotals":{"SD":2,"SM":2,"BDM":2,"HC":2,"IC":2,"RS":2,"HR":2,"HT":2,"MP":2,"RE":2,"AF":2,"TL":2,"AD":2,"CW":2,"SA":2,"MU":2,"OF":2,"CS":2,"RF":2,"BF":2,"LG":2,"EG":2},"totalExamples":23,"smoothing":1},"docs":[{"label":"SD","text":["io","net","app","angular","vuej","architect","android","actionscript","activex","algol","algorithm","api","applet","asp","aspi","assembl","assembl","babel","backend","back","end","sharp","clojur","compil","css","dart","django","ecmascript","front","end","frontend","full","stack","full","stack","go","languag","haskel","html","java","javascript","lisp","node","js","php","python","react","react","nativ","rubi","rust","scala","softwar","engin","sql","web","develop"]},{"label":"SM","text":["advertis","b2b","b2c","bant","brand","brand","data","entri","field","sale","junior","sale","rep","seo","social","seller","digit","market"]},{"label":"BDM","text":["strategist","product","manag","qualiti","manag","busi","develop","busi","oper"]},{"label":"HC","text":["nurs","doctor","hospit","health","care","specialist","therapist","surgic"]},{"label":"IC","text":["internship","siw","intern","intern","colleg"]},{"label":"RS","text":["analysi","data","mine","research","questionnair","statist","research"]},{"label":"HR","text":["human","resourc","director","hr"]},{"label":"HT","text":["lectur","interventionist","univers","trainer","train","teacher","teach","lectur","instructor","coach","cours","cours","certif"]},{"label":"MP","text":["manufactur","plant","product","oper","machin"]},{"label":"RE","text":["agent","estat","real"]},{"label":"AF","text":["agricultur","farm","anim","haverst","tractor","plant","fish"]},{"label":"TL","text":["driver","drive","uber","lyft","pilot","truck","deliveri","logist","transport","freight","broker"]},{"label":"AD","text":["graphic","design","illustr","ui","ux","web","design","respons","design","3d","artist","anim","caricatur","anim","portrait","cartoon","photoshop"]},{"label":"CW","text":["write","blog","editor","photoshop","content","writer","digit","media","creation","photojournalist","journalist"]},{"label":"SA","text":["linux","system","administr","system","cloud","specialist","azur","develop","oper","aw","azur","linod"]},{"label":"MU","text":["compos","musician","music","studio","produc","video","director","song","writer"]},{"label":"OF","text":["admin","execut","assist","excel","receptionist","recruit","webspher","offic"]},{"label":"CS","text":["custom","servic","care","associ","repres"]},{"label":"RF","text":["cook","restaur","banquet","server","dishwash","cater","manag","benihana","chef","kitchen","manag","sou","execut","chef","sushi","dietari","event","waiter","waitress"]},{"label":"BF","text":["banker","loan","offic","teller","custom","loan","processor","mortag","origin","busi","analyst","financ","manag","auditor","tax","specialist"]},{"label":"LG","text":["legal","commerci","financ","attornei","litig","attornei","execut","assist","clerk","paraleg"]},{"label":"EG","text":["electr","engin","mechan","engin","comput","physic","chemic","electron"]}],"features":{"io":1,"net":1,"app":1,"angular":1,"vuej":1,"architect":1,"android":1,"actionscript":1,"activex":1,"algol":1,"algorithm":1,"api":1,"applet":1,"asp":1,"aspi":1,"assembl":2,"babel":1,"backend":1,"back":1,"end":2,"sharp":1,"clojur":1,"compil":1,"css":1,"dart":1,"django":1,"ecmascript":1,"front":1,"frontend":1,"full":2,"stack":2,"go":1,"languag":1,"haskel":1,"html":1,"java":1,"javascript":1,"lisp":1,"node":1,"js":1,"php":1,"python":1,"react":2,"nativ":1,"rubi":1,"rust":1,"scala":1,"softwar":1,"engin":3,"sql":1,"web":2,"develop":3,"advertis":1,"b2b":1,"b2c":1,"bant":1,"brand":2,"data":2,"entri":1,"field":1,"sale":2,"junior":1,"rep":1,"seo":1,"social":1,"seller":1,"digit":2,"market":1,"strategist":1,"product":2,"manag":5,"qualiti":1,"busi":3,"oper":3,"nurs":1,"doctor":1,"hospit":1,"health":1,"care":2,"specialist":3,"therapist":1,"surgic":1,"internship":1,"siw":1,"intern":2,"colleg":1,"analysi":1,"mine":1,"research":2,"questionnair":1,"statist":1,"human":1,"resourc":1,"director":2,"hr":1,"lectur":2,"interventionist":1,"univers":1,"trainer":1,"train":1,"teacher":1,"teach":1,"instructor":1,"coach":1,"cours":2,"certif":1,"manufactur":1,"plant":2,"machin":1,"agent":1,"estat":1,"real":1,"agricultur":1,"farm":1,"anim":3,"haverst":1,"tractor":1,"fish":1,"driver":1,"drive":1,"uber":1,"lyft":1,"pilot":1,"truck":1,"deliveri":1,"logist":1,"transport":1,"freight":1,"broker":1,"graphic":1,"design":3,"illustr":1,"ui":1,"ux":1,"respons":1,"3d":1,"artist":1,"caricatur":1,"portrait":1,"cartoon":1,"photoshop":2,"write":1,"blog":1,"editor":1,"content":1,"writer":2,"media":1,"creation":1,"photojournalist":1,"journalist":1,"linux":1,"system":2,"administr":1,"cloud":1,"azur":2,"aw":1,"linod":1,"compos":1,"musician":1,"music":1,"studio":1,"produc":1,"video":1,"song":1,"admin":1,"execut":3,"assist":2,"excel":1,"receptionist":1,"recruit":1,"webspher":1,"offic":2,"custom":2,"servic":1,"associ":1,"repres":1,"cook":1,"restaur":1,"banquet":1,"server":1,"dishwash":1,"cater":1,"benihana":1,"chef":2,"kitchen":1,"sou":1,"sushi":1,"dietari":1,"event":1,"waiter":1,"waitress":1,"banker":1,"loan":2,"teller":1,"processor":1,"mortag":1,"origin":1,"analyst":1,"financ":2,"auditor":1,"tax":1,"legal":1,"commerci":1,"attornei":2,"litig":1,"clerk":1,"paraleg":1,"electr":1,"mechan":1,"comput":1,"physic":1,"chemic":1,"electron":1},"stemmer":{},"lastAdded":22,"events":{"domain":null,"_events":{},"_eventsCount":1}}
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # misc
10 | .DS_Store
11 | .env.local
12 | .env.development.local
13 | .env.test.local
14 | .env.production.local
15 |
16 | /build
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jobtweets",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "prop-types": "^15.6.1",
7 | "react": "^16.4.1",
8 | "react-content-loader": "^3.1.2",
9 | "react-dom": "^16.4.1",
10 | "react-redux": "^5.0.6",
11 | "react-scripts": "1.1.4",
12 | "react-twitter-widgets": "^1.7.1",
13 | "redux": "^4.0.0",
14 | "redux-thunk": "^2.2.0",
15 | "socket.io": "^2.1.1",
16 | "socket.io-client": "^2.1.1",
17 | "twitter": "^1.7.1"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject"
24 | },
25 | "proxy": "https://localhost:8080/"
26 | }
27 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flickz/jobtweets/aa1f0edb152ac6deb09b747483967f2634452b91/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Real Time Job Tweets
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
26 |
27 | React App
28 |
29 |
32 |
33 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Tweets from '../containers/tweets'
3 | import Header from './Header'
4 | // import Categories from './containers/categories'
5 | import './styles/App.css'
6 |
7 | export default class App extends React.PureComponent {
8 | render () {
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/client/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './styles/header.css'
3 |
4 | export default () => {
5 | return (
6 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/components/Preloader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ContentLoader from 'react-content-loader'
3 | import './styles/preloader.css'
4 |
5 | export default (props) => {
6 | return (
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './styles/sidebar.css'
3 | import {categoriesData} from '../util/categories'
4 |
5 | export default ({onCheck}) => {
6 | const categories = categoriesData.map((category, index) => {
7 | return (
8 |
9 |
13 |
14 | )
15 | })
16 |
17 | return (
18 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/components/Tweet.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Tweet } from 'react-twitter-widgets'
3 | import ContentLoader from './Preloader'
4 | import './styles/tweets.css'
5 |
6 | export default ({tweets, fetching}) => {
7 | if (tweets.length > 0) {
8 | const embededTweets = tweets.map((tweet) => {
9 | let id = tweet.id
10 | return (
11 |
12 |
13 |
)
14 | })
15 | const loader = (fetching)? () : ''
16 | return (
17 |
18 | { embededTweets }
19 | { loader }
20 |
21 | )
22 | } else {
23 | return (
24 |
25 |
26 |
)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/components/styles/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-intro {
18 | font-size: large;
19 | }
20 |
21 | @keyframes App-logo-spin {
22 | from { transform: rotate(0deg); }
23 | to { transform: rotate(360deg); }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/components/styles/header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | height: 60px;
6 | background-color: #fff;
7 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
8 | }
9 |
10 | a.logo{
11 | text-align: center !important;
12 | display: inline-block;
13 | text-decoration: none;
14 | color: #55acee;
15 | font-size: 2.1rem;
16 | font-weight: 400;
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/components/styles/index.css:
--------------------------------------------------------------------------------
1 | body{
2 | background: #e6ecf0;
3 | margin: 0;
4 | }
5 | a:hover, a:active{
6 | text-decoration: none;
7 | color: #55acee;
8 | outline: 0;
9 | }
10 | .tweet-section{
11 | margin-top: 25px;
12 | }
13 | .spinner{
14 | color: #55acee;
15 | text-align: center;
16 | margin-top: 80px;
17 | }
18 | .spinner i{
19 | font-size: 32px;
20 | }
--------------------------------------------------------------------------------
/client/src/components/styles/preloader.css:
--------------------------------------------------------------------------------
1 | .content-loader {
2 | background: #fff;
3 | width: 100%;
4 | padding: 10px;
5 | margin-top: 10px;
6 | border: 1px solid rgb(225, 232, 237);
7 | border-radius: 5px;
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/client/src/components/styles/sidebar.css:
--------------------------------------------------------------------------------
1 | .filter-sidebar{
2 | background: #fff;
3 | margin-top: 25px;
4 | padding: 5px;
5 | border: 1px solid #e1e8ed;
6 | }
7 | .filter-sidebar p{
8 | font-size: 18px;
9 | font-weight: 600;
10 | text-align: center;
11 | }
12 | .filter-form{
13 | margin-left: 10px;
14 | }
--------------------------------------------------------------------------------
/client/src/components/styles/tweets.css:
--------------------------------------------------------------------------------
1 | .tweets-wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | margin-bottom: 30px;
6 | width: 500px;
7 | }
8 |
9 | .content {
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | }
--------------------------------------------------------------------------------
/client/src/containers/categories.js:
--------------------------------------------------------------------------------
1 | // import React, {Component} from 'react'
2 | // import {connect} from 'react-redux'
3 | // import {addFilter, removeFilter} from '../actions'
4 | // import SideBar from '../components/sidebar'
5 |
6 | // class Categories extends Component {
7 | // constructor () {
8 | // super()
9 | // this.onCheck = (category) => {
10 | // let filters = this.props.filters
11 | // if (filters.indexOf(category) === -1) {
12 | // this.props.addFilter(category)
13 | // } else {
14 | // this.props.removeFilter(category)
15 | // }
16 | // }
17 | // }
18 |
19 | // render () {
20 | // return ()
21 | // }
22 | // }
23 |
24 | // function mapStateToProps (state) {
25 | // return {
26 | // filters: state.filters
27 | // }
28 | // }
29 |
30 | // function mapDispatchToProps (dispatch) {
31 | // return {
32 | // addFilter: (category) => {
33 | // dispatch(addFilter(category))
34 | // },
35 | // removeFilter: (category) => {
36 | // dispatch(removeFilter(category))
37 | // }
38 | // }
39 | // }
40 | // export default connect(mapStateToProps, mapDispatchToProps)(Categories)
41 |
--------------------------------------------------------------------------------
/client/src/containers/tweets.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { fetchTweets } from '../util/fetch'
3 | import Preloader from '../components/Preloader'
4 | import EmbededTweet from '../components/Tweet'
5 |
6 | export default class Tweets extends React.Component {
7 | constructor (props) {
8 | super(props)
9 | this.state = {
10 | fetching: true,
11 | tweets: [],
12 | page: 0
13 | }
14 | }
15 |
16 | componentDidMount () {
17 | window.addEventListener('scroll', this.handleScrollEvent)
18 | this.fetchNextTweets()
19 | }
20 |
21 | componentWillUnmount () {
22 | window.removeEventListener('scroll', this.handleScrollEvent)
23 | }
24 |
25 | updateTweetsList (newTweets) {
26 | const { tweets } = this.state
27 | this.setState({
28 | tweets: [...tweets, ...newTweets]
29 | })
30 | }
31 |
32 | setFetchingState (fetching) {
33 | this.setState({
34 | fetching
35 | })
36 | }
37 |
38 | updateTweetsPage (page) {
39 | this.setState({
40 | page
41 | })
42 | }
43 |
44 | async fetchNextTweets () {
45 | this.setFetchingState(true)
46 | const tweets = await fetchTweets(this.state.page)
47 | if (tweets) {
48 | this.updateTweetsList(tweets)
49 | this.setFetchingState(false)
50 | this.setState((prevState, props) => ({
51 | page: prevState.page + 1
52 | }))
53 | }
54 | }
55 |
56 | handleScrollEvent = (event) => {
57 | const { fetching } = this.state
58 | const windowHeight = window.innerHeight
59 | const scrollVerticalPos = window.scrollY
60 | const contentHeight = document.body.offsetHeight
61 | if ((windowHeight + scrollVerticalPos) >= (contentHeight)) {
62 | if (!fetching) {
63 | this.fetchNextTweets()
64 | .catch(error => {
65 | throw error
66 | })
67 | }
68 | }
69 | }
70 |
71 | render () {
72 | const { tweets, fetching } = this.state
73 |
74 | return (
75 |
76 |
77 |
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './components/styles/index.css'
4 | import App from './components/App'
5 | import registerServiceWorker from './registerServiceWorker'
6 |
7 | ReactDOM.render(
8 | ,
9 | document.getElementById('root'))
10 |
11 | registerServiceWorker()
12 |
--------------------------------------------------------------------------------
/client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/client/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhosts' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | )
20 |
21 | export default function register () {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl)
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl)
41 | }
42 | })
43 | }
44 | }
45 |
46 | function registerValidSW (swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.')
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.')
65 | }
66 | }
67 | }
68 | }
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error)
72 | })
73 | }
74 |
75 | function checkValidServiceWorker (swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload()
88 | })
89 | })
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl)
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | )
99 | })
100 | }
101 |
102 | export function unregister () {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister()
106 | })
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/client/src/util/categories.js:
--------------------------------------------------------------------------------
1 | export const categoriesData = [
2 | {
3 | acronym: 'AF',
4 | fullMeaning: 'Agriculture & Farming'
5 | },
6 | {
7 | acronym: 'AD',
8 | fullMeaning: 'Art & Design'
9 | },
10 | {
11 | acronym: 'BF',
12 | fullMeaning: 'Banking & Finance'
13 | },
14 | {
15 | acronym: 'BDM',
16 | fullMeaning: 'Business Development & Management'
17 | },
18 | {
19 | acronym: 'CW',
20 | fullMeaning: 'Content Writing'
21 | },
22 | {
23 | acronym: 'CS',
24 | fullMeaning: 'Customer Services'
25 | },
26 | {
27 | acronym: 'ET',
28 | fullMeaning: 'Education & Teaching'
29 | },
30 | {
31 | acronym: 'EG',
32 | fullMeaning: 'Engineering'
33 | },
34 | {
35 | acronym: 'HC',
36 | fullMeaning: 'Health Care'
37 | },
38 | {
39 | acronym: 'HR',
40 | fullMeaning: 'Human Resources'
41 | },
42 | {
43 | acronym: 'IC',
44 | fullMeaning: 'Internship & College'
45 | },
46 | {
47 | acronym: 'LG',
48 | fullMeaning: 'Legal'
49 | },
50 | {
51 | acronym: 'MP',
52 | fullMeaning: 'Manufacturing & Production'
53 | },
54 | {
55 | acronym: 'MU',
56 | fullMeaning: 'Music'
57 | },
58 | {
59 | acronym: 'OF',
60 | fullMeaning: 'Office'
61 | },
62 | {
63 | acronym: 'RE',
64 | fullMeaning: 'Real Estate'
65 | },
66 | {
67 | acronym: 'RF',
68 | fullMeaning: 'Restaurant & Food Service'
69 | },
70 | {
71 | acronym: 'RS',
72 | fullMeaning: 'Research'
73 | },
74 | {
75 | acronym: 'SM',
76 | fullMeaning: 'Sales & Marketing'
77 | },
78 | {
79 | acronym: 'SD',
80 | fullMeaning: 'Software Development'
81 | },
82 | {
83 | acronym: 'SA',
84 | fullMeaning: 'System Admin'
85 | },
86 | {
87 | acronym: 'TL',
88 | fullMeaning: 'Transportation & Logistics'
89 | }
90 | ]
91 |
--------------------------------------------------------------------------------
/client/src/util/config.js:
--------------------------------------------------------------------------------
1 | export const API_BASE_URL = 'http://localhost:8080/api/v1'
2 | export const STREAM_SOCKET_URL = 'localhost:8080/'
--------------------------------------------------------------------------------
/client/src/util/fetch.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client/lib'
2 | import { STREAM_SOCKET_URL, API_BASE_URL } from './config'
3 |
4 | export const stream = io(STREAM_SOCKET_URL)
5 |
6 | stream.on('error', (error) => {
7 | console.log('Error occoured..', error)
8 | })
9 |
10 | function receiveTweetsData (data) {
11 | console.log(data)
12 | }
13 |
14 | function handleFetchError (error) {
15 | console.log(error)
16 | }
17 |
18 | export const fetchTweets = (page) => {
19 | const url = `${API_BASE_URL}/tweets?page=${page}`
20 | return window.fetch(url)
21 | .then((response) => {
22 | if(!response.ok) {
23 | throw response
24 | }
25 | return response.json()
26 | })
27 | .catch(handleFetchError)
28 | }
29 |
--------------------------------------------------------------------------------
/controllers/__tests__/tweets.test.js:
--------------------------------------------------------------------------------
1 | const { addTweet, getTweet, getTweetKeys, getTweets } = require('../tweets')
2 | jest.mock('../../lib/redis')
3 |
4 | const dummyTweet = {
5 | id: '1212',
6 | first_class: 'SD',
7 | second_class: 'DO'
8 | }
9 |
10 | describe('#addTweet()', () => {
11 | it('should add tweet to redis', async () => {
12 | const result = await addTweet(dummyTweet)
13 | expect(result).toHaveLength(2)
14 | })
15 | })
16 |
17 | describe('#getTweet()', () => {
18 | it('should return tweet with the key', async () => {
19 | const result = await getTweet('tweet:1')
20 | expect(result.id).toEqual(dummyTweet.id)
21 | })
22 | })
23 |
24 | describe('#getTweetKeys()', () => {
25 | it('should return list of keys within specified range', async () => {
26 | const results = await getTweetKeys(-1, 0)
27 | expect(results).toHaveLength(1)
28 | })
29 | })
30 |
31 | describe('#getTweets()', () => {
32 | it('should return list of tweets within specified range', async () => {
33 | const results = await getTweets(-1, 0)
34 | expect(results).toHaveLength(1)
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/controllers/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('requireindex')(__dirname)
2 |
--------------------------------------------------------------------------------
/controllers/tweets.js:
--------------------------------------------------------------------------------
1 | const { isEmpty } = require('lodash')
2 | const logger = require('../lib/logger')()
3 | const { RedisClient } = require('../lib/redis')
4 |
5 | const redis = RedisClient()
6 | const listKey = 'tweets'
7 |
8 | exports.addTweet = async function (tweet) {
9 | let tweetId
10 | try {
11 | tweetId = await redis.incr('tweet_id')
12 | } catch (err) {
13 | logger.error(`Error incrementing tweet in redis`)
14 | logger.error(err)
15 | return err
16 | }
17 | const hashKey = `tweet:${tweetId}`
18 | return redis.multi().hmset(hashKey, tweet).rpush(listKey, hashKey).exec()
19 | }
20 |
21 | exports.getTweet = async function (key) {
22 | return redis.hgetall(key)
23 | }
24 |
25 | exports.getTweetKeys = async function (start, stop) {
26 | return redis.lrange(listKey, start, stop)
27 | }
28 |
29 | const getTweets = async function (start, stop) {
30 | const tweets = []
31 | const tweetKeys = await redis.lrange(listKey, start, stop)
32 | if (tweetKeys.length >= 1) {
33 | for (let i = 0; i < tweetKeys.length; i++) {
34 | const key = tweetKeys[i]
35 | const tweet = await redis.hgetall(key)
36 |
37 | if (isEmpty(tweet) === false) {
38 | tweets.push(tweet)
39 | }
40 | }
41 | return tweets
42 | }
43 | return tweets
44 | }
45 |
46 | exports.get = async function (req, res) {
47 | const page = parseInt(req.query.page)
48 |
49 | function getRange (page) {
50 | let from
51 | if (page === 0) {
52 | from = page * 10
53 | } else {
54 | from = page * 10 + 1
55 | }
56 | const to = (page + 1) * 10
57 | return [from, to]
58 | }
59 | const [from, to] = getRange(page)
60 | console.log(from, to)
61 |
62 | try {
63 | const tweets = await getTweets(from, to)
64 | if (tweets) {
65 | res.status(200).send(tweets)
66 | return
67 | }
68 | return res.status(204).send()
69 | } catch (err) {
70 | console.error(`Error occur from getting tweets in range (${from}-${to}`)
71 | console.error(err)
72 | res.status(400)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/__mocks__/redis.js:
--------------------------------------------------------------------------------
1 | const Redis = require('ioredis-mock')
2 |
3 | exports.RedisClient = function () {
4 | return new Redis()
5 | }
6 |
--------------------------------------------------------------------------------
/lib/classifier.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { BayesClassifier } = require('natural')
3 |
4 | const classifierPath = path.join(__dirname, '../classifier.json')
5 |
6 | exports.classifyTweetWithBayes = function (text, callback) {
7 | BayesClassifier.load(classifierPath, null, (err, classifier) => {
8 | if (err) {
9 | console.error(err)
10 | callback(err, null)
11 | return
12 | }
13 |
14 | text = removeHash(text)
15 | const classifications = []
16 | const result = classifier.getClassifications(text)
17 | // Get the 1st two classifications
18 | classifications.push(result[0].label)
19 | classifications.push(result[1].label)
20 |
21 | callback(null, classifications)
22 | })
23 | }
24 |
25 | const removeHash = (word) => {
26 | return word.replace(/#|RT|rt/g, '')
27 | }
28 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston')
2 |
3 | module.exports = function () {
4 | const myLogTransports = []
5 |
6 | switch (process.env.NODE_ENV) {
7 | case 'production':
8 | myLogTransports.push(new (winston.transports.Console)({ level: 'info' }))
9 | break
10 | case 'test':
11 | myLogTransports.push(new (winston.transports.Console)({ level: 'debug' }))
12 | break
13 | default:
14 | // Dev environments, usually
15 | myLogTransports.push(new (winston.transports.Console)({ level: 'debug' }))
16 | break
17 | }
18 |
19 | let logger = winston.createLogger({
20 | transports: myLogTransports
21 | })
22 |
23 | return logger
24 | }
25 |
--------------------------------------------------------------------------------
/lib/redis.js:
--------------------------------------------------------------------------------
1 | const Redis = require('ioredis')
2 |
3 | exports.RedisClient = function () {
4 | return new Redis({
5 | port: process.env.REDIS_PORT,
6 | host: process.env.REDIS_HOST
7 | })
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jobtweets",
3 | "version": "1.0.0",
4 | "description": "\"Find realtime job tweets\"",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "jest --watch",
8 | "start": "node server.js",
9 | "dev": "concurrently \"npm run server\" \"npm run client\"",
10 | "server": "node server.js",
11 | "client": "node start-client.js",
12 | "lint": "eslint \"**/*.js\" \"**/*.jsx\"",
13 | "lint:fix": "eslint \"**/*.js\" \"**/*.jsx\" --fix"
14 | },
15 | "author": "",
16 | "license": "ISC",
17 | "dependencies": {
18 | "asn1": "^0.2.4",
19 | "cors": "^2.8.5",
20 | "dotenv": "^4.0.0",
21 | "express": "^4.17.1",
22 | "ioredis": "^3.2.2",
23 | "lodash": "^4.17.15",
24 | "morgan": "^1.10.0",
25 | "natural": "^0.6.3",
26 | "requireindex": "^1.2.0",
27 | "socket.io": "^2.3.0",
28 | "twitter": "^1.7.1",
29 | "winston": "^3.2.1"
30 | },
31 | "devDependencies": {
32 | "babel-core": "^6.26.3",
33 | "babel-eslint": "^8.2.6",
34 | "babel-jest": "^23.6.0",
35 | "babel-plugin-transform-async-to-generator": "^6.24.1",
36 | "babel-preset-env": "^1.7.0",
37 | "babel-preset-react": "^6.24.1",
38 | "concurrently": "^3.6.1",
39 | "enzyme": "^3.11.0",
40 | "enzyme-adapter-react-16": "^1.15.2",
41 | "eslint": "^4.19.1",
42 | "eslint-config-standard": "^11.0.0",
43 | "eslint-config-standard-react": "^6.0.0",
44 | "eslint-plugin-import": "^2.20.2",
45 | "eslint-plugin-node": "^6.0.1",
46 | "eslint-plugin-promise": "^3.8.0",
47 | "eslint-plugin-react": "^7.20.0",
48 | "eslint-plugin-standard": "^3.1.0",
49 | "ioredis-mock": "^3.14.3",
50 | "jest": "^23.6.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const util = require('util')
3 | const express = require('express')
4 | const logger = require('morgan')
5 | const Twitter = require('twitter')
6 | const cors = require('cors')
7 | const socket = require('socket.io')
8 | const { Server } = require('http')
9 | const controllers = require('./controllers')
10 | const { classifyTweetWithBayes } = require('./lib/classifier')
11 |
12 | const classifyTweet = util.promisify(classifyTweetWithBayes)
13 | const app = express()
14 | const server = Server(app)
15 | const io = socket(server)
16 |
17 | // Express only serves static assets in production
18 | if (process.env.NODE_ENV === 'production') {
19 | app.use(express.static('client/build'))
20 | }
21 |
22 | app.use(cors())
23 | app.use(logger('dev'))
24 | app.get('/api/v1/tweets', controllers.tweets.get)
25 |
26 | const client = new Twitter({
27 | consumer_key: process.env.CONSUMER_KEY,
28 | consumer_secret: process.env.CONSUMER_SECRET,
29 | access_token_key: process.env.ACCESS_TOKEN_KEY,
30 | access_token_secret: process.env.ACCESS_TOKEN_SECRET
31 | })
32 |
33 | const searchOptions = {
34 | track: 'we are hiring, looking for interns, vacancy, we have job available',
35 | filter_level: 'low'
36 | }
37 |
38 | const stream = client.stream('statuses/filter', searchOptions)
39 |
40 | stream.on('error', (error) => {
41 | console.error('Twitter error occoured..', error)
42 | })
43 |
44 | io.on('connection', (socket) => {
45 | socket.on('error', (error) => {
46 | console.error('New socket Error', error)
47 | })
48 | })
49 |
50 | stream.on('data', async (data) => {
51 | const tweet = {}
52 | if (data.lang === 'en') {
53 | try {
54 | const classifications = await classifyTweet(data.text)
55 | tweet['id'] = data.id_str
56 | tweet['first_class'] = classifications[0]
57 | tweet['second_class'] = classifications[1]
58 | await controllers.tweets.addTweet(tweet)
59 | } catch (err) {
60 | console.error(err)
61 | }
62 | }
63 | })
64 |
65 | server.listen(process.env.PORT || 8080, () => console.log('Server listening...'))
66 |
--------------------------------------------------------------------------------
/start-client.js:
--------------------------------------------------------------------------------
1 | const args = ['start']
2 | const opts = {stdio: 'inherit', cwd: 'client', shell: true}
3 | require('child_process').spawn('npm', args, opts)
4 |
--------------------------------------------------------------------------------
/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Real Time Job Tweets
5 |
6 |
7 |
8 |
9 |
10 |
11 |
63 |
64 |
65 |
74 |
75 |
76 |
77 |
215 |
216 |
217 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
--------------------------------------------------------------------------------
/training-data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "field": "SD",
4 | "keywords": ["iOS",".NET","App", "Angular", "VueJS" ,"Architect","Android","ActionScript","ActiveX","Algol","Algorithm","API","Applet","ASP","ASPI","Assembler","Assembly","Babel","Backend", "Back-end","C","C sharp","C++","C#","Clojure","Compiler","CSS","Dart","Django","ECMAScript","F#","Front-end","Frontend", "Full-stack", "Full stack", "Go language","Haskell","HTML","Java","JavaScript","LISP","Node.js","PHP","Python","React","React Native","Ruby","Rust","Scala","Software","engineer","SQL","Web", "Developer"]
5 | },
6 | {
7 | "field": "SM",
8 | "keywords": ["Advertising","B2B","B2C","BANT","Branding","Brand", "Data entry","Field Sales","Junior Sales Rep", "SEO" ,"Social Seller", "Digital Marketing"]
9 | },
10 | {
11 | "field": "BDM",
12 | "keywords":["Strategist","Product", "Manager","Quality management","Business Developer", "Business", "Operation"]
13 | },
14 | {
15 | "field": "HC",
16 | "keywords": ["Nurse", "Doctor", "Hospital", "Health Care", "Specialist", "Therapist", "Surgical"]
17 | },
18 | {
19 | "field": "IC",
20 | "keywords": ["Internship", "IT", "SIWES", "Intern", "Interns", "College"]
21 | },
22 | {
23 | "field": "RS",
24 | "keywords": ["Analysis","Data Mining","Research","Questionnaire","Statistics","Researchers"]
25 | },
26 | {
27 | "field": "HR",
28 | "keywords":["Human", "Resources", "Director", "HR"]
29 | },
30 | {
31 | "field": "HT",
32 | "keywords": ["Lecturer", "interventionist", "University", "Trainer", "Training", "Teacher", "Teaching", "Lecturing", "Instructor", "Coach", "Course", "Courses", "Certificate"]
33 | },
34 | {
35 | "field": "MP",
36 | "keywords": ["Manufacturing", "Plants", "Production", "Operator", "Machine"]
37 | },
38 | {
39 | "field": "RE",
40 | "keywords": ["Agent", "Estate", "Real"]
41 | },
42 | {
43 | "field": "AF",
44 | "keywords": ["Agriculture", "Farming", "Animal", "Haverster", "Tractor", "Plants", "Fishing"]
45 | },
46 | {
47 | "field": "TL",
48 | "keywords": ["Driver", "Driving", "Uber", "Lyft", "Pilot", "Truck", "Delivery", "Logistics", "Transportation", "Freight Broker"]
49 | },
50 | {
51 | "field": "AD",
52 | "keywords": ["Art", "Graphic Designer", "Illustrator", "UI", "UX", "Web Designer", "Responsive", "Design", "3D Artist", "Animator", "Caricature", "Animation", "Portraits", "Cartoon", "Photoshop"]
53 | },
54 | {
55 | "field": "CW",
56 | "keywords": ["Writing", "Blogs", "Editor", "Photoshop", "Content Writer", "Digital Media Creation", "Photojournalist", "Journalist"]
57 | },
58 | {
59 | "field": "SA",
60 | "keywords":["Linux", "System", "Administrator", "Systems", "Cloud Specialist", "Azure", "Development Operation", "AWS", "Azure", "Linode"]
61 | },
62 | {
63 | "field": "MU",
64 | "keywords": ["Composer", "Musician", "Music Studio", "Producer", "Video Director", "Song Writer"]
65 | },
66 | {
67 | "field": "OF",
68 | "keywords": ["Admin", "Executive", "Assistant", "Excel", "Receptionist", "Recruiter", "Websphere", "Office"]
69 | },
70 | {
71 | "field": "CS",
72 | "keywords": ["Customer Service", "Care", "Associate", "Representative"]
73 | },
74 | {
75 | "field": "RF",
76 | "keywords": ["Cook", "Restaurant", "Banquet Server", "Dishwasher", "Catering Manager", "Benihana Chef", "Kitchen Manager", "Sous", "Executive Chef", "Sushi", "Dietary","Event", "Waiter Waitress"]
77 | },
78 | {
79 | "field": "BF",
80 | "keywords": ["Banker", "Loan Officer", "Teller", "Customer", "Loan Processor", "Mortage Originator", "Business Analyst", "Finance Manager", "Auditor", "Tax Specialist"]
81 | },
82 | {
83 | "field": "LG",
84 | "keywords": ["Legal", "Commercial Finance Attorney", "Litigation Attorney", "Executive Assistant", "Clerk", "Paralegal"]
85 | },
86 | {
87 | "field": "EG",
88 | "keywords": ["Electrical", "engineer", "Mechanical","Engineering", "Computer", "Physics", "Chemical", "Electronics"]
89 | }
90 | ]
--------------------------------------------------------------------------------
/training.js:
--------------------------------------------------------------------------------
1 | const {BayesClassifier} = require('natural')
2 | const {readFileSync} = require('fs')
3 | const {join} = require('lodash')
4 |
5 | const classifier = new BayesClassifier()
6 |
7 | function train () {
8 | var data = readFileSync('training-data.json', 'utf-8')
9 | data = JSON.parse(data)
10 |
11 | for (let i = 0; i < data.length; i++) {
12 | // The keywords is an array but natural classifier works better with a string
13 | // so keywords are turned to string of text.
14 | let text = join(data[i].keywords, ',')
15 | classifier.addDocument(text, data[i].field)
16 | }
17 | }
18 |
19 | train()
20 |
21 | classifier.events.on('trainedWithDocument', function (obj) {
22 | // console.log(obj);
23 | })
24 |
25 | classifier.train()
26 |
27 | classifier.save('classifier.json', (err, classifier) => {
28 | if (err) throw err
29 | })
30 |
31 | // function classifyTweet(text){
32 | // return new Promise((resolve, reject)=>{
33 | // BayesClassifier.load('classifier.json', null, (err, classifier)=>{
34 | // if(err)reject(err);
35 | // //text = removeHash(text);
36 | // let classification = classifier.classify(text);
37 | // if(classification.length > 0){
38 | // resolve(classification);
39 | // }
40 | // resolve(null);
41 | // });
42 | // })
43 | // }
44 |
45 | // classifyTweet("RT @rbanks: I\'m looking for two design interns for the summer to work at @MSFTResearchCam. One will focus on UX, working on ").then((data)=>{
46 | // console.log(data);
47 | // }).catch((err)=>{
48 | // console.log(err);
49 | // })
50 |
--------------------------------------------------------------------------------