├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── app.json ├── colors.json ├── theme.js └── webpack.js ├── logo.md ├── package-lock.json ├── package.json ├── src ├── components │ ├── App │ │ ├── index.css │ │ └── index.js │ ├── GitHub │ │ └── GitHubRepo.js │ ├── HackerNews │ │ └── HackerNewsStory.js │ ├── Header │ │ ├── index.css │ │ └── index.js │ ├── Icons │ │ └── index.js │ ├── Logo │ │ └── index.js │ ├── News │ │ ├── index.css │ │ └── index.js │ ├── NewsList │ │ ├── index.css │ │ └── index.js │ ├── PlaceholderShimmer │ │ ├── index.css │ │ └── index.js │ └── ProductHunt │ │ └── ProductHuntItem.js ├── data │ └── index.js ├── index.ejs ├── index.js └── static │ ├── CNAME │ ├── favicons │ ├── android-chrome-144x144.png │ ├── android-chrome-192x192.png │ ├── android-chrome-36x36.png │ ├── android-chrome-48x48.png │ ├── android-chrome-72x72.png │ ├── android-chrome-96x96.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg │ ├── icon.png │ ├── icon.svg │ ├── logo.png │ └── logo.svg └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | *.log 4 | 5 | # OS generated files # 6 | ###################### 7 | .DS_Store 8 | .DS_Store? 9 | ._* 10 | .Spotlight-V100 11 | .Trashes 12 | ehthumbs.db 13 | Thumbs.db 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | cache: 5 | directories: 6 | - node_modules 7 | before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi 8 | install: npm install 9 | script: npm run prod 10 | deploy: 11 | provider: surge 12 | project: ./build/ 13 | domain: devne.ws 14 | skip_cleanup: true 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 devnews 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 | [![devnews](https://devne.ws/logo.svg)](https://devne.ws/) 2 | 3 | [![StackShare](http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat)](http://stackshare.io/sunnysingh/devnews) 4 | [![Travis](https://img.shields.io/travis/devnews/web.svg?maxAge=2592000)](https://travis-ci.org/devnews/web) 5 | [![David](https://img.shields.io/david/devnews/web.svg?maxAge=2592000)](https://david-dm.org/devnews/web) 6 | 7 | ## About 8 | 9 | [Devnews](https://devne.ws/) aggregates top news stories from Hacker News, trending repositories from GitHub, and top tech from Product Hunt. 10 | 11 | ## Roadmap 12 | 13 | The [open issues](https://github.com/devnews/web/issues) are good indications of what we have planned. 14 | 15 | Also make sure you check out the [v2 milestone](https://github.com/devnews/web/milestone/1). 16 | 17 | ## Contributing 18 | 19 | Please stay consistent with the code. 20 | 21 | 1. Clone this repository (`devnews/web`). 22 | 2. Run `npm install`. 23 | 3. Run `npm start`. 24 | 4. Submit a pull request. 25 | -------------------------------------------------------------------------------- /config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devnews", 3 | "nameUpper": "Devnews", 4 | "description": "Developer news aggregator.", 5 | "twitter": "ninjalitydesign", 6 | "facebook": "ninjalitydesign", 7 | "facebook_app_id": "217662015276471" 8 | } 9 | -------------------------------------------------------------------------------- /config/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "brandPrimary": "#3d49ba", 3 | "bodyBackground": "#f5f5f5" 4 | } 5 | -------------------------------------------------------------------------------- /config/theme.js: -------------------------------------------------------------------------------- 1 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 2 | import { 3 | indigo900, 4 | grey100, grey300, grey400, grey500, 5 | white, lightBlack, darkBlack, 6 | } from 'material-ui/styles/colors'; 7 | 8 | const Theme = getMuiTheme({ 9 | palette: { 10 | primary1Color: grey300, 11 | primary2Color: grey400, 12 | primary3Color: lightBlack, 13 | accent1Color: indigo900, 14 | accent2Color: grey100, 15 | accent3Color: grey500, 16 | textColor: darkBlack, 17 | alternateTextColor: darkBlack, 18 | canvasColor: white, 19 | borderColor: grey300, 20 | } 21 | }); 22 | 23 | export default Theme; 24 | -------------------------------------------------------------------------------- /config/webpack.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const fs = require('fs'); 3 | const HtmlPlugin = require('html-webpack-plugin'); 4 | const CopyPlugin = require('copy-webpack-plugin'); 5 | const OpenBrowserPlugin = require('open-browser-webpack-plugin'); 6 | const postcssImport = require('postcss-import'); 7 | const cssnext = require('postcss-cssnext'); 8 | const fontMagician = require('postcss-font-magician'); 9 | 10 | const meta = JSON.parse(fs.readFileSync('./config/app.json', 'utf8')); 11 | const colors = JSON.parse(fs.readFileSync('./config/colors.json', 'utf8')); 12 | 13 | const PROD_ENV = process.env.NODE_ENV === 'production'; 14 | const DEV_ENV = !PROD_ENV; 15 | 16 | const cssModulesNameFormat = DEV_ENV ? '[path][name]__[local]___[hash:base64:5]' : '[hash:base64]'; 17 | 18 | const config = { 19 | entry: './src/index.js', 20 | output: { 21 | path: './build', 22 | filename: DEV_ENV ? 'dev.bundle.js' : '[hash].bundle.js', 23 | }, 24 | plugins: [ 25 | new HtmlPlugin({ 26 | template: './src/index.ejs', 27 | filename: 'index.html', 28 | minify: { 29 | collapseBooleanAttributes: true, 30 | collapseWhitespace: true, 31 | removeAttributeQuotes: true, 32 | removeComments: true, 33 | removeEmptyAttributes: true, 34 | removeRedundantAttributes: true, 35 | }, 36 | inject: false, 37 | meta: meta, 38 | colors: colors, 39 | baseUrl: DEV_ENV ? 'http://localhost:3000' : 'https://devne.ws', 40 | }), 41 | new CopyPlugin([ 42 | {from: './src/static', to: './'}, 43 | ]), 44 | new webpack.DefinePlugin({ 45 | 'process.env': { 46 | 'NODE_ENV': DEV_ENV ? JSON.stringify('development') : JSON.stringify('production'), 47 | } 48 | }), 49 | ], 50 | module: { 51 | loaders: [ 52 | { 53 | test: /.js$/, 54 | loader: 'babel', 55 | exclude: [/node_modules/, /.ejs$/], 56 | query: { 57 | presets: DEV_ENV ? ['env', 'react', 'react-hmre'] : ['env', 'react'], 58 | }, 59 | }, 60 | { 61 | test: /\.css$/, 62 | loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName='+cssModulesNameFormat+'!postcss-loader', 63 | exclude: [/node_modules/], 64 | }, 65 | { 66 | test: /\.json$/, 67 | loader: 'json', 68 | exclude: [/node_modules/], 69 | }, 70 | { 71 | test: /\.svg$/, 72 | loader: 'raw', 73 | exclude: [/node_modules/], 74 | }, 75 | ] 76 |   }, 77 | postcss: function () { 78 | return [ 79 | postcssImport, 80 | cssnext({ 81 | features: { 82 | customProperties: { 83 | variables: colors, 84 | }, 85 | }, 86 | }), 87 | fontMagician(), 88 | ]; 89 | }, 90 | }; 91 | 92 | if (DEV_ENV) { 93 | config.plugins.push( 94 | new OpenBrowserPlugin({ 95 | url: 'http://localhost:3000', 96 | }) 97 | ); 98 | config.devtool = '#inline-source-map'; 99 | } 100 | 101 | module.exports = config; 102 | -------------------------------------------------------------------------------- /logo.md: -------------------------------------------------------------------------------- 1 | ## Logo 2 | 3 | If you're linking to devnews somewhere, we'd love to hear about it! You can tweet [@getdevnews](https://twitter.com/getdevnews) or [@ninjalitydesign](https://twitter.com/ninjality.com). 4 | 5 | The logo is available in [SVG](https://devne.ws/logo.svg) and [PNG](https://devne.ws/logo.png) formats: 6 | 7 | [![logo.svg](https://devne.ws/logo.svg)](https://devne.ws/logo.svg) 8 | 9 | The icon is available in [SVG](https://devne.ws/icon.svg) and [PNG](https://devne.ws/icon.png) formats: 10 | 11 | [![icon.svg](https://devne.ws/icon.svg)](https://devne.ws/icon.svg) 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devnews", 3 | "version": "1.0.0", 4 | "description": "Developer news aggregator.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config config/webpack.js --port 3000 --hot --inline --progress --colors", 8 | "build": "webpack --config config/webpack.js", 9 | "prod": "NODE_ENV=production webpack --config config/webpack.js -p", 10 | "predeploy": "npm run prod", 11 | "deploy": "surge --project ./build", 12 | "test": "echo 'Error: no test specified'" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/devnews/web.git" 17 | }, 18 | "author": "Sunny Singh", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/devnews/web/issues" 22 | }, 23 | "homepage": "https://devne.ws/", 24 | "dependencies": { 25 | "autolinker": "^1.4.4", 26 | "babel-preset-react-hmre": "^1.1.1", 27 | "html-escape": "^2.0.0", 28 | "load-script": "^1.0.0", 29 | "localforage": "^1.5.2", 30 | "material-ui": "^0.19.4", 31 | "moment": "^2.19.1", 32 | "normalize.css": "^7.0.0", 33 | "prop-types": "^15.6.0", 34 | "react": "^16.0.0", 35 | "react-dom": "^16.0.0", 36 | "react-tap-event-plugin": "^3.0.2", 37 | "superagent": "^3.6.3" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.7.2", 41 | "babel-loader": "^6.2.4", 42 | "babel-preset-env": "^1.6.0", 43 | "babel-preset-react": "^6.5.0", 44 | "copy-webpack-plugin": "^4.0.1", 45 | "css-loader": "^0.26.0", 46 | "html-webpack-plugin": "^2.12.0", 47 | "json-loader": "^0.5.4", 48 | "open-browser-webpack-plugin": "0.0.3", 49 | "postcss-cssnext": "^2.5.1", 50 | "postcss-font-magician": "^1.4.0", 51 | "postcss-import": "^8.0.2", 52 | "postcss-loader": "^1.1.1", 53 | "raw-loader": "^0.5.1", 54 | "react-svg-inline": "^2.0.1", 55 | "style-loader": "^0.13.0", 56 | "surge": "^0.18.0", 57 | "webpack": "^1.12.14", 58 | "webpack-dev-server": "^1.14.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/App/index.css: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | 3 | :root { 4 | box-sizing: border-box; 5 | font-size: 16px; 6 | font-family: "Roboto", sans-serif; 7 | color: #212121; 8 | background: var(--bodyBackground); 9 | } 10 | 11 | *, 12 | *:before, 13 | *:after { 14 | box-sizing: inherit; 15 | } 16 | 17 | a:not([type="button"]) { 18 | color: #222; 19 | border-bottom: 1px solid #222; 20 | text-decoration: none; 21 | 22 | &:hover, 23 | &:focus { 24 | color: #444; 25 | border-color: transparent; 26 | text-decoration: none; 27 | transition: all .2s ease; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 3 | import Theme from '../../../config/theme'; 4 | import Header from '../Header'; 5 | import News from '../News'; 6 | import styles from './index.css'; 7 | 8 | class App extends React.Component { 9 | 10 | render () { 11 | return ( 12 | 13 |
14 |
15 | 16 |
17 |
18 | ) 19 | } 20 | 21 | }; 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/components/GitHub/GitHubRepo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Autolinker from 'autolinker'; 4 | import escape from 'html-escape'; 5 | import styles from '../NewsList/index.css'; 6 | 7 | const GitHubRepo = (props) => { 8 | 9 | let getDescription = () => { 10 | const safeDescription = escape(props.repo.description); 11 | return { 12 | __html: Autolinker.link(safeDescription, { 13 | email: false, 14 | phone: false, 15 | twitter: false, 16 | }), 17 | }; 18 | }; 19 | 20 | return ( 21 |
22 |

23 | 28 | {props.repo.user}/{props.repo.name} 29 | 30 |

31 |

32 |

50 |
51 | ) 52 | 53 | }; 54 | 55 | GitHubRepo.propTypes = { 56 | repo: PropTypes.object.isRequired, 57 | }; 58 | 59 | export default GitHubRepo; 60 | -------------------------------------------------------------------------------- /src/components/HackerNews/HackerNewsStory.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from '../NewsList/index.css'; 4 | 5 | const HackerNewsStory = (props) => { 6 | 7 | return ( 8 |
9 |

10 | 33 | 34 | 35 |

36 | ) 37 | 38 | }; 39 | 40 | HackerNewsStory.propTypes = { 41 | story: PropTypes.object.isRequired, 42 | }; 43 | 44 | export default HackerNewsStory; 45 | -------------------------------------------------------------------------------- /src/components/Header/index.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | padding: 15px; 3 | text-align: center; 4 | } 5 | 6 | .aboutBtnContainer { 7 | position: absolute; 8 | top: 3px; 9 | left: 3px; 10 | } 11 | 12 | .aboutContainer { 13 | padding: 30px; 14 | } 15 | 16 | .aboutHeading { 17 | margin: 0; 18 | font-size: 1.4rem; 19 | } 20 | 21 | .aboutText { 22 | margin: 15px 0 0 0; 23 | line-height: 1.5em; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Popover from 'material-ui/Popover'; 3 | import IconButton from 'material-ui/IconButton'; 4 | import InfoOutlineIcon from 'material-ui/svg-icons/action/info-outline'; 5 | import Logo from '../Logo'; 6 | import styles from './index.css'; 7 | 8 | class Header extends React.Component { 9 | 10 | constructor () { 11 | super(); 12 | 13 | this.state = { 14 | aboutOpen: false, 15 | }; 16 | } 17 | 18 | handleClick (event) { 19 | this.setState({ 20 | aboutOpen: true, 21 | anchorEl: event.currentTarget, 22 | }); 23 | } 24 | 25 | handleRequestClose () { 26 | this.setState({ 27 | aboutOpen: false, 28 | }); 29 | } 30 | 31 | render () { 32 | return ( 33 |
34 |
35 | 36 |
37 |
38 | 42 | 43 | 44 |
45 | 52 |
53 |

About

54 |

Devnews aggregates top news stories from Hacker News, trending repositories from GitHub, and top tech from Product Hunt.

55 |

This project was created by the ninjas at Ninjality and is open sourced on GitHub.

56 |

You can follow @getdevnews on Twitter for updates, or view our logo information for linking back.

57 |
58 | 59 |
60 | ) 61 | } 62 | }; 63 | 64 | export default Header; 65 | -------------------------------------------------------------------------------- /src/components/Icons/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SvgIcon from 'material-ui/SvgIcon'; 3 | 4 | export const HackerNewsIcon = (props) => ( 5 | 11 | {props.title && {props.title}} 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export const GitHubIcon = (props) => ( 20 | 26 | {props.title && {props.title}} 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | export const ProductHuntIcon = (props) => ( 34 | 40 | {props.title && {props.title}} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /src/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SVGInline from 'react-svg-inline'; 4 | import svg from '../../static/logo.svg'; 5 | 6 | const Logo = (props) => { 7 | return ( 8 | 15 | ); 16 | }; 17 | 18 | Logo.propTypes = { 19 | desc: PropTypes.string, 20 | width: PropTypes.string, 21 | height: PropTypes.string, 22 | }; 23 | 24 | Logo.defaultProps = { 25 | desc: 'devnews logo', 26 | width: 'auto', 27 | height: '20px', 28 | }; 29 | 30 | export default Logo; 31 | -------------------------------------------------------------------------------- /src/components/News/index.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | margin: 0; 3 | font-size: 1rem; 4 | } 5 | 6 | .tabsContainer { 7 | padding: 0; 8 | } 9 | 10 | .content { 11 | padding: 30px; 12 | } 13 | 14 | .storiesContainer { 15 | margin-top: 15px; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/News/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Tabs, Tab} from 'material-ui/Tabs'; 3 | import {HackerNewsIcon, GitHubIcon, ProductHuntIcon} from '../Icons'; 4 | import {hackernews, github, producthunt} from '../../data'; 5 | import NewsList from '../NewsList'; 6 | import styles from './index.css'; 7 | 8 | class News extends React.Component { 9 | 10 | constructor () { 11 | super(); 12 | 13 | this.state = { 14 | hackernews: { 15 | data: [], 16 | loaded: false, 17 | }, 18 | github: { 19 | data: [], 20 | loaded: false, 21 | }, 22 | producthunt: { 23 | data: [], 24 | loaded: false, 25 | }, 26 | }; 27 | } 28 | 29 | componentDidMount () { 30 | hackernews((data) => { 31 | this.setState({ 32 | hackernews: { 33 | data: data, 34 | loaded: true, 35 | }, 36 | }); 37 | }); 38 | } 39 | 40 | handleActiveTab (tab) { 41 | switch (tab.props.value) { 42 | case 'github': 43 | if (!this.state.github.loaded) { 44 | github((data) => { 45 | this.setState({ 46 | github: { 47 | data: data, 48 | loaded: true, 49 | }, 50 | }); 51 | }); 52 | } 53 | break; 54 | case 'producthunt': 55 | if (!this.state.producthunt.loaded) { 56 | producthunt((data) => { 57 | this.setState({ 58 | producthunt: { 59 | data: data, 60 | loaded: true, 61 | }, 62 | }); 63 | }); 64 | } 65 | break; 66 | } 67 | } 68 | 69 | render () { 70 | return ( 71 | 75 | 76 | }> 77 |

78 | Hacker News 79 |

80 | 81 | 87 | 88 | 89 | Go to Hacker News (page 2) 90 | 91 |
92 | 93 | } 95 | value="github" 96 | onActive={this.handleActiveTab.bind(this)} 97 | > 98 |

99 | GitHub Trending 100 |

101 | 102 | 108 | 109 | 110 | Go to GitHub Trending 111 | 112 |
113 | 114 | } 116 | value="producthunt" 117 | onActive={this.handleActiveTab.bind(this)} 118 | > 119 |

120 | Product Hunt Tech 121 |

122 | 123 | 129 | 130 | 131 | Go to Product Hunt Tech 132 | 133 |
134 | 135 |
136 | ) 137 | } 138 | }; 139 | 140 | export default News; 141 | -------------------------------------------------------------------------------- /src/components/NewsList/index.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-bottom: 30px; 3 | } 4 | 5 | .container a:visited { 6 | color: #555; 7 | } 8 | 9 | .heading { 10 | margin: 0; 11 | line-height: 1.6em; 12 | word-break: break-all; 13 | word-break: break-word; 14 | hyphens: auto; 15 | } 16 | 17 | .description { 18 | margin-top: 15px; 19 | margin-bottom: 0; 20 | word-break: break-all; 21 | word-break: break-word; 22 | hyphens: auto; 23 | } 24 | 25 | .footer { 26 | padding-top: 15px; 27 | line-height: 1.5em; 28 | } 29 | 30 | .footerItem { 31 | display: inline-block; 32 | margin-right: 15px; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/NewsList/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import PlaceholderShimmer from '../PlaceholderShimmer'; 4 | import HackerNewsStory from '../HackerNews/HackerNewsStory'; 5 | import GitHubRepo from '../GitHub/GitHubRepo'; 6 | import ProductHuntItem from '../ProductHunt/ProductHuntItem'; 7 | 8 | class NewsList extends React.Component { 9 | 10 | shouldComponentUpdate (nextProps) { 11 | return this.props.loaded !== nextProps.loaded; 12 | } 13 | 14 | render () { 15 | if (!this.props.loaded) { 16 | return ( 17 | 18 | ) 19 | } 20 | 21 | if (!this.props.data.length) { 22 | return ( 23 |
Oops, we were unable to load the stories :(
24 | ) 25 | } 26 | 27 | return ( 28 |
29 | { 30 | this.props.data.map(item => { 31 | if (this.props.source == 'hackernews') { 32 | return ( 33 | 37 | ) 38 | } 39 | if (this.props.source == 'github') { 40 | return ( 41 | 45 | ) 46 | } 47 | if (this.props.source == 'producthunt') { 48 | return ( 49 | 53 | ) 54 | } 55 | }) 56 | } 57 |
58 | ) 59 | } 60 | 61 | }; 62 | 63 | NewsList.propsTypes = { 64 | source: PropTypes.string.isRequired, 65 | getData: PropTypes.object.isRequired, 66 | data: PropTypes.object.isRequired, 67 | loaded: PropTypes.bool.isRequired, 68 | }; 69 | 70 | export default NewsList; 71 | -------------------------------------------------------------------------------- /src/components/PlaceholderShimmer/index.css: -------------------------------------------------------------------------------- 1 | .common { 2 | position: relative; 3 | width: 100%; 4 | height: 30px; 5 | background-color: #ddd; 6 | background-image: linear-gradient(to left, #ddd 0%, #e8e8e8 20%, #ddd 40%, #ddd 100%); 7 | background-size: 400%; 8 | animation-duration: 1s; 9 | animation-fill-mode: forwards; 10 | animation-iteration-count: infinite; 11 | animation-name: shimmer; 12 | animation-timing-function: linear; 13 | transform: translateZ(0); 14 | } 15 | 16 | .smallPlaceholder { 17 | margin: 15px 0; 18 | max-width: 300px; 19 | } 20 | 21 | .largePlaceholder { 22 | margin: 30px 0; 23 | max-width: 400px; 24 | } 25 | 26 | .heading { 27 | composes: common; 28 | } 29 | 30 | .footer { 31 | composes: common; 32 | display: inline-block; 33 | margin-top: 15px; 34 | &::before { 35 | content: ''; 36 | position: absolute; 37 | right: 30%; 38 | width: 15px; 39 | height: 38px; 40 | background: var(--bodyBackground); 41 | } 42 | } 43 | 44 | .smallFooter { 45 | composes: footer; 46 | &::before { 47 | right: 50%; 48 | } 49 | } 50 | 51 | @keyframes shimmer { 52 | 0% { 53 | background-position: 100%; 54 | } 55 | 100% { 56 | background-position: 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/PlaceholderShimmer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.css'; 3 | 4 | const PlaceholderShimmer = (props) => ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ); 16 | 17 | export default PlaceholderShimmer; 18 | -------------------------------------------------------------------------------- /src/components/ProductHunt/ProductHuntItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from '../NewsList/index.css'; 4 | 5 | const ProductHuntItem = (props) => { 6 | 7 | return ( 8 |
9 |

10 | 15 | {props.product.name} 16 | 17 |

18 |

{props.product.tagline}

19 | 33 |
34 | ) 35 | 36 | }; 37 | 38 | ProductHuntItem.propTypes = { 39 | product: PropTypes.object.isRequired, 40 | }; 41 | 42 | export default ProductHuntItem; 43 | -------------------------------------------------------------------------------- /src/data/index.js: -------------------------------------------------------------------------------- 1 | import request from "superagent"; 2 | import localforage from "localforage"; 3 | import moment from "moment"; 4 | 5 | const expiresDuration = 30; 6 | const expiresUnit = "minutes"; 7 | 8 | const wrapApiKey = "vZpCx0QXD65gAcUD4Q7gAL6y0GQB1pgT"; 9 | 10 | export const hackernews = (callback) => { 11 | const baseUrl = "https://hacker-news.firebaseio.com/v0"; 12 | 13 | localforage.getItem("hackernews").then((cache) => { 14 | if (cache) { 15 | const notExpired = moment().diff(cache.expires) < 0; 16 | if (notExpired) { 17 | callback(cache.data); 18 | return; 19 | } 20 | } 21 | 22 | request.get(baseUrl + "/topstories.json").end((error, response) => { 23 | let stories = response.body.slice(0, 30); // grab 30 items 24 | let data = []; 25 | let index = 0; 26 | 27 | for (let storyId of stories) { 28 | const apiUrl = baseUrl + "/item/" + storyId + ".json"; 29 | const cachedIndex = index + 1; 30 | 31 | request.get(apiUrl).end((error, response) => { 32 | data.push({ 33 | id: response.body.id, 34 | title: response.body.title, 35 | by: response.body.by, 36 | url: response.body.url, 37 | points: response.body.score, 38 | commentCount: response.body.descendants, 39 | ago: moment.unix(response.body.time).fromNow(), 40 | }); 41 | 42 | if (cachedIndex === stories.length) { 43 | localforage.setItem("hackernews", { 44 | expires: moment().add(expiresDuration, expiresUnit).valueOf(), 45 | data, 46 | }); 47 | callback(data); 48 | } 49 | }); 50 | 51 | index++; 52 | } 53 | }); 54 | }); 55 | }; 56 | 57 | export const github = (callback) => { 58 | const baseUrl = "https://ghapi.huchen.dev/repositories"; 59 | 60 | localforage.getItem("github").then((cache) => { 61 | if (cache) { 62 | const notExpired = moment().diff(cache.expires) < 0; 63 | if (notExpired) { 64 | callback(cache.data); 65 | return; 66 | } 67 | } 68 | 69 | request.get(baseUrl).end((error, response) => { 70 | let data = []; 71 | for (let repo of response.body) { 72 | data.push({ 73 | url: repo.url, 74 | user: repo.author, 75 | name: repo.name, 76 | description: repo.description ? repo.description.trim() : null, 77 | stars: parseInt(repo.stars), 78 | language: repo.language ? repo.language.trim() : null, 79 | }); 80 | } 81 | 82 | localforage.setItem("github", { 83 | expires: moment().add(expiresDuration, expiresUnit).valueOf(), 84 | data, 85 | }); 86 | 87 | callback(data); 88 | }); 89 | }); 90 | }; 91 | 92 | export const producthunt = (callback) => { 93 | const baseUrl = 94 | "https://wrapapi.com/use/sunnysingh/producthunt/todaytech/0.0.3?wrapAPIKey=" + 95 | wrapApiKey; 96 | 97 | localforage.getItem("producthunt").then((cache) => { 98 | if (cache) { 99 | const notExpired = moment().diff(cache.expires) < 0; 100 | if (notExpired) { 101 | callback(cache.data); 102 | return; 103 | } 104 | } 105 | 106 | request.get(baseUrl).end((error, response) => { 107 | let data = []; 108 | for (let product of response.body.data.posts) { 109 | data.push({ 110 | id: product.id, 111 | name: product.name, 112 | tagline: product.tagline, 113 | url: product.redirect_url, 114 | votesCount: product.votes_count, 115 | commentsCount: product.comments_count, 116 | discussionUrl: product.discussion_url, 117 | }); 118 | } 119 | 120 | localforage.setItem("producthunt", { 121 | expires: moment().add(expiresDuration, expiresUnit).valueOf(), 122 | data, 123 | }); 124 | 125 | callback(data); 126 | }); 127 | }); 128 | }; 129 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= htmlWebpackPlugin.options.meta.name %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> 74 | 75 | <% } %> 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import injectTapEventPlugin from 'react-tap-event-plugin'; 4 | import App from './components/App'; 5 | 6 | injectTapEventPlugin(); 7 | 8 | render(, document.getElementById('root')); 9 | -------------------------------------------------------------------------------- /src/static/CNAME: -------------------------------------------------------------------------------- 1 | devne.ws 2 | -------------------------------------------------------------------------------- /src/static/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /src/static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/static/favicons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/android-chrome-36x36.png -------------------------------------------------------------------------------- /src/static/favicons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/android-chrome-48x48.png -------------------------------------------------------------------------------- /src/static/favicons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/android-chrome-72x72.png -------------------------------------------------------------------------------- /src/static/favicons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/android-chrome-96x96.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #3d49ba 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /src/static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/static/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /src/static/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/favicon.ico -------------------------------------------------------------------------------- /src/static/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devnews", 3 | "icons": [ 4 | { 5 | "src": "\/favicons\/android-chrome-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": 0.75 9 | }, 10 | { 11 | "src": "\/favicons\/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": 1 15 | }, 16 | { 17 | "src": "\/favicons\/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": 1.5 21 | }, 22 | { 23 | "src": "\/favicons\/android-chrome-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": 2 27 | }, 28 | { 29 | "src": "\/favicons\/android-chrome-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": 3 33 | }, 34 | { 35 | "src": "\/favicons\/android-chrome-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": 4 39 | } 40 | ], 41 | "display": "standalone" 42 | } 43 | -------------------------------------------------------------------------------- /src/static/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /src/static/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /src/static/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /src/static/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /src/static/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /src/static/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 57 | 60 | 63 | 65 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/icon.png -------------------------------------------------------------------------------- /src/static/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnews/web/84620724f484e89f6717f95fdb14da5184a820f8/src/static/logo.png -------------------------------------------------------------------------------- /src/static/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------