├── src ├── styles │ ├── components │ │ ├── About.js │ │ ├── generalStyles.js │ │ ├── notFoundPageStyles.js │ │ ├── menubarStyles.js │ │ ├── pageStyles.js │ │ ├── animations.js │ │ ├── windowStyles.js │ │ ├── settingStyles.js │ │ ├── commentStyles.js │ │ ├── postStyles.js │ │ ├── headerStyles.js │ │ ├── tabStyles.js │ │ ├── exploreStyles.js │ │ ├── notificationbarStyles.js │ │ ├── inboxStyles.js │ │ ├── followingtabStyles.js │ │ ├── redditorStyles.js │ │ ├── userStyles.js │ │ ├── viewPostStyles.js │ │ ├── profileStyles.js │ │ └── searchbarStyles.js │ └── base │ │ ├── colors.js │ │ └── base.js ├── setupTests.js ├── components │ ├── About.js │ ├── NotFound.js │ ├── OptTab.js │ ├── App.js │ ├── Trophies.js │ ├── Settings.js │ ├── Dashboard.js │ ├── Comments.js │ ├── Menubar.js │ ├── RenderSubs.js │ ├── FollowingTab.js │ ├── Redditor.js │ ├── FetchToken.js │ ├── ContentBox.js │ ├── PostCard.js │ ├── Inbox.js │ ├── Notificationbar.js │ ├── User.js │ ├── Explore.js │ ├── Notifications.js │ ├── Header.js │ ├── Searchbar.js │ ├── Profile.js │ ├── SubredditProfile.js │ ├── LoadPost.js │ └── ViewPost.js ├── store │ └── index.js ├── reducers │ ├── index.js │ ├── settingsReducer.js │ ├── authenticationReducer.js │ ├── subredditReducer.js │ └── postReducer.js ├── router │ ├── PrivateRoute.js │ └── AppRouter.js ├── actions │ ├── action-types.js │ ├── api-calls.js │ └── index.js ├── index.js └── serviceWorker.js ├── public ├── favicon.png ├── robots.txt ├── images │ ├── dm.png │ ├── home.png │ ├── icon.png │ ├── like.png │ ├── chakra.png │ ├── comment.png │ ├── compas.png │ ├── growth.png │ ├── trophy.png │ ├── upvote.png │ ├── Astronaut.png │ ├── dm-active.png │ ├── dm-light.png │ ├── home-light.png │ ├── left-arrow.png │ ├── like-light.png │ ├── settings.png │ ├── compas-light.png │ ├── defaulticon.png │ ├── trophy-light.png │ ├── comment-light.png │ ├── downvote-active.png │ ├── settings-light.png │ ├── space-discovery.png │ ├── upvote-active.png │ └── left-arrow-light.png ├── loaders │ └── spinner.gif ├── index.html └── svg │ └── send.svg ├── .gitignore ├── server └── server.js ├── README.md └── package.json /src/styles/components/About.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-componenets"; 2 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/images/dm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/dm.png -------------------------------------------------------------------------------- /public/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/home.png -------------------------------------------------------------------------------- /public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/icon.png -------------------------------------------------------------------------------- /public/images/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/like.png -------------------------------------------------------------------------------- /public/images/chakra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/chakra.png -------------------------------------------------------------------------------- /public/images/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/comment.png -------------------------------------------------------------------------------- /public/images/compas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/compas.png -------------------------------------------------------------------------------- /public/images/growth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/growth.png -------------------------------------------------------------------------------- /public/images/trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/trophy.png -------------------------------------------------------------------------------- /public/images/upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/upvote.png -------------------------------------------------------------------------------- /public/images/Astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/Astronaut.png -------------------------------------------------------------------------------- /public/images/dm-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/dm-active.png -------------------------------------------------------------------------------- /public/images/dm-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/dm-light.png -------------------------------------------------------------------------------- /public/images/home-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/home-light.png -------------------------------------------------------------------------------- /public/images/left-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/left-arrow.png -------------------------------------------------------------------------------- /public/images/like-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/like-light.png -------------------------------------------------------------------------------- /public/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/settings.png -------------------------------------------------------------------------------- /public/loaders/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/loaders/spinner.gif -------------------------------------------------------------------------------- /public/images/compas-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/compas-light.png -------------------------------------------------------------------------------- /public/images/defaulticon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/defaulticon.png -------------------------------------------------------------------------------- /public/images/trophy-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/trophy-light.png -------------------------------------------------------------------------------- /public/images/comment-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/comment-light.png -------------------------------------------------------------------------------- /public/images/downvote-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/downvote-active.png -------------------------------------------------------------------------------- /public/images/settings-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/settings-light.png -------------------------------------------------------------------------------- /public/images/space-discovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/space-discovery.png -------------------------------------------------------------------------------- /public/images/upvote-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/upvote-active.png -------------------------------------------------------------------------------- /public/images/left-arrow-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaesharma/redditfornormies/HEAD/public/images/left-arrow-light.png -------------------------------------------------------------------------------- /src/styles/components/generalStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledDiv = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/styles/base/colors.js: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | gray1: "#f2f2f2", 3 | gray2: "#e2e2e2", 4 | gray3: "#b2b2b2", 5 | gray4: "#aaa", 6 | gray5: "#a2a2a2", 7 | gray6: "#999", 8 | gray7: "#929292", 9 | red: "#d9310b", 10 | blue: "#0095f6", 11 | dark: "black", 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/About.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyledCanvas } from "../styles/components/tabStyles"; 3 | import { StyledHeader } from "../styles/components/headerStyles"; 4 | 5 | const About = () => ( 6 | 7 | About Page 8 | 9 | ); 10 | 11 | export default About; 12 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import rootReducer from "../reducers"; 4 | 5 | const composeEnhancers = compose; 6 | 7 | const store = createStore( 8 | rootReducer, 9 | composeEnhancers(applyMiddleware(thunk)) 10 | ); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /.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 | 25 | 26 | .env.development -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import postReducer from "./postReducer"; 2 | import subredditReducer from "./subredditReducer"; 3 | import authenticationReducer from "./authenticationReducer"; 4 | import settingsReducer from "./settingsReducer"; 5 | import { combineReducers } from "redux"; 6 | 7 | const rootReducer = combineReducers({ 8 | authenticationReducer, 9 | postReducer, 10 | subredditReducer, 11 | settingsReducer, 12 | }); 13 | 14 | export default rootReducer; 15 | -------------------------------------------------------------------------------- /src/styles/components/notFoundPageStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledPageCover = styled.div` 4 | width: 100vw; 5 | height: 100vh; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | text-align: center; 11 | & > h1 { 12 | margin: 0; 13 | } 14 | & > h2 { 15 | color: gray; 16 | } 17 | `; 18 | 19 | export const StyledImg = styled.img` 20 | width: 25rem; 21 | height: 25rem; 22 | `; 23 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const port=process.env.PORT || 3000; 4 | const app = express(); 5 | const root = require('path').join(__dirname, '..', 'build'); 6 | const cors = require('cors'); 7 | 8 | app.use(cors()); 9 | app.options('*', cors()); 10 | app.use(express.static(root)); 11 | app.get('*', function(req, res) { 12 | res.sendFile(path.join(root,'index.html')); 13 | }); 14 | app.listen(port,()=>{ 15 | console.log('server is up'); 16 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The demonstration of this web-app is deployed at https://redditfornormies.herokuapp.com 2 | 3 | 1. Run this app locally by running `npm install` in project root directory to install dependencies, 4 | 5 | 2. Run `npm run build` to build react scripts. 6 | 7 | 3. Choose one of the following according to your environment:- 8 | 9 | a. `npm run devserver` to run local development server. 10 | 11 | b. `npm run start` to run production server. 12 | 13 | Feel free to PR for additional features & bug fixes. :D -------------------------------------------------------------------------------- /src/reducers/settingsReducer.js: -------------------------------------------------------------------------------- 1 | import { NIGHTMODE } from "../actions/action-types"; 2 | 3 | const initialState = { 4 | nightmode: false, 5 | }; 6 | 7 | const settingsReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case NIGHTMODE: 10 | window.localStorage.setItem("nightMode", action.payload.nightmode); 11 | return { 12 | ...state, 13 | nightmode: action.payload.nightmode, 14 | }; 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export default settingsReducer; 21 | -------------------------------------------------------------------------------- /src/styles/components/menubarStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledMenubar = styled.div` 4 | display: none; 5 | bottom: 0%; 6 | width: 100%; 7 | position: fixed; 8 | background: ${({ theme }) => theme.bg}; 9 | justify-content: space-around; 10 | height: 2.8rem; 11 | align-items: center; 12 | border: none; 13 | border-top: 1px ${({ theme }) => theme.themeborder} solid; 14 | z-index: 6; 15 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 16 | display: flex; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header"; 3 | import { 4 | StyledPageCover, 5 | StyledImg, 6 | } from "../styles/components/notFoundPageStyles"; 7 | 8 | const NotFound = () => { 9 | return ( 10 |
11 |
12 | 13 | 14 |

404

15 |

16 | The page you are looking for doesn't exist or has been 17 | moved. 18 |

19 |
20 |
21 | ); 22 | }; 23 | 24 | export default NotFound; 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | reddit for normies 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/styles/base/base.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | const GlobalStyles = createGlobalStyle` 4 | *{ 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | color: ${(props) => props.theme.color}; 10 | background: ${(props) => props.theme.bodybg}; 11 | margin: 0; 12 | font-family: 'Nunito Sans', sans-serif; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | a{ 17 | color: ${({ theme }) => theme.colors.gray5}; 18 | cursor: pointer; 19 | } 20 | a:visited{ 21 | color: #06205c; 22 | } 23 | iframe{ 24 | border: none; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | `; 29 | 30 | export default GlobalStyles; 31 | -------------------------------------------------------------------------------- /src/router/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Route, Redirect } from "react-router-dom"; 4 | import Header from "../components/Header"; 5 | 6 | export const PrivateRoute = ({ 7 | isAuthenticated, 8 | component: Component, 9 | ...rest 10 | }) => ( 11 | 14 | isAuthenticated ? ( 15 |
16 | 17 |
18 | ) : ( 19 | 20 | ) 21 | } 22 | /> 23 | ); 24 | 25 | const mapStateToProps = (state) => ({ 26 | isAuthenticated: state.authenticationReducer.authenticated, 27 | }); 28 | 29 | export default connect(mapStateToProps)(PrivateRoute); 30 | -------------------------------------------------------------------------------- /src/actions/action-types.js: -------------------------------------------------------------------------------- 1 | export const ADD_SUBREDDIT = "ADD_SUBREDDIT"; 2 | export const ADD_SUBREDDITS = "ADD_SUBREDDITS"; 3 | export const DESELECT_SUBREDDIT = "DESELECT_SUBREDDIT"; 4 | export const DOWNVOTE = "DOWNVOTE"; 5 | export const HOMEPAGE = "HOMEPAGE"; 6 | export const HOMEPAGEPOST_UPVOTE = "HOMEPAGEPOST_UPVOTE"; 7 | export const HOMEPAGEPOST_DOWNVOTE = "HOMEPAGEPOST_DOWNVOTE"; 8 | export const LOAD_HOMEPAGE = "LOAD_HOMEPAGE"; 9 | export const LOGGED_IN = "LOGGED_IN"; 10 | export const LOGOUT = "LOGOUT"; 11 | export const NIGHTMODE = "NIGHTMODE"; 12 | export const REQUEST_POSTS = "REQUEST_POSTS"; 13 | export const RECEIVE_POSTS = "RECEIVE_POSTS"; 14 | export const REMOVE_SUBREDDIT = "REMOVE_SUBREDDIT"; 15 | export const SELECT_SUBREDDIT = "SELECT_SUBREDDIT"; 16 | export const UPDATE_TOKENS = "UPDATE_TOKENS"; 17 | export const UPVOTE = "UPVOTE"; 18 | -------------------------------------------------------------------------------- /src/components/OptTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | StyledCanvas, 5 | StyledTab, 6 | StyledTabEntry, 7 | } from "../styles/components/tabStyles"; 8 | 9 | class OptTab extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | render() { 14 | return ( 15 | this.props.toggleOpt(e)} 18 | > 19 | 20 | this.props.editComment()}> 21 | Edit 22 | 23 | this.props.del_comment()} 27 | > 28 | Delete 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default OptTab; 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./components/App"; 4 | import { Provider } from "react-redux"; 5 | import store from "./store"; 6 | import { loggedIn, nightMode } from "./actions/index"; 7 | import { refreshData } from "./actions/api-calls"; 8 | import { addSubreddit } from "./actions"; 9 | 10 | window.localStorage.removeItem("subreddits"); 11 | let { 12 | data = "{}", 13 | nightMode: nightModeState = false, 14 | subreddits = "[]", 15 | } = window.localStorage; 16 | data = JSON.parse(data); 17 | 18 | nightModeState = nightModeState === "true"; 19 | store.dispatch(nightMode(nightModeState)); 20 | 21 | if (data.hasOwnProperty("access_token")) { 22 | refreshData(data.access_token, data.refresh_token).then((data) => { 23 | store.dispatch(loggedIn(data)); 24 | }); 25 | } else { 26 | JSON.parse(subreddits).map((sub) => { 27 | store.dispatch(addSubreddit(sub)); 28 | }); 29 | } 30 | 31 | ReactDOM.render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /src/styles/components/pageStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { FadeInLeft } from "./animations"; 3 | 4 | export const StyledPageCanvas = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | position: fixed; 8 | width: 100%; 9 | height: 100%; 10 | color: ${({ theme }) => theme.color} 11 | overflow-x: hidden; 12 | background: ${({ theme }) => theme.bodybg} 13 | transition: all .3s ease-in-out; 14 | animation: ${FadeInLeft} .3s ease-in-out; 15 | `; 16 | 17 | export const StyledPageHeading = styled.div` 18 | display: flex; 19 | flex-direction: row; 20 | position: fixed; 21 | width: 100%; 22 | z-index: 11; 23 | background: ${({ theme }) => theme.bg}; 24 | justify-content: flex-start; 25 | align-items: center; 26 | padding-left: 1rem; 27 | height: 4rem; 28 | border: none; 29 | border-bottom: 1px ${({ theme }) => theme.themeborder} solid; 30 | `; 31 | 32 | export const Styledbody = styled.div` 33 | display: flex; 34 | flex-direction: column; 35 | background: ${({ theme }) => theme.bg}; 36 | color: ${({ theme }) => theme.color} 37 | width: 100%; 38 | height: 100%; 39 | overflow-y: scroll; 40 | height: 100%; 41 | `; 42 | -------------------------------------------------------------------------------- /public/svg/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 12 | 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 | -------------------------------------------------------------------------------- /src/styles/components/animations.js: -------------------------------------------------------------------------------- 1 | import { keyframes } from "styled-components"; 2 | 3 | export const pop = keyframes` 4 | 0% { opacity: 0; -webkit-transform: scale(1); } 5 | 80% { opacity: 1; -webkit-transform: scale(1.1); } 6 | 101% { opacity: 1; -webkit-transform: scale(1); } 7 | `; 8 | 9 | export const popin = keyframes` 10 | 0% { opacity: 0; -webkit-transform: scale(0.5); } 11 | 80% { opacity: 0; -webkit-transform: scale(1.2); } 12 | 100% { opacity: 1; -webkit-transform: scale(1); } 13 | `; 14 | 15 | export const fadein = keyframes` 16 | 0% {opacity: 0;} 17 | 100% {opacity: 1;} 18 | `; 19 | 20 | export const FadeInUp = keyframes` 21 | 0% { 22 | opacity: 0; 23 | transform: translate3d(0, 100%, 0); 24 | } 25 | 26 | 100% { 27 | opacity: 1; 28 | transform: none; 29 | } 30 | `; 31 | 32 | export const FadeInRight = keyframes` 33 | 0% { 34 | opacity: 0; 35 | transform: translate3d(100%, 0, 0); 36 | } 37 | 38 | 100% { 39 | opacity: 1; 40 | transform: none; 41 | } 42 | `; 43 | 44 | export const FadeInLeft = keyframes` 45 | 0% { 46 | opacity: 0; 47 | transform: translate3d(-100%, 0, 0); 48 | } 49 | 50 | 100% { 51 | opacity: 1; 52 | transform: none; 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeProvider } from "styled-components"; 3 | import AppRouter from "../router/AppRouter"; 4 | import { connect } from "react-redux"; 5 | import GlobalStyles from "../styles/base/base"; 6 | import { colors } from "../styles/base/colors"; 7 | 8 | const lighttheme = { 9 | name: "light", 10 | bodybg: colors.gray1, 11 | border: colors.gray2, 12 | themeborder: colors.gray2, 13 | bg: "white", 14 | color: "black", 15 | breakpoint: "42rem", 16 | colors: colors, 17 | }; 18 | 19 | const darktheme = { 20 | name: "dark", 21 | bodybg: "#121212", 22 | border: "#363537", 23 | themeborder: colors.gray7, 24 | bg: "#363537", 25 | color: colors.gray4, 26 | breakpoint: "42rem", 27 | colors: colors, 28 | }; 29 | 30 | class App extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | } 34 | render() { 35 | return ( 36 | 39 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | 46 | const mapStateToProps = (state) => { 47 | return { 48 | nightmode: state.settingsReducer.nightmode, 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(App); 53 | -------------------------------------------------------------------------------- /src/styles/components/windowStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { FadeInLeft } from "./animations"; 3 | 4 | export const StyledMainWindow = styled.div` 5 | background: none; 6 | margin: 1.8rem 0; 7 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 8 | margin: 0; 9 | } 10 | transition: all 0.4s ease-in-out; 11 | `; 12 | 13 | export const StyledActiveWindow = styled.div` 14 | background: none; 15 | animation: ${FadeInLeft} 0.3s; 16 | transition: all 0.4s ease-in-out; 17 | `; 18 | 19 | export const StyledWindowContainer = styled.div` 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: space-around; 23 | height: 100vh; 24 | animation: ${FadeInLeft} 0.3s; 25 | transition: all 0.4s ease-in-out; 26 | `; 27 | 28 | export const StyledContentBox = styled.div` 29 | display: flex; 30 | flex-direction: column; 31 | width: 50%; 32 | margin-top: 2rem; 33 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 34 | margin: 2rem 0; 35 | width: 100%; 36 | } 37 | &::-webkit-scrollbar { 38 | width: 2px; 39 | } 40 | transition: all 0.4s ease-in-out; 41 | `; 42 | 43 | export const StyledInfoBox = styled.div` 44 | align-self: top; 45 | background: none; 46 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 47 | display: none; 48 | } 49 | transition: all 0.4s ease-in-out; 50 | `; 51 | -------------------------------------------------------------------------------- /src/components/Trophies.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getTrophies } from "../actions/api-calls"; 3 | import { 4 | StyledCanvas, 5 | StyledTab, 6 | StyledTabHead, 7 | StyledTabValues, 8 | StyledTabBody, 9 | } from "../styles/components/tabStyles"; 10 | import { StyledBtnImg } from "../styles/components/headerStyles"; 11 | 12 | class Trophies extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | trophies: {}, 17 | }; 18 | } 19 | handleCloseTab = (e) => { 20 | if (e.target.getAttribute("type") === "canvas") { 21 | this.props.closeTab("trophies"); 22 | } 23 | }; 24 | async componentDidMount() { 25 | this.setState({ trophies: await getTrophies(this.props.token) }); 26 | } 27 | render() { 28 | return ( 29 | this.handleCloseTab(e)}> 30 | 31 | Trophies 32 | 33 | {Object.entries(this.state.trophies).map( 34 | ([key, value], index) => { 35 | return ( 36 | 37 | 38 |

