├── screenshot ├── thumbnail.png └── votes_graph.PNG ├── src ├── client │ ├── routes.js │ ├── index.js │ ├── reducers │ │ ├── index.js │ │ ├── newsFeedsReducer.js │ │ └── voteCountReducer.js │ ├── store │ │ └── index.js │ ├── actions │ │ ├── constants.js │ │ └── index.js │ ├── App.js │ ├── css │ │ └── index.css │ └── components │ │ └── HomeComponent.js └── server │ └── index.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── webpack.client.js ├── README.md ├── webpack.server.js └── package.json /screenshot/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/react-js-hacker-news-clone/master/screenshot/thumbnail.png -------------------------------------------------------------------------------- /screenshot/votes_graph.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshumanpattnaik/react-js-hacker-news-clone/master/screenshot/votes_graph.PNG -------------------------------------------------------------------------------- /src/client/routes.js: -------------------------------------------------------------------------------- 1 | import Home from './components/HomeComponent'; 2 | 3 | const routes = [ 4 | { 5 | path: "/", 6 | exact: true, 7 | component: Home 8 | } 9 | ]; 10 | 11 | export default routes; -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { hydrate } from "react-dom"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import App from "./App"; 5 | 6 | hydrate( 7 | 8 | 9 | , 10 | document.querySelector("#root") 11 | ); -------------------------------------------------------------------------------- /src/client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | 3 | import newsFeedsReducer from './newsFeedsReducer'; 4 | import voteCountReducer from './voteCountReducer'; 5 | 6 | const rootReducer = combineReducers({ 7 | news: newsFeedsReducer, 8 | vote_count: voteCountReducer 9 | }); 10 | 11 | export default rootReducer; -------------------------------------------------------------------------------- /src/client/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from "redux"; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | 5 | import rootReducer from '../reducers'; 6 | 7 | const middle = applyMiddleware(thunk, logger); 8 | const store = createStore(rootReducer, middle); 9 | 10 | export default store; -------------------------------------------------------------------------------- /src/client/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const BASE_URL = 'https://hn.algolia.com/api/v1'; 2 | export const ITMES = '/items/'; 3 | export const FETCH_NEWS_FEED = 'FETCH_NEWS_FEED'; 4 | export const SET_UPVOTE_COUNT = 'SET_UPVOTE_COUNT'; 5 | export const NEWS_STORAGE_KEY = 'news_feed'; 6 | export const NEWS_FEED_SHOW = 0; 7 | export const NEWS_FEED_HIDE = 1; 8 | export const UP_ARROW_ICON = 'https://assets.hackbotone.com/images/icons/up_arrow.png'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | yarn.lock 25 | -------------------------------------------------------------------------------- /src/client/reducers/newsFeedsReducer.js: -------------------------------------------------------------------------------- 1 | import {FETCH_NEWS_FEED} from '../actions/constants'; 2 | 3 | const initialState = { 4 | news: '', 5 | }; 6 | 7 | const newsFeedsReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case FETCH_NEWS_FEED: { 10 | const newState = { 11 | ...state, 12 | news: action.payload, 13 | }; 14 | return newState; 15 | } 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default newsFeedsReducer; -------------------------------------------------------------------------------- /src/client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import routes from "./routes"; 4 | 5 | import store from './store'; 6 | import { Provider } from 'react-redux' 7 | 8 | class App extends React.Component { 9 | render() { 10 | return ( 11 | 12 | 13 | {routes.map((route, i) => ( 14 | 15 | ))} 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | export default App; -------------------------------------------------------------------------------- /src/client/reducers/voteCountReducer.js: -------------------------------------------------------------------------------- 1 | import {SET_UPVOTE_COUNT} from '../actions/constants'; 2 | 3 | const initialState = { 4 | vote_count: '', 5 | }; 6 | 7 | const voteCountReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case SET_UPVOTE_COUNT: { 10 | const newState = { 11 | ...state, 12 | vote_count: action.payload, 13 | }; 14 | return newState; 15 | } 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default voteCountReducer; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 11.10.1 4 | cache: yarn 5 | 6 | before_script : 7 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s 8 | - export PATH=$HOME/.yarn/bin:$PATH 9 | - yarn install 10 | 11 | script: 12 | - yarn run build 13 | 14 | deploy: 15 | provider: heroku 16 | api_key: 17 | secure: mAhBSCidpLVlx1bQI0gOi33jK4UOIyQZC0Vu9xFLThZzOuFrApPUsKtBxMYYM0PDVOyVhiCfpdx49cmVL7qAjqgfXJWJLJnFmdNg9vwgfMo6OC+UQqfzYT+n8LuMKSM9fzhNfScNAWroWOifXRus1knNBbZc1tGJHW/iqKfmxXpTgxgiKkd11HxuGqhXIdQabfop0IfYea5KIB9KUfOBvpT9x8ntI95T/IWunhdcBaw4/JzVRkvem3wq7omLYviE5qvtinDdKtwSB8188baE9RngUQUeUd+rizGZ5cddw/M7PfZibHimNch8g9y4sfdZ04kymDD9UN6CBRFNS+frt/zKOx/FpjLLvVf2pEK3IiApnsAu3bkXKA7BBBwXIXSGeUZpxz5sSiNyyIw366q7EMUguEfQlw3L/SEsmNY7Y2qbvY+SNzZnKwiM0fJI4nWGheSm8MNhQcJYuiOKwzfcv1tbEGoL2z6OZ2XsHvWzk67/gDQP7buEVL6HTjZ2wqeR6zFdfRyOgQDvGgN3yVlmDtfIqexttrajSHRUG140wI7p+SWuRU/5+kbFbE6T0Pfp1KhMbdDS/hIXR4UIaipLwu0qFfWcLiJrx6r5+XutpYv8rXkzRdEIIEpATpQZ3pFSqqMt/ow5O+PvKugghj26o5S8OvxAUTJpPKjFwjSCzDQ= 18 | app: 19 | master: react-hn-clone -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anshuman Pattnaik 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. -------------------------------------------------------------------------------- /webpack.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | target: 'node', 5 | entry: './src/client/index.js', 6 | output: { 7 | filename: 'client_bundle.js', 8 | path: path.resolve(__dirname, 'build/public'), 9 | publicPath: '/build/public' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: [/\.svg$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 15 | loader: "file-loader", 16 | options: { 17 | name: "build/media/[name].[ext]", 18 | publicPath: url => url.replace(/build/, "") 19 | } 20 | }, 21 | { 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | exclude: '/node_modules/', 25 | options: { 26 | presets: [ 27 | '@babel/preset-react', 28 | '@babel/env' 29 | ], 30 | "plugins": [ 31 | "@babel/plugin-proposal-class-properties" 32 | ] 33 | } 34 | }, 35 | { 36 | test: /\.css$/i, 37 | loader: ['style-loader', 'css-loader'] 38 | } 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Technical Overview 2 | 3 | The idea behind this project is to design the clone of hacker news website using react.js with Server-side rendering technique and the application modules bundled using [Webpack](https://webpack.js.org/) Javascript module bundler and it uses [babel-loader](https://webpack.js.org/loaders/babel-loader/) to load the plugins with the given presets. 4 | 5 | To populate the votes in a line chart, it uses [react-chartkick](https://www.npmjs.com/package/react-chartkick) npm modules. 6 | 7 | And the application has been built with [Travis-CI (continuous integration)](https://travis-ci.com/) and deployed to [Heroku](https://react-hn-clone.herokuapp.com/). 8 | 9 | 10 | 11 |

12 | 13 |

14 | 15 | ### Website link 16 | [https://react-hn-clone.herokuapp.com/](https://react-hn-clone.herokuapp.com/) 17 | 18 | ### Installation 19 | ```````````````````````````````````````````````````````````````````````````` 20 | git clone https://github.com/anshumanpattnaik/react-js-hacker-news-clone.git 21 | cd react-js-hacker-news-clone 22 | yarn add 23 | yarn run start 24 | ```````````````````````````````````````````````````````````````````````````` 25 | 26 | ### References 27 | 1. [https://webpack.js.org/](https://webpack.js.org/) 28 | 2. [https://webpack.js.org/loaders/babel-loader/](https://webpack.js.org/loaders/babel-loader/) 29 | 3. [https://travis-ci.com/](https://travis-ci.com/) 30 | 31 | ### License 32 | This project is licensed under the [MIT License](LICENSE) 33 | -------------------------------------------------------------------------------- /webpack.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpackNodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | target: 'node', 6 | entry: './src/server/index.js', 7 | output: { 8 | filename: 'bundle.js', 9 | path: path.resolve(__dirname, 'build'), 10 | publicPath: '/build' 11 | }, 12 | devServer: { 13 | inline: false, 14 | contentBase: path.resolve(__dirname, 'build'), 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: [/\.svg$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 20 | loader: "file-loader", 21 | options: { 22 | name: "build/media/[name].[ext]", 23 | publicPath: url => url.replace(/build/, ""), 24 | emit: false 25 | } 26 | }, 27 | { 28 | test: /\.js$/, 29 | loader: 'babel-loader', 30 | exclude: '/node_modules/', 31 | options: { 32 | "presets": [ 33 | ['@babel/preset-env',{ 34 | "targets": { 35 | "esmodules": true, 36 | }, 37 | }], 38 | '@babel/preset-react' 39 | ], 40 | plugins: [ 41 | ["@babel/plugin-proposal-class-properties"], 42 | ["@babel/transform-regenerator"], 43 | ["@babel/transform-runtime", { 44 | "regenerator": true 45 | }] 46 | ] 47 | } 48 | }, 49 | { 50 | test: /\.css$/, 51 | use: [ 52 | { 53 | loader: "css-loader" 54 | } 55 | ] 56 | }, 57 | ] 58 | }, 59 | externals: [webpackNodeExternals()] 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-js-hacker-news-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/cli": "^7.10.1", 7 | "@babel/core": "^7.10.2", 8 | "@babel/plugin-proposal-class-properties": "^7.10.1", 9 | "@babel/plugin-transform-async-to-generator": "^7.10.1", 10 | "@babel/plugin-transform-runtime": "^7.10.1", 11 | "@babel/polyfill": "^7.10.1", 12 | "@babel/preset-react": "^7.10.1", 13 | "@babel/runtime-corejs3": "^7.10.2", 14 | "@testing-library/jest-dom": "^4.2.4", 15 | "@testing-library/react": "^9.3.2", 16 | "@testing-library/user-event": "^7.1.2", 17 | "babel-loader": "^8.1.0", 18 | "babel-plugin-transform-runtime": "^6.23.0", 19 | "babel-preset-es2015": "^6.24.1", 20 | "babel-preset-stage-0": "^6.24.1", 21 | "body-parser": "^1.19.0", 22 | "chart.js": "^2.9.3", 23 | "css-loader": "^3.5.3", 24 | "express": "^4.17.1", 25 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 26 | "file-loader": "^6.0.0", 27 | "flatlist-react": "^1.4.2", 28 | "nodemon": "^2.0.4", 29 | "npm-run-all": "^4.1.5", 30 | "postcss-loader": "^3.0.0", 31 | "react": "^16.13.1", 32 | "react-chartkick": "^0.4.0", 33 | "react-dom": "^16.13.1", 34 | "react-redux": "^7.2.0", 35 | "react-router": "^5.2.0", 36 | "react-router-dom": "^5.2.0", 37 | "react-scripts": "3.4.1", 38 | "reactjs-localstorage": "^1.0.1", 39 | "redux": "^4.0.5", 40 | "redux-logger": "^3.0.6", 41 | "redux-thunk": "^2.3.0", 42 | "style-loader": "^1.2.1", 43 | "webpack": "^4.43.0", 44 | "webpack-cli": "^3.3.11", 45 | "webpack-node-externals": "^1.7.2" 46 | }, 47 | "scripts": { 48 | "start": "npm-run-all --parallel webpack:*", 49 | "webpack:client": "webpack --config webpack.client.js --watch", 50 | "webpack:server": "webpack --config webpack.server.js --watch", 51 | "webpack:start": "nodemon --watch build --exec node build/bundle.js", 52 | "build": "webpack --config webpack.client.js && webpack --config webpack.server.js" 53 | }, 54 | "eslintConfig": { 55 | "extends": "react-app" 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | 4 | import '@babel/polyfill'; 5 | import React from 'react'; 6 | import ReactDOMServer from 'react-dom/server'; 7 | import { StaticRouter } from 'react-router'; 8 | 9 | import App from '../client/App'; 10 | 11 | const app = express(); 12 | const PORT = process.env.PORT || 3000; 13 | 14 | app.use(bodyParser.json()); 15 | app.use(express.static('build/public')); 16 | 17 | app.get('*', (req, res) => { 18 | var title = "React Hacker News Clone"; 19 | var description = "The idea behind this project is to design the clone of hacker news website using react.js"; 20 | var thumb = "https://jayclouse.com/wp-content/uploads/2019/06/hacker_news-1000x525-1.jpg"; 21 | var favicon = "https://news.ycombinator.com/favicon.ico"; 22 | var link = ""; 23 | 24 | const context = {} 25 | 26 | const markup = ReactDOMServer.renderToString( 27 | 28 | 29 | 30 | ); 31 | 32 | const html = ` 33 | 34 | 35 | 36 | 37 | 38 | 39 | ${title} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
${markup}
60 | 61 | 62 | `; 63 | 64 | res.send(html); 65 | }) 66 | 67 | app.listen(PORT, () => { 68 | console.log(`Server listening on ${PORT}`); 69 | }) -------------------------------------------------------------------------------- /src/client/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | BASE_URL, 3 | ITMES, 4 | FETCH_NEWS_FEED, 5 | SET_UPVOTE_COUNT, 6 | NEWS_STORAGE_KEY, 7 | NEWS_FEED_SHOW, 8 | NEWS_FEED_HIDE 9 | } from './constants'; 10 | 11 | import 'regenerator-runtime/runtime'; 12 | 13 | export const dispatchNewsFeeds = data => ({ 14 | type: FETCH_NEWS_FEED, 15 | payload: data 16 | }); 17 | 18 | export const fetchNewsFeed = (start, end) => async (dispatch) => { 19 | 20 | console.log("fetchNewsFeed...." + start+" == "+end); 21 | 22 | var feeds = []; 23 | for (var i = start; i < end; i++) { 24 | await fetch(BASE_URL + ITMES + parseInt(i)) 25 | .then(response => response.json()) 26 | .then(data => { 27 | var results = JSON.parse(JSON.stringify(data)); 28 | 29 | var title = results.title; 30 | if (title != null) { 31 | var id = results.id; 32 | var author = results.author; 33 | var url = results.url; 34 | var timeStamp = results.created_at_i; 35 | var vote_count = results.points; 36 | var commentCount = results.children.length; 37 | 38 | var date = new Date(timeStamp * 1000); 39 | var posted_time = date.getMonth() + "/" + date.getDay() + "/" + date.getFullYear(); 40 | 41 | var storage_item = localStorage.getItem(NEWS_STORAGE_KEY + id); 42 | var parse_storage_item = JSON.parse(storage_item); 43 | 44 | var vote_storage_count = parse_storage_item != null ? parse_storage_item.vote_count : 0; 45 | var show_hide_status = (parse_storage_item !=null && parse_storage_item.hide && parse_storage_item.hide === NEWS_FEED_HIDE)? NEWS_FEED_HIDE: NEWS_FEED_SHOW 46 | 47 | if(show_hide_status === NEWS_FEED_SHOW){ 48 | var news_results = { 49 | "id": id, 50 | "title": title, 51 | "author": author, 52 | "time": posted_time, 53 | "url": url, 54 | "vote_count": vote_storage_count > vote_count ? vote_storage_count : vote_count, 55 | "comments": commentCount, 56 | "hide": show_hide_status 57 | } 58 | feeds.push(news_results) 59 | localStorage.setItem(NEWS_STORAGE_KEY + id, JSON.stringify(news_results)); 60 | dispatch(dispatchNewsFeeds(feeds)); 61 | } 62 | 63 | } 64 | }).catch(error => { 65 | console.log("Error Feed -- " + i + " == " + JSON.stringify(error)); 66 | }) 67 | } 68 | } 69 | 70 | export const dispatchVoteCount = count => ({ 71 | type: SET_UPVOTE_COUNT, 72 | payload: count 73 | }); 74 | 75 | export const setUpVoteCount = vote => dispatch => { 76 | 77 | console.log("Vote : setUpVoteCount Redux = " + JSON.stringify(vote)); 78 | dispatch(dispatchVoteCount(vote)); 79 | } 80 | 81 | export const hideNewsFeed = (feeds, id) => dispatch => { 82 | if(feeds != null) { 83 | var parseHide = JSON.parse(JSON.stringify(feeds)) 84 | if(parseHide !== undefined){ 85 | var news = parseHide.news 86 | let index = news.findIndex(el => el.id === id); 87 | var item = news[index]; 88 | news.splice(index, 1); 89 | 90 | var hide_results = { 91 | "id": item.id, 92 | "title": item.title, 93 | "author": item.author, 94 | "time": item.posted_time, 95 | "url": item.url, 96 | "vote_count": item.vote_count, 97 | "comments": item.comments, 98 | "hide": NEWS_FEED_HIDE 99 | } 100 | localStorage.setItem(NEWS_STORAGE_KEY + id, hide_results) 101 | dispatch(dispatchNewsFeeds(news)); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/client/css/index.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin: 0; 3 | font-family:Verdana, Geneva, sans-serif; 4 | } 5 | .parent-div-container { 6 | width: 100%; 7 | height: auto; 8 | display: grid; 9 | grid-template-columns: 5% 90% 5%; 10 | } 11 | .news-feed-container { 12 | width: 100%; 13 | height: auto; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | .news-feed-header { 18 | width: 100%; 19 | height: 1.5em; 20 | justify-content: center; 21 | background-color: #ff6600; 22 | } 23 | .desktop-table { 24 | display: block; 25 | } 26 | .mobile-table { 27 | display: none; 28 | } 29 | .header-label { 30 | font-size: 0.7rem; 31 | color: white; 32 | font-weight: bold; 33 | } 34 | .td-cloumn-width { 35 | width: 4.5em; 36 | height: 1.5em; 37 | } 38 | .td-header-cloumn-width { 39 | width: 4.8em; 40 | } 41 | .comments-span { 42 | font-size: 0.8rem; 43 | color: black; 44 | font-weight: 700; 45 | } 46 | .vote-span { 47 | font-size: 0.8rem; 48 | color: black; 49 | font-weight: 700; 50 | } 51 | .news-details-span { 52 | font-size: 0.9rem; 53 | color: black; 54 | } 55 | .up-arrow-icon { 56 | width: 0.5em; 57 | height: 0.5em; 58 | } 59 | .news-url-span { 60 | font-size: 0.65rem; 61 | color: #7a7979; 62 | margin-left: 1em; 63 | text-decoration: none; 64 | } 65 | .news-url-span:hover { 66 | font-size: 0.65rem; 67 | color: #7a7979; 68 | margin-left: 1em; 69 | text-decoration: underline; 70 | } 71 | .news-left-bracket { 72 | font-size: 0.65rem; 73 | color: #7a7979; 74 | margin-left: 1em; 75 | } 76 | .news-hide-label { 77 | font-size: 0.65rem; 78 | color: #000000; 79 | text-decoration: none; 80 | margin-left: 0.5em; 81 | } 82 | .news-right-bracket { 83 | font-size: 0.65rem; 84 | color: #7a7979; 85 | margin-left: 0.5em; 86 | } 87 | .news-author-span { 88 | font-size: 0.65rem; 89 | color: #000000; 90 | margin-left: 1em; 91 | } 92 | .news-time-span { 93 | font-size: 0.65rem; 94 | color: #7a7979; 95 | margin-left: 1em; 96 | } 97 | .news-feed-bottom-div { 98 | width: 100%; 99 | height: 0.3em; 100 | background-color: #ff6600; 101 | } 102 | .news-feed-line-chart-container { 103 | display: flex; 104 | flex-direction: row; 105 | justify-content: center; 106 | align-items: center; 107 | margin-top: 1em; 108 | } 109 | .line-chart-votes-label { 110 | font-size: 1.2rem; 111 | color: #000000; 112 | font-weight: bold; 113 | transform: rotate(-90deg); 114 | } 115 | .line-chart-id-div { 116 | width: 100%; 117 | display: flex; 118 | justify-content: center; 119 | align-items: center; 120 | } 121 | .line-chart-id-label { 122 | font-size: 1.2rem; 123 | color: #000000; 124 | font-weight: bold; 125 | } 126 | .pagination-container { 127 | width: 100%; 128 | height: 2.5em; 129 | display: grid; 130 | grid-template-columns: 80% 20%; 131 | } 132 | .pagination-btn-container { 133 | width: 95%; 134 | height: 2.5em; 135 | display: flex; 136 | justify-content: flex-end; 137 | align-items: center; 138 | } 139 | .prev-btn-label { 140 | font-size: 0.8rem; 141 | color: #ff6600; 142 | font-weight: bold; 143 | margin-right: 0.5em; 144 | text-decoration: none; 145 | } 146 | .next-btn-label { 147 | font-size: 0.8rem; 148 | color: #ff6600; 149 | font-weight: bold; 150 | margin-left: 0.5em; 151 | text-decoration: none; 152 | } 153 | .btn-separator-div { 154 | width: 0.15em; 155 | height: 1em; 156 | background-color: #ff6600; 157 | } 158 | @media only screen and (max-width: 1000px) { 159 | .desktop-table { 160 | display: none; 161 | } 162 | .parent-div-container { 163 | width: 100%; 164 | height: auto; 165 | display: grid; 166 | grid-template-columns: 100%; 167 | } 168 | .mobile-table { 169 | display: block; 170 | } 171 | .td-mobile-header-cloumn-width { 172 | width: 4em; 173 | height: 1.5em; 174 | } 175 | .mobile-header-label { 176 | font-size: 0.7rem; 177 | color: white; 178 | font-weight: bold; 179 | } 180 | .td-mobile-cloumn-width { 181 | width: 1.5em; 182 | } 183 | .mobile-up-arrow-icon { 184 | width: 0.5em; 185 | height: 0.5em; 186 | } 187 | .mobile-news-details-span { 188 | font-size: 0.8rem; 189 | color: black; 190 | } 191 | .mobile-news-url-span { 192 | font-size: 0.6rem; 193 | color: #7a7979; 194 | margin-left: 1em; 195 | text-decoration: none; 196 | } 197 | .mobile-news-url-span:hover { 198 | font-size: 0.6rem; 199 | color: #7a7979; 200 | margin-left: 1em; 201 | text-decoration: underline; 202 | } 203 | .mobile-news-footer-container { 204 | margin-top: 0.2em; 205 | } 206 | .mobile-news-by-span { 207 | font-size: 0.6rem; 208 | color: #7a7979; 209 | } 210 | .mobile-news-author-span { 211 | font-size: 0.6rem; 212 | color: #000000; 213 | margin-left: 1em; 214 | } 215 | .mobile-news-time-span { 216 | font-size: 0.6rem; 217 | color: #7a7979; 218 | margin-left: 1em; 219 | } 220 | .mobile-news-hide-label { 221 | font-size: 0.6rem; 222 | color: #000000; 223 | text-decoration: none; 224 | margin-left: 0.5em; 225 | } 226 | .mobile-news-left-bracket { 227 | font-size: 0.6rem; 228 | color: #7a7979; 229 | margin-left: 1em; 230 | } 231 | .mobile-news-right-bracket { 232 | font-size: 0.6rem; 233 | color: #7a7979; 234 | margin-left: 0.5em; 235 | } 236 | .mobile-news-comments { 237 | font-size: 0.6rem; 238 | color: #7a7979; 239 | margin-left: 1em; 240 | } 241 | .pagination-container { 242 | width: 100%; 243 | height: 2.5em; 244 | display: grid; 245 | grid-template-columns: 75% 25%; 246 | } 247 | .pagination-btn-container { 248 | width: 90%; 249 | height: 2.5em; 250 | display: flex; 251 | justify-content: flex-end; 252 | align-items: center; 253 | } 254 | .prev-btn-label { 255 | font-size: 0.8rem; 256 | color: #ff6600; 257 | font-weight: bold; 258 | margin-right: 0.5em; 259 | text-decoration: none; 260 | } 261 | .next-btn-label { 262 | font-size: 0.8rem; 263 | color: #ff6600; 264 | font-weight: bold; 265 | margin-left: 0.5em; 266 | text-decoration: none; 267 | } 268 | .btn-separator-div { 269 | width: 0.2em; 270 | height: 1em; 271 | background-color: #ff6600; 272 | } 273 | .line-chart-votes-label { 274 | font-size: 1rem; 275 | color: #000000; 276 | font-weight: bold; 277 | transform: rotate(-90deg); 278 | } 279 | .line-chart-id-div { 280 | width: 100%; 281 | display: flex; 282 | justify-content: center; 283 | align-items: center; 284 | } 285 | .line-chart-id-label { 286 | font-size: 1rem; 287 | color: #000000; 288 | font-weight: bold; 289 | } 290 | } -------------------------------------------------------------------------------- /src/client/components/HomeComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import "../css/index.css"; 3 | 4 | import { connect } from 'react-redux'; 5 | import FlatList from 'flatlist-react'; 6 | 7 | import { fetchNewsFeed, setUpVoteCount, hideNewsFeed } from '../actions'; 8 | import { NEWS_STORAGE_KEY, NEWS_FEED_SHOW, UP_ARROW_ICON } from '../actions/constants'; 9 | 10 | import { LineChart } from 'react-chartkick'; 11 | import 'chart.js' 12 | 13 | class HomeComponent extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | newsFeeds: [], 18 | startPage: 1, 19 | endPage: 31 20 | } 21 | } 22 | 23 | componentDidMount() { 24 | this.props.fetchNewsFeed(this.state.startPage, this.state.endPage); 25 | } 26 | 27 | getDomain(url) { 28 | if (url != undefined) { 29 | var domain = url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0]; 30 | return domain; 31 | } 32 | } 33 | 34 | setUpVoteCount = (item) => { 35 | var id = item.id; 36 | var title = item.title; 37 | var author = item.author; 38 | var posted_time = item.posted_time; 39 | var url = item.url; 40 | var comments = item.comments; 41 | 42 | var storage_item = localStorage.getItem(NEWS_STORAGE_KEY + id); 43 | var parse_storage_item = JSON.parse(storage_item); 44 | 45 | var parse_storage_title = parse_storage_item.title; 46 | var parse_storage_vote_count = parse_storage_item.vote_count; 47 | 48 | if (title == parse_storage_title) { 49 | var new_vote_count = parseInt(parse_storage_vote_count) + 1; 50 | var news_results = { 51 | "id": id, 52 | "title": title, 53 | "author": author, 54 | "time": posted_time, 55 | "url": url, 56 | "vote_count": new_vote_count, 57 | "comments": comments, 58 | "hide": NEWS_FEED_SHOW 59 | } 60 | localStorage.setItem(NEWS_STORAGE_KEY + id, JSON.stringify(news_results)); 61 | this.props.setUpVoteCount(news_results) 62 | } 63 | } 64 | 65 | renderVoteCount = (item) => { 66 | var id = item.id; 67 | var votes; 68 | var vote = JSON.parse(JSON.stringify(this.props.vote_count)).vote_count; 69 | 70 | if (id == vote.id) { 71 | votes = vote.vote_count; 72 | } else { 73 | var storage_item = localStorage.getItem(NEWS_STORAGE_KEY + id); 74 | var parse_storage_item = JSON.parse(storage_item); 75 | var vote_count = parse_storage_item != null ? parse_storage_item.vote_count : 0; 76 | votes = vote_count; 77 | } 78 | return ( 79 | {votes} 80 | ); 81 | } 82 | 83 | hideNewsFeed = (item) => { 84 | this.props.hideNewsFeed(this.props.news, item.id); 85 | } 86 | 87 | renderItem = (item, index) => { 88 | return ( 89 |
90 | 91 | 92 | 93 | 94 | 97 | 107 | 108 |
{item.comments}{this.renderVoteCount(item)} 95 | 96 | 98 | {item.title} 99 | ({this.getDomain(item.url)}) 100 | by 101 | {item.author} 102 | {item.time} 103 | [ 104 | hide 105 | ] 106 |
109 | 110 | 111 | 112 | 115 | 128 | 129 |
{this.renderVoteCount(item)} 113 | 114 | 116 | {item.title} 117 | ({this.getDomain(item.url)}) 118 |
119 | by 120 | {item.author} 121 | {item.time} 122 | [ 123 | hide 124 | ] 125 | {item.comments} comments 126 |
127 |
130 |
131 | ) 132 | } 133 | 134 | renderLineChart = results => { 135 | var statistics = []; 136 | 137 | for (var i = 0; i < results.length; i++) { 138 | var id = results[i].id.toString(); 139 | 140 | var storage_item = localStorage.getItem(NEWS_STORAGE_KEY + id); 141 | var parse_storage_item = JSON.parse(storage_item); 142 | var vote_count = parse_storage_item != null ? parse_storage_item.vote_count : 0; 143 | 144 | var votes = vote_count; 145 | 146 | var item = { [id]: votes } 147 | statistics.push(item); 148 | } 149 | var graph_item = JSON.stringify(statistics); 150 | graph_item = graph_item.replace(/[{}]/g, ""); 151 | graph_item = graph_item.replace('[', '{'); 152 | graph_item = graph_item.replace(']', '}'); 153 | 154 | var data = JSON.parse(graph_item); 155 | 156 | return ( 157 | 160 | ) 161 | } 162 | 163 | previousPage() { 164 | var endPage = this.state.endPage > 61 ? this.state.endPage - 30 : 31; 165 | var startPage = this.state.startPage > 31 ? this.state.startPage - 30 : 1; 166 | 167 | this.setState({ 168 | endPage: endPage, 169 | startPage: startPage 170 | }) 171 | this.props.fetchNewsFeed(startPage, endPage); 172 | } 173 | 174 | nextPage() { 175 | var endPage = this.state.endPage + 30; 176 | var startPage = this.state.startPage + 30; 177 | 178 | this.setState({ 179 | endPage: endPage, 180 | startPage: startPage 181 | }) 182 | this.props.fetchNewsFeed(startPage, endPage); 183 | } 184 | 185 | render() { 186 | var results = JSON.parse(JSON.stringify(this.props.news)).news; 187 | return ( 188 |
189 |
190 |
191 |
192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 |
CommentsVote CountUpVoteNews Details
200 | 201 | 202 | 203 | 204 | 205 | 206 |
Vote CountUpVoteNews Details
207 |
208 |
209 | {results.length > 0 ? 210 | 217 | : null} 218 |
219 |
220 |
221 |
222 | Previous 223 |
224 | Next 225 |
226 |
227 |
228 |
229 |
230 |

Votes

231 |
232 | {this.renderLineChart(results)} 233 |
234 |
235 |

ID

236 |
237 |
238 |
239 |
240 |
241 | ); 242 | } 243 | } 244 | 245 | const stateProps = state => ({ 246 | news: state.news, 247 | vote_count: state.vote_count 248 | }); 249 | 250 | const dispatchProps = dispatch => ({ 251 | fetchNewsFeed: (start, end) => dispatch(fetchNewsFeed(start, end)), 252 | setUpVoteCount: item => dispatch(setUpVoteCount(item)), 253 | hideNewsFeed: (feeds, id) => dispatch(hideNewsFeed(feeds, id)) 254 | }); 255 | 256 | export default connect(stateProps, dispatchProps)(HomeComponent); --------------------------------------------------------------------------------