├── .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 | 
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 |
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 |
84 |
85 |
86 |
87 | {this.state.time}
88 |
89 |
90 |
91 |
92 |
93 | )
94 | }
95 | }
96 |
97 |
98 | export default Container
99 |
--------------------------------------------------------------------------------