{value.name}

39 |
40 | ); 41 | } 42 | )} 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | export default Trophies; 51 | -------------------------------------------------------------------------------- /src/components/Settings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { nightMode } from "../actions"; 4 | import { 5 | StyledSwitch, 6 | StyledLabelText, 7 | } from "../styles/components/settingStyles"; 8 | import { StyledCanvas, StyledTab } from "../styles/components/tabStyles"; 9 | 10 | class Settings extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | checked: true, 15 | }; 16 | } 17 | handleCloseSettings = (e) => { 18 | if (e.target.getAttribute("type") === "canvas") { 19 | this.props.closeTab("settings"); 20 | } 21 | }; 22 | render() { 23 | return ( 24 | this.handleCloseSettings(e)} 27 | > 28 | 29 |
30 | 31 | this.props.dispatch(nightMode())} 35 | /> 36 | 37 | 38 | Night Mode 39 |
40 |
41 |
42 | ); 43 | } 44 | } 45 | 46 | const mapStateToProps = (state) => { 47 | return { 48 | nightmode: state.settingsReducer.nightmode, 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(Settings); 53 | -------------------------------------------------------------------------------- /src/styles/components/settingStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { fadein, popin } from "./animations"; 3 | 4 | export const StyledSwitch = styled.label` 5 | position: relative; 6 | display: inline-block; 7 | float: left; 8 | width: 38px; 9 | height: 18px; 10 | & > input { 11 | opacity: 0; 12 | width: 0; 13 | height: 0; 14 | } 15 | & > .slider { 16 | position: absolute; 17 | cursor: pointer; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | bottom: 0; 22 | background-color: #ccc; 23 | -webkit-transition: 0.4s; 24 | transition: 0.4s; 25 | } 26 | & > .slider, 27 | & > .round { 28 | border-radius: 34px; 29 | } 30 | & > .slider:before { 31 | position: absolute; 32 | content: ""; 33 | height: 16px; 34 | width: 16px; 35 | top: 1px; 36 | left: 2px; 37 | background-color: white; 38 | -webkit-transition: 0.4s; 39 | transition: 0.4s; 40 | } 41 | input:checked + .slider { 42 | background-color: #2196f3; 43 | } 44 | 45 | input:focus + .slider { 46 | box-shadow: 0 0 1px #2196f3; 47 | } 48 | 49 | input:checked + .slider:before { 50 | -webkit-transform: translateX(18px); 51 | -ms-transform: translateX(18px); 52 | transform: translateX(18px); 53 | } 54 | 55 | .slider.round:before { 56 | border-radius: 50%; 57 | } 58 | `; 59 | 60 | export const StyledLabelText = styled.div` 61 | float: left; 62 | margin: 0 1rem; 63 | font-size: 0.7rem; 64 | font-size: 1rem; 65 | `; 66 | -------------------------------------------------------------------------------- /src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Redditor from "./Redditor"; 3 | import Header from "./Header"; 4 | import ContentBox from "./ContentBox"; 5 | import Menubar from "./Menubar"; 6 | import User from "./User"; 7 | import { connect } from "react-redux"; 8 | import { getHomePage } from "../actions/index"; 9 | import { 10 | StyledWindowContainer, 11 | StyledContentBox, 12 | StyledInfoBox, 13 | } from "../styles/components/windowStyles"; 14 | 15 | class Dashboard extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.textInputRef = React.createRef(); 19 | this.focusTextInput = this.focusTextInput.bind(this); 20 | } 21 | 22 | focusTextInput = () => { 23 | this.textInputRef.current.focus(); 24 | }; 25 | 26 | componentDidMount() { 27 | this.props.dispatch(getHomePage()); 28 | } 29 | render() { 30 | return ( 31 |
32 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default connect()(Dashboard); 54 | -------------------------------------------------------------------------------- /src/components/Comments.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | StyledComment, 4 | StyledCommentUps, 5 | StyledLinkBtn, 6 | } from "../styles/components/commentStyles"; 7 | 8 | const Comments = (props) => ( 9 |
10 | {!props.count ? ( 11 | 12 | view comments 13 | 14 | ) : ( 15 |
16 | {props.comments.slice(0, props.count).map((data, index) => { 17 | if (!data.hasOwnProperty("comment")) return; 18 | return ( 19 | 20 |
21 | {data.author}  22 | {data.comment} 23 |
24 |
25 | {data.author === props.username && ( 26 | 30 | props.toggleOpt(e, data.name, index) 31 | } 32 | > 33 | ... 34 | 35 | )} 36 | +{data.ups} 37 |
38 |
39 | ); 40 | })} 41 | {props.count < props.comments.length ? ( 42 | 43 | view more 44 | 45 | ) : ( 46 | 47 | hide comments 48 | 49 | )} 50 |
51 | )} 52 |
53 | ); 54 | 55 | export default Comments; 56 | -------------------------------------------------------------------------------- /src/router/AppRouter.js: -------------------------------------------------------------------------------- 1 | import { Router, Switch, Route } from "react-router-dom"; 2 | import React from "react"; 3 | import { createBrowserHistory } from "history"; 4 | import Dashboard from "../components/Dashboard"; 5 | import Explore from "../components/Explore"; 6 | import FetchToken from "../components/FetchToken"; 7 | import Inbox from "../components/Inbox"; 8 | import Notifications from "../components/Notifications"; 9 | import SubredditProfile from "../components/SubredditProfile"; 10 | import Profile from "../components/Profile"; 11 | import About from "../components/About"; 12 | import NotFound from "../components/NotFound"; 13 | import PrivateRoute from "./PrivateRoute"; 14 | 15 | export const history = createBrowserHistory(); 16 | 17 | const AppRouter = () => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | ); 38 | 39 | export default AppRouter; 40 | -------------------------------------------------------------------------------- /src/components/Menubar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { deselect_subreddit } from "../actions"; 5 | import { StyledBtnImg } from "../styles/components/headerStyles"; 6 | import { StyledMenubar } from "../styles/components/menubarStyles"; 7 | 8 | const Menubar = (props) => ( 9 | 10 | 11 | props.dispatch(deselect_subreddit())} 13 | type="home" 14 | src={ 15 | props.nightmode 16 | ? "/images/home-light.png" 17 | : "/images/home.png" 18 | } 19 | alt="home" 20 | /> 21 | 22 | 23 | 32 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | const mapStateToProps = (state) => { 51 | return { 52 | icon_img: state.authenticationReducer.user.icon_img, 53 | nightmode: state.settingsReducer.nightmode, 54 | }; 55 | }; 56 | 57 | export default connect(mapStateToProps)(Menubar); 58 | -------------------------------------------------------------------------------- /src/styles/components/commentStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledComment = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | & > div > .opt { 8 | display: none; 9 | cursor: pointer; 10 | } 11 | &:hover { 12 | & > div > .opt { 13 | display: block; 14 | } 15 | } 16 | transition: all 0.2s ease; 17 | `; 18 | 19 | export const StyledCommentUps = styled.b` 20 | float: right; 21 | `; 22 | 23 | export const StyledLinkBtn = styled.p` 24 | color: gray; 25 | cursor: pointer; 26 | `; 27 | 28 | export const StyledCommentBox = styled.div` 29 | display: flex; 30 | flex-direction: row; 31 | align-self: flex-start; 32 | margin-left: -1rem; 33 | width: 100%; 34 | scroll-behavior: smooth; 35 | transition: all 0.2s ease; 36 | `; 37 | 38 | export const StyledCommentInput = styled.input` 39 | outline: none; 40 | width: 100%; 41 | height: 2rem; 42 | padding: 0.1rem 0.8rem; 43 | background: ${({ theme }) => 44 | theme.name === "light" ? theme.colors.gray2 : theme.colors.gray4}; 45 | border: none; 46 | &:placeholder { 47 | color: ${({ theme }) => 48 | theme.name === "light" ? theme.colors.gray1 : theme.colors.gray7}; 49 | } 50 | `; 51 | 52 | export const StyledPostButton = styled.button` 53 | border: none; 54 | outline: none; 55 | background: transparent; 56 | cursor: pointer; 57 | margin-right: -1rem; 58 | &:disabled { 59 | color: ${({ theme }) => theme.colors.gray3}; 60 | } 61 | &:enabled { 62 | color: ${({ theme }) => theme.colors.blue}; 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /src/reducers/authenticationReducer.js: -------------------------------------------------------------------------------- 1 | import { LOGGED_IN, LOGOUT, UPDATE_TOKENS } from "../actions/action-types"; 2 | 3 | const initialState = { 4 | authenticated: false, 5 | access_token: undefined, 6 | expires_in: 3600, 7 | refresh_token: undefined, 8 | scope: 9 | "account creddits edit privatemessages mysubreddits history identity submit subscribe vote", 10 | user: { 11 | icon_img: "/images/defaulticon.png", 12 | id: "user101", 13 | name: "reddituser", 14 | num_friends: undefined, 15 | pref_nightmode: false, 16 | total_karma: undefined, 17 | }, 18 | }; 19 | 20 | const authenticationReducer = (state = initialState, action) => { 21 | switch (action.type) { 22 | case LOGGED_IN: { 23 | const { 24 | access_token, 25 | expires_in, 26 | refresh_token, 27 | scope, 28 | } = action.payload.data; 29 | const { 30 | icon_img, 31 | id = "randomid", 32 | name = "reddituser", 33 | num_friends, 34 | pref_nightmode = false, 35 | total_karma, 36 | } = action.payload.userInfo; 37 | return { 38 | authenticated: true, 39 | access_token, 40 | expires_in, 41 | refresh_token, 42 | scope, 43 | user: { 44 | icon_img, 45 | id, 46 | name, 47 | num_friends, 48 | pref_nightmode, 49 | total_karma, 50 | }, 51 | }; 52 | } 53 | case LOGOUT: { 54 | return { 55 | ...initialState, 56 | }; 57 | } 58 | case UPDATE_TOKENS: { 59 | return { 60 | ...state, 61 | access_token: action.payload.access_token, 62 | }; 63 | } 64 | default: 65 | return state; 66 | } 67 | }; 68 | 69 | export default authenticationReducer; 70 | -------------------------------------------------------------------------------- /src/styles/components/postStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledPostContainer = styled.div` 4 | display: flex; 5 | background: ${({ theme }) => theme.bg}; 6 | border: ${({ theme }) => theme.border} 1px solid; 7 | border-radius: 3px; 8 | height: 100%; 9 | flex-direction: column; 10 | margin-bottom: 4rem; 11 | overflow-wrap: break-word; 12 | text-align: justify; 13 | width: 100.4%; 14 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 15 | margin: 0; 16 | padding-bottom: 1.8rem; 17 | border: none; 18 | } 19 | `; 20 | 21 | export const StyledPostTop = styled.div` 22 | border: none; 23 | border-bottom: 1px ${({ theme }) => theme.themeborder} solid; 24 | font-weight: 600; 25 | height: 4rem; 26 | padding-top: 0.3rem; 27 | & > .pt__username { 28 | color: ${({ theme }) => theme.color}; 29 | display: flex; 30 | text-decoration: none; 31 | padding-top: 1rem; 32 | } 33 | `; 34 | export const StyledPostMid = styled.div` 35 | min-height: 14rem; 36 | & > .text-post { 37 | color: ${({ theme }) => 38 | (theme.name === "dark" && "white") || "inherit"}; 39 | padding: 0 1.5rem; 40 | min-height: 4rem; 41 | max-height: 22rem; 42 | overflow-y: scroll; 43 | } 44 | & > .text-post > a { 45 | color: ${({ theme }) => 46 | (theme.name === "dark" && "white") || "inherit"}; 47 | } 48 | & > .text-post::-webkit-scrollbar { 49 | width: 8px; 50 | } 51 | transition: all 0.1s ease-in-out; 52 | `; 53 | 54 | export const StyledPostBottom = styled.div` 55 | border: none; 56 | padding: 1rem; 57 | `; 58 | 59 | export const StyledInlineButtons = styled.div` 60 | display: flex; 61 | flex-direction: row; 62 | `; 63 | -------------------------------------------------------------------------------- /src/components/RenderSubs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { addSubreddit, select_subreddit, fetchPosts } from "../actions"; 4 | import { 5 | StyledSubContainer, 6 | StyledSub, 7 | StyledSubIcon, 8 | } from "../styles/components/redditorStyles"; 9 | import { StyledAddBtn } from "../styles/components/searchbarStyles"; 10 | 11 | class RenderSubs extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.switchSub = this.switchSub.bind(this); 15 | } 16 | 17 | switchSub = (e) => { 18 | this.props.dispatch(select_subreddit(e.target.name)); 19 | this.props.dispatch(fetchPosts(e.target.name)); 20 | }; 21 | 22 | render() { 23 | const that = this; 24 | return ( 25 | 26 | 27 | + 28 | 29 | {Object.entries(this.props.subs) 30 | .reverse() 31 | .map(([subName, sub], index) => { 32 | return ( 33 | 34 | 39 |

46 | 47 | r/{subName.substring(0, 8)} 48 | {subName.length > 8 && ..} 49 | 50 |

51 |
52 | ); 53 | }, that)} 54 |
55 | ); 56 | } 57 | } 58 | 59 | export default connect()(RenderSubs); 60 | -------------------------------------------------------------------------------- /src/components/FollowingTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { removeSubreddit } from "../actions"; 4 | import { 5 | StyledFollowingTab, 6 | StyledFTab, 7 | StyledFTabHeader, 8 | StyledFTabText, 9 | StyledFTabCloseBtn, 10 | StyledFollowings, 11 | StyledFollowing, 12 | StyledUnfollowBtn, 13 | } from "../styles/components/followingtabStyles"; 14 | 15 | const Followings = ({ followings, closeTab, dispatch }) => { 16 | return ( 17 | 18 | 19 | 20 | Following 21 | closeTab("followingTab")} 23 | > 24 | + 25 | 26 | 27 | 28 | {Object.entries(followings).map( 29 | ([subname, details], index) => { 30 | return ( 31 | 32 | 36 | 37 | {subname.length > 12 ? ( 38 |

{subname.substring(0, 12)}...

39 | ) : ( 40 |

{subname}

41 | )} 42 |
43 |
44 | { 45 | 47 | dispatch( 48 | removeSubreddit(subname) 49 | ) 50 | } 51 | > 52 | Unfollow 53 | 54 | } 55 |
56 |
57 | ); 58 | } 59 | )} 60 |
61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Followings; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redditfornormies", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.21.1", 10 | "cors": "^2.8.5", 11 | "cross-env": "^7.0.2", 12 | "dotenv": "^8.2.0", 13 | "express": "^4.17.1", 14 | "history": "^4.10.1", 15 | "install": "^0.13.0", 16 | "js-cookie": "^2.2.1", 17 | "lodash": "^4.17.19", 18 | "lodash.debounce": "^4.0.8", 19 | "moment": "^2.27.0", 20 | "node-sass": "^4.14.1", 21 | "nodemon": "^2.0.4", 22 | "npm": "^7.4.3", 23 | "query-string": "^6.13.1", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1", 26 | "react-redux": "^7.2.0", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "^4.0.1", 29 | "redux": "^4.0.5", 30 | "redux-cookies-middleware": "^0.2.1", 31 | "redux-thunk": "^2.3.0", 32 | "styled-components": "^5.1.1", 33 | "superagent": "^5.3.1", 34 | "websocket-extensions": "^0.1.4" 35 | }, 36 | "scripts": { 37 | "start": "node server/server.js", 38 | "build": "react-scripts build", 39 | "test": "react-scripts test", 40 | "eject": "react-scripts eject", 41 | "devserver": "react-scripts start", 42 | "heroku-postbuild": "npm run build" 43 | }, 44 | "engines": { 45 | "node": "12.13.1" 46 | }, 47 | "eslintConfig": { 48 | "extends": "react-app" 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | }, 62 | "devDependencies": { 63 | "prettier": "^2.0.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Redditor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import RenderSubs from "./RenderSubs"; 4 | import { 5 | StyledRedditor, 6 | StyledScrollBtn, 7 | } from "../styles/components/redditorStyles"; 8 | 9 | class Redditor extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | scrollpos: 0, 14 | scrollWidth: undefined, 15 | }; 16 | this.redditor = React.createRef(); 17 | } 18 | 19 | scrollLeft = () => { 20 | this.redditor.current.scrollLeft -= 500; 21 | this.setState({ 22 | scrollpos: this.redditor.current.scrollLeft - 500, 23 | scrollWidth: this.redditor.current.scrollWidth, 24 | }); 25 | }; 26 | 27 | scrollRight = () => { 28 | this.redditor.current.scrollLeft += 500; 29 | this.setState({ 30 | scrollpos: this.redditor.current.scrollLeft + 500, 31 | scrollWidth: this.redditor.current.scrollWidth, 32 | }); 33 | }; 34 | 35 | componentDidMount() { 36 | this.setState({ scrollWidth: this.redditor.current.scrollWidth }); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | {this.state.scrollpos > 50 && ( 43 | 44 | > 45 | 46 | )} 47 | 48 | 53 | 54 | {this.state.scrollpos < this.state.scrollWidth - 450 && ( 55 | 59 | > 60 | 61 | )} 62 |
63 | ); 64 | } 65 | } 66 | 67 | const mapStateToProps = (state, props) => { 68 | return { 69 | subreddits: state.postReducer.data, 70 | activeSub: state.subredditReducer.activeSubreddit, 71 | }; 72 | }; 73 | 74 | export default connect(mapStateToProps)(Redditor); 75 | -------------------------------------------------------------------------------- /src/reducers/subredditReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_SUBREDDIT, 3 | ADD_SUBREDDITS, 4 | SELECT_SUBREDDIT, 5 | DESELECT_SUBREDDIT, 6 | LOGGED_IN, 7 | LOGOUT, 8 | REMOVE_SUBREDDIT, 9 | } from "../actions/action-types"; 10 | 11 | const initialState = { 12 | activeSubreddit: undefined, 13 | subreddits: [], 14 | }; 15 | 16 | const subredditReducer = (state = initialState, action) => { 17 | switch (action.type) { 18 | case ADD_SUBREDDIT: 19 | if (state.subreddits.includes(action.payload.subreddit)) 20 | return state; 21 | let subreddits = 22 | state.subreddits !== undefined 23 | ? [...state.subreddits, action.payload.subreddit] 24 | : [action.payload.subreddit]; 25 | if (!action.payload.authenticated) { 26 | window.localStorage.setItem( 27 | "subreddits", 28 | JSON.stringify(subreddits) 29 | ); 30 | } 31 | return { 32 | ...state, 33 | subreddits, 34 | }; 35 | case ADD_SUBREDDITS: 36 | return { 37 | ...state, 38 | subreddits: [...state.subreddits, ...action.payload.subs], 39 | }; 40 | case SELECT_SUBREDDIT: 41 | return { 42 | ...state, 43 | activeSubreddit: action.payload.subreddit, 44 | }; 45 | case DESELECT_SUBREDDIT: 46 | return { 47 | ...state, 48 | activeSubreddit: undefined, 49 | }; 50 | case REMOVE_SUBREDDIT: { 51 | const subreddits = state.subreddits.filter( 52 | (sub) => sub !== action.payload.subreddit 53 | ); 54 | window.localStorage.setItem( 55 | "subreddits", 56 | JSON.stringify(subreddits) 57 | ); 58 | const activeSubreddit = 59 | state.activeSubreddit === action.payload.subreddit 60 | ? undefined 61 | : state.activeSubreddit; 62 | return { 63 | activeSubreddit, 64 | subreddits, 65 | }; 66 | } 67 | case LOGGED_IN: 68 | return { 69 | activeSubreddit: undefined, 70 | subreddits: [], 71 | }; 72 | case LOGOUT: { 73 | window.location.reload(false); 74 | return { 75 | ...state, 76 | subreddits, 77 | }; 78 | } 79 | default: 80 | return state; 81 | } 82 | }; 83 | 84 | export default subredditReducer; 85 | -------------------------------------------------------------------------------- /src/styles/components/headerStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledHeader = styled.div` 4 | background: ${(props) => props.theme.bg}; 5 | border: 2px ${({ theme }) => theme.border} solid; 6 | display: flex; 7 | flex-direction: row; 8 | height: 3.5rem; 9 | align-items: center; 10 | justify-content: space-between; 11 | position: fixed; 12 | top: 0; 13 | width: 100%; 14 | z-index: 10; 15 | @media (max-width: ${(props) => props.theme.breakpoint}) { 16 | height: 2.5rem; 17 | border: none; 18 | } 19 | & > .header-text { 20 | font-family: "Grand Hotel", cursive; 21 | font-size: 2.2rem; 22 | font-weight: 550; 23 | color: ${({ theme }) => theme.color}; 24 | text-decoration: none; 25 | margin-left: 12%; 26 | @media (max-width: ${(props) => props.theme.breakpoint}) { 27 | margin: 0; 28 | margin-left: 38%; 29 | font-size: 1.6rem; 30 | } 31 | } 32 | `; 33 | 34 | export const StyledHeaderBtns = styled.div` 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: space-around; 38 | height: 2rem; 39 | padding-right: 14%; 40 | @media (max-width: ${(props) => props.theme.breakpoint}) { 41 | display: none; 42 | } 43 | `; 44 | 45 | export const StyledBtnImg = styled.img` 46 | border: none; 47 | border-radius: 50%; 48 | cursor: pointer; 49 | height: 1.6rem; 50 | width: 1.6rem; 51 | margin: 0 .6rem; 52 | transition: all .1s ease; 53 | ${(props) => { 54 | switch (props.type) { 55 | case "inbox": 56 | return "transform: scale(1.2);margin: 0 .4rem;"; 57 | case "compas": 58 | return "height: 1.5rem;"; 59 | case "like": 60 | return "height: 1.55rem;"; 61 | case "upvote": 62 | return "margin: 0;padding: 0;height: 1.4rem;"; 63 | case "downvote": 64 | return "margin: 0;padding: 0;height: 1.4rem;transform: rotate(180deg);"; 65 | case "headerInbox": 66 | return "display: none;position: fixed;transform: rotate(15deg);width: 2rem;height: 2rem;z-index: 11;right:1%;"; 67 | case "arrow": 68 | return "display: none;position: absolute;left: 0;top: 2%;transform: scale(1);"; 69 | case "comment": 70 | return "width: 1rem;height: 1rem;"; 71 | } 72 | }} 73 | @media(max-width: ${({ theme }) => theme.breakpoint}){ 74 | display: block; 75 | } 76 | `; 77 | -------------------------------------------------------------------------------- /src/styles/components/tabStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { fadein, popin } from "./animations"; 3 | 4 | export const StyledCanvas = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | position: fixed; 9 | background: rgba(0, 0, 0, 0.4); 10 | width: 100vw; 11 | height: 100vh; 12 | top: 0; 13 | left: 0; 14 | z-index: 11; 15 | transition: all 0.1s ease-in-out; 16 | animation: ${fadein} 0.1s; 17 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 18 | height: 100%; 19 | width: 100%; 20 | } 21 | `; 22 | 23 | export const StyledTab = styled.div` 24 | background: ${({ theme }) => theme.bg}; 25 | display: flex; 26 | flex-direction: column; 27 | align-items: flex-start; 28 | min-width: 16rem; 29 | max-width: 16rem; 30 | min-height: ${(props) => (props.height && props.height) || "22rem"}; 31 | max-height: 22rem; 32 | padding: 1rem; 33 | border: none; 34 | border-radius: 6px; 35 | z-index: 12; 36 | animation: ${popin} 0.3s; 37 | ${(props) => { 38 | switch (props.type) { 39 | case "opt": 40 | return "justify-content: center;align-items: center;"; 41 | } 42 | }} 43 | `; 44 | 45 | export const StyledTabHead = styled.div` 46 | width: 100%; 47 | display: flex; 48 | justify-content: center; 49 | align-self: center; 50 | border-bottom: 1px solid ${({ theme }) => theme.colors.gray7}; 51 | font-weight: 600; 52 | padding: 0.1rem 0; 53 | `; 54 | 55 | export const StyledTabValues = styled.div` 56 | display: flex; 57 | flex-direction: row; 58 | align-items: center; 59 | justify-content: flex-start; 60 | border-bottom: 1px solid ${({ theme }) => theme.colors.gray5}; 61 | min-width: 100%; 62 | max-height: 3rem; 63 | flex-basis: 100%; 64 | text-align: center; 65 | text-overflow: elipsis; 66 | overflow-wrap: wrap; 67 | animation: ${fadein} 0.1s; 68 | `; 69 | 70 | export const StyledTabBody = styled.div` 71 | display: flex; 72 | flex-direction: column; 73 | width: 100%; 74 | min-height: 100%; 75 | overflow-y: scroll; 76 | &::-webkit-scrollbar { 77 | width: 0px; 78 | } 79 | `; 80 | 81 | export const StyledTabEntry = styled.p` 82 | display: flex; 83 | flex-direction: row; 84 | align-items: center; 85 | justify-content: center; 86 | cursor: pointer; 87 | color: ${(props) => (props.color && props.color) || props.theme.color}; 88 | `; 89 | -------------------------------------------------------------------------------- /src/styles/components/exploreStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { popin, FadeInRight } from "./animations"; 3 | 4 | export const StyledExplorer = styled.div` 5 | display: flex; 6 | flex-flow: row wrap; 7 | float: left; 8 | justify-content: flex-start; 9 | margin: 5rem 8rem; 10 | width: 80%; 11 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 12 | width: 100%; 13 | margin: 0; 14 | } 15 | transition: all 0.1s ease-in-out; 16 | animation: ${FadeInRight} 0.1s ease; 17 | `; 18 | 19 | export const StyledExplorePost = styled.div` 20 | background-color: white; 21 | border: 1px solid ${({ theme }) => theme.border}; 22 | display: flex; 23 | flex-direction: column; 24 | flex-grow: 4; 25 | flex-shrink: 1; 26 | height: 18rem; 27 | min-height: 15rem; 28 | margin: 0.5rem; 29 | max-width: 15.8rem; 30 | min-width: 15.8rem; 31 | overflow: hidden; 32 | position: relative; 33 | cursor: pointer; 34 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 35 | width: 8.6rem; 36 | min-width: 8.6rem; 37 | min-height: 8rem; 38 | height: 12rem; 39 | margin: 0.1rem; 40 | } 41 | animation: ${popin} 1s ease-in-out; 42 | `; 43 | 44 | export const StyledExplorePostImg = styled.img` 45 | background-size: cover; 46 | background: transparent 47 | url("https://static.wixstatic.com/media/d63215_a6ccc92841c74c6d835a02dac366b158~mv2.gif") 48 | center no-repeat; 49 | height: 100%; 50 | width: 100%; 51 | &:hover { 52 | filter: brightness(70%); 53 | transition: filter 0.1s ease-in-out; 54 | } 55 | `; 56 | 57 | export const StyledExploreTextPost = styled.div` 58 | height: 100%; 59 | padding: 1rem; 60 | text-align: left; 61 | background: ${({ theme }) => theme.bg}; 62 | color: ${({ theme }) => theme.color}; 63 | word-wrap: break-word; 64 | &:hover { 65 | background: ${({ theme }) => 66 | (theme.name === "light" && "rgba(0,0,0,.2)") || 67 | "rgba(54, 53, 55,.9)"}; 68 | height: 100%; 69 | transition: background 0.1s ease-in-out; 70 | } 71 | `; 72 | 73 | export const StyledHoverDisplay = styled.h2` 74 | align-self: center; 75 | color: white; 76 | text-shadow: 1px ${({ theme }) => theme.colors.gray2}; 77 | top: 40%; 78 | position: absolute; 79 | z-index: 5; 80 | `; 81 | 82 | export const StyledUpvoteIcon = styled.img` 83 | height: 1.5rem; 84 | width: 1.2rem; 85 | z-index: 7; 86 | `; 87 | -------------------------------------------------------------------------------- /src/styles/components/notificationbarStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledNotificationBarContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | position: fixed; 7 | background: ${({ theme }) => 8 | (theme.name === "light" && "white") || theme.bg}; 9 | top: 4rem; 10 | border-radius: 6px; 11 | right: 12%; 12 | width: 40%; 13 | min-height: 6rem; 14 | max-height: 20rem; 15 | overflow-x: hidden; 16 | overflow-y: scroll; 17 | z-index: 3; 18 | ${(props) => { 19 | if (props.theme.name === "light") { 20 | return "box-shadow: 4px 4px 1rem gray;border: 1px ${({theme})=>theme.color} solid;"; 21 | } else if (props.theme.name === "dark") { 22 | return "border: 1px ${({theme})=>theme.colors.gray7} solid;"; 23 | } 24 | }} 25 | `; 26 | 27 | export const StyledNotification = styled.div` 28 | width: 100%; 29 | min-height: 5.4rem; 30 | background: ${({ theme }) => theme.bg}; 31 | border-bottom: 1px solid ${({ theme }) => theme.themeborder}; 32 | padding: 0 1rem; 33 | overflow: hidden; 34 | cursor: pointer; 35 | display: flex; 36 | flex-direction: column; 37 | `; 38 | 39 | export const StyledNFooter = styled.div` 40 | display: flex; 41 | flex-direction: row; 42 | font-size: 12px; 43 | align-items: flex-end; 44 | justify-content: space-between; 45 | & > div { 46 | display: flex; 47 | flex-direction: row; 48 | margin-bottom: 0.8rem; 49 | } 50 | `; 51 | export const StyledNotificationBar = styled.div` 52 | &::before { 53 | position: fixed; 54 | right: 17.8%; 55 | top: 2.85rem; 56 | width: 0; 57 | height: 0; 58 | margin-top: -10px; 59 | border-top: 15px solid transparent; 60 | border-left: 15px solid transparent; 61 | border-right: 15px solid transparent; 62 | border-bottom: 15px solid 63 | ${({ theme }) => (theme.name === "light" && "white") || theme.bg}; 64 | z-index: 99; 65 | content: ""; 66 | } 67 | &::after { 68 | position: fixed; 69 | top: 2.85rem; 70 | right: 17.8%; 71 | width: 0; 72 | height: 0; 73 | filter: blur(2px); 74 | margin-top: -12px; 75 | border-top: 15px solid transparent; 76 | border-left: 15px solid transparent; 77 | border-right: 15px solid transparent; 78 | border-bottom: 13px solid 79 | ${({ theme }) => 80 | (theme.name === "light" && theme.colors.gray3) || theme.bodybg}; 81 | z-index: 98; 82 | content: ""; 83 | } 84 | `; 85 | -------------------------------------------------------------------------------- /src/components/FetchToken.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header"; 3 | import { Redirect } from "react-router-dom"; 4 | import { connect } from "react-redux"; 5 | import { loggedIn } from "../actions"; 6 | import queryString from "query-string"; 7 | import { 8 | StyledPageCover, 9 | StyledImg, 10 | } from "../styles/components/notFoundPageStyles"; 11 | 12 | class FetchToken extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | loading: true, 17 | error: undefined, 18 | }; 19 | this.setAccessToken = this.setAccessToken.bind(this); 20 | } 21 | setAccessToken() { 22 | const params = queryString.parse(this.props.location.search); 23 | const code = params.code; 24 | const clientID = process.env.REACT_APP_CLIENTID; 25 | const clientSecret = process.env.REACT_APP_CLIENTSECRET; 26 | const redirectUri = process.env.REACT_APP_REDIRECTURI; 27 | if (!code) return null; 28 | const encode = btoa( 29 | `${process.env.REACT_APP_CLIENTID}:${process.env.REACT_APP_CLIENTSECRET}` 30 | ); 31 | const redditTokens = fetch( 32 | "https://www.reddit.com/api/v1/access_token", 33 | { 34 | method: "POST", 35 | body: `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&duration=permanent`, 36 | headers: { 37 | "Content-Type": "application/x-www-form-urlencoded", 38 | Authorization: `basic ${encode}`, 39 | }, 40 | } 41 | ) 42 | .then((res) => res.json()) 43 | .then((data) => { 44 | if (data.hasOwnProperty("error")) 45 | this.setState({ error: data.error, loading: false }); 46 | if (data.hasOwnProperty("access_token")) { 47 | this.setState({ loading: false }); 48 | this.props.dispatch(loggedIn(data)); 49 | this.props.history.push("/"); 50 | } 51 | }) 52 | .catch((error) => { 53 | this.setState({ error, loading: false }); 54 | }); 55 | } 56 | componentDidMount() { 57 | this.setAccessToken(); 58 | } 59 | render() { 60 | return ( 61 |
62 | {this.state.loading && ( 63 | 64 | 65 |

Loading...

66 |
67 | )} 68 | {!this.state.loading && this.state.error && ( 69 |
70 | 71 | 72 |

Somthing went wrong

73 |
74 |
75 | )} 76 |
77 | ); 78 | } 79 | } 80 | 81 | export default connect()(FetchToken); 82 | -------------------------------------------------------------------------------- /src/styles/components/inboxStyles.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import { FadeInRight } from "./animations"; 3 | 4 | export const StyledDM = styled.div` 5 | display: flex; 6 | flex-direction: row; 7 | position: fixed; 8 | top: 12%; 9 | justify-content: center; 10 | margin: 0 15%; 11 | background: ${({ theme }) => theme.bg}; 12 | overflow: hidden; 13 | width: 70%; 14 | height: 85%; 15 | border: 1px solid ${({ theme }) => theme.themeborder}; 16 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 17 | positon: default; 18 | border: none; 19 | width: 100%; 20 | height: 100%; 21 | margin: 0; 22 | padding: 0; 23 | top: 0%; 24 | z-index: 12; 25 | } 26 | transition: all 0.1s ease; 27 | animation: ${FadeInRight} 0.3s ease-in-out; 28 | `; 29 | 30 | export const StyledDMUsers = styled.div` 31 | width: 50%; 32 | float: left; 33 | border: none; 34 | border-right: 1px solid ${({ theme }) => theme.themeborder}; 35 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 36 | width: 100%; 37 | } 38 | `; 39 | 40 | export const StyledDMHead = styled.div` 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | border-bottom: 1px solid ${({ theme }) => theme.themeborder}; 45 | & > p { 46 | color: ${({ theme }) => theme.color}; 47 | font-weight: 700; 48 | font-size: 1rem; 49 | } 50 | `; 51 | 52 | export const StyledDMUserThread = styled.div` 53 | height: 90%; 54 | padding: 0.4rem; 55 | overflow-y: scroll; 56 | `; 57 | 58 | export const StyledMessages = styled.div` 59 | width: 100%; 60 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 61 | display: none; 62 | } 63 | `; 64 | 65 | export const StyledChatHome = styled.div` 66 | display: flex; 67 | flex-direction: column; 68 | justify-content: center; 69 | align-items: center; 70 | height: 100%; 71 | & > img { 72 | width: 6rem; 73 | height: 6rem; 74 | border: 2px solid black; 75 | border-radius: 50%; 76 | padding: 0.8rem 1rem 0.4rem 0.4rem; 77 | top: 39%; 78 | margin-bottom: 1rem; 79 | } 80 | & > h2 { 81 | font-weight: 200; 82 | margin: 0.6rem; 83 | } 84 | > p { 85 | font-size: 0.9rem; 86 | color: ${({ theme }) => theme.colors.gray4}; 87 | margin: 0; 88 | } 89 | `; 90 | 91 | export const StyledUser = styled.div` 92 | display: flex; 93 | flex-direction: row; 94 | padding: 0.5rem; 95 | cursor: pointer; 96 | & > img { 97 | width: 3rem; 98 | height: 3rem; 99 | border: none; 100 | border-radius: 50%; 101 | } 102 | `; 103 | -------------------------------------------------------------------------------- /src/components/ContentBox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import LoadPost from "./LoadPost"; 4 | import debounce from "lodash.debounce"; 5 | import { 6 | StyledMainWindow, 7 | StyledActiveWindow, 8 | } from "../styles/components/windowStyles.js"; 9 | import { StyledLoader } from "../styles/components/profileStyles"; 10 | import { StyledDiv } from "../styles/components/generalStyles"; 11 | import { getHomePage, fetchPosts } from "../actions/index"; 12 | 13 | const ContentBox = (props) => { 14 | const { activeSub, data } = props; 15 | const ishome = !!!props.activeSub; 16 | if (!ishome) { 17 | var items = data[activeSub].items; 18 | } 19 | window.onscroll = debounce(() => { 20 | if ( 21 | window.innerHeight + document.documentElement.scrollTop === 22 | document.documentElement.scrollHeight 23 | ) { 24 | if (ishome) props.dispatch(getHomePage()); 25 | else props.dispatch(fetchPosts()); 26 | } 27 | }, 100); 28 | return ( 29 | 30 | {ishome ? ( 31 |
32 | {Object.entries(props.homepage.fetchedPosts).map( 33 | ([key, value], index) => { 34 | return ( 35 |
36 | 43 |
44 | ); 45 | } 46 | )} 47 |
48 | ) : ( 49 | 50 | {Object.entries(props.posts).map(([key, value], index) => { 51 | if (items.indexOf(key) !== -1) { 52 | return ( 53 | 61 | ); 62 | } 63 | })} 64 | 65 | )} 66 | 67 | {props.homepage.loading && ( 68 | 73 | )} 74 | 75 |
76 | ); 77 | }; 78 | 79 | const mapStateToProps = (state, props) => { 80 | return { 81 | auth: state.authenticationReducer, 82 | subinfo: state.postReducer.data, 83 | posts: state.postReducer.posts, 84 | activeSub: state.subredditReducer.activeSubreddit, 85 | data: state.postReducer.data, 86 | homepage: state.postReducer.homepage, 87 | }; 88 | }; 89 | 90 | export default connect(mapStateToProps)(ContentBox); 91 | -------------------------------------------------------------------------------- /src/styles/components/followingtabStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { popin } from "./animations"; 3 | 4 | export const StyledFollowingTab = styled.div` 5 | width: 100vw; 6 | height: 100vh; 7 | background: rgba(0, 0, 0, 0.4); 8 | background-size: cover; 9 | backdrop-filter: blur(2px); 10 | display: flex; 11 | justify-content: center; 12 | position: fixed; 13 | z-index: 22; 14 | transition: all 1s ease-in-out; 15 | `; 16 | 17 | export const StyledFTab = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | margin-top: 4rem; 21 | background: ${({ theme }) => theme.bg}; 22 | height: 26rem; 23 | width: 20rem; 24 | border: none; 25 | border-radius: 1rem; 26 | box-shadow: 5px 10px 2rem 27 | ${({ theme }) => (theme.name === "light" ? "gray" : "black")}; 28 | position: fixed; 29 | top: 8%; 30 | z-index: 12; 31 | transition: all 1s ease-in-out; 32 | animation: ${popin} 0.1s; 33 | `; 34 | 35 | export const StyledFTabHeader = styled.div` 36 | display: flex; 37 | flex-direction: row; 38 | border-radius: 1rem 1rem 0 0; 39 | border-bottom: 1px ${({ theme }) => theme.colors.gray2} solid; 40 | text-align: center; 41 | height: 3rem; 42 | `; 43 | 44 | export const StyledFTabText = styled.p` 45 | width: 90%; 46 | padding-left: 2rem; 47 | `; 48 | 49 | export const StyledFTabCloseBtn = styled.p` 50 | transform: rotate(45deg); 51 | font-size: 2rem; 52 | cursor: pointer; 53 | width: 3rem; 54 | height: 3rem; 55 | margin: 0; 56 | outline: none; 57 | border: none; 58 | `; 59 | 60 | export const StyledFollowings = styled.div` 61 | display: flex; 62 | flex-direction: column; 63 | overflow-y: scroll; 64 | height: 100%; 65 | padding: 1rem; 66 | `; 67 | 68 | export const StyledFollowing = styled.div` 69 | display: flex; 70 | flex-direction: row; 71 | & > .following_details { 72 | cursor: pointer; 73 | display: flex; 74 | width: 100%; 75 | height: 3rem; 76 | padding: 1px 0; 77 | img { 78 | width: 2rem; 79 | height: 2rem; 80 | border: none; 81 | border-radius: 50%; 82 | align-self: center; 83 | } 84 | , 85 | p { 86 | padding-left: 1rem; 87 | text-decoration: none; 88 | color: ${({ theme }) => theme.color}; 89 | text-overflow: ellipsis; 90 | } 91 | } 92 | `; 93 | 94 | export const StyledUnfollowBtn = styled.button` 95 | background: ${({ theme }) => theme.colors.gray7}; 96 | color: white; 97 | border: none; 98 | outline: none; 99 | border-radius: 6px; 100 | width: 4rem; 101 | height: 2rem; 102 | cursor: pointer; 103 | margin-top: 0.5rem; 104 | `; 105 | -------------------------------------------------------------------------------- /src/styles/components/redditorStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledScrollBtn = styled.div` 4 | background: ${({ theme }) => theme.bg}; 5 | border: none; 6 | border-radius: 50%; 7 | box-shadow: 1px 2px 5px ${({ theme }) => 8 | theme.name === "light" ? "gray" : "black"}; 9 | cursor: pointer; 10 | font-size: 1rem; 11 | height: 1.7rem; 12 | opacity: .9; 13 | padding-top: .1rem; 14 | position: absolute; 15 | text-align: center; 16 | top: 9rem; 17 | width: 1.7rem; 18 | ${(props) => { 19 | if (props.position === "left") { 20 | return "left: 13.6%;transform: rotate(175deg);"; 21 | } else if (props.position === "right") { 22 | return "left: 59%;"; 23 | } 24 | }} 25 | @media(max-width: ${({ theme }) => theme.breakpoint}){ 26 | top: 4.4rem; 27 | ${(props) => { 28 | if (props.position === "left") { 29 | return "left: 2%;"; 30 | } else if (props.position === "right") { 31 | return "left: 90%;"; 32 | } 33 | }} 34 | } 35 | `; 36 | 37 | export const StyledRedditor = styled.div` 38 | background: ${({ theme }) => theme.bg}; 39 | border: 1px ${({ theme }) => theme.border} solid; 40 | border-radius: 2px; 41 | display: flex; 42 | flex-direction: row; 43 | height: 8rem; 44 | justify-content: flex-start; 45 | margin-top: 4rem; 46 | overflow-x: scroll; 47 | overflow-y: hidden; 48 | scroll-behavior: smooth; 49 | width: 100%; 50 | &::-webkit-scrollbar { 51 | display: none; 52 | } 53 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 54 | background: none; 55 | border: none; 56 | border: 1px ${({ theme }) => theme.border} solid; 57 | height: 7.5rem; 58 | margin: 0; 59 | overflow-y: hidden; 60 | } 61 | `; 62 | 63 | export const StyledSubContainer = styled.div` 64 | align-self: center; 65 | display: flex; 66 | flex-direction: row; 67 | `; 68 | 69 | export const StyledSub = styled.div` 70 | margin: 2rem 0.6rem; 71 | align-self: center; 72 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 73 | padding-top: 1rem; 74 | } 75 | & > .sub__name { 76 | color: ${({ theme }) => theme.colors.gray7}; 77 | font-size: 0.9rem; 78 | text-align: center; 79 | margin: 0; 80 | } 81 | & > .sub__name-active { 82 | color: red; 83 | } 84 | `; 85 | 86 | export const StyledSubIcon = styled.img` 87 | border: 2px ${({ theme }) => theme.colors.red} solid; 88 | border-radius: 50%; 89 | cursor: pointer; 90 | height: 4.1rem; 91 | align-self: center; 92 | margin: 0 0.2rem; 93 | outline: none; 94 | padding: 0.2rem; 95 | width: 4.2rem; 96 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 97 | height: 3.7rem; 98 | width: 3.8rem; 99 | } 100 | `; 101 | -------------------------------------------------------------------------------- /src/styles/components/userStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledUserInfoContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | left: 65%; 8 | margin-top: 7rem; 9 | position: fixed; 10 | width: 30%; 11 | `; 12 | 13 | export const StyledUserInfo = styled.div` 14 | float: left; 15 | margin-bottom: 1rem; 16 | `; 17 | 18 | export const StyledUserInfoText = styled.div` 19 | float: left; 20 | margin-left: 1rem; 21 | margin-top: 0.4rem; 22 | & > .name { 23 | color: #888; 24 | font-size: 0.8rem; 25 | margin: 0; 26 | margin-top: 0.2rem; 27 | } 28 | & > .username { 29 | color: ${({ theme }) => theme.color}; 30 | font-weight: 600; 31 | text-decoration: none; 32 | } 33 | `; 34 | 35 | export const StyledUserName = styled.p` 36 | color: ${({ theme }) => theme.color} 37 | text-decoration: none; 38 | margin: 0; 39 | font-size: .9rem; 40 | font-weight: 600; 41 | `; 42 | 43 | export const StyledUserAvatar = styled.img` 44 | border: none; 45 | border-radius: 50%; 46 | float: left; 47 | height: 3.4rem; 48 | width: 3.4rem; 49 | `; 50 | 51 | export const StyledSuggestions = styled.div` 52 | width: 22rem; 53 | display: flex; 54 | flex-direction: column; 55 | height: 22rem; 56 | margin-bottom: 1rem; 57 | & > b { 58 | color: $gray6; 59 | font-size: 1rem; 60 | font-weight: 500; 61 | margin: 0.4rem 0 0.6rem 0.5rem; 62 | } 63 | `; 64 | 65 | export const StyledSuggestion = styled.div` 66 | display: flex; 67 | flex-direction: row; 68 | justify-content: space-between; 69 | margin: 0.6rem; 70 | align-items: center; 71 | & > div { 72 | height: 3rem; 73 | margin: 0; 74 | margin-left: 1rem; 75 | padding: 0; 76 | } 77 | & > img { 78 | border: none; 79 | border-radius: 50%; 80 | float: left; 81 | height: 2.8rem; 82 | width: 2.8rem; 83 | } 84 | & > button { 85 | font-size: 2.5rem; 86 | display: flex; 87 | right: 10%; 88 | margin-bottom: 4rem; 89 | position: absolute; 90 | } 91 | `; 92 | 93 | export const StyledSuggestionText = styled.div` 94 | width: 2rem; 95 | position: absolute; 96 | left: 4rem; 97 | & > p { 98 | font-size: 1rem; 99 | font-weight: 400; 100 | margin: 0; 101 | } 102 | & > .suggestionTextTitle { 103 | color: ${({ theme }) => theme.color}; 104 | text-decoration: none; 105 | font-weight: 600; 106 | } 107 | `; 108 | 109 | export const StyledUserInfoFooter = styled.div` 110 | color: #aaa; 111 | font-size: 0.6rem; 112 | margin-top: 1rem; 113 | text-decoration: none; 114 | & > .footertext { 115 | color: inherit; 116 | font-size: 0.8rem; 117 | margin: 1rem 0.8rem 1rem 0; 118 | text-decoration: none; 119 | } 120 | `; 121 | -------------------------------------------------------------------------------- /src/components/PostCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | StyledExplorePost, 4 | StyledExplorePostImg, 5 | StyledExploreTextPost, 6 | StyledHoverDisplay, 7 | StyledUpvoteIcon, 8 | } from "../styles/components/exploreStyles"; 9 | 10 | class PostCard extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | hovering: false, 15 | }; 16 | this.MouseEnter = this.MouseEnter.bind(this); 17 | this.MouseLeave = this.MouseLeave.bind(this); 18 | } 19 | MouseEnter = (e) => { 20 | this.setState({ hovering: true }); 21 | }; 22 | MouseLeave = (e) => { 23 | this.setState({ hovering: false }); 24 | }; 25 | render() { 26 | let { 27 | id, 28 | url, 29 | caption, 30 | post, 31 | username, 32 | subreddit, 33 | score, 34 | likes, 35 | is_video, 36 | hovering, 37 | } = this.props.value; 38 | const ext = url.substring(url.length - 4, url.length); 39 | let ifimg = 40 | ext === ".jpg" || 41 | ext === ".png" || 42 | ext === ".gif" || 43 | ext === "gifv"; 44 | if (!ifimg && url.includes("imgur")) { 45 | url += ".png"; 46 | ifimg = true; 47 | } 48 | is_video = url.includes("youtu"); 49 | return ( 50 | { 53 | this.props.viewPost(this.props.value.id); 54 | }} 55 | onMouseEnter={this.MouseEnter} 56 | onMouseLeave={this.MouseLeave} 57 | > 58 | {this.state.hovering && ( 59 | 60 | 61 |  {score} 62 | 63 | )} 64 | {ifimg && ( 65 | 69 | (e.target.src = 70 | "https://pics.me.me/couldnt-load-image-tap-to-retry-15674211.png") 71 | } 72 | /> 73 | )} 74 | {is_video && ( 75 | 83 | )} 84 | {!ifimg && !is_video && ( 85 | 86 | {caption} 87 | {post.substring(0, 400)} 88 |

89 | {!post && ( 90 | 95 | {url} 96 | 97 | )} 98 |

99 |
100 | )} 101 |
102 | ); 103 | } 104 | } 105 | 106 | export default PostCard; 107 | -------------------------------------------------------------------------------- /src/components/Inbox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header"; 3 | import { connect } from "react-redux"; 4 | import { 5 | StyledDM, 6 | StyledDMUsers, 7 | StyledDMHead, 8 | StyledDMUserThread, 9 | StyledMessages, 10 | StyledChatHome, 11 | StyledUser, 12 | } from "../styles/components/inboxStyles"; 13 | import { StyledBtnImg } from "../styles/components/headerStyles"; 14 | import { getSubInfo } from "../actions/api-calls"; 15 | 16 | class Inbox extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | show_chat_for: undefined, 21 | obj: {}, 22 | }; 23 | } 24 | showChatFor = (id) => { 25 | this.setState({ show_chat_for: id }); 26 | }; 27 | playground = async (token) => { 28 | let x = undefined; 29 | await getSubInfo(token, x); 30 | }; 31 | render() { 32 | return ( 33 |
34 |
35 | 36 | 37 | 38 | 48 |

