├── 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 | | {item.comments} |
93 | {this.renderVoteCount(item)} |
94 |
95 |
96 | |
97 |
98 | {item.title}
99 | ({this.getDomain(item.url)})
100 | by
101 | {item.author}
102 | {item.time}
103 | [
104 | hide
105 | ]
106 | |
107 |
108 |
109 |
110 |
111 | | {this.renderVoteCount(item)} |
112 |
113 |
114 | |
115 |
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 | |
128 |
129 |
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 | | Comments |
195 | Vote Count |
196 | UpVote |
197 | News Details |
198 |
199 |
200 |
201 |
202 | | Vote Count |
203 | UpVote |
204 | News Details |
205 |
206 |
207 |
208 |
209 | {results.length > 0 ?
210 |
217 | : null}
218 |
219 |
227 |
228 |
229 |
232 | {this.renderLineChart(results)}
233 |
234 |
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);
--------------------------------------------------------------------------------