<file_sep>'use strict';

const Tweet = require('./../models').tweet;

class StartsController {
  /**
   * @apiUrl {get} /stars/ Get stared
   *
   * @apiResult {Boolean} success
   * @apiResult {Array<Tweet>} items Array of items
   */
  getStarred(req, res, next) {
    Tweet
      .findAll({order: ['stared']})
      .then(tweets => {
        res.send({
          success: true,
          items: tweets
        })
      })
      .catch(err => next(err))
  }

  /**
   * @apiUrl {get} /stars/add Add tweet to stared
   *
   * @apiParam {Object} Raw tweet object
   *
   * @apiResult {Boolean} success
   */
  starTweet(req, res, next) {
    if (!req.body || !req.body.id) {
      return next('Bad params');
    }
    
    req.body.stared = new Date();
    
    Tweet
      .create(req.body)
      .then(() => {
        res.send({
          success: true
        })
      })
      .catch(err => next(err))
  }

  /**
   * @apiUrl {get} /stars/remove/:id Remove tweet by id
   *
   * @apiParam {Number} id Tweet id
   *
   * @apiResult {Boolean} success
   */
  unstarTweet(req, res, next) {
    if (!req.params || !req.params.id) {
      return next('Bad params. Id not specified');
    }
    Tweet
      .findById(req.params.id)
      .then((tweet) => {
        return tweet.destroy()
      })
      .then(() => {
        res.send({
          success: true
        })
      })
      .catch(err => next(err))
  }
}

module.exports = StartsController;<file_sep># Twitter Test

This project contain two parts: Backend and frontend.
It's search tweets by keywords from config.
For check how this work you should start both parts.

### Requirements

- Node.js 6.2+
- PostgreSQL server
- Angular Cli Tool

### Installation

Please check `server/config.js` before start, for make sure you have configuration for database and twitter account 

- `git clone https://github.com/YaphetS1/tweet-test.git`
- `cd tweet-test`
- `npm i`
- `npm start`
- Now follow to `http://localhost:4000` in your browser

![alt text](./scr.png)
<file_sep>'use strict';

function Schema(sequelize, DataTypes) {
  let Schema = {
    id: {
      type: DataTypes.BIGINT,
      primaryKey: true
    },
    idStr: DataTypes.STRING,
    text: DataTypes.STRING,
    source: DataTypes.STRING,
    user: DataTypes.JSON,
    media: DataTypes.JSON,
    createdAt: DataTypes.DATE,
    timestampMs: DataTypes.BIGINT,
    stared: DataTypes.DATE
  };
  let Model = sequelize.define('tweet', Schema);

  /**
   * Convert raw twitter to model style
   * @param tweetObject
   * @return {{id, idStr: string, text, source, user, media: string, createdAt: string, timestampMs: string}}
   */
  Model.normalizeTweet = (tweetObject) => {
    let media = tweetObject.extended_entities &&
      tweetObject.extended_entities.media || null;
    return {
      id: tweetObject.id,
      idStr: tweetObject.id_str,
      text: tweetObject.text,
      source: tweetObject.source,
      user: tweetObject.user,
      media: media,
      createdAt: typeof tweetObject.created_at === 'string' ? new Date(tweetObject.created_at) : tweetObject.created_at,
      timestampMs: tweetObject.timestamp_ms
    }
  };
  
  return Model;
}

module.exports = Schema;<file_sep>export const environment = {
  production: true,
  apiUrl: 'https://twitter-test-project.yaphets.com/api'
};
<file_sep>'use strict';

const express = require('express')
  , router = express.Router()

  , StartsController = require('./../controllers/stars')
  , startsCtrl = new StartsController();

router.get('/', startsCtrl.getStarred.bind(startsCtrl));
router.post('/add', startsCtrl.starTweet.bind(startsCtrl));
router.get('/remove/:id', startsCtrl.unstarTweet.bind(startsCtrl));

module.exports = router;<file_sep>const express = require('express')
  , router = express.Router()

  , TwitterController = require('./../controllers/twitter')
  , twitterCtrl = new TwitterController();

router.get('/', twitterCtrl.getTweets.bind(twitterCtrl));

module.exports = router;<file_sep># Frontend

To run this do following steps:

- npm i
- ng serve
- follow to `http://localhost:4000`
<file_sep># Server

To run this do following steps:

- npm i
- node app.js

<file_sep>import { Component } from '@angular/core';
import { TweetService } from './services/tweet';
import { StaredService } from './services/stared';
import Tweet from './models/tweet';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  tweets: Array<Tweet> = [];
  staredTweets: Array<Tweet> = [];

  constructor(private tweetService: TweetService,
              private staredService: StaredService) {
  }

  ngOnInit(){
    this.updateTweets();
    this.updateStared();
  }

  updateTweets() {
    return this
      .tweetService
      .getTweets()
      .subscribe(items => {
        this.tweets = items;
      }, err => {
        console.error(err);
      })
  }

  updateStared() {
    return this
      .staredService
      .getStared()
      .subscribe(items => {
        this.staredTweets = items;
      }, err => {
        console.error(err);
      })
  }

  onTweetChanged(tweet) {
    if (tweet.stared) {
      this.staredTweets.push(tweet);
    } else {
      this.staredTweets.splice(this.staredTweets.indexOf(tweet), 1);
    }
  }
}
<file_sep>'use strict';

const Twit = require('twit')
  , appConfig = require('../config')
  , Tweet = require('./../models').tweet;