Direct

49 |
50 | 51 |

52 | Reddit does not allow this yet, I have plan to 53 | implement some type of another chat client here, 54 | until then, this place is deserted ;_; 55 |

56 | {Object.entries(this.state.obj).map( 57 | ([key, val], index) => { 58 | return ( 59 | 61 | this.showChatFor(key) 62 | } 63 | > 64 | profile 68 |

{val.name}

69 |
70 | ); 71 | } 72 | )} 73 |
74 |
75 | 76 | {!!!this.state.show_chat_for ? ( 77 | 78 | dm-logo 79 |

Your Messages

80 |

Send private messages to a reddit user

81 | 88 |
89 | ) : ( 90 |
91 | showing chat for {this.state.show_chat_for} 92 |
93 | )} 94 |
95 |
96 |
97 | ); 98 | } 99 | } 100 | 101 | const mapStateToProps = (state) => { 102 | return { 103 | access_token: state.authenticationReducer.access_token, 104 | nightmode: state.settingsReducer.nightmode, 105 | }; 106 | }; 107 | 108 | export default connect(mapStateToProps)(Inbox); 109 | -------------------------------------------------------------------------------- /src/components/Notificationbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { 4 | StyledNotificationBar, 5 | StyledNotificationBarContainer, 6 | StyledNotification, 7 | StyledNFooter, 8 | } from "../styles/components/notificationbarStyles"; 9 | import { StyledBtnImg } from "../styles/components/headerStyles"; 10 | import { StyledLoader } from "../styles/components/profileStyles"; 11 | 12 | class Notificationbar extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | notifications: [], 17 | }; 18 | } 19 | componentDidMount() { 20 | return fetch(`https://oauth.reddit.com/message/inbox`, { 21 | method: "GET", 22 | headers: { 23 | Authorization: `bearer ${this.props.token}`, 24 | }, 25 | }) 26 | .then((res) => res.json()) 27 | .then((json) => { 28 | const after = json.data.after; 29 | let notifications = json.data.children.map((child) => { 30 | const { 31 | author, 32 | body, 33 | created, 34 | id, 35 | link_title, 36 | num_comments, 37 | subreddit_name_prefixed: subreddit, 38 | subject, 39 | } = child.data; 40 | const obj = { 41 | author, 42 | body, 43 | created, 44 | id, 45 | link_title, 46 | num_comments, 47 | subreddit, 48 | subject, 49 | }; 50 | return obj; 51 | }); 52 | this.setState({ notifications }); 53 | }) 54 | .catch((err) => { 55 | console.log("something went wrong", err); 56 | }); 57 | } 58 | render() { 59 | return ( 60 | 61 | 62 | {this.state.notifications.length === 0 && ( 63 | 64 | )} 65 | {this.state.notifications.map((notification) => { 66 | return ( 67 | 68 |

69 | {notification.author}  70 | {notification.subreddit && ( 71 | ({notification.subreddit}) 72 | )} 73 |

74 | 75 |

{notification.subject}

76 | {notification.num_comments && ( 77 |
78 | 87 | {notification.num_comments} 88 |
89 | )} 90 |

91 | {moment 92 | .unix(notification.created) 93 | .format("DD/MM HH:mm")} 94 |

95 |
96 |
97 | ); 98 | })} 99 |
100 |
101 | ); 102 | } 103 | } 104 | 105 | export default Notificationbar; 106 | -------------------------------------------------------------------------------- /src/components/User.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import _ from "lodash"; 5 | import { 6 | StyledUserInfoContainer, 7 | StyledUserInfo, 8 | StyledUserInfoText, 9 | StyledUserAvatar, 10 | StyledUserName, 11 | StyledSuggestions, 12 | StyledSuggestion, 13 | StyledSuggestionText, 14 | StyledUserInfoFooter, 15 | } from "../styles/components/userStyles"; 16 | import { StyledAddBtn } from "../styles/components/searchbarStyles"; 17 | 18 | //hardcoded suggestions 19 | const subs = [ 20 | "memes", 21 | "history", 22 | "dankmemes", 23 | "redditdev", 24 | "javascript", 25 | "news", 26 | "pewdiepiesubmissions", 27 | "videos", 28 | ]; 29 | 30 | class User extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | suggestions: {}, 35 | }; 36 | } 37 | getObj = (data) => { 38 | const { id, display_name: title, icon_img, subscribers } = data; 39 | let obj = {}; 40 | obj[id] = { 41 | id, 42 | title, 43 | icon_img, 44 | subscribers, 45 | }; 46 | return obj; 47 | }; 48 | componentDidMount() { 49 | const that = this; 50 | _.shuffle(subs) 51 | .slice(0, 5) 52 | .map((sub) => { 53 | fetch(`https://www.reddit.com/r/${sub}/about/.json`) 54 | .then((res) => res.json()) 55 | .then((json) => 56 | that.setState({ 57 | suggestions: { 58 | ...that.getObj(json.data), 59 | ...that.state.suggestions, 60 | }, 61 | }) 62 | ); 63 | }, that); 64 | } 65 | render() { 66 | const that = this; 67 | return ( 68 | 69 | 70 | 74 | 75 | 76 | u/{this.props.user.name} 77 | 78 |

