├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── actions.js ├── components │ ├── Navbar.js │ └── Post.js ├── configureStore.js ├── constants.js ├── containers │ ├── App.js │ ├── Posts.js │ └── Root.js ├── index.js ├── reducers.js └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | yarn-lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frontend Developer Forum 2 | 3 | Browse popular discussions and posts about popular Frontend threads such as _JavaScript, React, Web Development, Open Source, Programming_ and so on. 4 | 5 | ![Giphy](https://media.giphy.com/media/4NtPrzG6Wc45V2m2oA/giphy.gif) 6 | [Live Demo](https://piyushbhangale.github.io/Reddit-frontend-react/) 7 | 8 | Uses Reddit JSON API as data source, Redux for state management and React to wrap up all up into a WebApp. Semantic UI and Remarkable added as helpers. 9 | 10 | ## Development 11 | 12 | Install all the required packages/dependencies using npm install 13 | 14 | ### `npm install` 15 | 16 | Serve the app to browser 17 | 18 | ### `npm start` 19 | 20 | ## Contributing 21 | 22 | Feel free to open issues and pull requests! 23 | 24 | ## To do : 25 | - [ ] Make the navbar responsive 26 | - [ ] Add routes 27 | - [ ] Sorting Option. Sort as Hot,New,Controversial,Rising 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-dev-talk", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "cross-fetch": "^2.1.0", 7 | "react": "^16.2.0", 8 | "react-dom": "^16.2.0", 9 | "react-redux": "^5.0.7", 10 | "react-scripts": "1.1.1", 11 | "redux": "^3.7.2", 12 | "redux-logger": "^3.0.6", 13 | "redux-thunk": "^2.2.0", 14 | "remarkable": "^1.7.1", 15 | "semantic-ui-css": "^2.3.1", 16 | "semantic-ui-react": "^0.79.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "predeploy": "npm run build", 22 | "deploy": "gh-pages -d build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "homepage": "https://piyushbhangale.github.io/Reddit-frontend-react/", 27 | "description": "Browse popular discussions and posts about popular Frontend threads such as JavaScript, React, Web Development, Open Source, Programming and so on.", 28 | "main": "index.js", 29 | "author": "", 30 | "license": "ISC", 31 | "devDependencies": { 32 | "gh-pages": "^2.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiyushBhangale/Reddit-frontend-react/6e033a725e4d303efac2e661f61e823226a4fc78/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Frontend Developer Forum 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SELECT_SUBREDDIT, 3 | INVALIDATE_SUBREDDIT, 4 | REQUEST_POSTS, 5 | RECEIVE_POSTS 6 | } from './constants' 7 | 8 | // Import fetch API in case of browser compatiblity issues 9 | import fetch from 'cross-fetch' 10 | 11 | // Action creators for all functionality 12 | // Basically wraps up type and subreddit selected into an object 13 | 14 | export const selectSubreddit = (subreddit) => ( 15 | { 16 | type: SELECT_SUBREDDIT, 17 | subreddit 18 | } 19 | ) 20 | 21 | export const invalidateSubreddit = (subreddit) => ( 22 | { 23 | type: INVALIDATE_SUBREDDIT, 24 | subreddit 25 | } 26 | ) 27 | 28 | const requestPosts = (subreddit) => ( 29 | { 30 | type: REQUEST_POSTS, 31 | subreddit 32 | } 33 | ) 34 | 35 | const recievePosts = (subreddit, json) => ( 36 | { 37 | type: RECEIVE_POSTS, 38 | subreddit, 39 | posts: json.data.children.map(child => child.data), 40 | recievedAt: Date.now() 41 | } 42 | ) 43 | 44 | // Helper function to fetch JSON data from Reddit API 45 | const fetchPosts = (subreddit) => { 46 | return (dispatch) => { 47 | // Dispatch requestPosts action just before attempting to fetching data 48 | dispatch(requestPosts(subreddit)) 49 | 50 | // Fetch data and dispatch recievePosts if no errors 51 | // Catch shouldn't be used to handle errors as 52 | return fetch(`https://www.reddit.com/r/${subreddit}.json`) 53 | .then( 54 | response => response.json(), 55 | error => console.log('An error occured', error) 56 | ) 57 | .then( 58 | json => dispatch(recievePosts(subreddit, json)) 59 | ) 60 | } 61 | } 62 | 63 | const shouldFetchPosts = (state, subreddit) => { 64 | // Get posts by subreddit from state 65 | const posts = state.postsBySubreddit[subreddit] 66 | 67 | // Should fetch posts if no posts exists and not fetching already 68 | // If posts exists and not fetching then depends on if posts have invalidated 69 | if (!posts) { 70 | return true 71 | } else if (posts.isFetching) { 72 | return false 73 | } else { 74 | return posts.didInvalidate 75 | } 76 | } 77 | 78 | export const fetchPostsIfNeeded = (subreddit) => { 79 | return (dispatch, getState) => { 80 | if (shouldFetchPosts(getState(), subreddit)) { 81 | return dispatch(fetchPosts(subreddit)) 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Menu, Icon } from "semantic-ui-react"; 3 | 4 | const Navbar = ({ selectedSub, handleMenuChange, handleRefreshClick }) => ( 5 |
6 | 7 | 12 | 17 | 22 | 27 | 32 | 37 | 42 | 43 | handleRefreshClick()}> 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | ); 53 | 54 | export default Navbar; 55 | -------------------------------------------------------------------------------- /src/components/Post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Image } from 'semantic-ui-react' 3 | import Remarkable from 'remarkable' 4 | 5 | const getMarkup = (text) => { 6 | if (text) { 7 | const md = new Remarkable() 8 | return { __html: md.render(text)} 9 | } else { 10 | return { __html: ``} 11 | } 12 | } 13 | 14 | const notImage = [ 'self', 'default' ] 15 | 16 | const Post = ({ url, title, author, selftext, thumbnail }) => ( 17 | 18 | 19 | { !notImage.includes(thumbnail) && } 20 | {title} 21 | by {author} 22 | 23 | 24 | 25 | ) 26 | 27 | export default Post -------------------------------------------------------------------------------- /src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import { createLogger } from 'redux-logger' 4 | 5 | import rootReducer from './reducers' 6 | 7 | const loggerMiddleware = createLogger() 8 | 9 | export default function configureStore(preloadedState) { 10 | return createStore( 11 | rootReducer, 12 | preloadedState, 13 | applyMiddleware( 14 | thunkMiddleware, 15 | loggerMiddleware 16 | ) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // Using constants prevents errors when working with names 2 | 3 | export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT' 4 | export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT' 5 | export const REQUEST_POSTS = 'REQUEST_POSTS' 6 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | // Import actions 5 | import { 6 | selectSubreddit, 7 | fetchPostsIfNeeded, 8 | invalidateSubreddit 9 | } from '../actions' 10 | 11 | // Import components 12 | import Navbar from '../components/Navbar' 13 | import Posts from './Posts' 14 | 15 | import { Dimmer, Loader } from 'semantic-ui-react' 16 | 17 | 18 | class App extends Component { 19 | constructor(props) { 20 | super(props) 21 | 22 | this.handleMenuChange = this.handleMenuChange.bind(this) 23 | this.handleRefreshClick = this.handleRefreshClick.bind(this) 24 | } 25 | 26 | componentDidMount() { 27 | const { dispatch, selectedSubreddit } = this.props 28 | dispatch(fetchPostsIfNeeded(selectedSubreddit)) 29 | } 30 | 31 | handleMenuChange(e, { name }) { 32 | const sub = name.replace(/ /g, '') 33 | this.props.dispatch(selectSubreddit(sub)) 34 | this.props.dispatch(fetchPostsIfNeeded(sub)) 35 | } 36 | 37 | handleRefreshClick() { 38 | const { dispatch, selectedSubreddit } = this.props 39 | dispatch(invalidateSubreddit(selectedSubreddit)) 40 | dispatch(fetchPostsIfNeeded(selectedSubreddit)) 41 | } 42 | 43 | render() { 44 | const { selectedSubreddit, posts, isFetching } = this.props 45 | 46 | return ( 47 |
48 | 49 | Loading 50 | 51 | 56 | {posts.length > 0 && 57 | 58 | } 59 |
60 | ) 61 | } 62 | } 63 | 64 | function mapStateToProps(state) { 65 | const { selectedSubreddit, postsBySubreddit } = state 66 | const { isFetching, items: posts } = postsBySubreddit[selectedSubreddit] || { isFetching: true, items: [] } 67 | 68 | return { 69 | selectedSubreddit, 70 | posts, 71 | isFetching 72 | } 73 | } 74 | 75 | export default connect(mapStateToProps)(App) -------------------------------------------------------------------------------- /src/containers/Posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Post from '../components/Post' 4 | 5 | const Posts = ({ posts }) => ( 6 |
7 | {posts.map((post, i) => )} 8 |
9 | ) 10 | 11 | export default Posts -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'react-redux' 3 | 4 | import configureStore from '../configureStore' 5 | import App from './App' 6 | 7 | // Configure redux store 8 | const store = configureStore() 9 | 10 | // Base of the app 11 | export default class Root extends Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import Root from './containers/Root' 4 | 5 | import 'semantic-ui-css/semantic.min.css'; 6 | 7 | import registerServiceWorker from './registerServiceWorker'; 8 | 9 | render( 10 | , 11 | document.getElementById('root') 12 | ) 13 | registerServiceWorker(); 14 | -------------------------------------------------------------------------------- /src/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SELECT_SUBREDDIT, 3 | INVALIDATE_SUBREDDIT, 4 | REQUEST_POSTS, 5 | RECEIVE_POSTS 6 | } from './constants' 7 | 8 | import { combineReducers } from 'redux' 9 | 10 | // Reducer handling the currently selected subreddit 11 | const selectedSubreddit = (state = 'reactjs', action) => { 12 | switch (action.type) { 13 | case SELECT_SUBREDDIT: 14 | return action.subreddit 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | // Helper function for handling posts 21 | const posts = ( 22 | state = { 23 | isFetching: false, 24 | didInvalidate: false, 25 | items: [] 26 | }, 27 | action 28 | ) => { 29 | switch (action.type) { 30 | case INVALIDATE_SUBREDDIT: 31 | return { ...state, 32 | didInvalidate: true 33 | } 34 | case REQUEST_POSTS: 35 | return { ...state, 36 | isFetching: true, 37 | didInvalidate: false 38 | } 39 | case RECEIVE_POSTS: 40 | return { ...state, 41 | isFetching: false, 42 | didInvalidate: false, 43 | items: action.posts, 44 | lastUpdated: action.receivedAt 45 | } 46 | default: 47 | return state 48 | 49 | } 50 | } 51 | 52 | // Reducer handling posts by subreddit 53 | const postsBySubreddit = ( 54 | state = {}, 55 | action 56 | ) => { 57 | switch(action.type) { 58 | case INVALIDATE_SUBREDDIT: 59 | case RECEIVE_POSTS: 60 | case REQUEST_POSTS: 61 | return { ...state, 62 | [action.subreddit]: posts(state[action.subreddit], action) 63 | } 64 | default: 65 | return state 66 | } 67 | } 68 | 69 | // Combine reducers 70 | const rootReducer = combineReducers({ 71 | postsBySubreddit, 72 | selectedSubreddit 73 | }) 74 | 75 | export default rootReducer -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------