├── .gitignore ├── favicon.ico ├── assects └── Screenshot.png ├── .babelrc ├── index.html ├── src ├── components │ ├── Boxes │ │ ├── style.css │ │ └── index.js │ ├── Box │ │ ├── index.js │ │ └── style.css │ ├── style.css │ └── index.js ├── index.js ├── config.js ├── utils │ ├── getHeadlines.js │ ├── getTimeType.js │ ├── getTimeOfNow.js │ └── changeTime.js └── index.html ├── Readme.md ├── .vscode └── launch.json ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | .module-cache 4 | *.log* 5 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timqian/my-headline/HEAD/favicon.ico -------------------------------------------------------------------------------- /assects/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timqian/my-headline/HEAD/assects/Screenshot.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": [ 4 | ["transform-runtime", { 5 | "polyfill": false, 6 | "regenerator": true 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirecting... 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Boxes/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | } 4 | 5 | @media screen and (min-width: 601px) { 6 | .boxes { 7 | margin: auto; 8 | column-count: 2; 9 | max-width: 900px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | // import { Router, Route, IndexRoute, browserHistory } from 'react-router' 5 | 6 | import Main from './components' 7 | 8 | render(
, document.getElementById('root')) 9 | -------------------------------------------------------------------------------- /src/components/Boxes/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Box from '../Box' 4 | import style from './style.css' 5 | 6 | function Boxes({ headlines }) { 7 | const boxArr = Object.keys(headlines).map(site => { 8 | return 9 | }) 10 | 11 | return ( 12 |
13 |
14 | { boxArr } 15 |
16 |
17 | ) 18 | } 19 | 20 | export default Boxes 21 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const dataUrl = 'https://raw.githubusercontent.com/timqian/my-headline-crawler/master/data' 2 | export const serverBase = 'http://localhost:8080' 3 | 4 | export const apis = { 5 | getGithubAccessToken: `${serverBase}/githubAccessToken` 6 | } 7 | 8 | export const siteColor = { 9 | HN: '#FF6600', 10 | github: '#F5F5F5', 11 | v2ex: '#FFFFFF', 12 | reddit: '#CEE3F8', 13 | medium: '#19AA6E', 14 | productHunt: '#da552f', 15 | } 16 | 17 | export const timeTypes = { 18 | DAILY: 'daily', 19 | WEEKLY: 'weekly', 20 | MONTHLY: 'monthly', 21 | } -------------------------------------------------------------------------------- /src/utils/getHeadlines.js: -------------------------------------------------------------------------------- 1 | 2 | import axios from 'axios' 3 | import notie from 'corner-notie' 4 | import { dataUrl } from '../config' 5 | 6 | async function getHeadlines(timeSplat) { 7 | let data = {} 8 | try { 9 | data = ( await axios.get(`${dataUrl}/${timeSplat}.json`, {timeout: 7000}) ).data 10 | } catch (error) { 11 | notie('Seems no data there~' , { 12 | type: 'warning', // info | warning | success | danger 13 | autoHide: true, 14 | timeout: 3000, 15 | position: 'bottom-center', 16 | width: 250 17 | }) 18 | } 19 | 20 | return data 21 | } 22 | 23 | export default getHeadlines -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | > Deprecated, visit https://github.com/headllines 2 | 3 | # my-headline 4 | 5 | Shows 10 hotest posts on HN, github trending, Reddit, Medium and v2ex everyday. 6 | 7 | ## The crawler 8 | 9 | https://github.com/timqian/my-headline-crawler 10 | 11 | ## Setup 12 | 13 | `$ npm install` 14 | 15 | ## Running 16 | 17 | `$ npm start` 18 | 19 | ## Build 20 | 21 | `$ npm run build` 22 | 23 | ## Contributors 24 | 25 | - [timqian](https://github.com/timqian) 26 | - [Eric](https://github.com/erichuang1994) 27 | 28 | ## Thanks 29 | 30 | [Tj](https://github.com/tj/frontend-boilerplate) for his boilerplate 31 | 32 | ## Screen Shot 33 | ![](./assects/Screenshot.png) 34 | 35 | # License 36 | 37 | MIT 38 | 39 | -------------------------------------------------------------------------------- /src/utils/getTimeType.js: -------------------------------------------------------------------------------- 1 | 2 | import { timeTypes } from '../config' 3 | 4 | /** 5 | * Get time type from time splat 6 | * 7 | * Examples: 8 | * 2016/06/01 ==> timeTypes.DAILY 9 | * 2016/06/w1 ==> timeTypes.WEEKLY 10 | * 2016/05/m1 ==> timeTypes.MONTHLY 11 | */ 12 | function getTimeType(timeSplat) { 13 | if(timeSplat.slice(-2, -1) === 'w') { 14 | return timeTypes.WEEKLY 15 | } else if (timeSplat.slice(-2, -1) === 'm') { 16 | return timeTypes.MONTHLY 17 | } else { 18 | return timeTypes.DAILY 19 | } 20 | } 21 | 22 | // test 23 | // console.log( getTimeType('2016/06/w1') ) 24 | // console.log( getTimeType('2016/06/01') ) 25 | // console.log( getTimeType('2016/06/m1') ) 26 | 27 | export default getTimeType -------------------------------------------------------------------------------- /src/components/Box/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import style from './style.css' 4 | import {siteColor} from '../../config' 5 | 6 | function Box({ site, links }) { 7 | return ( 8 |
9 |

10 | {site} 🔥 11 |

12 | { links.map( (link, i) => { 13 | return 14 | })} 15 |
16 | ) 17 | } 18 | 19 | function OneHeadline({title, url, score, i}) { 20 | return ( 21 |
22 | 23 | {i}. 24 | {title} 25 | 26 | {score} 27 |
28 | ) 29 | } 30 | 31 | export default Box 32 | -------------------------------------------------------------------------------- /src/utils/getTimeOfNow.js: -------------------------------------------------------------------------------- 1 | 2 | import moment from 'moment' 3 | import { timeTypes } from '../config'; 4 | 5 | /** 6 | * @param {String} timeType 7 | * @return {String} time of now of the given type 8 | * 9 | * @example 10 | * 11 | */ 12 | function getTimeOfNow(timeType) { 13 | switch (timeType) { 14 | case timeTypes.DAILY: 15 | return moment().subtract(1, 'days').format('YYYY/MM/DD') 16 | 17 | case timeTypes.MONTHLY: 18 | return moment().subtract(7, 'days').subtract(1, 'months').format('YYYY/MM') + '/mm' 19 | 20 | case timeTypes.WEEKLY: 21 | const weekOfMonth = moment().subtract(1, 'days').day(-7).week() - moment().day(-8).startOf('month').week() + 1 22 | return `${moment().subtract(1, 'days').day(-7).format('YYYY/MM')}/w${weekOfMonth}` 23 | } 24 | } 25 | 26 | // test 27 | // console.log( getTimeOfNow(timeTypes.WEEKLY) ) 28 | 29 | export default getTimeOfNow -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/index.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": false, 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/components/Box/style.css: -------------------------------------------------------------------------------- 1 | 2 | .box { 3 | text-align: left; 4 | display: inline-block; 5 | max-width: 500px; 6 | min-width: 300px; 7 | margin: .6em; 8 | background: #fff; 9 | border-radius: 4px; 10 | box-shadow: 3px 4px rgba( 0, 0, 0, 0.2 ); 11 | } 12 | 13 | .site { 14 | color: #111; 15 | text-align: center; 16 | font-weight:bold; 17 | /*padding: .1em;*/ 18 | margin: 0; 19 | } 20 | 21 | .oneHeadline { 22 | position: relative; 23 | display: block; 24 | padding: .6em; 25 | border-top: solid 1px #ddd; 26 | } 27 | 28 | .link { 29 | display: inline; 30 | margin-right: 18px; 31 | color: #333; 32 | } 33 | 34 | .link:link, .link:visited, .link:active { 35 | text-decoration: none; 36 | } 37 | 38 | .link:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | .score { 43 | display: inline; 44 | border-radius: 10px; 45 | padding: 2px 6px 2px 6px; 46 | font-size: 13px; 47 | background-color: #aab0c6; 48 | opacity: 0.6; 49 | color: white; 50 | text-decoration: none; 51 | cursor: auto; 52 | position: absolute; 53 | /*top: -20px;*/ 54 | right: 3px; 55 | /*width: 500px;*/ 56 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var rucksack = require('rucksack-css') 2 | var webpack = require('webpack') 3 | var path = require('path') 4 | 5 | module.exports = { 6 | entry: './src/index.js', 7 | output: { 8 | path: path.join(__dirname, './build'), 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.css$/, 15 | include: /src/, 16 | loaders: [ 17 | 'style-loader', 18 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 19 | 'postcss-loader' 20 | ] 21 | }, 22 | { 23 | test: /\.css$/, 24 | exclude: /src/, 25 | loader: 'style!css' 26 | }, 27 | { 28 | test: /\.(js|jsx)$/, 29 | exclude: /node_modules/, 30 | loaders: [ 31 | 'react-hot', 32 | 'babel-loader' 33 | ] 34 | }, 35 | ], 36 | }, 37 | resolve: { 38 | extensions: ['', '.js', '.jsx'] 39 | }, 40 | postcss: [ 41 | rucksack({ 42 | autoprefixer: true 43 | }) 44 | ], 45 | devServer: { 46 | contentBase: './src', 47 | hot: true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-headline", 3 | "version": "1.0.0", 4 | "description": "headlines for myself", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server -d --history-api-fallback --hot --inline --progress --colors --port 3000", 9 | "build": "webpack -p --progress --colors" 10 | }, 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-loader": "^6.2.4", 14 | "babel-plugin-transform-runtime": "^6.9.0", 15 | "babel-preset-es2015": "^6.5.0", 16 | "babel-preset-react": "^6.5.0", 17 | "babel-preset-stage-0": "^6.5.0", 18 | "css-loader": "^0.23.1", 19 | "file-loader": "^0.8.5", 20 | "postcss-loader": "^0.8.1", 21 | "react": "^0.14.7", 22 | "react-dom": "^0.14.7", 23 | "react-hot-loader": "^1.3.0", 24 | "rucksack-css": "^0.8.5", 25 | "style-loader": "^0.13.0", 26 | "webpack": "^1.12.14", 27 | "webpack-dev-server": "^1.14.1", 28 | "webpack-hot-middleware": "^2.7.1" 29 | }, 30 | "dependencies": { 31 | "axios": "^0.9.1", 32 | "babel-runtime": "^6.9.2", 33 | "corner-notie": "^1.2.0", 34 | "moment": "^2.12.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Headline 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font: normal 100% Helvetica, Arial, sans-serif; 6 | background: #efefef; 7 | } 8 | 9 | .container { 10 | /*max-width: 900px;*/ 11 | background-image: url("http://static.v2ex.com/tiles/random.jpg"); 12 | background-repeat: repeat; 13 | 14 | } 15 | 16 | .header { 17 | background-color: #444; 18 | /*text-align: center;*/ 19 | } 20 | 21 | .div { 22 | margin: auto; 23 | max-width: 900px; 24 | } 25 | 26 | .ul { 27 | list-style: none; 28 | /*text-align: center;*/ 29 | padding: 0; 30 | margin: 0; 31 | } 32 | .li { 33 | text-align: center; 34 | font-family: 'Oswald', sans-serif; 35 | font-size: 1.1em; 36 | line-height: 40px; 37 | height: 40px; 38 | border-bottom: 1px solid #888; 39 | } 40 | 41 | .a { 42 | text-decoration: none; 43 | color: #fff; 44 | display: block; 45 | transition: .3s background-color; 46 | } 47 | 48 | .a:hover { 49 | background-color: #70B7FD; 50 | } 51 | 52 | .a:active { 53 | background-color: #fff; 54 | color: #444; 55 | cursor: default; 56 | } 57 | 58 | .aActive{ 59 | background-color: #70B7FD; 60 | } 61 | 62 | @media screen and (min-width: 300px) { 63 | .li { 64 | width: 100px; 65 | border-bottom: none; 66 | height: 50px; 67 | line-height: 50px; 68 | font-size: 1.2em; 69 | } 70 | 71 | .li { 72 | display: inline-block; 73 | margin-right: -4px; 74 | } 75 | } 76 | 77 | .header2 { 78 | text-align: center; 79 | /*color: #efefef;*/ 80 | font-size: 1.7em; 81 | /*background-color: #616161;*/ 82 | /*padding:.3em;*/ 83 | margin-top: .3em; 84 | } 85 | 86 | .btn { 87 | border: none; 88 | background: none; 89 | font-size: 1.0em; 90 | cursor: pointer; 91 | padding:0.1em; 92 | /*color: #efefef;*/ 93 | } -------------------------------------------------------------------------------- /src/utils/changeTime.js: -------------------------------------------------------------------------------- 1 | 2 | import moment from 'moment' 3 | import { timeTypes } from '../config' 4 | import getTimeType from '../utils/getTimeType' 5 | 6 | /** 7 | * add or minus timeSplat 8 | * 9 | * Example: 10 | * 11 | */ 12 | function changeTime(timeSplat, add = true) { 13 | const [year, month] = timeSplat.split('/') 14 | if(add) { 15 | switch ( getTimeType(timeSplat) ) { 16 | case timeTypes.DAILY: 17 | return moment(timeSplat).add(1, 'days').format('YYYY/MM/DD') 18 | 19 | case timeTypes.WEEKLY: { 20 | const monthlyWeekNum = Number( timeSplat.slice(-1) ) 21 | // console.log(monthlyWeekNum, `${year}/${month}`) 22 | const passedWeeks = moment(`${year}/${month}`, 'YYYY/MM').week() + monthlyWeekNum - 1 23 | // console.log(passedWeeks, `${year}W${passedWeeks}`) 24 | const newMoment = moment(`${year}W${passedWeeks}`) 25 | // console.log(newMoment.format('YYYY/MM/DD')) 26 | const newYearMonth = newMoment.format('YYYY/MM') 27 | const newMonthlyWeekNum = newMoment.week() - newMoment.startOf('month').week() + 1 28 | return `${newYearMonth}/w${newMonthlyWeekNum}` 29 | } 30 | 31 | case timeTypes.MONTHLY: 32 | const newMonth = moment(`${year}/${month}`, 'YYYY/MM').add(1, 'months').format('YYYY/MM') 33 | return newMonth + '/mm' 34 | } 35 | } else { 36 | switch ( getTimeType(timeSplat) ) { 37 | case timeTypes.DAILY: 38 | return moment(timeSplat).subtract(1, 'days').format('YYYY/MM/DD') 39 | 40 | case timeTypes.WEEKLY: { 41 | const monthlyWeekNum = Number( timeSplat.slice(-1) ) 42 | // console.log(monthlyWeekNum, `${year}/${month}`) 43 | const passedWeeks = moment(`${year}/${month}`, 'YYYY/MM').week() + monthlyWeekNum - 1 44 | // console.log(passedWeeks, `${year}W${passedWeeks}`) 45 | const newMoment = moment(`${year}W${passedWeeks}`).subtract(2, 'weeks') 46 | // console.log(newMoment.format('YYYY/MM/DD')) 47 | const newYearMonth = newMoment.format('YYYY/MM') 48 | const newMonthlyWeekNum = newMoment.week() - newMoment.startOf('month').week() + 1 49 | return `${newYearMonth}/w${newMonthlyWeekNum}` 50 | } 51 | 52 | case timeTypes.MONTHLY: 53 | const newMonth = moment(`${year}/${month}`, 'YYYY/MM').subtract(1, 'months').format('YYYY/MM') 54 | return newMonth + '/mm' 55 | } 56 | } 57 | } 58 | 59 | 60 | // tests 61 | 62 | // console.log( changeTime('2016/06/01', true) ) 63 | // console.log( changeTime('2016/06/01', false) ) 64 | // console.log( changeTime('2016/05/w4', true) ) 65 | // console.log( changeTime('2016/05/w4', false) ) 66 | // console.log( changeTime('2016/06/w2', true) ) 67 | // console.log( changeTime('2016/06/w2', false) ) 68 | // console.log( changeTime('2016/07/w5', true) ) 69 | // console.log( changeTime('2016/06/mm', true) ) 70 | // console.log( changeTime('2016/06/mm', false) ) 71 | 72 | 73 | export default changeTime -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import moment from 'moment' 4 | import axios from 'axios' 5 | import { dataUrl, apis, timeTypes } from '../config' 6 | import defineTimeType from '../utils/getTimeType' 7 | import getHeadlines from '../utils/getHeadlines' 8 | import changeTime from '../utils/changeTime' 9 | import getTimeOfNow from '../utils/getTimeOfNow' 10 | import Boxes from './Boxes' 11 | import style from './style.css' 12 | 13 | // headline url: https://raw.githubusercontent.com/timqian/my-headline-crawler/master/data/2016/04/11.json 14 | class Container extends React.Component { 15 | constructor(props, context) { 16 | super(props, context) 17 | const time = location.hash.slice(1) ? location.hash.slice(1) : moment().subtract(1, 'days').format('YYYY/MM/DD') 18 | console.log(time) 19 | this.state = { 20 | time, 21 | timeType: defineTimeType(time), 22 | headlines: {}, 23 | } 24 | } 25 | 26 | async componentDidMount() { 27 | const headlines = await getHeadlines(this.state.time) 28 | this.setState({ headlines }) 29 | } 30 | 31 | async handleTimeChange(add = true) { 32 | const time = changeTime(this.state.time, add) 33 | const headlines = await getHeadlines(time) 34 | this.setState({ time, headlines }) 35 | location.hash = time 36 | } 37 | 38 | async handleTimeTypeChange(timeType) { 39 | const time = getTimeOfNow(timeType) 40 | const headlines = await getHeadlines(time) 41 | this.setState({ 42 | time, headlines, 43 | timeType: defineTimeType(time) 44 | }) 45 | location.hash = time 46 | } 47 | 48 | isActive(timeType) { 49 | if(timeType === this.state.timeType) { 50 | return style.aActive 51 | } else { 52 | return '' 53 | } 54 | } 55 | 56 | render() { 57 | return ( 58 |
59 | 60 |
61 |
62 | 82 |
83 |
84 | 85 |
86 | 87 | {this.state.time} 88 | 89 |
90 | 91 | 92 |
93 | ) 94 | } 95 | } 96 | 97 | 98 | export default Container 99 | --------------------------------------------------------------------------------