{this.props.user.name}

79 |
80 |
81 | 82 | Suggestions For You 83 | {Object.entries(this.state.suggestions).map( 84 | ([key, value], index) => { 85 | const { id, title, icon_img, subscribers } = value; 86 | return ( 87 | 88 | 89 | 90 | 94 | {title} 95 | 96 |

{subscribers}

97 |
98 |
99 | ); 100 | }, 101 | that 102 | )} 103 |
104 | 105 | 106 | About 107 | 108 | 113 | Github 114 | 115 |

© REDDIT FROM PARALLEL TIMELINE

116 |
117 |
118 | ); 119 | } 120 | } 121 | 122 | const mapStateToProps = (state) => { 123 | return { 124 | user: state.authenticationReducer.user, 125 | }; 126 | }; 127 | 128 | export default connect(mapStateToProps)(User); 129 | -------------------------------------------------------------------------------- /src/styles/components/viewPostStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { popin, pop } from "./animations"; 3 | 4 | export const StyledViewPostContent = styled.div` 5 | background: ${({ theme }) => theme.bg}; 6 | width: 35%; 7 | height: 90%; 8 | word-wrap: break-word; 9 | overflow: hidden; 10 | margin: 2rem 0 2rem 2rem; 11 | img { 12 | width: 100%; 13 | height: 100%; 14 | display: flex; 15 | justify-content: space-around; 16 | } 17 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 18 | margin: 0; 19 | padding: 0; 20 | height: 55%; 21 | width: 90%; 22 | border-radius: 0.3rem 0.3rem 0 0; 23 | transition: all 1s ease-in-out; 24 | animation: ${pop} 0.3s ease-in-out; 25 | } 26 | `; 27 | 28 | export const StyledViewPostDetails = styled.div` 29 | display: flex; 30 | flex-direction: column; 31 | background: ${({ theme }) => theme.bg}; 32 | height: 90%; 33 | width: 30%; 34 | border-left: 1px ${({ theme }) => theme.themeborder} solid; 35 | margin: 2rem 2rem 2rem 0; 36 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 37 | display: none; 38 | } 39 | `; 40 | 41 | export const StyledPostHead = styled.div` 42 | display: none; 43 | flex-direction: row; 44 | justify-content: space-between; 45 | align-items: center; 46 | background: ${({ theme }) => 47 | (theme.name === "light" && theme.bg) || theme.bodybg}; 48 | color: ${({ theme }) => theme.color} 49 | width: 100%; 50 | height: 3.7rem; 51 | border-radius: .3rem .3rem 0 0; 52 | border-bottom: 1px solid ${({ theme }) => theme.themeborder}; 53 | @media(max-width: ${({ theme }) => theme.breakpoint}){ 54 | display: flex; 55 | } 56 | `; 57 | 58 | export const StyledCommentSection = styled.div` 59 | overflow-y: scroll; 60 | overflow-x: wrap; 61 | text-align: left; 62 | padding: 1rem; 63 | margin-bottom: 8rem; 64 | &::-webkit-scrollbar { 65 | width: 0px; 66 | } 67 | `; 68 | 69 | export const StyledDetailSection = styled.div` 70 | min-height: 4rem; 71 | display: flex; 72 | flex-direction: row; 73 | align-items: center; 74 | padding-left: 2rem; 75 | border: none; 76 | border-bottom: 0.6px ${({ theme }) => theme.themeborder} solid; 77 | & > .details__username { 78 | text-decoration: none; 79 | font-weight: 600; 80 | color: ${({ theme }) => (theme.name === "light" && "black") || "white"}; 81 | } 82 | `; 83 | 84 | export const StyledDetailsFooter = styled.div` 85 | border: none; 86 | border-top: 0.6px ${({ theme }) => theme.themeborder} solid; 87 | display: flex; 88 | flex-direction: column; 89 | background: ${({ theme }) => theme.bg}; 90 | position: fixed; 91 | bottom: 5%; 92 | align-items: flex-start; 93 | padding: 1rem; 94 | width: inherit; 95 | & > .footerbtns { 96 | display: flex; 97 | flex-direction: row; 98 | } 99 | & > b { 100 | margin: 0.6rem 0; 101 | } 102 | `; 103 | 104 | export const StyledVPTextPost = styled.div` 105 | display: flex; 106 | background: ${({ theme }) => theme.bg}; 107 | height: 100%; 108 | padding: 2rem; 109 | text-align: left; 110 | flex-direction: column; 111 | overflow-wrap: break-word; 112 | overflow-y: scroll; 113 | text-align: justify; 114 | &::-webkit-scrollbar { 115 | width: 8px; 116 | } 117 | `; 118 | 119 | export const StyledVPCloseBtn = styled.div` 120 | position: fixed; 121 | font-size: 3rem; 122 | color: white; 123 | cursor: pointer; 124 | right: 2%; 125 | top: -2%; 126 | transform: rotate(45deg); 127 | `; 128 | 129 | export const StyledFollowBtn = styled.div` 130 | background: none; 131 | cursor: pointer; 132 | color: ${({ theme }) => theme.colors.blue}; 133 | margin-left: 1rem; 134 | `; 135 | -------------------------------------------------------------------------------- /src/components/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Header from "./Header"; 3 | import Searchbar from "./Searchbar"; 4 | import { getobj } from "../actions/"; 5 | import PostCard from "./PostCard"; 6 | import Menubar from "./Menubar"; 7 | import ViewPost from "./ViewPost"; 8 | import debounce from "lodash.debounce"; 9 | import { StyledExplorer } from "../styles/components/exploreStyles"; 10 | import { StyledLoader } from "../styles/components/profileStyles"; 11 | 12 | class Explore extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | fetchedPosts: {}, 17 | after: undefined, 18 | loading: false, 19 | currPostid: undefined, 20 | viewPost: false, 21 | window_width: window.innerWidth, 22 | }; 23 | 24 | window.onscroll = debounce(() => { 25 | if ( 26 | window.innerHeight + document.documentElement.scrollTop === 27 | document.documentElement.scrollHeight 28 | ) { 29 | this.loadposts(); 30 | } 31 | }, 100); 32 | 33 | this.updateWidth = this.updateWidth.bind(this); 34 | this.viewPost = this.viewPost.bind(this); 35 | this.hidepost = this.hidepost.bind(this); 36 | this.handleChanges = this.handleChanges.bind(this); 37 | } 38 | componentDidMount() { 39 | this.loadposts(); 40 | } 41 | updateWidth() { 42 | this.setState({ window_width: window.innerWidth }); 43 | } 44 | loadposts() { 45 | this.setState({ loading: true }); 46 | let posts = {}; 47 | fetch(`https://www.reddit.com/.json?limit=60&after=${this.state.after}`) 48 | .then((res) => res.json()) 49 | .then((json) => { 50 | this.setState({ after: json.data.after }); 51 | json.data.children.map((child) => 52 | Object.assign(posts, getobj(child.data)) 53 | ); 54 | }) 55 | .then((now) => 56 | this.setState({ 57 | fetchedPosts: { ...this.state.fetchedPosts, ...posts }, 58 | loading: false, 59 | }) 60 | ); 61 | } 62 | viewPost(id) { 63 | this.setState({ viewPost: true, currPostid: id }); 64 | } 65 | hidepost(e) { 66 | this.setState({ viewPost: false, currPost: {} }); 67 | } 68 | handleChanges(id, score, likes) { 69 | this.setState((prevState) => ({ 70 | fetchedPosts: { 71 | ...prevState.fetchedPosts, 72 | [id]: { 73 | ...prevState.fetchedPosts[id], 74 | score, 75 | likes, 76 | }, 77 | }, 78 | })); 79 | } 80 | render() { 81 | window.addEventListener("resize", this.updateWidth); 82 | return ( 83 |
84 | {this.state.window_width >= 740 ? ( 85 |
86 | ) : ( 87 | 88 | )} 89 | {this.state.viewPost && ( 90 | 95 | )} 96 | 97 | {Object.entries(this.state.fetchedPosts) 98 | .filter(([key, value], index) => { 99 | return !value.is_video; 100 | }) 101 | .map(([key, value], index) => { 102 | if (value) { 103 | return ( 104 | 110 | ); 111 | } else { 112 | return
; 113 | } 114 | })} 115 |
116 | {this.state.loading && ( 117 | 122 | )} 123 |
124 |
125 | 126 |
127 | ); 128 | } 129 | } 130 | 131 | export default Explore; 132 | -------------------------------------------------------------------------------- /src/components/Notifications.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { connect } from "react-redux"; 4 | import { 5 | StyledPageHeading, 6 | StyledPageCanvas, 7 | Styledbody, 8 | } from "../styles/components/pageStyles"; 9 | import { StyledBtnImg } from "../styles/components/headerStyles"; 10 | import { 11 | StyledNotification, 12 | StyledNFooter, 13 | } from "../styles/components/notificationbarStyles"; 14 | import Menubar from "./Menubar"; 15 | 16 | class Notifications extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | notifications: [], 21 | }; 22 | } 23 | componentDidMount() { 24 | return fetch(`https://oauth.reddit.com/message/inbox`, { 25 | method: "GET", 26 | headers: { 27 | Authorization: `bearer ${this.props.token}`, 28 | }, 29 | }) 30 | .then((res) => res.json()) 31 | .then((json) => { 32 | const after = json.data.after; 33 | let notifications = json.data.children.map((child) => { 34 | const { 35 | author, 36 | body, 37 | created, 38 | id, 39 | link_title, 40 | num_comments, 41 | subreddit_name_prefixed: subreddit, 42 | subject, 43 | } = child.data; 44 | const obj = { 45 | author, 46 | body, 47 | created, 48 | id, 49 | link_title, 50 | num_comments, 51 | subreddit, 52 | subject, 53 | }; 54 | return obj; 55 | }); 56 | this.setState({ notifications }); 57 | }) 58 | .catch((err) => { 59 | console.log("something went wrong", err); 60 | }); 61 | } 62 | render() { 63 | return ( 64 | 65 | 66 |

Activity

67 |
68 | {(this.props.authenticated && ( 69 | 72 | {this.state.notifications.map((notification) => { 73 | return ( 74 | 75 |

76 | {notification.author}  77 | {notification.subreddit && ( 78 | 79 | ({notification.subreddit}) 80 | 81 | )} 82 |

83 | 84 |

{notification.subject}

85 | {notification.num_comments && ( 86 |
87 | 96 | {notification.num_comments} 97 |
98 | )} 99 |

100 | {moment 101 | .unix(notification.created) 102 | .format("DD/MM HH:mm")} 103 |

104 |
105 |
106 | ); 107 | })} 108 |
109 | )) || ( 110 | 117 |

You are not logged in

118 |
119 | )} 120 | 121 |
122 | ); 123 | } 124 | } 125 | 126 | const mapStateToProps = (state) => { 127 | return { 128 | authenticated: state.authenticationReducer.authenticated, 129 | token: state.authenticationReducer.access_token, 130 | nightmode: state.settingsReducer.nightmode, 131 | }; 132 | }; 133 | 134 | export default connect(mapStateToProps)(Notifications); 135 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { deselect_subreddit } from "../actions"; 5 | import Searchbar from "./Searchbar"; 6 | import Notificationbar from "./Notificationbar"; 7 | import { 8 | StyledHeader, 9 | StyledHeaderBtns, 10 | StyledBtnImg, 11 | } from "../styles/components/headerStyles"; 12 | 13 | class Header extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | showNotification: false, 18 | }; 19 | this.wrapperRef = React.createRef(); 20 | } 21 | handleClickOutside = (event) => { 22 | const that = this; 23 | if ( 24 | this.wrapperRef && 25 | this.wrapperRef.current && 26 | !this.wrapperRef.current.contains(event.target) 27 | ) { 28 | setTimeout( 29 | function () { 30 | that.setState({ showNotification: false }); 31 | }, 32 | 100, 33 | that 34 | ); 35 | } else { 36 | this.setState({ showNotification: !this.state.showNotification }); 37 | } 38 | }; 39 | componentDidMount() { 40 | document.addEventListener("mousedown", this.handleClickOutside); 41 | } 42 | render() { 43 | return ( 44 |
45 | 46 | 47 |

49 | this.props.dispatch(deselect_subreddit()) 50 | } 51 | > 52 | @reddit 53 |

54 |
55 | 60 | 61 | 62 | 64 | this.props.dispatch(deselect_subreddit()) 65 | } 66 | type="home" 67 | src={ 68 | this.props.nightmode 69 | ? "/images/home-light.png" 70 | : "/images/home.png" 71 | } 72 | alt="home" 73 | /> 74 | 75 | {this.props.isAuthenticated && ( 76 | 77 | 86 | 87 | )} 88 | 89 | 98 | 99 | 110 | 111 | 112 | 113 | 114 |
115 | {this.props.isAuthenticated && 116 | this.props.hasOwnProperty("dashboard") && ( 117 | 118 | 127 | 128 | )} 129 | {this.state.showNotification && ( 130 | 131 | )} 132 |
133 | ); 134 | } 135 | } 136 | 137 | const mapStateToProps = (state, props) => { 138 | return { 139 | icon_img: state.authenticationReducer.user.icon_img, 140 | isAuthenticated: state.authenticationReducer.authenticated, 141 | access_token: state.authenticationReducer.access_token, 142 | nightmode: state.settingsReducer.nightmode, 143 | }; 144 | }; 145 | 146 | export default connect(mapStateToProps)(Header); 147 | -------------------------------------------------------------------------------- /src/reducers/postReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_POSTS, 3 | RECEIVE_POSTS, 4 | ADD_SUBREDDIT, 5 | ADD_SUBREDDITS, 6 | HOMEPAGE, 7 | HOMEPAGEPOST_UPVOTE, 8 | HOMEPAGEPOST_DOWNVOTE, 9 | UPVOTE, 10 | DOWNVOTE, 11 | LOAD_HOMEPAGE, 12 | LOGGED_IN, 13 | LOGOUT, 14 | REMOVE_SUBREDDIT, 15 | } from "../actions/action-types"; 16 | import _ from "lodash"; 17 | 18 | const initialState = { 19 | data: {}, 20 | posts: {}, 21 | homepage: { 22 | loading: false, 23 | fetchedPosts: {}, 24 | after: undefined, 25 | lastUpdated: undefined, 26 | }, 27 | }; 28 | 29 | const postReducer = (state = initialState, action) => { 30 | switch (action.type) { 31 | case REQUEST_POSTS: 32 | return { 33 | ...state, 34 | data: { 35 | ...state.data, 36 | [action.payload.subreddit]: { 37 | ...state.data[action.payload.subreddit], 38 | fetching: true, 39 | }, 40 | }, 41 | }; 42 | case RECEIVE_POSTS: 43 | let { 44 | subreddit, 45 | items, 46 | posts: fetchedPosts, 47 | after, 48 | lastUpdated, 49 | } = action.payload; 50 | let { data, posts } = state; 51 | posts = { ...posts, ...fetchedPosts }; 52 | posts = _.shuffle(posts); 53 | posts = _.keyBy(posts, "id"); 54 | return { 55 | ...state, 56 | data: { 57 | ...data, 58 | [subreddit]: { 59 | ...state.data[subreddit], 60 | fetching: false, 61 | items, 62 | after, 63 | lastUpdated, 64 | }, 65 | }, 66 | posts, 67 | }; 68 | case LOAD_HOMEPAGE: { 69 | return { 70 | ...state, 71 | homepage: { 72 | ...state.homepage, 73 | loading: true, 74 | }, 75 | }; 76 | } 77 | case HOMEPAGE: { 78 | let { fetchedPosts, after, lastUpdated } = action.payload; 79 | return { 80 | ...state, 81 | homepage: { 82 | loading: false, 83 | fetchedPosts: { 84 | ...state.homepage.fetchedPosts, 85 | ...fetchedPosts, 86 | }, 87 | after, 88 | lastUpdated, 89 | }, 90 | }; 91 | } 92 | case HOMEPAGEPOST_UPVOTE: { 93 | let { id, likes, score } = action.payload; 94 | return { 95 | ...state, 96 | homepage: { 97 | ...state.homepage, 98 | fetchedPosts: { 99 | ...state.homepage.fetchedPosts, 100 | [id]: { 101 | ...state.homepage.fetchedPosts[id], 102 | likes, 103 | score, 104 | }, 105 | }, 106 | }, 107 | }; 108 | } 109 | case HOMEPAGEPOST_DOWNVOTE: { 110 | let { id, likes, score } = action.payload; 111 | return { 112 | ...state, 113 | homepage: { 114 | ...state.homepage, 115 | fetchedPosts: { 116 | ...state.homepage.fetchedPosts, 117 | [id]: { 118 | ...state.homepage.fetchedPosts[id], 119 | likes, 120 | score, 121 | }, 122 | }, 123 | }, 124 | }; 125 | } 126 | case ADD_SUBREDDIT: 127 | return { 128 | ...state, 129 | data: { 130 | ...state.data, 131 | [action.payload.subreddit]: { 132 | fetching: false, 133 | items: [], 134 | icon: action.payload.icon, 135 | after: undefined, 136 | lastUpdated: undefined, 137 | }, 138 | }, 139 | }; 140 | case ADD_SUBREDDITS: 141 | return { 142 | ...state, 143 | data: { 144 | ...state.data, 145 | ...action.payload.subdata, 146 | }, 147 | }; 148 | case REMOVE_SUBREDDIT: { 149 | const subreddit = action.payload.subreddit; 150 | let items = state.data[subreddit].items; 151 | let posts = state.posts; 152 | Object.keys(posts).map((key) => { 153 | if (items.includes(key)) { 154 | delete posts[key]; 155 | } 156 | }); 157 | let data = state.data; 158 | delete data[action.payload.subreddit]; 159 | return { 160 | ...state, 161 | data: { ...data }, 162 | posts: posts, 163 | }; 164 | } 165 | case LOGGED_IN: { 166 | return { 167 | ...initialState, 168 | }; 169 | } 170 | case LOGOUT: { 171 | return { 172 | ...initialState, 173 | }; 174 | } 175 | case UPVOTE: { 176 | return { 177 | ...state, 178 | posts: { 179 | ...state.posts, 180 | [action.payload.id]: { 181 | ...state.posts[action.payload.id], 182 | score: action.payload.score, 183 | likes: action.payload.likes, 184 | }, 185 | }, 186 | }; 187 | } 188 | case DOWNVOTE: { 189 | return { 190 | ...state, 191 | posts: { 192 | ...state.posts, 193 | [action.payload.id]: { 194 | ...state.posts[action.payload.id], 195 | score: action.payload.score, 196 | likes: action.payload.likes, 197 | }, 198 | }, 199 | }; 200 | } 201 | default: 202 | return state; 203 | } 204 | }; 205 | 206 | export default postReducer; 207 | -------------------------------------------------------------------------------- /src/styles/components/profileStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { FadeInRight } from "./animations"; 3 | 4 | export const StyledProfile = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | width: 60%; 8 | position: absolute; 9 | margin: 0 20%; 10 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 11 | margin: 0; 12 | width: 100vw; 13 | overflow: hidden; 14 | } 15 | animation: ${FadeInRight} 0.3s ease; 16 | `; 17 | 18 | export const StyledProfileInfo = styled.div` 19 | display: inherit; 20 | flex-direction: row; 21 | border-bottom: 1px ${({ theme }) => theme.colors.gray3} solid; 22 | padding: 6rem 0; 23 | float: left; 24 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 25 | overflow-wrap: break-word; 26 | padding-top: 3rem; 27 | } 28 | `; 29 | 30 | export const StyledProfileName = styled.p` 31 | margin: 0; 32 | margin-right: 1rem; 33 | font-size: 1.8rem; 34 | font-weight: 80; 35 | `; 36 | 37 | export const StyledProfileDetails = styled.div` 38 | display: inherit; 39 | flex-direction: column; 40 | position: relative; 41 | top: 1rem; 42 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 43 | left: 1rem; 44 | width: 70%; 45 | } 46 | & > .userdetails { 47 | display: flex; 48 | flex-direction: row; 49 | justify-content: flex-start; 50 | overflow-wrap: break-word; 51 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 52 | flex-direction: column; 53 | } 54 | } 55 | & > .subbtn { 56 | display: flex; 57 | flex-direction: row; 58 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 59 | margin: 0; 60 | flex-direction: column; 61 | } 62 | } 63 | & > p { 64 | width: 90%; 65 | text-align: justify; 66 | overflow-wrap: break-word; 67 | } 68 | `; 69 | 70 | export const StyledButtonGroup = styled.div` 71 | display: flex; 72 | flex-direction: row; 73 | `; 74 | 75 | export const StyledProfilePosts = styled.div` 76 | display: flex; 77 | justify-content: center; 78 | margin-top: 1rem; 79 | `; 80 | 81 | export const StyledProfileIcon = styled.img` 82 | border: none; 83 | border-radius: 50%; 84 | width: 9rem; 85 | height: 9em; 86 | padding: 1rem; 87 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 88 | padding: 0; 89 | width: 5rem; 90 | height: 5rem; 91 | margin-top: 1.2rem; 92 | } 93 | `; 94 | 95 | export const StyledProfileGallery = styled.div` 96 | display: flex; 97 | flex-flow: row wrap; 98 | justify-content: flex-start; 99 | `; 100 | 101 | export const StyledButton = styled.div` 102 | display: flex; 103 | align-self: center; 104 | justify-content: center; 105 | padding-top: .2rem; 106 | position: relative; 107 | width: 5rem; 108 | height: 2rem; 109 | border: none; 110 | outline: none; 111 | border-radius: 4px; 112 | font-weight: 550; 113 | cursor: pointer; 114 | ${(props) => { 115 | switch (props.type) { 116 | case "primary": 117 | return "background: #0075f6;color: white;"; 118 | case "secondary": 119 | return "background: gray; color: white;"; 120 | } 121 | }} 122 | @media(max-width: ${({ theme }) => theme.breakpoint}){ 123 | width: 8rem; 124 | align-self: flex-start; 125 | margin: .2rem 0; 126 | } 127 | `; 128 | 129 | export const StyledLoader = styled.img` 130 | position: fixed; 131 | width: 3rem; 132 | display: flex; 133 | align-self: center; 134 | ${(props) => { 135 | switch (props.size) { 136 | case "sm": 137 | return "width:1rem;height:1rem;"; 138 | case "mid": 139 | return "height: 3rem;margin-bottom: 3rem;"; 140 | } 141 | }} 142 | ${(props) => { 143 | switch (props.type) { 144 | case "input": 145 | return ` 146 | top: 1.1rem; 147 | height: 1rem; 148 | right: 44.5%; 149 | position: absolute; 150 | width: 1rem; 151 | z-index: 6; 152 | @media(max-width: ${props.theme.breakpoint}){ 153 | top: 1%; 154 | right: 2%; 155 | }`; 156 | case "pageload": 157 | return ` 158 | bottom: 0%; 159 | right: 48%; 160 | @media(max-width: ${props.theme.breakpoint}){ 161 | right: 44%; 162 | }`; 163 | default: 164 | return "position: default;margin: 1rem;"; 165 | } 166 | }} 167 | `; 168 | 169 | export const StyledLoginBoard = styled.div` 170 | display: flex; 171 | flex-direction: column; 172 | align-items: center; 173 | align-self: center; 174 | padding-top: 4rem; 175 | & > span { 176 | display: flex; 177 | flex-direction: column; 178 | justify-content: center; 179 | align-items: center; 180 | align-self: center; 181 | transition: all 0.1s ease; 182 | } 183 | transition: all 0.1s ease; 184 | `; 185 | 186 | export const StyledInfoBlock = styled.div` 187 | display: flex; 188 | flex-direction: row; 189 | align-items: center; 190 | `; 191 | 192 | export const StyledInlineDiv = styled.div` 193 | display: flex; 194 | align-items: center; 195 | & > span { 196 | font-weight: bold; 197 | margin-right: 2rem; 198 | } 199 | `; 200 | -------------------------------------------------------------------------------- /src/components/Searchbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { withRouter } from "react-router-dom"; 4 | import { history } from "../router/AppRouter"; 5 | import { select_subreddit, addSubreddit } from "../actions"; 6 | import { sub } from "../actions/api-calls"; 7 | import styled from "styled-components"; 8 | import { 9 | StyledSearchbar, 10 | StyledSearchInput, 11 | StyledSearchIcon, 12 | StyledSearchResults, 13 | StyledClearBtn, 14 | StyledAddBtn, 15 | StyledSearchedSub, 16 | StyledSearchedSubContainer, 17 | StyledSearchedResultsArrow, 18 | } from "../styles/components/searchbarStyles"; 19 | import { StyledLoader } from "../styles/components/profileStyles"; 20 | 21 | let timeout = null; 22 | 23 | class Searchbar extends React.Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | focus: false, 28 | subs: [], 29 | value: "", 30 | loading: false, 31 | }; 32 | } 33 | onChangeHandler = (e) => { 34 | this.setState({ focus: true, value: e.target.value, loading: true }); 35 | clearTimeout(timeout); 36 | let value = e.target.value; 37 | timeout = setTimeout( 38 | function (query, that) { 39 | let obj = {}; 40 | fetch(`https://www.reddit.com/search.json?q=${query}&type=sr`) 41 | .then((res) => res.json()) 42 | .then((json) => 43 | json.data.children.map((sub) => { 44 | const { 45 | id, 46 | display_name: name, 47 | title, 48 | icon_img, 49 | subscribers, 50 | public_description: description, 51 | } = sub.data; 52 | obj = { 53 | id, 54 | name, 55 | title, 56 | icon_img, 57 | subscribers, 58 | description, 59 | }; 60 | return obj; 61 | }) 62 | ) 63 | .then((subs) => that.setState({ subs, loading: false })) 64 | .catch((err) => { 65 | that.setState({ loading: false }); 66 | }); 67 | }, 68 | 1000, 69 | value, 70 | this 71 | ); 72 | }; 73 | onFocusHandler = () => { 74 | this.setState({ focus: true }); 75 | }; 76 | onBlurHandler = (e) => { 77 | setTimeout(() => { 78 | this.setState({ focus: false }); 79 | }, 1000); 80 | }; 81 | addSub = (sub) => { 82 | this.setState({ value: "", subs: [] }); 83 | this.props.dispatch(addSubreddit(sub.slice(2))); 84 | if (this.props.authenticated) { 85 | sub(this.props.access_token, this.state.subdetails.full_name); 86 | } 87 | }; 88 | clear = () => { 89 | this.setState({ value: "", subs: [] }); 90 | }; 91 | render() { 92 | return ( 93 | 94 | 104 | 108 | 🔍︎ 109 | 110 | {this.state.focus && !!this.state.value.length && ( 111 | 112 | {this.state.loading ? ( 113 | 118 | ) : ( 119 | 120 | + 121 | 122 | )} 123 | 124 | )} 125 | {this.state.focus && this.state.subs.length > 0 && ( 126 | 127 | 128 | {this.state.subs.map((sub) => { 129 | let icon = ""; 130 | { 131 | sub.icon_img === "" 132 | ? (icon = "/images/icon.png") 133 | : (icon = sub.icon_img); 134 | } 135 | return ( 136 | 137 | { 139 | this.setState({ 140 | value: "", 141 | subs: [], 142 | }); 143 | this.props.history.push( 144 | `/r/${sub.name}` 145 | ); 146 | }} 147 | name={sub.name} 148 | key={sub.id} 149 | > 150 | 151 | r/{sub.name} 152 |