class TwitterService {
  constructor() {
    this.twitter = new Twit(appConfig.twitter);

    if (!appConfig.keywords || !appConfig.keywords.length) {
      throw 'Config error! Keywords not set';
    }
  }

  getTweets(count) {
    return new Promise((resolve, reject) => {
      this.twitter.get('search/tweets', {q: appConfig.keywords, count: count || 50}, (err, data) => {
        if (err) {
          return reject(err);
        }
        let items = [];
        if (data.statuses && data.statuses.length) {
          items = data
            .statuses
            .map(tweet => Tweet.normalizeTweet(tweet));
        }
        resolve(items);
      });
    })
  }
}

module.exports = new TwitterService();<file_sep>module.exports = {
  server: {
    port: 4000,
    // database: 'postgres://postgres:postgres@localhost:5432/tweet'
    database: 'postgres://localhost:5432/tweet'
  },
  twitter: {
    consumer_key: '0uJ9168FNKyo5pPEOaZWgvDgF',
    consumer_secret: '<KEY>',
    access_token: '<KEY>',
    access_token_secret: '<KEY>'
  },
  keywords: 'eCommerce video'
}<file_sep>'use strict';

const tweets = require('./tweets')
  , stars = require('./starts');

module.exports = function (app) {
  app.use('/api/tweets', tweets);
  app.use('/api/stars', stars);

  // Catch 404 and forward to error handler
  app.use((req, res, next) => {
    next('Route not found');
  });

  // Catch other errors
  app.use((err, req, res, next) => {
    console.error(err);
    res.send({
      success: false,
      error: err
    });
  });

  return app;
};<file_sep>export default class Tweet {
  "created_at": string;
  "id": number;
  "id_str": string;
  "text": string;
  "source": string;
  "user": {
    "id": number;
    "id_str": string;
    "name": string;
    "screen_name": string;
    "profile_image_url": string;
    "profile_image_url_https": string;
  };
  "media": [
    {
      "id": number;
      "id_str": string;
      "media_url": string;
      "media_url_https": string;
      "url": string;
      "display_url": string;
      "expanded_url": string;
      "type": string;
      "sizes": any;
    }
  ];
  "timestamp_ms": string;
  "stared": Date;

  constructor(tweetObj) {
    Object.assign(this, tweetObj);
    this.stared = tweetObj.stared ? new Date(tweetObj.stared) : null;
  }
}
<file_sep>import {Component, Input, OnInit} from '@angular/core';
import Tweet from '../../models/tweet';
import {StaredService} from '../../services/stared';

@Component({
  selector: 'tweet',
  templateUrl: './tweet.component.html',
  styleUrls: ['./tweet.component.css']
})
export class TweetComponent implements OnInit {
  @Input() tweet: Tweet;
  @Input() onChange: Function;

  constructor(private staredService: StaredService) {
  }

  ngOnInit() {
  }

  star() {
    // Check if tweet already stared
    if (this.tweet.stared) {
      return;
    }
    this
      .staredService
      .starTweet(this.tweet)
      .subscribe(() => {
        this.tweet.stared = new Date();
        this.onChange(this.tweet);
      })
  }

  unstar() {
    // Check if tweet already stared
    if (!this.tweet.stared) {
      return;
    }
    this
      .staredService
      .unstarTweet(this.tweet.id)
      .subscribe(() => {
        delete this.tweet.stared;
        this.onChange(this.tweet);
      })
  }
}
<file_sep>import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {environment as env} from '../../environments/environment';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import Tweet from '../models/tweet';

@Injectable()
export class StaredService {
  constructor(private http: Http) {
  }

  /**
   * Return stared tweets
   * @return {Observable<Array<Tweet>>}
   */
  getStared(): Observable<Array<Tweet>> {
    return this.http
      .get(env.apiUrl + '/stars')
      .map((res: Response) => {
        let body = res.json();
        let result: any = body || {};
        if (!result.success && result.error) {
          throw {type: 'api', error: result.error || null};
        }
        return result.items.map(item => new Tweet(item));
      })
  }

  /**
   * Star tweet
   * @param tweetObject Tweet full object
   * @return {Observable<Boolean>}
   */
  starTweet(tweetObject: Tweet) {
    return this.http
      .post(env.apiUrl + '/stars/add', tweetObject)
      .map((res: Response) => {
        let body = res.json();
        let result: any = body || {};
        if (!result.success && result.error) {
          throw {type: 'api', error: result.error || null};
        }
        return true;
      })
  }

  /**
   * Remove stared tweet
   * @param tweetId Tweet id
   * @return {Observable<Boolean>}
   */
  unstarTweet(tweetId: number) {
    return this.http
      .get(env.apiUrl + '/stars/remove/' + tweetId)
      .map((res: Response) => {
        let body = res.json();
        let result: any = body || {};
        if (!result.success && result.error) {
          throw {type: 'api', error: result.error || null};
        }
        return true;
      })
  }
}
<file_sep>import { Injectable } from '@angular/core';
import {Http, Response} from '@angular/http';
import { environment as env } from '../../environments/environment';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import Tweet from '../models/tweet';

@Injectable()
export class TweetService {
  constructor(private http: Http) {
  }

  /**
   * Return tweets
   * @param count
   * @return {Observable<Array<Tweet>>}
   */
  getTweets(count: number = 20): Observable<Array<Tweet>> {
    return this.http
      .get(env.apiUrl + '/tweets/?count=' + count)
      .map((res: Response) => {
        let body = res.json();
        let result: any = body || {};
        if (!result.success && result.error) {
          throw {type: 'api', error: result.error || null};
        }
        return result.items.map(item => new Tweet(item));
      })
  }
}