{sub.title}

153 |
154 | {!this.props.subreddits.includes( 155 | sub.name 156 | ) && ( 157 | 160 | this.addSub(`r/${sub.name}`) 161 | } 162 | > 163 | + 164 | 165 | )} 166 |
167 | ); 168 | })} 169 |
170 |
171 | )} 172 |
173 | ); 174 | } 175 | } 176 | 177 | const mapStateToProps = (state) => { 178 | return { 179 | subreddits: state.subredditReducer.subreddits, 180 | }; 181 | }; 182 | 183 | export default withRouter(connect(mapStateToProps)(Searchbar)); 184 | -------------------------------------------------------------------------------- /src/actions/api-calls.js: -------------------------------------------------------------------------------- 1 | export const getUserInfo = (token) => { 2 | return fetch(`https://oauth.reddit.com/api/v1/me`, { 3 | method: "GET", 4 | headers: { 5 | Authorization: `bearer ${token}`, 6 | }, 7 | }) 8 | .then((res) => res.json()) 9 | .then((json) => { 10 | let { 11 | icon_img, 12 | id, 13 | name, 14 | num_friends, 15 | pref_nightmode, 16 | total_karma, 17 | } = json; 18 | icon_img = /(.*)\?/.exec(icon_img)[1]; 19 | const userinfo = { 20 | icon_img, 21 | id, 22 | name, 23 | num_friends, 24 | pref_nightmode, 25 | total_karma, 26 | }; 27 | return userinfo; 28 | }) 29 | .catch((err) => { 30 | console.log("something went wrong", err); 31 | }); 32 | }; 33 | 34 | export const getSubInfo = async (token, after = undefined) => { 35 | return await fetch( 36 | `https://oauth.reddit.com/subreddits/mine/subscriber?after=${after}&limit=100`, 37 | { 38 | method: "GET", 39 | headers: { 40 | Authorization: `bearer ${token}`, 41 | }, 42 | } 43 | ) 44 | .then((res) => res.json()) 45 | .then((json) => { 46 | const after = json.data.after; 47 | const subs = json.data.children.map((child) => { 48 | const { 49 | created, 50 | description, 51 | icon_img: icon, 52 | display_name: name, 53 | id, 54 | subscribers, 55 | title, 56 | } = child.data; 57 | const sub = { 58 | created, 59 | description, 60 | icon, 61 | name, 62 | id, 63 | subscribers, 64 | title, 65 | }; 66 | if (!sub.icon) sub.icon = "/images/icon.png"; 67 | return sub; 68 | }); 69 | return { after, subs }; 70 | }) 71 | .then(async (obj) => { 72 | if (obj.subs.length < 100) { 73 | return obj; 74 | } else { 75 | let x = await getSubInfo(token, obj.after); 76 | let subs = [...x.subs, ...obj.subs]; 77 | const after = x.after; 78 | return { subs, after }; 79 | } 80 | }) 81 | .catch((err) => { 82 | console.log("error while fetching subs, ", err); 83 | }); 84 | }; 85 | 86 | export const sub = (token, full_name) => { 87 | fetch(`https://oauth.reddit.com/api/subscribe`, { 88 | method: "POST", 89 | headers: { 90 | Authorization: `bearer ${token}`, 91 | "Content-Type": "application/x-www-form-urlencoded", 92 | skip_initial_defaults: "1", 93 | }, 94 | body: `action=sub&sr=${full_name}`, 95 | }).catch((err) => { 96 | console.log("sub req. failed... cannot join this subreddit", err); 97 | }); 98 | }; 99 | 100 | export const unsub = (token, full_name) => { 101 | fetch(`https://oauth.reddit.com/api/subscribe`, { 102 | method: "POST", 103 | headers: { 104 | Authorization: `bearer ${token}`, 105 | "Content-Type": "application/x-www-form-urlencoded", 106 | }, 107 | body: `action=unsub&sr=${full_name}`, 108 | }).catch((err) => { 109 | console.log("unsub req. failed... cannot leave this subreddit", err); 110 | }); 111 | }; 112 | 113 | export const getTrophies = (token) => { 114 | return fetch(`https://oauth.reddit.com/api/v1/me/trophies`, { 115 | method: "GET", 116 | headers: { 117 | Authorization: `bearer ${token}`, 118 | }, 119 | }) 120 | .then((res) => res.json()) 121 | .then((json) => { 122 | let data = {}; 123 | json.data.trophies.map((trophy, index) => { 124 | data[index] = { 125 | icon: trophy.data.icon_70, 126 | name: trophy.data.name, 127 | }; 128 | }); 129 | return data; 130 | }); 131 | }; 132 | 133 | export const deleteCall = (token, id) => { 134 | fetch(`https://oauth.reddit.com/api/del`, { 135 | method: "POST", 136 | headers: { 137 | Authorization: `bearer ${token}`, 138 | "Content-Type": "application/x-www-form-urlencoded", 139 | skip_initial_defaults: "1", 140 | }, 141 | body: `id=${id}`, 142 | }).catch((err) => { 143 | console.log("delete req. failed", err); 144 | }); 145 | }; 146 | 147 | export const editText = async (token, id, comment) => { 148 | await fetch(`https://oauth.reddit.com/api/editusertext`, { 149 | method: "POST", 150 | headers: { 151 | Authorization: `bearer ${token}`, 152 | "Content-Type": "application/x-www-form-urlencoded", 153 | }, 154 | body: `api_type=json&return_rtjson=true&text=${comment}&thing_id=${id}`, 155 | }) 156 | .then((res) => console.log(res)) 157 | .catch((err) => { 158 | console.log("err with text editing", err); 159 | }); 160 | }; 161 | 162 | export const refreshData = async (access_token, refresh_token) => { 163 | const clientID = process.env.REACT_APP_CLIENTID; 164 | const clientSecret = process.env.REACT_APP_CLIENTSECRET; 165 | const encode = btoa( 166 | `${process.env.REACT_APP_CLIENTID}:${process.env.REACT_APP_CLIENTSECRET}` 167 | ); 168 | return await fetch("https://www.reddit.com/api/v1/access_token", { 169 | method: "POST", 170 | body: `grant_type=refresh_token&refresh_token=${refresh_token}&duration=permanent`, 171 | headers: { 172 | "Content-Type": "application/x-www-form-urlencoded", 173 | Authorization: `basic ${encode}`, 174 | }, 175 | }) 176 | .then((res) => res.json()) 177 | .then((data) => { 178 | data["refresh_token"] = refresh_token; 179 | return data; 180 | }) 181 | .catch((error) => { 182 | console.log("error occured while refreshing the token", error); 183 | }); 184 | }; 185 | -------------------------------------------------------------------------------- /src/styles/components/searchbarStyles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledSearchbar = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | float: left; 7 | width: 14rem; 8 | margin-left: 6rem; 9 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 10 | ${(props) => { 11 | if (props.ishome) { 12 | return "display: none;"; 13 | } else { 14 | return "width: 100%;margin: 0;"; 15 | } 16 | }} 17 | } 18 | `; 19 | 20 | export const StyledSearchInput = styled.input` 21 | background: ${({ theme }) => 22 | theme.name === "light" ? theme.colors.gray1 : theme.colors.gray7}; 23 | border: 1px 24 | ${({ theme }) => 25 | theme.name === "light" ? theme.colors.gray3 : theme.colors.gray7} 26 | solid; 27 | border-radius: 3px; 28 | color: ${({ theme }) => (theme.name === "light" ? "gray" : "black")}; 29 | font-size: 1rem; 30 | height: 2rem; 31 | outline: none; 32 | padding: 0 2rem; 33 | width: 100%; 34 | &::placeholder { 35 | color: ${({ theme }) => 36 | theme.name === "light" ? theme.colors.gray3 : theme.colors.gray2}; 37 | font-size: 1rem; 38 | text-align: center; 39 | } 40 | &:focus::placeholder { 41 | text-align: left; 42 | } 43 | &:focus + .icon { 44 | right: 12.5rem; 45 | } 46 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 47 | margin: 0; 48 | padding: 0 0.6rem; 49 | } 50 | `; 51 | 52 | export const StyledSearchIcon = styled.span` 53 | align-self: center; 54 | font-size: .9rem; 55 | position: relative; 56 | color: ${({ theme }) => 57 | theme.name === "light" ? theme.colors.gray3 : theme.colors.gray2}; 58 | right: 9.1rem; 59 | ${(props) => { 60 | if (props.type === "fix") { 61 | return "right:89%;"; 62 | } else if (props.type === "pseudo") { 63 | return "right: 67%;"; 64 | } 65 | }} 66 | @media(max-width: ${({ theme }) => theme.breakpoint}){ 67 | display: none; 68 | } 69 | transition: all .1s ease; 70 | `; 71 | 72 | export const StyledSearchResults = styled.div` 73 | display: flex; 74 | background: ${({ theme }) => theme.bg}; 75 | border-bottom: 1px ${({ theme }) => theme.colors.gray1} solid; 76 | border-radius: 2px; 77 | flex-direction: column; 78 | margin: 3.5rem 0 0 -2rem; 79 | min-height: 0; 80 | max-height: 24rem; 81 | overflow-y: scroll; 82 | overflow-x: hidden; 83 | position: absolute; 84 | width: 19rem; 85 | left: 40%; 86 | border: 1px ${({ theme }) => theme.themeborder} solid; 87 | word-wrap: break-word; 88 | z-index: 6; 89 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 90 | left: 0; 91 | margin: 0; 92 | margin-top: 2.2rem; 93 | width: 100vw; 94 | } 95 | `; 96 | 97 | export const StyledClearBtn = styled.span` 98 | border: none; 99 | color: ${({ theme }) => 100 | (theme.name === "light" && theme.colors.gray4) || "black"}; 101 | cursor: pointer; 102 | font-size: 1.5rem; 103 | right: 44.5%; 104 | margin-bottom: 0.3rem; 105 | position: absolute; 106 | transform: rotate(45deg); 107 | transition: all 0.1s ease; 108 | z-index: 6; 109 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 110 | top: 0; 111 | right: 2%; 112 | } 113 | `; 114 | 115 | export const StyledAddBtn = styled.button` 116 | position: relative; 117 | background: none; 118 | border: none; 119 | border-radius: 50%; 120 | color: gray; 121 | margin-top: 3rem; 122 | cursor: pointer; 123 | outline: none; 124 | align-self: center; 125 | ${(props) => { 126 | switch (props.size) { 127 | case "sm": 128 | return "height: 2px;"; 129 | case "mid": 130 | return "font-size: 2.4rem;margin: 1rem .4rem 1.3rem 1.4rem;"; 131 | case "big": 132 | return "font-size: 3.2rem;margin: 1rem .4rem 1.3rem 1.4rem;"; 133 | } 134 | }} 135 | &:hover { 136 | color: #0095f6; 137 | transition: color 0.2s ease; 138 | } 139 | @media (max-width: ${({ theme }) => theme.breakpoint}) { 140 | display: none; 141 | } 142 | `; 143 | 144 | export const StyledSearchedSub = styled.div` 145 | flex-direction: row; 146 | overflow-wrap: break-word; 147 | padding: 0.6rem; 148 | & > img { 149 | border: none; 150 | border-radius: 50%; 151 | float: left; 152 | height: 2.5rem; 153 | width: 2.5rem; 154 | } 155 | & > b { 156 | margin: 1rem; 157 | } 158 | & > p { 159 | margin: 0; 160 | margin-left: 3.3rem; 161 | padding: 0; 162 | } 163 | `; 164 | 165 | export const StyledSearchedSubContainer = styled.div` 166 | display: flex; 167 | flex-direction: row; 168 | border: none; 169 | border-bottom: 1px ${({ theme }) => theme.colors.gray2} solid; 170 | min-height: 4.8rem; 171 | align-self: center; 172 | overflow-y: hidden; 173 | width: 100%; 174 | justify-content: space-between; 175 | margin-left: 0.1rem; 176 | &:hover { 177 | background: ${({ theme }) => theme.colors.gray1}; 178 | cursor: pointer; 179 | transition: background 0.2s ease; 180 | } 181 | `; 182 | 183 | export const StyledSearchedResultsArrow = styled.div` 184 | &::before { 185 | position: absolute; 186 | bottom: -14.5px; 187 | left: 48%; 188 | width: 0; 189 | height: 0; 190 | margin-top: -10px; 191 | border-top: 15px solid transparent; 192 | border-left: 15px solid transparent; 193 | border-right: 15px solid transparent; 194 | border-bottom: 15px solid ${({ theme }) => theme.bg}; 195 | content: ""; 196 | z-index: 7; 197 | } 198 | `; 199 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import Header from "./Header"; 4 | import { getobj } from "../actions"; 5 | import Menubar from "./Menubar"; 6 | import PostCard from "./PostCard"; 7 | import ViewPost from "./ViewPost"; 8 | import FollowingTab from "./FollowingTab"; 9 | import Trophies from "./Trophies"; 10 | import Settings from "./Settings"; 11 | import debounce from "lodash.debounce"; 12 | import { addSubreddit, removeSubreddit, logout } from "../actions"; 13 | import { 14 | StyledProfile, 15 | StyledProfileInfo, 16 | StyledProfileName, 17 | StyledProfileDetails, 18 | StyledProfilePosts, 19 | StyledProfileIcon, 20 | StyledButton, 21 | StyledInfoBlock, 22 | StyledProfileGallery, 23 | StyledInlineDiv, 24 | StyledLoginBoard, 25 | StyledButtonGroup, 26 | StyledLoader, 27 | } from "../styles/components/profileStyles"; 28 | import { StyledBtnImg } from "../styles/components/headerStyles"; 29 | 30 | class Profile extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | data: {}, 35 | after: undefined, 36 | loading: false, 37 | currPost: {}, 38 | viewPost: false, 39 | datafetched: false, 40 | followingTab: false, 41 | settings: false, 42 | trophies: false, 43 | }; 44 | window.onscroll = debounce(() => { 45 | if ( 46 | window.innerHeight + document.documentElement.scrollTop === 47 | document.documentElement.scrollHeight + 12 48 | ) { 49 | this.setState({ loading: true }); 50 | } 51 | }, 100); 52 | } 53 | 54 | viewPost = (data) => { 55 | this.setState({ viewPost: true, currPost: data }); 56 | }; 57 | hidepost = () => { 58 | this.setState({ viewPost: false, currPost: {} }); 59 | }; 60 | closeTab = (entity) => { 61 | this.setState({ [entity]: false }); 62 | }; 63 | trophies = () => { 64 | this.setState((prevState) => ({ trophies: !prevState.trophies })); 65 | }; 66 | settings = () => { 67 | this.setState((prevState) => ({ settings: !prevState.settings })); 68 | }; 69 | render() { 70 | let { authenticated, user } = this.props.authdata; 71 | return ( 72 |
73 |
74 | {this.state.viewPost && ( 75 | 79 | )} 80 | {this.state.followingTab && ( 81 | 86 | )} 87 | {this.state.trophies && ( 88 | 92 | )} 93 | {this.state.settings && } 94 | 95 | 96 | 97 | 98 |
99 | {`u/${user.name}`} 100 | 101 | this.trophies()} 108 | alt="trophies" 109 | type="trophies" 110 | /> 111 | this.settings()} 118 | alt="settings" 119 | type="settings" 120 | /> 121 | 122 |
123 | 124 | {user.fname} 125 | {authenticated && ( 126 | 127 | 133 | 134 | { 135 | this.props.authdata.user 136 | .total_karma 137 | } 138 | 139 | 140 | )} 141 |

144 | this.setState({ followingTab: true }) 145 | } 146 | > 147 | {this.props.subreddits.length}{" "} 148 | followings 149 |

150 |
151 |
152 |
153 | 154 | 155 | {(authenticated && ( 156 | 159 | this.props.dispatch(logout()) 160 | } 161 | > 162 | logout 163 | 164 | )) || ( 165 | 166 |

You are not logged in

167 | 171 | 172 | Login 173 | 174 | 175 |
176 | )} 177 |
178 | 179 | {Object.entries(this.state.data).map( 180 | ([key, value], index) => { 181 | return ( 182 | 188 | ); 189 | } 190 | )} 191 | 192 |
193 |
194 |
195 | {this.state.loading && ( 196 | 201 | )} 202 |
203 | 204 |
205 | ); 206 | } 207 | } 208 | 209 | const mapStateToProps = (state, props) => { 210 | return { 211 | subreddits: state.subredditReducer.subreddits, 212 | subdata: state.postReducer.data, 213 | authdata: state.authenticationReducer, 214 | access_token: state.authenticationReducer.access_token, 215 | nightmode: state.settingsReducer.nightmode, 216 | }; 217 | }; 218 | 219 | export default connect(mapStateToProps)(Profile); 220 | -------------------------------------------------------------------------------- /src/components/SubredditProfile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import Header from "./Header"; 4 | import { getobj } from "../actions"; 5 | import Menubar from "./Menubar"; 6 | import PostCard from "./PostCard"; 7 | import ViewPost from "./ViewPost"; 8 | import FollowingTab from "./FollowingTab"; 9 | import debounce from "lodash.debounce"; 10 | import { addSubreddit, removeSubreddit } from "../actions"; 11 | import { sub, unsub } from "../actions/api-calls"; 12 | 13 | import { 14 | StyledProfile, 15 | StyledProfileInfo, 16 | StyledProfileName, 17 | StyledProfileDetails, 18 | StyledProfilePosts, 19 | StyledProfileIcon, 20 | StyledProfileGallery, 21 | StyledButton, 22 | StyledLoader, 23 | } from "../styles/components/profileStyles"; 24 | 25 | class SubredditProfile extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | subdetails: {}, 30 | posts: {}, 31 | after: undefined, 32 | loading: false, 33 | currPostId: undefined, 34 | viewPost: false, 35 | subscriberTab: false, 36 | }; 37 | window.onscroll = debounce(() => { 38 | if ( 39 | window.innerHeight + document.documentElement.scrollTop === 40 | document.documentElement.scrollHeight 41 | ) { 42 | this.setState({ loading: true }); 43 | this.loadposts(); 44 | } 45 | }, 100); 46 | this.loadContext = this.loadContext.bind(this); 47 | this.viewPost = this.viewPost.bind(this); 48 | this.hidePost = this.hidePost.bind(this); 49 | this.closeftab = this.closeftab.bind(this); 50 | this.unlisten = this.props.history.listen((location, action) => { 51 | this.setState({ posts: {}, after: undefined }); 52 | this.loadContext(location.pathname); 53 | this.loadposts(location.pathname); 54 | this.forceUpdate(); 55 | }); 56 | } 57 | getObj = (data) => { 58 | const { 59 | id, 60 | title, 61 | created, 62 | description, 63 | name: full_name, 64 | display_name: name, 65 | header_img, 66 | icon_img, 67 | public_description, 68 | subscribers, 69 | banner_background_color, 70 | } = data; 71 | const obj = { 72 | id, 73 | title, 74 | created, 75 | description, 76 | name, 77 | full_name, 78 | header_img, 79 | icon_img, 80 | public_description, 81 | subscribers, 82 | banner_background_color, 83 | }; 84 | return obj; 85 | }; 86 | 87 | viewPost(id) { 88 | this.setState({ viewPost: true, currPostId: id }); 89 | } 90 | hidePost() { 91 | this.setState({ viewPost: false, currPost: {} }); 92 | } 93 | closeftab() { 94 | this.setState({ followingTab: false }); 95 | } 96 | 97 | loadContext(location = `r/${this.props.match.params.subreddit}`) { 98 | fetch(`https://www.reddit.com/${location}/about/.json`) 99 | .then((res) => res.json()) 100 | .then((json) => this.getObj(json.data)) 101 | .then((data) => this.setState({ subdetails: data })) 102 | .catch((err) => { 103 | return; 104 | }); 105 | } 106 | loadposts(subreddit = this.props.match.params.subreddit) { 107 | if (this.props.authenticated) { 108 | const token = this.props.access_token; 109 | var fetchedPosts = {}; 110 | fetch(`https://oauth.reddit.com/r/${subreddit}/new`, { 111 | method: "GET", 112 | headers: { 113 | Authorization: `bearer ${token}`, 114 | "Content-Type": "application/x-www-form-urlencoded", 115 | }, 116 | }) 117 | .then((res) => res.json()) 118 | .then((json) => { 119 | this.setState({ after: json.data.after }); 120 | json.data.children.map((child) => { 121 | Object.assign(fetchedPosts, getobj(child.data)); 122 | }); 123 | }) 124 | .then((posts) => this.setState({ posts: fetchedPosts })) 125 | .catch((err) => 126 | console.log( 127 | "error while fetching posts from ", 128 | subreddit, 129 | err 130 | ) 131 | ); 132 | } else { 133 | fetch( 134 | `https://www.reddit.com/r/${this.props.match.params.subreddit}/.json?limit=7&after=${this.state.after}` 135 | ) 136 | .then((res) => res.json()) 137 | .then((json) => json.data) 138 | .then((data) => { 139 | data.children.map((post) => { 140 | const posts = { 141 | ...this.state.posts, 142 | ...getobj(post.data), 143 | }; 144 | this.setState({ posts, after: data.after }); 145 | }); 146 | }) 147 | .then((wait) => this.setState({ loading: false })) 148 | .catch((err) => { 149 | console.log(err); 150 | this.setState({ loading: false }); 151 | }); 152 | } 153 | } 154 | unsubHandler = () => { 155 | this.props.dispatch(removeSubreddit(this.props.match.params.subreddit)); 156 | if (this.props.authenticated) { 157 | unsub(this.props.access_token, this.state.subdetails.full_name); 158 | } 159 | }; 160 | subHandler = () => { 161 | this.props.dispatch(addSubreddit(this.props.match.params.subreddit)); 162 | if (this.props.authenticated) { 163 | sub(this.props.access_token, this.state.subdetails.full_name); 164 | } 165 | }; 166 | handleChanges = (id, score, likes) => { 167 | this.setState((prevState) => ({ 168 | posts: { 169 | ...prevState.posts, 170 | [id]: { 171 | ...prevState.posts[id], 172 | score, 173 | likes, 174 | }, 175 | }, 176 | })); 177 | }; 178 | componentDidMount() { 179 | this.loadContext(); 180 | this.loadposts(); 181 | } 182 | 183 | componentWillUnmount() { 184 | this.unlisten(); 185 | } 186 | render() { 187 | let subreddit = this.props.match.params.subreddit; 188 | let { 189 | id, 190 | title, 191 | created, 192 | description, 193 | name, 194 | header_img, 195 | icon_img, 196 | public_description, 197 | subscribers, 198 | banner_background_color, 199 | } = this.state.subdetails; 200 | if (icon_img === "") { 201 | icon_img = "/images/icon.png"; 202 | } 203 | return ( 204 |
205 |
206 | {this.state.viewPost && ( 207 | 212 | )} 213 | {this.state.followingTab && ( 214 | 219 | )} 220 | 221 | 222 | 223 | 224 |
225 | {`r/${subreddit}`} 226 | {this.props.subreddits.includes(subreddit) ? ( 227 | this.unsubHandler()} 230 | > 231 | unfollow 232 | 233 | ) : ( 234 | this.subHandler()} 237 | > 238 | Follow 239 | 240 | )} 241 |
242 |
243 |

244 | {subscribers} followers 245 |

246 |
247 | {title} 248 |

249 | {public_description} 250 |

251 |
252 |
253 | 254 | 255 | {Object.entries(this.state.posts).map( 256 | ([key, value], index) => { 257 | return ( 258 | 264 | ); 265 | } 266 | )} 267 | 268 | 269 |
270 |
271 | {this.state.loading && ( 272 | 277 | )} 278 |
279 | 280 |
281 | ); 282 | } 283 | } 284 | 285 | const mapStateToProps = (state, props) => { 286 | return { 287 | subreddits: state.subredditReducer.subreddits, 288 | authenticated: state.authenticationReducer.authenticated, 289 | access_token: state.authenticationReducer.access_token, 290 | }; 291 | }; 292 | 293 | export default connect(mapStateToProps)(SubredditProfile); 294 | -------------------------------------------------------------------------------- /src/components/LoadPost.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import OptTab from "./OptTab"; 5 | import Comments from "./Comments"; 6 | import { upvote, downvote } from "../actions"; 7 | import { editText, deleteCall } from "../actions/api-calls"; 8 | import { StyledBtnImg } from "../styles/components/headerStyles"; 9 | import { 10 | StyledCommentBox, 11 | StyledCommentInput, 12 | StyledPostButton, 13 | } from "../styles/components/commentStyles"; 14 | import { 15 | StyledPostContainer, 16 | StyledPostTop, 17 | StyledPostMid, 18 | StyledInlineButtons, 19 | StyledPostBottom, 20 | } from "../styles/components/postStyles"; 21 | 22 | class LoadPost extends Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | expanded: false, 27 | comments: [], 28 | count: 0, 29 | data: {}, 30 | id_in_scope: undefined, 31 | index_in_scope: undefined, 32 | showopt: false, 33 | editmode: false, 34 | value: "", 35 | }; 36 | this.commentRef = React.createRef(); 37 | } 38 | readMore = (e) => 39 | this.setState({ 40 | expanded: true, 41 | }); 42 | loadComments = (cmnt = "") => { 43 | fetch( 44 | `https://www.reddit.com/r/${this.props.data.subreddit}/comments/${this.props.data.id}.json` 45 | ) 46 | .then((res) => res.json()) 47 | .then((json) => 48 | json[1].data.children.map((data) => { 49 | const { 50 | author, 51 | body: comment, 52 | created, 53 | name, 54 | depth, 55 | id, 56 | replies, 57 | ups, 58 | } = data.data; 59 | let obj = { 60 | author, 61 | comment, 62 | created, 63 | depth, 64 | name, 65 | id, 66 | replies, 67 | ups, 68 | }; 69 | return obj; 70 | }) 71 | ) 72 | .then((comments) => { 73 | let index = comments.length; 74 | Object.assign(comments, { [index]: cmnt }, comments); 75 | this.setState({ comments, count: this.state.count + 10 }); 76 | }); 77 | }; 78 | loadMore = () => this.setState({ count: this.state.count + 10 }); 79 | hideComments = () => this.setState({ count: 0 }); 80 | postComment = () => { 81 | fetch(`https://oauth.reddit.com/api/comment`, { 82 | method: "POST", 83 | headers: { 84 | Authorization: `bearer ${this.props.auth.access_token}`, 85 | "Content-Type": "application/x-www-form-urlencoded", 86 | }, 87 | body: `api_type=json&return_rtjson=true&thing_id=${this.props.data.name}&text=${this.state.value}`, 88 | }) 89 | .then((res) => res.json()) 90 | .then((json) => { 91 | this.setState({ value: "" }); 92 | const { 93 | author, 94 | body: comment, 95 | created, 96 | depth, 97 | id, 98 | replies, 99 | ups, 100 | } = json; 101 | let obj = { 102 | author, 103 | comment, 104 | created, 105 | depth, 106 | id, 107 | replies, 108 | ups, 109 | }; 110 | this.loadComments(obj); 111 | }) 112 | .catch((err) => { 113 | console.log("something went wrong", err); 114 | }); 115 | }; 116 | editComment = () => { 117 | let comments = this.state.comments; 118 | let comment = comments.splice(this.state.index_in_scope, 1); 119 | this.setState((prevState) => ({ 120 | showopt: !prevState.showopt, 121 | editmode: true, 122 | value: comment[0].comment, 123 | comments, 124 | })); 125 | this.commentRef.current.focus(); 126 | }; 127 | submitEdited = () => { 128 | editText( 129 | this.props.auth.access_token, 130 | this.state.id_in_scope, 131 | this.state.value 132 | ); 133 | this.setState({ 134 | value: "", 135 | id_in_scope: undefined, 136 | index_in_scope: undefined, 137 | editmode: false, 138 | expanded: false, 139 | count: 0, 140 | }); 141 | }; 142 | CommentChangeHandler = (e) => { 143 | this.setState({ value: e.target.value }); 144 | }; 145 | _handleKeyDown = (e) => { 146 | if (e.key === "Enter") { 147 | if (this.state.editmode) this.submitEdited(); 148 | else this.postComment(); 149 | } 150 | }; 151 | del_comment = () => { 152 | let comments = this.state.comments; 153 | comments.splice(this.state.index_in_scope, 1); 154 | deleteCall(this.props.auth.access_token, this.state.id_in_scope); 155 | this.setState({ comments, id_in_scope: false, index_in_scope: false }); 156 | }; 157 | toggleOpt = (e, full_name = undefined, index = undefined) => { 158 | if (e.target.getAttribute("type") === "optcanvas") { 159 | this.setState((prevState) => ({ 160 | showopt: !prevState.showopt, 161 | id_in_scope: full_name, 162 | index_in_scope: index, 163 | })); 164 | } 165 | }; 166 | render() { 167 | const ishome = this.props.ishome; 168 | const token = this.props.auth.access_token; 169 | let { 170 | id, 171 | name, 172 | url, 173 | post, 174 | caption, 175 | username, 176 | subreddit, 177 | ups, 178 | likes, 179 | score, 180 | is_video, 181 | } = this.props.data; 182 | const ext = url.substring(url.length - 4, url.length); 183 | let img = 184 | ext === ".jpg" || 185 | ext === ".png" || 186 | ext === ".gif" || 187 | ext === "gifv"; 188 | if (!img && url.includes("imgur")) { 189 | url += ".png"; 190 | img = true; 191 | } 192 | is_video = url.includes("youtu"); 193 | return ( 194 | 195 | {this.state.showopt && ( 196 | 201 | )} 202 | 203 | {ishome ? ( 204 | 208 | 215 | r/{subreddit} 216 | 217 | ) : ( 218 |
222 | u/{username} 223 |
224 | )} 225 |
226 | 227 | {is_video && ( 228 | 236 | )} 237 | {img && ( 238 | 242 | (e.target.src = 243 | "https://pics.me.me/couldnt-load-image-tap-to-retry-15674211.png") 244 | } 245 | /> 246 | )} 247 | {!img && !is_video && ( 248 |

249 | {post} 250 | {!post && {url}} 251 |

252 | )} 253 |
254 | 255 | {this.props.auth.authenticated && ( 256 | 257 | 260 | this.props.dispatch( 261 | upvote( 262 | ishome, 263 | token, 264 | id, 265 | name, 266 | score, 267 | likes 268 | ) 269 | ) 270 | } 271 | src={ 272 | likes 273 | ? "/images/upvote-active.png" 274 | : "/images/upvote.png" 275 | } 276 | /> 277 | 280 | this.props.dispatch( 281 | downvote( 282 | ishome, 283 | token, 284 | id, 285 | name, 286 | score, 287 | likes 288 | ) 289 | ) 290 | } 291 | src={ 292 | likes === false 293 | ? "/images/downvote-active.png" 294 | : "/images/upvote.png" 295 | } 296 | /> 297 | 298 | )} 299 | 300 | {score} {(parseInt(score) > 1 && "upvotes") || "upvote"} 301 | 302 |
303 | {this.state.expanded || caption.length < 80 ? ( 304 |

305 | 306 | u/{username} 307 | 308 |  {caption} 309 |

310 | ) : ( 311 |

312 | 313 | u/{username} 314 | 315 |   316 | {caption.substring(0, 80)} 317 | ... more 318 |

319 | )} 320 | 329 | {this.props.auth.authenticated && ( 330 | 331 | 334 | 339 | this.CommentChangeHandler(e) 340 | } 341 | onKeyDown={this._handleKeyDown} 342 | placeholder="Add Comment" 343 | value={this.state.value} 344 | /> 345 | {(!this.state.editmode && ( 346 | this.postComment()} 349 | > 350 | POST 351 | 352 | )) || ( 353 | this.submitEdited()} 356 | > 357 | EDIT 358 | 359 | )} 360 | 361 | )} 362 |
363 |
364 |
365 | ); 366 | } 367 | } 368 | 369 | export default connect()(LoadPost); 370 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_SUBREDDIT, 3 | ADD_SUBREDDITS, 4 | SELECT_SUBREDDIT, 5 | LOGGED_IN, 6 | UPDATE_TOKENS, 7 | UPVOTE, 8 | DOWNVOTE, 9 | DESELECT_SUBREDDIT, 10 | NIGHTMODE, 11 | LOAD_HOMEPAGE, 12 | LOGOUT, 13 | HOMEPAGE, 14 | HOMEPAGEPOST_UPVOTE, 15 | HOMEPAGEPOST_DOWNVOTE, 16 | REQUEST_POSTS, 17 | RECEIVE_POSTS, 18 | REMOVE_SUBREDDIT, 19 | } from "./action-types"; 20 | import store from "../store"; 21 | import { 22 | getUserInfo, 23 | getSubInfo, 24 | getSubreddit, 25 | refreshData, 26 | } from "./api-calls"; 27 | 28 | const request_posts = (subreddit) => ({ 29 | type: REQUEST_POSTS, 30 | payload: { subreddit }, 31 | }); 32 | 33 | const receive_posts = (subreddit, items, posts, after) => ({ 34 | type: RECEIVE_POSTS, 35 | payload: { 36 | subreddit, 37 | items, 38 | posts, 39 | after, 40 | lastUpdated: Date.now(), 41 | }, 42 | }); 43 | 44 | export const homepage = (fetchedPosts, after) => ({ 45 | type: HOMEPAGE, 46 | payload: { 47 | fetchedPosts, 48 | after, 49 | lastUpdated: Date.now(), 50 | }, 51 | }); 52 | 53 | export const select_subreddit = (subreddit) => ({ 54 | type: SELECT_SUBREDDIT, 55 | payload: { subreddit }, 56 | }); 57 | 58 | export const deselect_subreddit = () => ({ 59 | type: DESELECT_SUBREDDIT, 60 | }); 61 | 62 | const add_subreddit = (subreddit, icon, authenticated = false) => ({ 63 | type: ADD_SUBREDDIT, 64 | payload: { subreddit, icon, authenticated }, 65 | }); 66 | 67 | const add_subreddits = (subs, subdata) => ({ 68 | type: ADD_SUBREDDITS, 69 | payload: { subs, subdata }, 70 | }); 71 | 72 | export const remove_subreddit = (subreddit) => ({ 73 | type: REMOVE_SUBREDDIT, 74 | payload: { subreddit }, 75 | }); 76 | 77 | export const logged_in = (data, userInfo) => ({ 78 | type: LOGGED_IN, 79 | payload: { data, userInfo }, 80 | }); 81 | 82 | export const getobj = (data) => { 83 | const { 84 | id, 85 | name, 86 | created, 87 | title: caption, 88 | selftext: post, 89 | author: username, 90 | downs, 91 | score, 92 | num_comments, 93 | subreddit, 94 | ups, 95 | likes, 96 | url, 97 | is_video, 98 | } = data; 99 | let obj = {}; 100 | obj[id] = { 101 | id, 102 | name, 103 | created, 104 | caption, 105 | post, 106 | username, 107 | subreddit, 108 | num_comments, 109 | ups, 110 | downs, 111 | likes, 112 | score, 113 | url, 114 | is_video, 115 | hovering: false, 116 | }; 117 | return obj; 118 | }; 119 | 120 | export const fetchPosts = (subreddit) => { 121 | return async (dispatch, getState) => { 122 | const token = getState().authenticationReducer.access_token; 123 | const data = getState().postReducer.data; 124 | let after = undefined; 125 | if (data.hasOwnProperty(subreddit)) { 126 | after = getState().postReducer.data[subreddit].after; 127 | } 128 | dispatch(request_posts(subreddit)); 129 | if (getState().authenticationReducer.authenticated) { 130 | return await fetch( 131 | `https://oauth.reddit.com/r/${subreddit}/new?after=${after}`, 132 | { 133 | method: "GET", 134 | headers: { 135 | Authorization: `bearer ${token}`, 136 | "Content-Type": "application/x-www-form-urlencoded", 137 | }, 138 | } 139 | ) 140 | .then((res) => res.json()) 141 | .then((json) => { 142 | after = json.data.after; 143 | let posts = {}; 144 | let items = []; 145 | json.data.children.map((child) => { 146 | items.push(child.data.id); 147 | posts = { ...posts, ...getobj(child.data) }; 148 | }); 149 | return { items, posts, after }; 150 | }) 151 | .then(({ items, posts, after }) => { 152 | dispatch(receive_posts(subreddit, items, posts, after)); 153 | }) 154 | .catch((err) => 155 | console.log("error while fetching posts from ", subreddit) 156 | ); 157 | } else { 158 | return fetch( 159 | `https://www.reddit.com/r/${subreddit}.json?after=${after}` 160 | ) 161 | .then((resp) => resp.json()) 162 | .then((json) => { 163 | after = json.data.after; 164 | let items = []; 165 | let posts = {}; 166 | json.data.children.map((child) => { 167 | items.push(child.data.id); 168 | posts = { ...posts, ...getobj(child.data) }; 169 | }); 170 | return { items, posts, after }; 171 | }) 172 | .then(({ items, posts, after }) => 173 | dispatch(receive_posts(subreddit, items, posts, after)) 174 | ) 175 | .catch((err) => 176 | console.log("error while fetching posts from ", subreddit) 177 | ); 178 | } 179 | }; 180 | }; 181 | 182 | export const getHomePage = () => { 183 | return async (dispatch, getState) => { 184 | dispatch({ type: LOAD_HOMEPAGE }); 185 | const token = getState().authenticationReducer.access_token; 186 | const after = getState().postReducer.homepage.after; 187 | var posts = {}; 188 | if (getState().authenticationReducer.authenticated) { 189 | fetch(`https://oauth.reddit.com/new?after=${after}`, { 190 | method: "GET", 191 | headers: { 192 | Authorization: `bearer ${token}`, 193 | "Content-Type": "application/x-www-form-urlencoded", 194 | }, 195 | }) 196 | .then((res) => res.json()) 197 | .then((json) => { 198 | const after = json.data.after; 199 | json.data.children.map((child) => { 200 | let post = getobj(child.data); 201 | posts = { ...posts, ...post }; 202 | }); 203 | return { posts, after }; 204 | }) 205 | .then(({ posts, after }) => dispatch(homepage(posts, after))) 206 | .catch((err) => console.log("error while fetching homepage")); 207 | } else { 208 | fetch(`https://www.reddit.com/.json?after=${after}`) 209 | .then((resp) => resp.json()) 210 | .then((json) => { 211 | const after = json.data.after; 212 | json.data.children.map((child) => { 213 | let post = getobj(child.data); 214 | posts = { ...posts, ...post }; 215 | }); 216 | return { posts, after }; 217 | }) 218 | .then(({ posts, after }) => dispatch(homepage(posts, after))) 219 | .catch((err) => 220 | console.log("error while fetching homepage", err) 221 | ); 222 | } 223 | }; 224 | }; 225 | 226 | export const addSubreddit = (subreddit) => { 227 | return (dispatch, getState) => { 228 | return fetch(`https://www.reddit.com/r/${subreddit}/about.json`) 229 | .then((resp) => resp.json()) 230 | .then((json) => json.data) 231 | .then((data) => { 232 | let { icon_img } = data; 233 | if (!icon_img.length) 234 | icon_img = "https://i.redd.it/130am13nj6201.png"; 235 | dispatch( 236 | add_subreddit( 237 | subreddit, 238 | icon_img, 239 | getState().authenticationReducer.authenticated 240 | ) 241 | ); 242 | }) 243 | .catch((err) => console.log("error while adding subreddit", err)); 244 | }; 245 | }; 246 | 247 | export const addSubreddits = (subInfo, authenticated = false) => { 248 | let subdata = {}; 249 | let subs = []; 250 | subInfo.subs.map((sub) => { 251 | subs.push(sub.name); 252 | subdata[sub.name] = { 253 | fetching: false, 254 | items: [], 255 | icon: sub.icon, 256 | lastUpdate: Date.now(), 257 | }; 258 | }); 259 | return (dispatch, getState) => { 260 | dispatch(add_subreddits(subs, subdata)); 261 | }; 262 | }; 263 | 264 | export const removeSubreddit = (subreddit) => { 265 | return (dispatch, getState) => { 266 | dispatch(remove_subreddit(subreddit)); 267 | }; 268 | }; 269 | 270 | export const loggedIn = (data) => { 271 | return async (dispatch, getState) => { 272 | const token = data.access_token; 273 | const userInfo = await getUserInfo(token); 274 | dispatch(logged_in(data, userInfo)); 275 | dispatch(getHomePage()); 276 | dispatch(nightMode(userInfo.pref_nightmode)); 277 | setTimeout(() => { 278 | getRefreshToken(data.access_token, data.refresh_token); 279 | }, 3540000); 280 | window.localStorage.setItem("data", JSON.stringify(data)); 281 | window.localStorage.setItem("nightMode", userInfo.pref_nightmode); 282 | const subInfo = await getSubInfo(token); 283 | dispatch(addSubreddits(subInfo)); 284 | }; 285 | }; 286 | 287 | export const logout = () => { 288 | window.localStorage.removeItem("data"); 289 | window.localStorage.removeItem("nightMode"); 290 | return (dispatch, getState) => { 291 | dispatch({ type: LOGOUT }); 292 | }; 293 | }; 294 | 295 | export const upvote = (ishome = false, token, id, name, score, likes) => { 296 | if (likes) { 297 | var dir = 0; 298 | var likes = null; 299 | var score = score - 1; 300 | } else if (likes === false) { 301 | var dir = 0; 302 | var likes = true; 303 | var score = score + 2; 304 | } else { 305 | var dir = 1; 306 | var likes = true; 307 | var score = score + 1; 308 | } 309 | fetch("https://oauth.reddit.com/api/vote", { 310 | method: "POST", 311 | headers: { 312 | Authorization: `bearer ${token}`, 313 | "Content-Type": "application/x-www-form-urlencoded", 314 | }, 315 | body: `dir=${dir}&id=${name}&rank=3`, 316 | }).catch((err) => console.log("error occured while upvoting: ", err)); 317 | if (ishome) { 318 | return (dispatch, getState) => { 319 | dispatch({ 320 | type: HOMEPAGEPOST_UPVOTE, 321 | payload: { id, likes, score }, 322 | }); 323 | }; 324 | } 325 | return (dispatch, getState) => { 326 | dispatch({ type: UPVOTE, payload: { id, likes, score } }); 327 | }; 328 | }; 329 | 330 | export const downvote = (ishome, token, id, name, score, likes) => { 331 | if (likes) { 332 | var dir = -1; 333 | var likes = false; 334 | var score = score - 2; 335 | } else if (likes === false) { 336 | var dir = 0; 337 | var likes = null; 338 | var score = score + 1; 339 | } else { 340 | var dir = -1; 341 | var likes = false; 342 | var score = score - 1; 343 | } 344 | fetch("https://oauth.reddit.com/api/vote", { 345 | method: "POST", 346 | headers: { 347 | Authorization: `bearer ${token}`, 348 | "Content-Type": "application/x-www-form-urlencoded", 349 | }, 350 | body: `dir=${dir}&id=${name}&rank=3`, 351 | }).catch((err) => console.log("error occured while downvoting: ", err)); 352 | if (ishome) { 353 | return (dispatch, getState) => { 354 | dispatch({ 355 | type: HOMEPAGEPOST_DOWNVOTE, 356 | payload: { id, likes, score }, 357 | }); 358 | }; 359 | } 360 | return (dispatch, getState) => { 361 | dispatch({ type: DOWNVOTE, payload: { id, likes, score } }); 362 | }; 363 | }; 364 | 365 | export const nightMode = (toggleState = "not_specified") => { 366 | if (toggleState === "not_specified") { 367 | return (dispatch, getState) => { 368 | dispatch({ 369 | type: NIGHTMODE, 370 | payload: { nightmode: !getState().settingsReducer.nightmode }, 371 | }); 372 | }; 373 | } else { 374 | return (dispatch, getState) => { 375 | dispatch({ type: NIGHTMODE, payload: { nightmode: toggleState } }); 376 | }; 377 | } 378 | }; 379 | 380 | export const getRefreshToken = async (access_token, refresh_token) => { 381 | refreshData(access_token, refresh_token).then(async (data) => { 382 | store.dispatch({ type: UPDATE_TOKENS, payload: data }); 383 | await new Promise((r) => setTimeout(r, 3540000)); 384 | if (store.getState().authenticationReducer.authenticated) 385 | await getRefreshToken(data.access_token, refresh_token); 386 | }); 387 | }; 388 | -------------------------------------------------------------------------------- /src/components/ViewPost.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { NavLink, withRouter } from "react-router-dom"; 4 | import Comments from "./Comments"; 5 | import OptTab from "./OptTab"; 6 | import { upvote, downvote } from "../actions"; 7 | import { editText, deleteCall } from "../actions/api-calls"; 8 | import store from "../store"; 9 | import { addSubreddit, removeSubreddit } from "../actions"; 10 | import { StyledBtnImg } from "../styles/components/headerStyles"; 11 | import { 12 | StyledViewPostContent, 13 | StyledViewPostDetails, 14 | StyledCommentSection, 15 | StyledDetailsFooter, 16 | StyledDetailSection, 17 | StyledVPTextPost, 18 | StyledVPCloseBtn, 19 | StyledPostHead, 20 | StyledFollowBtn, 21 | } from "../styles/components/viewPostStyles"; 22 | import { StyledCanvas } from "../styles/components/tabStyles"; 23 | import { 24 | StyledCommentBox, 25 | StyledCommentInput, 26 | StyledPostButton, 27 | } from "../styles/components/commentStyles"; 28 | 29 | class ViewPost extends React.Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | count: 0, 34 | comments: [], 35 | id_in_scope: undefined, 36 | index_in_scope: undefined, 37 | showopt: false, 38 | value: "", 39 | }; 40 | this.commentRef = React.createRef(); 41 | this.loadComments = this.loadComments.bind(this); 42 | this.loadMore = this.loadMore.bind(this); 43 | this.hideComments = this.hideComments.bind(this); 44 | } 45 | editComment = () => { 46 | let comments = this.state.comments; 47 | let comment = comments.splice(this.state.index_in_scope, 1); 48 | this.setState((prevState) => ({ 49 | showopt: !prevState.showopt, 50 | editmode: true, 51 | value: comment[0].comment, 52 | comments, 53 | })); 54 | this.commentRef.current.focus(); 55 | }; 56 | submitEdited = () => { 57 | editText( 58 | this.props.auth.access_token, 59 | this.state.id_in_scope, 60 | this.state.value 61 | ); 62 | this.setState({ 63 | value: "", 64 | id_in_scope: undefined, 65 | index_in_scope: undefined, 66 | editmode: false, 67 | expanded: false, 68 | count: 0, 69 | }); 70 | }; 71 | del_comment = () => { 72 | let comments = this.state.comments; 73 | comments.splice(this.state.index_in_scope, 1); 74 | deleteCall(this.props.auth.access_token, this.state.id_in_scope); 75 | this.setState({ comments, id_in_scope: false, index_in_scope: false }); 76 | }; 77 | loadComments = (cmnt = "") => { 78 | fetch( 79 | `https://www.reddit.com/r/${this.props.data.subreddit}/comments/${this.props.data.id}.json` 80 | ) 81 | .then((res) => res.json()) 82 | .then((json) => 83 | json[1].data.children.map((data) => { 84 | const { 85 | author, 86 | body: comment, 87 | created, 88 | depth, 89 | name, 90 | id, 91 | replies, 92 | ups, 93 | } = data.data; 94 | let obj = { 95 | author, 96 | comment, 97 | created, 98 | depth, 99 | name, 100 | id, 101 | replies, 102 | ups, 103 | }; 104 | return obj; 105 | }) 106 | ) 107 | .then((comments) => { 108 | let index = comments.length; 109 | Object.assign(comments, { [index]: cmnt }, comments); 110 | this.setState({ comments, count: this.state.count + 10 }); 111 | }); 112 | }; 113 | handleUpvote = (token, id, name, score, likes) => { 114 | this.props.dispatch(upvote(token, id, name, score, likes)); 115 | if (store.getState().postReducer.posts.hasOwnProperty(id)) { 116 | const { score, likes } = store.getState().postReducer.posts[id]; 117 | this.props.handleChanges(id, score, likes); 118 | } 119 | }; 120 | handleDownvote = (token, id, name, score, likes) => { 121 | this.props.dispatch(downvote(token, id, name, score, likes)); 122 | if (store.getState().postReducer.posts.hasOwnProperty(id)) { 123 | const { score, likes } = store.getState().postReducer.posts[id]; 124 | this.props.handleChanges(id, score, likes); 125 | } 126 | }; 127 | loadMore = () => this.setState({ count: this.state.count + 10 }); 128 | hideComments = () => this.setState({ count: 0 }); 129 | 130 | handleCloseSettings = (e) => { 131 | if (e.target.getAttribute("type") === "canvas") { 132 | this.props.hidePost(); 133 | } 134 | }; 135 | 136 | toggleOpt = (e, full_name = undefined) => { 137 | if (e.target.getAttribute("type") === "optcanvas") { 138 | this.setState((prevState) => ({ 139 | showopt: !prevState.showopt, 140 | id_in_scope: full_name, 141 | })); 142 | } 143 | }; 144 | postComment = () => { 145 | fetch(`https://oauth.reddit.com/api/comment`, { 146 | method: "POST", 147 | headers: { 148 | Authorization: `bearer ${this.props.auth.access_token}`, 149 | "Content-Type": "application/x-www-form-urlencoded", 150 | }, 151 | body: `api_type=json&return_rtjson=true&thing_id=${this.props.data.name}&text=${this.state.value}`, 152 | }) 153 | .then((res) => res.json()) 154 | .then((json) => { 155 | this.setState({ value: "" }); 156 | const { 157 | author, 158 | body: comment, 159 | created, 160 | depth, 161 | name, 162 | id, 163 | replies, 164 | ups, 165 | } = json; 166 | let obj = { 167 | author, 168 | comment, 169 | created, 170 | name, 171 | depth, 172 | id, 173 | replies, 174 | ups, 175 | }; 176 | this.loadComments(obj); 177 | }) 178 | .catch((err) => { 179 | console.log("something went wrong", err); 180 | }); 181 | }; 182 | CommentChangeHandler = (e) => { 183 | this.setState({ value: e.target.value }); 184 | }; 185 | _handleKeyDown = (e) => { 186 | if (e.key === "Enter") { 187 | if (this.state.editmode) this.submitEdited(); 188 | else this.postComment(); 189 | } 190 | }; 191 | render() { 192 | let token = this.props.auth.access_token; 193 | let { 194 | id, 195 | name, 196 | url, 197 | post, 198 | caption, 199 | username, 200 | subreddit, 201 | ups, 202 | likes, 203 | score, 204 | is_video, 205 | } = this.props.data; 206 | const ext = url.substring(url.length - 4, url.length); 207 | let img = 208 | ext === ".jpg" || 209 | ext === ".png" || 210 | ext === ".gif" || 211 | ext === "gifv"; 212 | if (!img && url.includes("imgur")) { 213 | url += ".png"; 214 | img = true; 215 | } 216 | is_video = url.includes("youtu"); 217 | 218 | return ( 219 | this.handleCloseSettings(e)} 222 | > 223 | {this.state.showopt && ( 224 | 229 | )} 230 | 231 | + 232 | 233 | 234 | 235 | 236 | {(this.props.location.pathname === 237 | `/r/${this.props.data.subreddit}` && ( 238 | 239 | r/{subreddit} 240 | 241 | )) || ( 242 | 246 | r/{subreddit} 247 | 248 | )} 249 | {this.props.subreddits.includes(subreddit) ? ( 250 | 253 | this.props.dispatch( 254 | removeSubreddit(subreddit) 255 | ) 256 | } 257 | > 258 | unfollow 259 | 260 | ) : ( 261 | 263 | this.props.dispatch( 264 | addSubreddit(subreddit) 265 | ) 266 | } 267 | > 268 | Follow 269 | 270 | )} 271 | 272 | 273 | {img && post} 274 | {is_video && ( 275 | 284 | )} 285 | {!img && !is_video && ( 286 | 287 | {caption} 288 | {post.substring(0, 400)} 289 |

290 | {!post && ( 291 | 297 | {url} 298 | 299 | )} 300 |

301 |
302 | )} 303 |
304 | 305 | 306 | {(this.props.location.pathname === 307 | `/r/${this.props.data.subreddit}` && ( 308 | r/{subreddit} 309 | )) || ( 310 | 314 | r/{subreddit} 315 | 316 | )} 317 | {this.props.subreddits.includes(subreddit) ? ( 318 | 321 | this.props.dispatch( 322 | removeSubreddit(subreddit) 323 | ) 324 | } 325 | > 326 | unfollow 327 | 328 | ) : ( 329 | 331 | this.props.dispatch(addSubreddit(subreddit)) 332 | } 333 | > 334 | Follow 335 | 336 | )} 337 | 338 | 339 | u/{username}   340 | {caption} 341 | 350 | 351 | 352 | {this.props.auth.authenticated && ( 353 |
354 | 357 | this.handleUpvote( 358 | token, 359 | id, 360 | name, 361 | score, 362 | likes 363 | ) 364 | } 365 | src={ 366 | likes 367 | ? "/images/upvote-active.png" 368 | : "/images/upvote.png" 369 | } 370 | /> 371 | 374 | this.handleDownvote( 375 | token, 376 | id, 377 | name, 378 | score, 379 | likes 380 | ) 381 | } 382 | src={ 383 | likes === false 384 | ? "/images/downvote-active.png" 385 | : "/images/upvote.png" 386 | } 387 | /> 388 |
389 | )} 390 | 391 | {score}{" "} 392 | {(parseInt(score) > 1 && "upvotes") || "upvote"} 393 | 394 | {this.props.auth.authenticated && ( 395 | 396 | 399 | 404 | this.CommentChangeHandler(e) 405 | } 406 | onKeyDown={this._handleKeyDown} 407 | placeholder="Add Comment" 408 | value={this.state.value} 409 | /> 410 | {(!this.state.editmode && ( 411 | this.postComment()} 414 | > 415 | POST 416 | 417 | )) || ( 418 | this.submitEdited()} 421 | > 422 | EDIT 423 | 424 | )} 425 | 426 | )} 427 |
428 |
429 |
430 | ); 431 | } 432 | } 433 | 434 | const mapStateToProps = (state) => { 435 | return { 436 | subreddits: state.subredditReducer.subreddits, 437 | auth: state.authenticationReducer, 438 | posts: state.postReducer.posts, 439 | }; 440 | }; 441 | 442 | export default withRouter(connect(mapStateToProps)(ViewPost)); 443 | --------------------------------------------------------------------------------