├── .gitignore
├── .netlify
└── state.json
├── README.md
├── netlify.toml
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── GlobalStyle.ts
├── components
│ ├── auth
│ │ └── AuthProvider.tsx
│ ├── common
│ │ └── TextField.tsx
│ ├── layout
│ │ ├── Loading.tsx
│ │ ├── Navbar.tsx
│ │ ├── Ripple.tsx
│ │ ├── SubNav.tsx
│ │ └── SubSubNav.tsx
│ ├── leftnavigation
│ │ ├── LeftNav.tsx
│ │ └── LeftNavSearch.tsx
│ ├── post
│ │ ├── Comment.tsx
│ │ ├── CommentItemMenu.tsx
│ │ ├── Comments.tsx
│ │ └── Post.tsx
│ ├── rightnavigation
│ │ └── RightNav.tsx
│ ├── style
│ │ └── basicStyles.ts
│ ├── subreddit
│ │ ├── GettingMorePosts.tsx
│ │ ├── NoMorePosts.tsx
│ │ ├── PageIndicator.tsx
│ │ ├── Subreddit.tsx
│ │ └── SubredditPost.tsx
│ └── user
│ │ ├── User.tsx
│ │ ├── UserComment.tsx
│ │ ├── UserHeader.tsx
│ │ ├── UserPosts.tsx
│ │ └── UserTrophies.tsx
├── context
│ ├── auth
│ │ ├── AuthState.tsx
│ │ ├── authContext.ts
│ │ ├── authReducer.ts
│ │ └── authTypes.ts
│ ├── reddit
│ │ ├── RedditState.tsx
│ │ ├── redditContext.ts
│ │ ├── redditReducer.ts
│ │ └── redditTypes.ts
│ ├── types.ts
│ └── user
│ │ ├── UserState.tsx
│ │ ├── userContext.ts
│ │ ├── userReducer.ts
│ │ └── userTypes.ts
├── hooks
│ └── useDebouncedRippleCleanup.ts
├── index.tsx
├── react-app-env.d.ts
├── redux
│ ├── actions
│ │ ├── loadingActions.ts
│ │ └── themeActions.ts
│ ├── reducers
│ │ ├── index.ts
│ │ ├── loadingReducer.ts
│ │ └── themeReducer.ts
│ └── store.ts
├── styled.d.ts
├── themes
│ ├── MyThemeProvider.tsx
│ └── my-theme.ts
└── utils
│ ├── customEase.ts
│ ├── decodeHtml.ts
│ ├── defaultSubredditsParser.ts
│ ├── setAuthToken.ts
│ ├── subredditParser.ts
│ └── variants.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | .env
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/.netlify/state.json:
--------------------------------------------------------------------------------
1 | {
2 | "siteId": "1863a944-b7ac-4c49-b77a-8fd700099b07"
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Demo
2 | https://redditsyncr.netlify.app/
3 |
4 | Built for mobile, this is a Reddit Sync clone that runs in the browser. This app is an alternative way to view one of the most popular websites on the internet, Reddit. All the data is fetched from Reddit's robust api. It features custom transitions made with FramerMotion and Styled Components. The state is handled by Redux, and React's context API. Credit for the design goes to the Reddit Sync team.
5 |
6 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
7 |
8 | ## Available Scripts
9 |
10 | In the project directory, you can run:
11 |
12 | ### `yarn start`
13 |
14 | Runs the app in the development mode.
15 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
16 |
17 | The page will reload if you make edits.
18 | You will also see any lint errors in the console.
19 |
20 | ### `yarn test`
21 |
22 | Launches the test runner in the interactive watch mode.
23 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
24 |
25 | ### `yarn build`
26 |
27 | Builds the app for production to the `build` folder.
28 | It correctly bundles React in production mode and optimizes the build for the best performance.
29 |
30 | The build is minified and the filenames include the hashes.
31 | Your app is ready to be deployed!
32 |
33 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
34 |
35 | ### `yarn eject`
36 |
37 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
38 |
39 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
40 |
41 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
42 |
43 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
44 |
45 | ## Learn More
46 |
47 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
48 |
49 | To learn React, check out the [React documentation](https://reactjs.org/).
50 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish="build"
3 |
4 |
5 | [[redirects]]
6 | from = "/*"
7 | to = "/index.html"
8 | status = 200
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reddit-refactor",
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 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.12.47",
11 | "@types/qs": "^6.9.3",
12 | "@types/react": "^16.9.36",
13 | "@types/react-dom": "^16.9.8",
14 | "@types/react-redux": "^7.1.9",
15 | "@types/react-router-dom": "^5.1.5",
16 | "@types/styled-components": "^5.1.0",
17 | "@types/uuid": "^8.0.0",
18 | "axios": "^0.21.2",
19 | "framer-motion": "^1.11.1",
20 | "moment": "^2.26.0",
21 | "moment-timezone": "^0.5.31",
22 | "qs": "^6.9.4",
23 | "react": "^16.13.1",
24 | "react-dom": "^16.13.1",
25 | "react-intersection-observer": "^8.26.2",
26 | "react-moment": "^0.9.7",
27 | "react-redux": "^7.2.0",
28 | "react-router-dom": "^5.2.0",
29 | "react-scripts": "3.4.1",
30 | "react-swipeable": "^5.5.1",
31 | "redux": "^4.0.5",
32 | "redux-devtools-extension": "^2.13.8",
33 | "redux-thunk": "^2.3.0",
34 | "styled-components": "^5.1.1",
35 | "styled-normalize": "^8.0.7",
36 | "typescript": "~3.7.2",
37 | "uuid": "^8.1.0"
38 | },
39 | "scripts": {
40 | "start": "react-scripts start",
41 | "build": "react-scripts build",
42 | "test": "react-scripts test",
43 | "eject": "react-scripts eject"
44 | },
45 | "eslintConfig": {
46 | "extends": "react-app"
47 | },
48 | "browserslist": {
49 | "production": [
50 | ">0.2%",
51 | "not dead",
52 | "not op_mini all"
53 | ],
54 | "development": [
55 | "last 1 chrome version",
56 | "last 1 firefox version",
57 | "last 1 safari version"
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dustinkiselbach/reddit-refactor/3cb8a79b4a39a4973fc0b90c242d46b0e4553c01/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
20 |
24 |
25 | RedditSyncr
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dustinkiselbach/reddit-refactor/3cb8a79b4a39a4973fc0b90c242d46b0e4553c01/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dustinkiselbach/reddit-refactor/3cb8a79b4a39a4973fc0b90c242d46b0e4553c01/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "RedditSyncr",
3 | "name": "RedditSyncr",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
3 |
4 | import AuthState from './context/auth/AuthState'
5 | import RedditState from './context/reddit/RedditState'
6 | import UserState from './context/user/UserState'
7 |
8 | import { GlobalStyle } from './GlobalStyle'
9 |
10 | import { Navbar } from './components/layout/Navbar'
11 | import { Subreddit } from './components/subreddit/Subreddit'
12 | import { Post } from './components/post/Post'
13 |
14 | import { store } from './redux/store'
15 | import { Provider } from 'react-redux'
16 | import { MyThemeProvider } from './themes/MyThemeProvider'
17 | import { User } from './components/user/User'
18 | import { AuthProvider } from './components/auth/AuthProvider'
19 |
20 | const App: React.FC = () => {
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | >
49 | )
50 | }
51 |
52 | export default App
53 |
--------------------------------------------------------------------------------
/src/GlobalStyle.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 | import { normalize } from 'styled-normalize'
3 |
4 | export const GlobalStyle = createGlobalStyle`
5 | ${normalize}
6 | * {
7 | text-decoration: none;
8 |
9 | }
10 |
11 | html {
12 | box-sizing: border-box;
13 | -webkit-font-smoothing: antialiased;
14 | font-size: 12px;
15 |
16 | }
17 |
18 | body {
19 | font-family: 'Open Sans', sans-serif;
20 | font-weight: 400;
21 | background: ${props => props.theme.colors.backgroundColor};
22 | color: ${props => props.theme.colors.textColor};
23 |
24 | ul {
25 | list-style: none;
26 | margin: 0;
27 | padding: 0;
28 | }
29 |
30 | a {
31 | color: ${props => props.theme.colors.textColor};
32 | }
33 | }
34 |
35 | `
36 |
--------------------------------------------------------------------------------
/src/components/auth/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import AuthContext from '../../context/auth/authContext'
3 |
4 | interface AuthProviderProps {
5 | children: React.ReactNode
6 | }
7 |
8 | export const AuthProvider: React.FC = ({ children }) => {
9 | const authContext = useContext(AuthContext)
10 |
11 | return <>{authContext.authenticated ? children : }>
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/common/TextField.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | interface TextFieldProps {
5 | placeholder: string
6 | onChange: (e: React.ChangeEvent) => void
7 | value: string
8 | }
9 |
10 | export const TextField: React.FC = ({
11 | placeholder,
12 | value,
13 | onChange
14 | }) => {
15 | return (
16 |
22 | )
23 | }
24 |
25 | const Input = styled.input`
26 | padding: 1rem 0;
27 | width: calc(100% - 2rem);
28 | background-color: ${props => props.theme.colors.backgroundColor};
29 | border: none;
30 | border-bottom: 1px solid ${props => props.theme.colors.textColorFaded};
31 | font-family: inherit;
32 | color: ${props => props.theme.colors.textColor};
33 | font-weight: bold;
34 | overflow: visible;
35 |
36 | &::placeholder {
37 | color: ${props => props.theme.colors.textColorFaded};
38 | }
39 |
40 | &:focus {
41 | outline: none;
42 | }
43 | `
44 |
--------------------------------------------------------------------------------
/src/components/layout/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 |
4 | export const Loading: React.FC = () => {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | const Container = styled.div`
13 | height: 100vh;
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | text-align: center;
18 | `
19 |
20 | const rotate = keyframes`
21 | from {
22 | transform: rotate(0deg);
23 | }
24 |
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | `
29 |
30 | const Spinner = styled.div`
31 | display: inline-block;
32 | width: 80px;
33 | height: 80px;
34 |
35 | &::after {
36 | content: ' ';
37 | display: block;
38 | width: 64px;
39 | height: 64px;
40 | margin: 8px;
41 | border-radius: 50%;
42 | border: 6px solid #fff;
43 | border-color: #fff transparent #fff transparent;
44 | animation: ${rotate} 0.76s linear infinite;
45 | }
46 | `
47 |
--------------------------------------------------------------------------------
/src/components/layout/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from 'react'
2 | import styled from 'styled-components'
3 | import RedditContext from '../../context/reddit/redditContext'
4 | import UserContext from '../../context/user/userContext'
5 | import { SubNav } from './SubNav'
6 | import { AnimatePresence } from 'framer-motion'
7 | import { LeftNav } from '../leftnavigation/LeftNav'
8 | import { useHistory } from 'react-router-dom'
9 | import { useSwipeable } from 'react-swipeable'
10 | import { RightNav } from '../rightnavigation/RightNav'
11 | import { SubSubNav } from './SubSubNav'
12 |
13 | interface Props {}
14 |
15 | const postsOptions = ['hot', 'new', 'rising', 'top', 'controversial']
16 | const commentsOptions = ['top', 'best', 'new', 'old', 'controversial', 'Q&A']
17 | const userOptions = ['new', 'hot', 'top', 'controversial']
18 |
19 | export const Navbar: React.FC = () => {
20 | const [prevScrollPos, setScrollPos] = useState(window.pageYOffset)
21 | const [visible, setVisible] = useState(true)
22 | const [showSort, setShowSort] = useState(false)
23 | const [showSubSort, setShowSubSort] = useState(false)
24 | const [sortLabel, setSortLabel] = useState(null)
25 | const [showLeft, setShowLeft] = useState(false)
26 | const [showRight, setShowRight] = useState(false)
27 |
28 | const handlers = useSwipeable({
29 | onSwipedLeft: () => setShowLeft(false),
30 | onSwipedRight: () => setShowRight(false)
31 | })
32 |
33 | const rightHandler = useSwipeable({
34 | onSwipedRight: () => setShowLeft(true)
35 | })
36 |
37 | const leftHandler = useSwipeable({
38 | onSwipedLeft: () => setShowRight(true)
39 | })
40 |
41 | let history = useHistory()
42 |
43 | const redditContext = useContext(RedditContext)
44 | const {
45 | trendingSubreddits,
46 | searchTerm,
47 | subreddit,
48 | subredditInfo,
49 | sortBy,
50 | sortByInterval,
51 | sortCommentsBy,
52 | defaultSubreddits,
53 | basicSubreddits,
54 | autocompleteSubreddits,
55 | post,
56 | clearPosts,
57 | changeSortBy,
58 | changeSearchTerm,
59 | changeSortCommentsBy,
60 | setSubreddit,
61 | subredditAutocomplete
62 | } = redditContext
63 |
64 | const userContext = useContext(UserContext)
65 | const {
66 | userName,
67 | sortUserContentBy,
68 | getUserPosts,
69 | changeSortUserContentBy
70 | } = userContext
71 |
72 | useEffect(() => {
73 | window.addEventListener('scroll', handleScroll)
74 |
75 | return () => window.removeEventListener('scroll', handleScroll)
76 | })
77 |
78 | // Hide or show the menu.
79 | const handleScroll = () => {
80 | const currentScrollPos = window.pageYOffset
81 | const visible = prevScrollPos > currentScrollPos
82 |
83 | setScrollPos(currentScrollPos)
84 | setVisible(visible)
85 | }
86 |
87 | if (post) {
88 | return (
89 | <>
90 |
120 | >
121 | )
122 | } else if (userName) {
123 | return (
124 | <>
125 |
155 | >
156 | )
157 | } else {
158 | return (
159 | <>
160 |
161 |
162 |
163 | {showLeft && defaultSubreddits && (
164 |
165 |
175 |
176 | )}
177 |
178 |
179 | {showRight && (
180 |
181 | {subredditInfo ? (
182 |
187 | ) : (
188 |
193 | )}
194 |
195 | )}
196 |
197 |
244 | >
245 | )
246 | }
247 | }
248 |
249 | const Nav = styled.nav<{ visible: boolean }>`
250 | display: flex;
251 | padding: 1rem 0;
252 | align-items: center;
253 | justify-content: space-evenly;
254 | position: fixed;
255 | width: calc(100%);
256 | background-color: ${props => props.theme.colors.backgroundColor};
257 | box-shadow: ${props => props.theme.boxShadow};
258 | transform: translateY(${props => (props.visible ? '0' : '-100px')});
259 | transition: all 0.2s ease-in-out;
260 | z-index: 1;
261 | div {
262 | h2 {
263 | margin: 0.5rem 0;
264 | }
265 | label {
266 | font-weight: 300;
267 | text-transform: uppercase;
268 | }
269 | }
270 | `
271 | const NavIcon = styled.div`
272 | border-radius: 100%;
273 | display: flex;
274 | align-items: center;
275 | justify-content: center;
276 | text-align: center;
277 | width: 3rem;
278 | height: 3rem;
279 | transition: all 0.2s ease-in-out;
280 | position: relative;
281 |
282 | span {
283 | font-weight: 300;
284 | font-size: 2.2rem;
285 | }
286 |
287 | &:active {
288 | background-color: ${props => props.theme.colors.navActive};
289 | }
290 | `
291 |
292 | const SwipeRight = styled.div`
293 | height: 100%;
294 | background-color: transparent;
295 | width: 15px;
296 | z-index: 3;
297 | position: fixed;
298 | `
299 |
300 | const SwipeLeft = styled.div`
301 | height: 100%;
302 | background-color: transparent;
303 | width: 15px;
304 | z-index: 3;
305 | position: fixed;
306 | right: 0;
307 | `
308 |
--------------------------------------------------------------------------------
/src/components/layout/Ripple.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, MouseEvent } from 'react'
2 | import styled from 'styled-components'
3 | import { useDebouncedRippleCleanUp } from '../../hooks/useDebouncedRippleCleanup'
4 |
5 | interface RippleProps {
6 | duration?: number
7 | color?: string
8 | }
9 |
10 | interface NewRipple {
11 | x: number
12 | y: number
13 | size: number
14 | }
15 |
16 | export const Ripple: React.FC = ({
17 | duration = 850,
18 | color = '#fff'
19 | }) => {
20 | const [rippleArray, setRippleArray] = useState([])
21 |
22 | useDebouncedRippleCleanUp(rippleArray.length, duration, () => {
23 | setRippleArray([])
24 | })
25 |
26 | const addRipple = (event: MouseEvent) => {
27 | const rippleContainer = event.currentTarget.getBoundingClientRect()
28 | const size =
29 | rippleContainer.width > rippleContainer.height
30 | ? rippleContainer.width
31 | : rippleContainer.height
32 |
33 | const x =
34 | event.pageX -
35 | rippleContainer.x -
36 | rippleContainer.width / 2 -
37 | window.scrollX
38 | const y =
39 | event.pageY -
40 | rippleContainer.y -
41 | rippleContainer.width / 2 -
42 | window.scrollY
43 | const newRipple = {
44 | x,
45 | y,
46 | size
47 | }
48 |
49 | setRippleArray(prevState => [...prevState, newRipple])
50 | }
51 |
52 | return (
53 |
54 | {rippleArray.length > 0 &&
55 | rippleArray.map((ripple, index) => {
56 | return (
57 |
66 | )
67 | })}
68 |
69 | )
70 | }
71 |
72 | const RippleContainer = styled.div<{ color: string; duration: number }>`
73 | position: absolute;
74 | top: 0;
75 | right: 0;
76 | bottom: 0;
77 | left: 0;
78 |
79 | span {
80 | transform: scale(0);
81 | border-radius: 100%;
82 | position: absolute;
83 | opacity: 0.75;
84 | background-color: ${props => props.color};
85 | animation-name: ripple;
86 | animation-duration: ${props => props.duration}ms;
87 | }
88 |
89 | @keyframes ripple {
90 | to {
91 | opacity: 0;
92 | transform: scale(2);
93 | }
94 | }
95 | `
96 |
--------------------------------------------------------------------------------
/src/components/layout/SubNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction } from 'react'
2 | import styled from 'styled-components'
3 | import { motion } from 'framer-motion'
4 | import { customEase } from '../../utils/customEase'
5 |
6 | interface SubNavProps {
7 | changeSortBy: ((sortBy: string) => void) | undefined
8 | options: string[]
9 | subSubEnabled?: boolean
10 | setShowSubSort?: Dispatch>
11 | setSortLabel?: Dispatch>
12 | }
13 |
14 | export const SubNav: React.FC = ({
15 | changeSortBy,
16 | options,
17 | subSubEnabled,
18 | setShowSubSort,
19 | setSortLabel
20 | }) => {
21 | const openSubSubMenu = (option: string) => {
22 | setShowSubSort!(true)
23 | setSortLabel!(option)
24 | }
25 |
26 | return (
27 |
34 | {options.map(option => (
35 | - {
38 | subSubEnabled && (option === 'top' || option === 'controversial')
39 | ? // open subsub menu
40 | openSubSubMenu(option)
41 | : changeSortBy!(option)
42 | }}
43 | >
44 | {option}
45 | {subSubEnabled && option === 'top' && (
46 | arrow_right
47 | )}
48 | {subSubEnabled && option === 'controversial' && (
49 | arrow_right
50 | )}
51 |
52 | ))}
53 |
54 |
55 | )
56 | }
57 |
58 | const Menu = styled(motion.div)`
59 | position: absolute;
60 | width: 189px;
61 | text-align: left;
62 | top: 0;
63 | left: 0;
64 | z-index: 2;
65 | margin-top: -17px;
66 | margin-left: -60px;
67 | border-radius: 2px;
68 | background-color: ${props => props.theme.colors.subMenuColor};
69 | box-shadow: ${props => props.theme.boxShadow};
70 | overflow: hidden;
71 |
72 | ul {
73 | li {
74 | overflow: hidden;
75 | padding: 1rem;
76 | margin: 0.5rem 0;
77 | font-size: 1.5rem;
78 | position: relative;
79 | text-transform: capitalize;
80 | display: flex;
81 | justify-content: space-between;
82 | align-items: center;
83 |
84 | &:first-child {
85 | margin-top: 0;
86 | }
87 | &:last-child {
88 | margin-bottom: 0;
89 | }
90 | &::after {
91 | position: absolute;
92 | content: '';
93 | display: inline-block;
94 | top: 50%;
95 | left: 50%;
96 | transform: translate(-50%, -50%);
97 | width: 5px;
98 | height: 5px;
99 | background-color: transparent;
100 | border-radius: 50px;
101 | transition: 0.2s all ease-in-out;
102 | z-index: -1;
103 | }
104 | &:active {
105 | &::after {
106 | background-color: ${props => props.theme.colors.navActive};
107 | transform: scale(50);
108 | }
109 | }
110 | }
111 | }
112 | `
113 |
--------------------------------------------------------------------------------
/src/components/layout/SubSubNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction } from 'react'
2 | import styled from 'styled-components'
3 | import { motion } from 'framer-motion'
4 | import { customEase } from '../../utils/customEase'
5 |
6 | interface SubSubNavProps {
7 | changeSortBy: ((sortBy: string, sortByInterval?: string) => void) | undefined
8 | setShowSubSort: Dispatch>
9 | setShowSort: Dispatch>
10 | sortLabel: string | null
11 | }
12 |
13 | const options = ['hour', 'day', 'week', 'month', 'year', 'all']
14 |
15 | export const SubSubNav: React.FC = ({
16 | changeSortBy,
17 | setShowSubSort,
18 | setShowSort,
19 | sortLabel
20 | }) => {
21 | return (
22 | <>
23 | {sortLabel && (
24 |
31 | {sortLabel}
32 | {options.map(option => (
33 | - {
35 | changeSortBy!(sortLabel, option)
36 | setShowSubSort(false)
37 | setShowSort(false)
38 | }}
39 | key={option}
40 | >
41 | {option}
42 |
43 | ))}
44 |
45 |
46 | )}
47 | >
48 | )
49 | }
50 |
51 | const Menu = styled(motion.div)`
52 | position: absolute;
53 | width: 189px;
54 | text-align: left;
55 | top: 0;
56 | left: 0;
57 | z-index: 2;
58 | margin-top: -17px;
59 | margin-left: -60px;
60 | border-radius: 2px;
61 | background-color: ${props => props.theme.colors.subMenuColor};
62 | box-shadow: ${props => props.theme.boxShadow};
63 | overflow: hidden;
64 |
65 | h4 {
66 | padding: 1rem;
67 | margin: 0.5rem 0;
68 | font-size: 1.5rem;
69 | position: relative;
70 | text-transform: capitalize;
71 | display: flex;
72 | justify-content: space-between;
73 | align-items: center;
74 | }
75 |
76 | ul {
77 | li {
78 | overflow: hidden;
79 | padding: 1rem;
80 | margin: 0.5rem 0;
81 | font-size: 1.5rem;
82 | position: relative;
83 | text-transform: capitalize;
84 | display: flex;
85 | justify-content: space-between;
86 | align-items: center;
87 |
88 | &:first-child {
89 | margin-top: 0;
90 | }
91 | &:last-child {
92 | margin-bottom: 0;
93 | }
94 | &::after {
95 | position: absolute;
96 | content: '';
97 | display: inline-block;
98 | top: 50%;
99 | left: 50%;
100 | transform: translate(-50%, -50%);
101 | width: 5px;
102 | height: 5px;
103 | background-color: transparent;
104 | border-radius: 50px;
105 | transition: 0.2s all ease-in-out;
106 | z-index: -1;
107 | }
108 | &:active {
109 | &::after {
110 | background-color: ${props => props.theme.colors.navActive};
111 | transform: scale(50);
112 | }
113 | }
114 | }
115 | }
116 | `
117 |
--------------------------------------------------------------------------------
/src/components/leftnavigation/LeftNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { connect } from 'react-redux'
3 | import styled from 'styled-components'
4 | import { DefaultSubreddit } from '../../context/reddit/redditTypes'
5 | import { motion } from 'framer-motion'
6 | import { customEase } from '../../utils/customEase'
7 |
8 | import { LeftNavSearch } from './LeftNavSearch'
9 | import { toggleTheme } from '../../redux/actions/themeActions'
10 | import { DarkenBackground } from '../style/basicStyles'
11 | import { Link } from 'react-router-dom'
12 |
13 | interface LeftNavProps {
14 | defaultSubreddits: DefaultSubreddit[]
15 | basicSubreddits: string[]
16 | autocompleteSubreddits?: DefaultSubreddit[] | null
17 | setSubreddit: (subreddit: string | null) => void
18 | setShowLeft: React.Dispatch
19 | subredditAutocomplete: (query: string) => void
20 | toggleTheme: any
21 | searchTerm?: string | null
22 | changeSearchTerm: (search: string) => void
23 | }
24 |
25 | const fallbackIconUrl =
26 | 'https://media.wired.com/photos/5954a1b05578bd7594c46869/master/w_550,c_limit/reddit-alien-red-st.jpg'
27 |
28 | const LeftNavPre: React.FC = ({
29 | defaultSubreddits,
30 | autocompleteSubreddits,
31 | basicSubreddits,
32 | setSubreddit,
33 | setShowLeft,
34 | searchTerm,
35 | changeSearchTerm,
36 | subredditAutocomplete,
37 | toggleTheme
38 | }) => {
39 | useEffect(() => {
40 | document.body.style.overflow = 'hidden'
41 |
42 | return () => {
43 | document.body.style.overflow = 'scroll'
44 | }
45 | }, [])
46 |
47 | return (
48 | <>
49 |
55 |
61 |
62 |
63 | guest
64 |
65 |
66 |
67 | nights_stay{' '}
68 | Night Mode
69 |
70 |
71 |
72 |
73 |
80 |
81 | {
83 | setShowLeft(false)
84 | subredditAutocomplete('')
85 | }}
86 | >
87 | {autocompleteSubreddits ? (
88 | <>
89 | {autocompleteSubreddits.map(subreddit => (
90 | setSubreddit(subreddit.name)}
93 | >
94 |
99 | {subreddit.name}
100 |
101 | ))}
102 | {/* LINK TO GO TO USER */}
103 |
104 |
105 |
106 |
107 | /u/{searchTerm}
108 |
109 |
110 | >
111 | ) : (
112 | <>
113 | {basicSubreddits.map(subreddit => (
114 | setSubreddit(subreddit)}
117 | >
118 |
119 | {subreddit}
120 |
121 | ))}
122 | {defaultSubreddits.map(subreddit => (
123 | setSubreddit(subreddit.name)}
126 | >
127 |
132 | {subreddit.name}
133 |
134 | ))}
135 | >
136 | )}
137 |
138 |
139 | >
140 | )
141 | }
142 |
143 | export const LeftNav = connect(null, { toggleTheme })(LeftNavPre)
144 |
145 | const LeftNavMenu = styled(motion.div)`
146 | background-color: ${props => props.theme.colors.backgroundColor};
147 | position: fixed;
148 | width: 80%;
149 | height: 100%;
150 | z-index: 4;
151 | overflow-y: scroll;
152 | `
153 |
154 | const LeftNavTop = styled.div``
155 |
156 | const Me = styled.div`
157 | height: 100px;
158 | background-image: linear-gradient(
159 | 45deg,
160 | ${props => props.theme.colors.primaryColor},
161 | ${props => props.theme.colors.secondaryColor}
162 | );
163 | display: flex;
164 | flex-direction: column;
165 | justify-content: flex-end;
166 | padding: 0 1rem;
167 | `
168 |
169 | const MeStuff = styled.ul``
170 |
171 | const MeItem = styled.li`
172 | display: flex;
173 | align-items: center;
174 | position: relative;
175 | overflow: hidden;
176 |
177 | span {
178 | margin: 1rem;
179 | }
180 |
181 | &::after {
182 | position: absolute;
183 | content: '';
184 | display: inline-block;
185 | top: 50%;
186 | left: 50%;
187 | transform: translate(-50%, -50%);
188 | width: 5px;
189 | height: 5px;
190 | background-color: transparent;
191 | border-radius: 50px;
192 | transition: 0.2s all ease-in-out;
193 | z-index: -1;
194 | }
195 | &:active {
196 | &::after {
197 | background-color: ${props => props.theme.colors.navActive};
198 | transform: scale(100);
199 | }
200 | }
201 | `
202 |
203 | const SubredditsList = styled.ul``
204 |
205 | const SubredditItem = styled.li`
206 | display: flex;
207 | align-items: center;
208 | position: relative;
209 | overflow: hidden;
210 |
211 | &::after {
212 | position: absolute;
213 | content: '';
214 | display: inline-block;
215 | top: 50%;
216 | left: 50%;
217 | transform: translate(-50%, -50%);
218 | width: 5px;
219 | height: 5px;
220 | background-color: transparent;
221 | border-radius: 50px;
222 | transition: 0.2s all ease-in-out;
223 | z-index: -1;
224 | }
225 |
226 | &:active {
227 | &::after {
228 | background-color: ${props => props.theme.colors.navActive};
229 | transform: scale(100);
230 | }
231 | }
232 | `
233 |
234 | const SubredditIcon = styled.div<{ icon: string }>`
235 | background-image: url(${props => props.icon});
236 | background-size: cover;
237 | background-position: center;
238 | border-radius: 100%;
239 |
240 | width: 2.5rem;
241 | height: 2.5rem;
242 |
243 | margin: 1rem;
244 | `
245 | const SearchContainer = styled.div`
246 | padding: 1rem;
247 | display: flex;
248 | align-items: center;
249 | border-top: 1px solid ${props => props.theme.colors.textColorFaded};
250 | `
251 |
--------------------------------------------------------------------------------
/src/components/leftnavigation/LeftNavSearch.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { TextField } from '../common/TextField'
3 | import styled from 'styled-components'
4 |
5 | interface LeftNavSearchProps {
6 | placeholder: string
7 | setSubreddit: (subreddit: string | null) => void
8 | setShowLeft: React.Dispatch
9 | subredditAutocomplete: (query: string) => void
10 | changeSearchTerm: (search: string) => void
11 | }
12 |
13 | export const LeftNavSearch: React.FC = ({
14 | placeholder,
15 | setSubreddit,
16 | setShowLeft,
17 | subredditAutocomplete,
18 | changeSearchTerm
19 | }) => {
20 | const [field, setField] = useState('')
21 |
22 | const onChange = (e: React.ChangeEvent) => {
23 | setField(e.target.value)
24 | subredditAutocomplete(e.target.value)
25 | changeSearchTerm(e.target.value)
26 | }
27 |
28 | const onSubmit = () => {
29 | setSubreddit(field)
30 | setShowLeft(false)
31 | }
32 |
33 | return (
34 | <>
35 |
36 | {field.length > 0 && (
37 | <>
38 | {
41 | setField('')
42 | subredditAutocomplete('')
43 | }}
44 | >
45 | close
46 |
47 | Go
48 | >
49 | )}
50 | >
51 | )
52 | }
53 |
54 | const Icon = styled.span`
55 | margin-left: -2rem;
56 | color: ${props => props.theme.colors.textColorFaded};
57 | `
58 | const SubmitButton = styled.button`
59 | border: none;
60 | background-color: ${props => props.theme.colors.backgroundColor};
61 | color: ${props => props.theme.colors.textColorFaded};
62 | outline: none;
63 | `
64 |
--------------------------------------------------------------------------------
/src/components/post/Comment.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, Dispatch, SetStateAction } from 'react'
2 | import { CommentData, CommentMore } from '../../context/reddit/redditTypes'
3 | import { Comments } from './Comments'
4 | import styled from 'styled-components'
5 | import Moment from 'react-moment'
6 | import { motion } from 'framer-motion'
7 | import { childVariants } from '../../utils/variants'
8 | import { customEase } from '../../utils/customEase'
9 | import { decodeHTML } from '../../utils/decodeHtml'
10 | import { CommentItemMenu } from './CommentItemMenu'
11 |
12 | interface CommentProps {
13 | clickedId: string | null
14 | setClickedId: Dispatch>
15 | comment: CommentData | CommentMore
16 | number: number
17 | more?: boolean
18 | postName: string
19 | getMoreComments: (linkId: string, children: string[]) => Promise
20 | }
21 |
22 | const isComment = (variableToCheck: any): variableToCheck is CommentData =>
23 | (variableToCheck as CommentData).kind === 't1'
24 |
25 | const colors = ['#de3e49', '#3e56de', '#d433ce', '#33d453', '#ed683b']
26 |
27 | export const Comment: React.FC = ({
28 | clickedId,
29 | setClickedId,
30 | comment,
31 | number,
32 | more,
33 | postName,
34 | getMoreComments
35 | }) => {
36 | const [loadMore, setLoadMore] = useState(false)
37 |
38 | const [moreComments, setMoreComments] = useState(null)
39 |
40 | const getMoreReplies = async (children: string[]) => {
41 | setLoadMore(true)
42 | const res = await getMoreComments(postName, children)
43 | setMoreComments(res)
44 | }
45 | // console.log(comment)
46 | // console.log(moreComments)
47 |
48 | if (isComment(comment)) {
49 | const {
50 | data: {
51 | author,
52 | author_flair_text,
53 | body_html,
54 | created_utc,
55 | distinguished,
56 | id,
57 | is_submitter,
58 | replies,
59 | score,
60 | score_hidden,
61 | stickied
62 | }
63 | } = comment
64 |
65 | return (
66 | <>
67 | setClickedId(id)}
76 | clicked={clickedId === id}
77 | >
78 |
79 |
80 |
85 | {author}
86 |
87 |
88 | {author_flair_text && (
89 |
90 | {author_flair_text}
91 |
92 | )}
93 | {distinguished && (
94 | [{distinguished[0]}]
95 | )}
96 |
97 | {score_hidden ? '[score hidden]' : `${score} points`}
98 |
99 |
100 |
101 |
102 | {created_utc}
103 |
104 |
105 |
106 |
113 |
114 |
115 | {clickedId === id && }
116 | {/* recursivly calling replies */}
117 | {replies instanceof Object && (
118 |
126 | )}
127 | >
128 | )
129 | } else {
130 | const {
131 | data: { count, children, id }
132 | } = comment
133 |
134 | if (id === '_') {
135 | return null
136 | }
137 |
138 | return (
139 | <>
140 | {!moreComments ? (
141 | getMoreReplies(children)}
150 | >
151 | {loadMore ? (
152 | Loading . . .
153 | ) : (
154 | View More ({count})
155 | )}
156 |
157 | ) : (
158 | <>
159 | {moreComments &&
160 | moreComments.map((comment: CommentData, index: number) => (
161 | <>
162 |
172 | >
173 | ))}
174 | >
175 | )}
176 | >
177 | )
178 | }
179 | }
180 |
181 | const CommentContainer = styled(motion.div)<{
182 | labelColor: string
183 | clicked?: boolean
184 | more?: boolean
185 | }>`
186 | position: relative;
187 | margin: 1px 0;
188 | &:before {
189 | content: '';
190 | width: 5px;
191 | height: 100%;
192 | background-color: ${props => props.labelColor};
193 | position: absolute;
194 | margin-left: -0.5rem;
195 | }
196 | a {
197 | color: ${props => props.theme.colors.primaryColor};
198 | text-decoration: underline;
199 | }
200 | color: ${props =>
201 | props.more
202 | ? props.theme.colors.primaryColor
203 | : props.theme.colors.textColor};
204 |
205 | background-color: ${props => props.clicked && props.theme.colors.highlight};
206 | `
207 | const CommentItem = styled.div`
208 | padding: 0.5rem;
209 | overflow-x: hidden;
210 | `
211 | const CommentMeta = styled.ul`
212 | display: flex;
213 | align-items: center;
214 | color: ${props => props.theme.colors.textColorFaded};
215 | font-size: 0.8rem;
216 | `
217 |
218 | const CommentMetaItem = styled.li<{
219 | isSubmitter?: boolean
220 | isSticked?: boolean
221 | isDistinguished?: boolean
222 | isFlair?: boolean
223 | }>`
224 | border-radius: 2.5px;
225 | margin-right: 0.4rem;
226 |
227 | ${props => {
228 | if (props.isSubmitter) {
229 | return `
230 | padding: 0.2rem 0.4rem;
231 | background-color: #3e56de;
232 | color: ${props.theme.colors.textColor};
233 | `
234 | } else if (props.isSticked || props.isDistinguished) {
235 | return `
236 | padding: 0.2rem 0.4rem;
237 | background-color: #33d453;
238 | color: ${props.theme.colors.textColor};
239 | `
240 | } else if (props.isFlair) {
241 | return `
242 | color: ${props.theme.colors.primaryColor};
243 | `
244 | }
245 | }}
246 | `
247 |
--------------------------------------------------------------------------------
/src/components/post/CommentItemMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { motion } from 'framer-motion'
4 | import { customEase } from '../../utils/customEase'
5 | import { Link } from 'react-router-dom'
6 |
7 | interface CommentItemMenuProps {
8 | author: string
9 | }
10 |
11 | export const CommentItemMenu: React.FC = ({ author }) => {
12 | return (
13 |
18 | arrow_upward
19 | arrow_downward
20 | star
21 |
22 | account_circle
23 |
24 | reply
25 | expand_less
26 | more_vert
27 |
28 | )
29 | }
30 |
31 | const CommentMenuItemContainer = styled(motion.div)`
32 | background-color: ${props => props.theme.colors.subMenuColor};
33 | width: 100%;
34 | display: flex;
35 | align-items: center;
36 | justify-content: space-evenly;
37 | `
38 |
39 | const Icon = styled.span`
40 | padding: 1rem;
41 | `
42 |
--------------------------------------------------------------------------------
/src/components/post/Comments.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction } from 'react'
2 | import { CommentData } from '../../context/reddit/redditTypes'
3 | import { Comment } from './Comment'
4 |
5 | interface CommentsProps {
6 | clickedId: string | null
7 | setClickedId: Dispatch>
8 | comments: CommentData[] | string
9 | number?: number
10 | postName: string
11 | getMoreComments: (linkId: string, children: string[]) => Promise
12 | }
13 |
14 | export const Comments: React.FC = ({
15 | clickedId,
16 | setClickedId,
17 | comments,
18 | number = 0,
19 | postName,
20 | getMoreComments
21 | }) => {
22 | return (
23 | <>
24 | {comments instanceof Object && (
25 | <>
26 | {comments.map((comment, index) => (
27 |
36 | ))}
37 | >
38 | )}
39 | >
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/post/Post.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from 'react'
2 | import styled from 'styled-components'
3 | import { useSelector } from 'react-redux'
4 | import { RouteComponentProps } from 'react-router-dom'
5 | import RedditContext from '../../context/reddit/redditContext'
6 | import { SubredditPost } from '../subreddit/SubredditPost'
7 | import { Comments } from './Comments'
8 | import { motion } from 'framer-motion'
9 | import { parentVariants } from '../../utils/variants'
10 | import { customEase } from '../../utils/customEase'
11 | import { Container } from '../style/basicStyles'
12 | import { ReduxState } from '../../redux/store'
13 | import { Loading } from '../layout/Loading'
14 |
15 | interface PostProps
16 | extends RouteComponentProps<{
17 | subreddit: string
18 | id: string
19 | title: string
20 | name: string
21 | }> {}
22 |
23 | export const Post: React.FC = ({ match }) => {
24 | const [clickedId, setClickedId] = useState(null)
25 |
26 | const state = useSelector((state: ReduxState) => state.loading)
27 |
28 | const {
29 | params: { subreddit, id, title, name }
30 | } = match
31 |
32 | const redditContext = useContext(RedditContext)
33 | const {
34 | post,
35 | comments,
36 | sortCommentsBy,
37 | clearPostDetail,
38 | clearCommentDetail,
39 | getPostDetail,
40 | getMoreComments
41 | } = redditContext
42 |
43 | useEffect(() => {
44 | getPostDetail!(`${subreddit}/comments/${id}/${title}`, name)
45 |
46 | return () => {
47 | clearPostDetail!()
48 | clearCommentDetail!()
49 | }
50 | }, [sortCommentsBy])
51 |
52 | return (
53 | <>
54 | {state.loading ? (
55 |
56 | ) : (
57 |
62 | {post && (
63 | <>
64 |
65 | >
66 | )}
67 | {comments && (
68 | <>
69 |
74 |
81 |
82 | >
83 | )}
84 |
85 | )}
86 | >
87 | )
88 | }
89 |
90 | const CommentsContainer = styled(motion.div)`
91 | box-shadow: ${props => props.theme.boxShadow};
92 | `
93 |
--------------------------------------------------------------------------------
/src/components/rightnavigation/RightNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { motion } from 'framer-motion'
3 | import styled from 'styled-components'
4 | import { customEase } from '../../utils/customEase'
5 | import { DarkenBackground } from '../style/basicStyles'
6 | import { decodeHTML } from '../../utils/decodeHtml'
7 | import { SubredditInfo } from '../../context/reddit/redditTypes'
8 |
9 | interface RightNavProps {
10 | subredditInfo?: SubredditInfo
11 | trendingSubreddits?: string[] | null
12 | setSubreddit: (subreddit: string) => void
13 | setShowRight: any
14 | }
15 |
16 | export const RightNav: React.FC = ({
17 | subredditInfo,
18 | setSubreddit,
19 | setShowRight,
20 | trendingSubreddits
21 | }) => {
22 | useEffect(() => {
23 | document.body.style.overflow = 'hidden'
24 |
25 | return () => {
26 | document.body.style.overflow = 'scroll'
27 | }
28 | }, [])
29 |
30 | if (subredditInfo) {
31 | const {
32 | data: {
33 | accounts_active,
34 | description_html,
35 | display_name_prefixed,
36 | icon_img,
37 | subscribers
38 | }
39 | } = subredditInfo
40 |
41 | return (
42 | <>
43 |
49 |
55 |
56 |
57 |
58 | {display_name_prefixed}
59 |
60 | {subscribers} subscribers - {accounts_active} active
61 |
62 |
63 |
64 |
65 |
68 |
69 | >
70 | )
71 | } else {
72 | return (
73 | <>
74 |
80 |
86 | {
88 | setShowRight(false)
89 | setSubreddit('trendingsubreddits')
90 | }}
91 | >
92 | trending_up
93 | Trending
94 |
95 |
96 | {trendingSubreddits && (
97 | <>
98 | {trendingSubreddits.map(item => (
99 | {
102 | setShowRight(false)
103 | setSubreddit(item)
104 | }}
105 | >
106 | /r/{item}
107 |
108 | ))}
109 | >
110 | )}
111 |
112 |
113 | >
114 | )
115 | }
116 | }
117 |
118 | const RightNavMenu = styled(motion.div)`
119 | background-color: ${props => props.theme.colors.backgroundColor};
120 | position: fixed;
121 | width: 80%;
122 | height: 100%;
123 | z-index: 4;
124 | overflow-y: scroll;
125 | right: 0;
126 | padding: 1rem;
127 |
128 | a {
129 | color: ${props => props.theme.colors.primaryColor};
130 | text-decoration: underline;
131 | }
132 | `
133 | const SubredditHeader = styled.div`
134 | display: flex;
135 | align-items: center;
136 | `
137 |
138 | const SubredditHeaderInfo = styled.div`
139 | h4 {
140 | margin: 2px;
141 | }
142 |
143 | h5 {
144 | margin: 2px;
145 | color: ${props => props.theme.colors.textColorFaded};
146 | }
147 | `
148 |
149 | const Icon = styled.span`
150 | margin-right: 1rem;
151 | `
152 |
153 | const TrendingButton = styled.div`
154 | background-color: ${props => props.theme.colors.subMenuColor};
155 | display: flex;
156 | align-items: center;
157 | padding: 1rem;
158 | div {
159 | font-size: 1.2rem;
160 | }
161 | `
162 |
163 | const TrendingSubreddits = styled.ul`
164 | margin: 1rem 0;
165 | `
166 |
167 | const TrendingSubredditItem = styled.li`
168 | padding: 1rem;
169 | position: relative;
170 | overflow: hidden;
171 |
172 | &::after {
173 | position: absolute;
174 | content: '';
175 | display: inline-block;
176 | top: 50%;
177 | left: 50%;
178 | transform: translate(-50%, -50%);
179 | width: 5px;
180 | height: 5px;
181 | background-color: transparent;
182 | border-radius: 50px;
183 | transition: 0.2s all ease-in-out;
184 | z-index: -1;
185 | }
186 | &:active {
187 | &::after {
188 | background-color: ${props => props.theme.colors.navActive};
189 | transform: scale(100);
190 | }
191 | }
192 | `
193 |
194 | const SubredditIcon = styled.div<{ icon: string }>`
195 | background-image: url(${props => props.icon});
196 | background-size: cover;
197 | background-position: center;
198 | border-radius: 100%;
199 |
200 | width: 2.5rem;
201 | height: 2.5rem;
202 |
203 | margin: 1rem;
204 | `
205 |
--------------------------------------------------------------------------------
/src/components/style/basicStyles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { motion } from 'framer-motion'
3 |
4 | export const Container = styled(motion.div)`
5 | padding: 6rem 1rem 1rem 1rem;
6 | `
7 |
8 | export const DarkenBackground = styled(motion.div)`
9 | width: 100%;
10 | position: fixed;
11 | height: 100%;
12 | z-index: 2;
13 | background-color: rgba(0, 0, 0, 0.5);
14 | `
15 |
--------------------------------------------------------------------------------
/src/components/subreddit/GettingMorePosts.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export const GettingMorePosts: React.FC = () => {
5 | return
6 | }
7 |
8 | const Loader = styled.div`
9 | width: 100%;
10 | height: 8px;
11 | background: #eaeaea;
12 | position: relative;
13 | overflow: hidden;
14 | display: flex;
15 | align-items: stretch;
16 | justify-content: flex-start;
17 | border-radius: 4px;
18 |
19 | &::before {
20 | content: ' ';
21 | display: block;
22 | position: absolute;
23 | width: 50%;
24 | height: 100%;
25 | background: ${props => props.theme.colors.primaryColor};
26 | animation: 0.4s progressIndeterminate infinite;
27 | }
28 |
29 | @keyframes progressIndeterminate {
30 | from {
31 | width: 0;
32 | margin-left: 0;
33 | margin-right: 100%;
34 | }
35 |
36 | 50% {
37 | width: 100%;
38 | margin-left: 0;
39 | margin-right: 0;
40 | }
41 |
42 | to {
43 | width: 0;
44 | margin-left: 100%;
45 | margin-right: 0;
46 | }
47 | }
48 | `
49 |
--------------------------------------------------------------------------------
/src/components/subreddit/NoMorePosts.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export const NoMorePosts: React.FC = () => {
5 | return No more posts were loaded
6 | }
7 |
8 | const MessageContainer = styled.div`
9 | background-color: ${props => props.theme.colors.subMenuColor};
10 | padding: 1rem;
11 | width: calc(100%-2rem);
12 | text-align: center;
13 | `
14 |
--------------------------------------------------------------------------------
/src/components/subreddit/PageIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | interface PageIndicatorProps {
5 | index: number
6 | }
7 |
8 | export const PageIndicator: React.FC = ({ index }) => {
9 | return (
10 |
11 | window.scrollTo(0, 0)}>
12 | keyboard_arrow_up
13 | {' '}
14 | Page {index + 1}{' '}
15 | window.scrollTo(0, document.body.scrollHeight)}
18 | >
19 | keyboard_arrow_down
20 |
21 |
22 | )
23 | }
24 |
25 | const PageIndicatorContainer = styled.div`
26 | padding: 1rem;
27 | width: calc(100%-2rem);
28 | text-align: center;
29 | display: flex;
30 | align-items: center;
31 | justify-content: space-between;
32 | color: ${props => props.theme.colors.textColorFaded};
33 | `
34 |
--------------------------------------------------------------------------------
/src/components/subreddit/Subreddit.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from 'react'
2 | import { useSelector } from 'react-redux'
3 |
4 | import RedditContext from '../../context/reddit/redditContext'
5 |
6 | import { SubredditPost } from './SubredditPost'
7 | import { Loading } from '../layout/Loading'
8 |
9 | import { useInView } from 'react-intersection-observer'
10 | import { PostData } from '../../context/reddit/redditTypes'
11 | import { NoMorePosts } from './NoMorePosts'
12 | import { GettingMorePosts } from './GettingMorePosts'
13 | import { PageIndicator } from './PageIndicator'
14 |
15 | import { parentVariants } from '../../utils/variants'
16 | import { Container } from '../style/basicStyles'
17 | import { ReduxState } from '../../redux/store'
18 |
19 | export const Subreddit: React.FC = () => {
20 | const [ref, inView] = useInView({
21 | triggerOnce: true,
22 | rootMargin: '400px 0px'
23 | })
24 |
25 | const state = useSelector((state: ReduxState) => state.loading)
26 |
27 | const redditContext = useContext(RedditContext)
28 | const {
29 | after,
30 | posts,
31 | subreddit,
32 | sortBy,
33 | getPosts,
34 | getSubredditInfo
35 | } = redditContext
36 |
37 | // useEffect(() => {
38 | // setSubreddit!(match.params.subreddit)
39 | // console.log('from 1')
40 |
41 | // return () => {
42 | // console.log('left')
43 | // }
44 | // }, [])
45 |
46 | // TODO fix this second page onward will load new posts
47 | useEffect(() => {
48 | if (subreddit && posts?.length === 0) {
49 | getPosts!()
50 | getSubredditInfo!()
51 | }
52 | }, [posts, subreddit, sortBy])
53 |
54 | useEffect(() => {
55 | if (inView && after) {
56 | getPosts!()
57 | }
58 | }, [inView])
59 |
60 | return (
61 | <>
62 | {state.loading ? (
63 |
64 | ) : (
65 |
66 |
71 | {posts && posts.length > 0 && (
72 | <>
73 | {posts.map((grouping, index) => (
74 |
75 | {index !== 0 && }
76 | {grouping.map((post: PostData, index: number) => (
77 |
78 |
79 |
80 | ))}
81 |
82 |
83 | ))}
84 | >
85 | )}
86 | {!after ? : }
87 |
88 |
89 | )}
90 | >
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/subreddit/SubredditPost.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, MouseEvent } from 'react'
2 | import styled from 'styled-components'
3 | import { Link } from 'react-router-dom'
4 | import Moment from 'react-moment'
5 | import { getPostType, getMedia } from '../../utils/subredditParser'
6 | import { Loading } from '../layout/Loading'
7 |
8 | import { motion, AnimatePresence } from 'framer-motion'
9 | import { customEase } from '../../utils/customEase'
10 | import { PostData } from '../../context/reddit/redditTypes'
11 | import { childVariants } from '../../utils/variants'
12 |
13 | interface SubredditPostProps {
14 | post: PostData
15 | detail?: boolean
16 | }
17 |
18 | export const SubredditPost: React.FC = ({
19 | post,
20 | detail
21 | }) => {
22 | const [clicked, setClicked] = useState(false)
23 | const [gifLoading, setGifLoading] = useState(false)
24 | const [animateStart, setAnimateStart] = useState(0)
25 |
26 | const {
27 | data: {
28 | author,
29 | created_utc,
30 | domain,
31 | link_flair_text,
32 | name,
33 | num_comments,
34 | preview,
35 | permalink,
36 | score,
37 | selftext,
38 | selftext_html,
39 | subreddit,
40 | stickied,
41 | title,
42 | url
43 | }
44 | } = post
45 |
46 | const type = getPostType(post.data)
47 | const media = getMedia(post.data, type)
48 |
49 | const onClick = (e: MouseEvent) => {
50 | const smallMediaContainer = e.currentTarget.getBoundingClientRect()
51 |
52 | // 300px is rougly the size of how far the start
53 | // needs to move up
54 | setAnimateStart(smallMediaContainer.y - 300)
55 | if (document.body.style.overflow === 'hidden') {
56 | document.body.style.overflow = 'scroll'
57 | } else {
58 | document.body.style.overflow = 'hidden'
59 | }
60 |
61 | setClicked(!clicked)
62 | }
63 |
64 | const onLoadStart = () => {
65 | if (type === 'video:hosted' || type === 'video:outside') {
66 | setGifLoading(true)
67 | }
68 | }
69 |
70 | const onLoadEnd = () => {
71 | setGifLoading(false)
72 | }
73 |
74 | return (
75 |
79 |
80 | {clicked && (
81 |
89 | {type.split(':')[0] === 'video' && (
90 | <>
91 | {gifLoading ? : null}
92 |
102 | >
103 | )}
104 |
105 | )}
106 |
107 | {/* this is image preview if is a gif video or article with preview */}
108 |
109 | {type !== 'self' && preview && (
110 |
114 | {(type === 'link:preview' || type === 'link:video') && (
115 |
121 | )}
122 |
123 | )}
124 |
125 |
126 |
127 |
128 | {title}
129 |
130 |
131 | {link_flair_text && (
132 | <>
133 | {link_flair_text}
134 | •{' '}
135 | >
136 | )}
137 | {author} •{' '}
138 |
139 | {created_utc}
140 | {' '}
141 | •
142 | {subreddit} • (
143 | {domain})
144 |
145 |
146 | {/* // text only link*/}
147 | {type === 'link' && (
148 |
149 | open_in_browser
150 |
151 |
{domain}
152 | {url}
153 |
154 |
155 | )}
156 |
157 | {/* if this is a self post you need to show this on the post preview */}
158 | {type === 'self' && (
159 | <>
160 | {detail ? (
161 |
162 | ) : (
163 | {selftext.split('\n')[0]}
164 | )}
165 | >
166 | )}
167 |
168 |
169 |
170 | - {score} points
171 | - {num_comments} comments
172 |
173 |
174 |
175 |
176 | )
177 | }
178 |
179 | const Post = styled(motion.div)`
180 | margin: 0.5rem 0;
181 | position: relative;
182 | overflow: hidden;
183 | box-shadow: ${props => props.theme.boxShadow};
184 | `
185 |
186 | const PostPreview = styled.div``
187 |
188 | const PreviewImage = styled.div<{ thumbnail: string }>`
189 | background-image: url(${props => props.thumbnail});
190 | background-size: cover;
191 | background-position: center;
192 | height: 15rem;
193 | display: flex;
194 | align-items: flex-end;
195 | justify-content: flex-end;
196 |
197 | div {
198 | width: 100%;
199 |
200 | background-color: rgba(0, 0, 0, 0.5);
201 | display: flex;
202 | flex-direction: column;
203 | white-space: nowrap;
204 | span {
205 | display: inline-block;
206 | padding: 0.5rem;
207 | }
208 | a {
209 | padding: 0.5rem;
210 | display: inline-block;
211 | overflow: hidden;
212 | }
213 | }
214 | `
215 |
216 | const PreviewText = styled.div`
217 | margin: 0.5rem 0;
218 | font-size: 1.1rem;
219 | font-weight: 300;
220 | `
221 |
222 | const PreviewLink = styled.a`
223 | white-space: nowrap;
224 | display: grid;
225 | grid-template-columns: 6rem min-content;
226 | align-items: center;
227 |
228 | span {
229 | height: 4rem;
230 | width: 4rem;
231 | display: flex;
232 | justify-content: center;
233 | align-items: center;
234 | border-radius: 100%;
235 | border: 1px solid ${props => props.theme.colors.textColor};
236 |
237 | margin: 1rem;
238 | }
239 | `
240 |
241 | const PostTitle = styled.div<{ stickied: boolean }>`
242 | a {
243 | color: ${props =>
244 | props.stickied
245 | ? props.theme.colors.primaryColor
246 | : props.theme.colors.textColor};
247 | font-weight: ${props => (props.stickied ? 600 : 400)};
248 | }
249 | `
250 |
251 | const PostTitleLabel = styled.label`
252 | color: ${props => props.theme.colors.textColorFaded};
253 | `
254 |
255 | const PostTitleFlair = styled.span`
256 | color: ${props => props.theme.colors.secondaryColor};
257 | `
258 |
259 | const PostTitleSubreddit = styled.span`
260 | font-weight: 600;
261 | color: ${props => props.theme.colors.secondaryColor};
262 | `
263 |
264 | const PostFooter = styled.div`
265 | color: ${props => props.theme.colors.textColorFaded};
266 | ul {
267 | padding: 1rem 0;
268 | }
269 | `
270 |
271 | const Container = styled.div`
272 | padding: 0.5rem;
273 | font-weight: 300;
274 | `
275 |
276 | const PicDetail = styled(motion.div)<{ thumbnail: string }>`
277 | z-index: 2;
278 | position: fixed;
279 |
280 | top: 0;
281 | left: 0;
282 | height: 100vh;
283 | width: 100%;
284 | background-color: ${props => props.theme.colors.backgroundColor};
285 | background-image: url(${props => props.thumbnail});
286 | background-size: contain;
287 | background-repeat: no-repeat;
288 | background-position: center center;
289 | overflow-y: hidden;
290 | `
291 |
--------------------------------------------------------------------------------
/src/components/user/User.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect } from 'react'
2 | import { useSelector } from 'react-redux'
3 | import UserContext from '../../context/user/userContext'
4 | import { RouteComponentProps } from 'react-router-dom'
5 | import { UserHeader } from './UserHeader'
6 | import { UserTrophies } from './UserTrophies'
7 | import { Container } from '../style/basicStyles'
8 | import { UserPosts } from './UserPosts'
9 | import { ReduxState } from '../../redux/store'
10 | import { Loading } from '../layout/Loading'
11 | import { useInView } from 'react-intersection-observer'
12 | import { parentVariants } from '../../utils/variants'
13 | import { motion } from 'framer-motion'
14 |
15 | interface UserProps
16 | extends RouteComponentProps<{
17 | userName: string
18 | }> {}
19 |
20 | export const User: React.FC = ({ match }) => {
21 | const [inViewRef, inView] = useInView({
22 | triggerOnce: true,
23 | rootMargin: '400px 0px'
24 | })
25 |
26 | const state = useSelector((state: ReduxState) => state.loading)
27 |
28 | const userContext = useContext(UserContext)
29 |
30 | const {
31 | after,
32 | userData,
33 | userPosts,
34 | userTrophies,
35 | sortUserContentBy,
36 | getUserInfo,
37 | getUserPosts
38 | } = userContext
39 |
40 | useEffect(() => {
41 | getUserInfo!(match.params.userName)
42 |
43 | return () => {
44 | getUserInfo!(null)
45 | getUserPosts!(null)
46 | }
47 | }, [])
48 |
49 | useEffect(() => {
50 | if (userPosts?.length === 0) {
51 | getUserPosts!(match.params.userName)
52 | }
53 | }, [sortUserContentBy, userPosts])
54 |
55 | useEffect(() => {
56 | if (inView && after) {
57 | getUserPosts!(match.params.userName)
58 | }
59 | }, [inView])
60 |
61 | return (
62 |
63 | {userData && userTrophies && (
64 | <>
65 |
66 |
67 | >
68 | )}
69 | {state.loading ? (
70 |
71 | ) : (
72 |
77 | {userPosts && (
78 |
83 | )}
84 |
85 | )}
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/user/UserComment.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { CommentData } from '../../context/reddit/redditTypes'
3 | import RedditContext from '../../context/reddit/redditContext'
4 | import styled from 'styled-components'
5 | import Moment from 'react-moment'
6 | import { Link } from 'react-router-dom'
7 |
8 | interface UserCommentProps {
9 | comment: CommentData
10 | }
11 |
12 | // quick fix to format the link to work seamlessly
13 | // with the way my post component works
14 | const fixPermalinkUrl = (permalink: string, linkId: string) => {
15 | let linkArr = permalink.split('/')
16 | linkArr[linkArr.length - 2] = linkId
17 | return linkArr.join('/')
18 | }
19 |
20 | export const UserComment: React.FC = ({ comment }) => {
21 | const redditContext = useContext(RedditContext)
22 | const {
23 | data: {
24 | author,
25 | body_html,
26 | created_utc,
27 | link_title,
28 | link_id,
29 | permalink,
30 | score,
31 | subreddit
32 | }
33 | } = comment
34 |
35 | return (
36 | redditContext.clearPosts!()}
39 | >
40 |
41 | {link_title}
42 |
43 | {author}
44 | {score} points
45 |
46 |
47 | {created_utc}
48 |
49 |
50 | •
51 | {subreddit}
52 |
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | const UserCommentPostTitle = styled.h4`
60 | font-style: italic;
61 | `
62 |
63 | const UserCommentContainer = styled.div`
64 | position: relative;
65 | margin: 1px 0;
66 | padding: 0.5rem;
67 | overflow-x: hidden;
68 |
69 | a {
70 | color: ${props => props.theme.colors.primaryColor};
71 | text-decoration: underline;
72 | }
73 | color: ${props => props.theme.colors.textColor};
74 | `
75 | const UserCommentMeta = styled.ul`
76 | display: flex;
77 | align-items: center;
78 | color: ${props => props.theme.colors.textColorFaded};
79 | font-size: 0.8rem;
80 | `
81 |
82 | const UserCommentMetaItem = styled.li`
83 | border-radius: 2.5px;
84 | margin-right: 0.4rem;
85 | `
86 |
--------------------------------------------------------------------------------
/src/components/user/UserHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { UserData } from '../../context/user/userTypes'
3 | import styled from 'styled-components'
4 | import Moment from 'react-moment'
5 |
6 | interface UserHeaderProps {
7 | userData: UserData
8 | }
9 |
10 | export const UserHeader: React.FC = ({ userData }) => {
11 | const {
12 | data: { comment_karma, created_utc, name, link_karma }
13 | } = userData
14 |
15 | return (
16 |
17 |
18 | {link_karma}
19 |
20 |
21 |
22 | {comment_karma}
23 |
24 |
25 |
26 |
27 | redditor since{' '}
28 |
29 | {created_utc}
30 |
31 |
32 |
33 | joined on{' '}
34 |
35 | {created_utc}
36 |
37 |
38 |
39 | + Friends
40 |
41 | )
42 | }
43 |
44 | const UserDataContainer = styled.div`
45 | display: grid;
46 | grid-template-columns: 1fr 1fr;
47 | align-items: center;
48 | justify-items: center;
49 | `
50 | const UserKarma = styled.div`
51 | padding: 1rem;
52 |
53 | h2 {
54 | font-size: 2rem;
55 | text-align: center;
56 | margin: 2px;
57 | }
58 |
59 | label {
60 | text-transform: uppercase;
61 | color: ${props => props.theme.colors.textColorFaded};
62 | }
63 | `
64 | const UserDates = styled.div`
65 | padding: 1rem;
66 | p {
67 | color: ${props => props.theme.colors.textColorFaded};
68 | margin: 2px;
69 | }
70 | `
71 |
--------------------------------------------------------------------------------
/src/components/user/UserPosts.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { CommentData, PostData } from '../../context/reddit/redditTypes'
3 | import { SubredditPost } from '../subreddit/SubredditPost'
4 | import { UserComment } from './UserComment'
5 | import { PageIndicator } from '../subreddit/PageIndicator'
6 | import { NoMorePosts } from '../subreddit/NoMorePosts'
7 | import { GettingMorePosts } from '../subreddit/GettingMorePosts'
8 | import { motion } from 'framer-motion'
9 | import { childVariants } from '../../utils/variants'
10 | import { customEase } from '../../utils/customEase'
11 |
12 | interface UserPostsProps {
13 | after: string | null
14 | inViewRef: (node?: Element | null) => void
15 | userPosts: (PostData | CommentData)[][]
16 | }
17 |
18 | export const UserPosts: React.FC = ({
19 | after,
20 | inViewRef,
21 | userPosts
22 | }) => {
23 | return (
24 | <>
25 | {userPosts.map((userPostArr, index) => (
26 |
27 | {index !== 0 && }
28 | {userPostArr.map((userPost, index2) => (
29 |
34 | {userPost.kind === 't1' ? (
35 |
36 | ) : (
37 |
38 | )}
39 |
40 | ))}
41 |
42 |
43 | ))}
44 | {!after ? : }
45 | >
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/user/UserTrophies.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TrophyList, Trophy } from '../../context/user/userTypes'
3 | import styled from 'styled-components'
4 |
5 | interface UserTrophiesProps {
6 | userTrophies: TrophyList
7 | }
8 |
9 | export const UserTrophies: React.FC = ({ userTrophies }) => {
10 | const {
11 | data: { trophies }
12 | } = userTrophies
13 |
14 | return (
15 |
16 | {trophies.map((trophy: Trophy, index: number) => (
17 |
18 |
19 |
20 |
21 | ))}
22 |
23 | )
24 | }
25 |
26 | const TrophyCase = styled.div`
27 | display: flex;
28 | overflow-x: scroll;
29 | `
30 | const TrophyItem = styled.div`
31 | margin: 0.5rem;
32 | display: grid;
33 | grid-template-rows: 1fr max-content;
34 | align-items: center;
35 | justify-items: center;
36 |
37 | img {
38 | margin: 0.5rem;
39 | }
40 |
41 | label {
42 | color: ${props => props.theme.colors.textColorFaded};
43 | white-space: nowrap;
44 | }
45 | `
46 |
--------------------------------------------------------------------------------
/src/context/auth/AuthState.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useReducer } from 'react'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import AuthContext from './authContext'
4 | import authReducer from './authReducer'
5 |
6 | import { AUTHENTICATE } from '../types'
7 | import { Props, State } from './authTypes'
8 | import axios, { AxiosRequestConfig } from 'axios'
9 | import qs from 'qs'
10 | import moment from 'moment'
11 | import { setAuthToken } from '../../utils/setAuthToken'
12 |
13 | // const redditAuthUrl = `https://www.reddit.com/api/v1/authorize?client_id=${process.env.REACT_APP_CLIENT_ID}&response_type=code&state=12345678&redirect_uri=http://localhost:3000&scope=identity`
14 | const AuthState: React.FC = ({ children }) => {
15 | const initialState: State = {
16 | authenticated: false,
17 | test: 'test',
18 | stateToken: uuidv4()
19 | }
20 |
21 | const [state, dispatch] = useReducer(authReducer, initialState)
22 |
23 | useEffect(() => {
24 | const currentTime = moment()
25 |
26 | if (
27 | localStorage.getItem('token') &&
28 | moment(localStorage.getItem('exp')) > currentTime
29 | ) {
30 | setAuthToken(`bearer ${localStorage.getItem('token')}`)
31 | setAuthenticated()
32 | } else if (
33 | localStorage.getItem('token') &&
34 | moment(localStorage.getItem('exp')) < currentTime
35 | ) {
36 | localStorage.clear()
37 | applicationOnlyAuth()
38 | } else {
39 | applicationOnlyAuth()
40 | }
41 | }, [])
42 |
43 | const applicationOnlyAuth = async () => {
44 | const body = {
45 | grant_type: 'client_credentials'
46 | }
47 |
48 | const config: AxiosRequestConfig = {
49 | headers: {
50 | 'Content-Type': 'application/x-www-form-urlencoded'
51 | },
52 | auth: {
53 | username: process.env.REACT_APP_CLIENT_ID!,
54 | password: process.env.REACT_APP_SECRET_KEY!
55 | }
56 | }
57 |
58 | try {
59 | const res = await axios.post(
60 | 'https://www.reddit.com/api/v1/access_token',
61 | qs.stringify(body),
62 | config
63 | )
64 | console.log(res.data)
65 |
66 | const exp = moment().add(res.data.expires_in, 's')
67 |
68 | localStorage.setItem('token', res.data.access_token)
69 | localStorage.setItem('exp', exp.toString())
70 |
71 | setAuthToken(`bearer ${res.data.access_token}`)
72 | setAuthenticated()
73 | } catch (err) {
74 | throw err
75 | }
76 | }
77 |
78 | const setAuthenticated = () => {
79 | dispatch({ type: AUTHENTICATE, payload: true })
80 | }
81 |
82 | return (
83 |
89 | {children}
90 |
91 | )
92 | }
93 |
94 | export default AuthState
95 |
--------------------------------------------------------------------------------
/src/context/auth/authContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | import { State } from './authTypes'
3 |
4 | const activitiesContext = createContext({})
5 |
6 | export default activitiesContext
7 |
--------------------------------------------------------------------------------
/src/context/auth/authReducer.ts:
--------------------------------------------------------------------------------
1 | import { TEST_TYPE, AUTHENTICATE } from '../types'
2 | import { State, AllActions } from './authTypes'
3 |
4 | export default (state: State, action: AllActions) => {
5 | switch (action.type) {
6 | case TEST_TYPE: {
7 | return {
8 | ...state,
9 | test: 'tested'
10 | }
11 | }
12 | case AUTHENTICATE: {
13 | return {
14 | ...state,
15 | authenticated: action.payload
16 | }
17 | }
18 | default:
19 | return state
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/context/auth/authTypes.ts:
--------------------------------------------------------------------------------
1 | export interface Props {
2 | children: React.ReactNode
3 | }
4 |
5 | export type State = {
6 | authenticated?: boolean
7 | test?: string
8 | stateToken?: string
9 | tryTest?: () => void
10 | }
11 |
12 | type ActionInterface = {
13 | type: T
14 | payload: U
15 | }
16 |
17 | export type AllActions =
18 | | ActionInterface<'TEST_TYPE', null>
19 | | ActionInterface<'AUTHENTICATE', boolean>
20 |
--------------------------------------------------------------------------------
/src/context/reddit/RedditState.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useReducer } from 'react'
2 | import RedditContext from './redditContext'
3 | import redditReducer from './redditReducer'
4 |
5 | import axios from 'axios'
6 |
7 | import {
8 | GET_POSTS,
9 | CLEAR_POSTS,
10 | TEST_TYPE,
11 | SET_SUBREDDIT,
12 | GET_DEFAULT_SUBREDDITS,
13 | FILTER_POST_FROM_POSTS,
14 | CHANGE_SORT_BY,
15 | CHANGE_SORT_BY_INTERVAL,
16 | SET_AFTER,
17 | GET_POST_DETAIL,
18 | CLEAR_POST_DETAIL,
19 | SUBREDDIT_AUTOCOMPLETE,
20 | CHANGE_SORT_COMMENTS_BY,
21 | GET_SUBREDDIT_INFO,
22 | GET_POST_ON_REFRESH,
23 | CLEAR_COMMENT_DETAIL,
24 | GET_TRENDING_SUBREDDITS,
25 | CLEAR_SUBREDDIT_INFO,
26 | CHANGE_SEARCH_TERM
27 | } from '../types'
28 | import { Props } from './redditTypes'
29 | import { defaultSubredditsParser } from '../../utils/defaultSubredditsParser'
30 | import { connect } from 'react-redux'
31 | import { setLoading } from '../../redux/actions/loadingActions'
32 | import activitiesContext from '../auth/authContext'
33 |
34 | // subreddit
35 | // https://www.reddit.com/api/info.json?id={subreddit_id}
36 | // detail
37 | // https://www.reddit.com/{permalink}
38 | // defaults
39 | // https://www.reddit.com/subreddits/default.json
40 |
41 | const RedditState: React.FC = ({ children, setLoading }) => {
42 | const initialState = {
43 | trendingSubreddits: null,
44 | subreddit: 'all',
45 | subredditInfo: null,
46 | defaultSubreddits: null,
47 | autocompleteSubreddits: null,
48 | searchTerm: null,
49 | sortBy: 'hot',
50 | sortByInterval: null,
51 | sortCommentsBy: 'suggested',
52 | posts: [],
53 | post: null,
54 | comments: null,
55 | after: null,
56 | basicSubreddits: ['all', 'popular']
57 | }
58 |
59 | const [state, dispatch] = useReducer(redditReducer, initialState)
60 |
61 | useEffect(() => {
62 | getDefaultSubreddits()
63 | getTrendingSubreddits()
64 | }, [])
65 |
66 | const getPosts = async () => {
67 | if (!state.after) {
68 | setLoading()
69 | }
70 | try {
71 | const res = await axios.get(
72 | `https://oauth.reddit.com/r/${state.subreddit}/${state.sortBy}.json?raw_json=1&after=${state.after}&t=${state.sortByInterval}`
73 | )
74 |
75 | console.log(res)
76 |
77 | dispatch({
78 | type: GET_POSTS,
79 | payload: res.data.data.children
80 | })
81 |
82 | dispatch({
83 | type: SET_AFTER,
84 | payload: res.data.data.after
85 | })
86 | } catch (err) {
87 | throw err
88 | }
89 | if (!state.after) {
90 | setLoading()
91 | }
92 | }
93 |
94 | const clearPosts = () => {
95 | dispatch({ type: CLEAR_POSTS, payload: null })
96 | }
97 |
98 | const filterPostFromPosts = (name: string) => {
99 | dispatch({ type: FILTER_POST_FROM_POSTS, payload: name })
100 | }
101 |
102 | const getPostDetail = async (permalink: string, name: string) => {
103 | // FASTER IF USER ALREADY HAS POSTS LOADED
104 | if (state.posts!.length > 0) {
105 | filterPostFromPosts(name)
106 | try {
107 | const res = await axios.get(
108 | `https://oauth.reddit.com/r/${permalink}.json?raw_json=1&sort=${state.sortCommentsBy}`
109 | )
110 |
111 | dispatch({
112 | type: GET_POST_DETAIL,
113 | payload: res.data[1].data.children
114 | })
115 | } catch (err) {
116 | throw err
117 | }
118 | } else {
119 | // SLOWER BUT WILL WORK IF PAGE IS RELOADED
120 | // GET THE SUBREDDIT FROM THE URL
121 | setSubreddit(permalink.split('/')[0])
122 | setLoading()
123 | try {
124 | const res = await axios.get(
125 | `https://oauth.reddit.com/r/${permalink}.json?raw_json=1&sort=${state.sortCommentsBy}`
126 | )
127 |
128 | dispatch({
129 | type: GET_POST_DETAIL,
130 | payload: res.data[1].data.children
131 | })
132 | dispatch({
133 | type: GET_POST_ON_REFRESH,
134 | payload: res.data[0].data.children[0]
135 | })
136 | setLoading()
137 | } catch (err) {
138 | throw err
139 | }
140 | }
141 | }
142 |
143 | const getMoreComments = async (linkId: string, children: string[]) => {
144 | try {
145 | const res = await axios.get(
146 | `https://oauth.reddit.com/api/morechildren?api_type=json&link_id=${linkId}&children=${children.join()}`
147 | )
148 | return res.data.json.data.things
149 | } catch (err) {
150 | throw err
151 | }
152 | }
153 |
154 | const getDefaultSubreddits = async () => {
155 | try {
156 | const res = await axios.get(
157 | 'https://oauth.reddit.com/subreddits/default.json'
158 | )
159 |
160 | dispatch({
161 | type: GET_DEFAULT_SUBREDDITS,
162 | payload: defaultSubredditsParser(res.data.data.children)
163 | })
164 | } catch (err) {
165 | throw err
166 | }
167 | }
168 |
169 | // using fetch because this endpoint won't accept headers
170 | const getTrendingSubreddits = async () => {
171 | try {
172 | const data = await (
173 | await fetch('https://www.reddit.com/api/trending_subreddits.json')
174 | ).json()
175 | dispatch({ type: GET_TRENDING_SUBREDDITS, payload: data.subreddit_names })
176 | } catch (err) {
177 | throw err
178 | }
179 | }
180 |
181 | const subredditAutocomplete = async (query: string) => {
182 | if (query.length === 0) {
183 | dispatch({
184 | type: SUBREDDIT_AUTOCOMPLETE,
185 | payload: null
186 | })
187 | } else {
188 | try {
189 | const res = await axios.get(
190 | `https://oauth.reddit.com/api/subreddit_autocomplete_v2?query=${query}&include_over_18=true&include_profiles=false`
191 | )
192 | dispatch({
193 | type: SUBREDDIT_AUTOCOMPLETE,
194 | payload: defaultSubredditsParser(res.data.data.children)
195 | })
196 | } catch (err) {
197 | throw err
198 | }
199 | }
200 | }
201 |
202 | const getSubredditInfo = async () => {
203 | if (state.subreddit && state.basicSubreddits) {
204 | if (state.basicSubreddits.includes(state.subreddit)) {
205 | dispatch({
206 | type: CLEAR_SUBREDDIT_INFO,
207 | payload: null
208 | })
209 | } else {
210 | try {
211 | const res = await axios.get(
212 | `https://oauth.reddit.com/r/${state.subreddit}/about`
213 | )
214 |
215 | dispatch({
216 | type: GET_SUBREDDIT_INFO,
217 | payload: res.data
218 | })
219 | } catch (err) {
220 | throw err
221 | }
222 | }
223 | }
224 | }
225 | const subredditSearch = async (q: string) => {
226 | try {
227 | const res = await axios.post(
228 | `https://oauth.reddit.com/api/search_subreddits?query=${q}&include_over_18`
229 | )
230 | console.log(res)
231 | } catch (err) {
232 | throw err
233 | }
234 | }
235 |
236 | const clearPostDetail = () => {
237 | dispatch({ type: CLEAR_POST_DETAIL, payload: null })
238 | }
239 |
240 | const clearCommentDetail = () => {
241 | dispatch({ type: CLEAR_COMMENT_DETAIL, payload: null })
242 | }
243 |
244 | const setSubreddit = (subreddit: string | null) => {
245 | dispatch({ type: SET_SUBREDDIT, payload: subreddit })
246 | }
247 |
248 | // const setLoading = () => {
249 | // dispatch({ type: SET_LOADING, payload: null })
250 | // }
251 |
252 | const changeSortBy = (sortBy: string, sortByInterval?: string) => {
253 | dispatch({ type: CHANGE_SORT_BY, payload: sortBy })
254 | if (sortByInterval) {
255 | dispatch({ type: CHANGE_SORT_BY_INTERVAL, payload: sortByInterval })
256 | } else {
257 | dispatch({ type: CHANGE_SORT_BY_INTERVAL, payload: null })
258 | }
259 | }
260 | const changeSortCommentsBy = (sortCommentsBy: string) => {
261 | dispatch({ type: CHANGE_SORT_COMMENTS_BY, payload: sortCommentsBy })
262 | }
263 |
264 | const changeSearchTerm = (search: string) => {
265 | dispatch({ type: CHANGE_SEARCH_TERM, payload: search })
266 | }
267 |
268 | const tryTest = () => {
269 | dispatch({ type: TEST_TYPE, payload: null })
270 | }
271 |
272 | return (
273 |
304 | {children}
305 |
306 | )
307 | }
308 |
309 | export default connect(null, { setLoading })(RedditState)
310 |
--------------------------------------------------------------------------------
/src/context/reddit/redditContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | import { State } from './redditTypes'
3 |
4 | const redditContext = createContext({})
5 |
6 | export default redditContext
7 |
--------------------------------------------------------------------------------
/src/context/reddit/redditReducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GET_POSTS,
3 | CLEAR_POSTS,
4 | TEST_TYPE,
5 | SET_SUBREDDIT,
6 | GET_DEFAULT_SUBREDDITS,
7 | CHANGE_SORT_BY,
8 | SET_AFTER,
9 | GET_POST_DETAIL,
10 | CLEAR_POST_DETAIL,
11 | CLEAR_COMMENT_DETAIL,
12 | FILTER_POST_FROM_POSTS,
13 | SUBREDDIT_AUTOCOMPLETE,
14 | CHANGE_SORT_COMMENTS_BY,
15 | GET_SUBREDDIT_INFO,
16 | CHANGE_SORT_BY_INTERVAL,
17 | GET_POST_ON_REFRESH,
18 | GET_TRENDING_SUBREDDITS,
19 | CLEAR_SUBREDDIT_INFO,
20 | CHANGE_SEARCH_TERM
21 | } from '../types'
22 | import { State, AllActions, PostData } from './redditTypes'
23 |
24 | export default (state: State, action: AllActions): State => {
25 | switch (action.type) {
26 | case TEST_TYPE: {
27 | return {
28 | ...state
29 | // posts: action.payload
30 | }
31 | }
32 | case GET_POSTS: {
33 | return {
34 | ...state,
35 | posts: [...state.posts!, action.payload]
36 | }
37 | }
38 | case CLEAR_POSTS: {
39 | return {
40 | ...state,
41 | posts: [],
42 | after: null
43 | }
44 | }
45 | case GET_POST_DETAIL: {
46 | return {
47 | ...state,
48 | comments: action.payload
49 | }
50 | }
51 | case GET_POST_ON_REFRESH: {
52 | return {
53 | ...state,
54 | post: action.payload
55 | }
56 | }
57 | case GET_SUBREDDIT_INFO: {
58 | return {
59 | ...state,
60 | subredditInfo: action.payload
61 | }
62 | }
63 | case FILTER_POST_FROM_POSTS: {
64 | return {
65 | ...state,
66 | post: state.posts!.map(postArr =>
67 | postArr.filter((post: PostData) => post.data.name === action.payload)
68 | )[state.posts!.length - 1][0]
69 | }
70 | }
71 | case CLEAR_POST_DETAIL: {
72 | return {
73 | ...state,
74 | post: null
75 | }
76 | }
77 | case CLEAR_SUBREDDIT_INFO: {
78 | return {
79 | ...state,
80 | subredditInfo: null
81 | }
82 | }
83 | case CLEAR_COMMENT_DETAIL: {
84 | return {
85 | ...state,
86 | comments: null
87 | }
88 | }
89 |
90 | case SET_SUBREDDIT: {
91 | return {
92 | ...state,
93 | subreddit: action.payload,
94 | after: null,
95 | posts: []
96 | }
97 | }
98 | case GET_DEFAULT_SUBREDDITS: {
99 | return {
100 | ...state,
101 | defaultSubreddits: action.payload
102 | }
103 | }
104 | case GET_TRENDING_SUBREDDITS: {
105 | return {
106 | ...state,
107 | trendingSubreddits: action.payload
108 | }
109 | }
110 | case SUBREDDIT_AUTOCOMPLETE: {
111 | return {
112 | ...state,
113 | autocompleteSubreddits: action.payload
114 | }
115 | }
116 | case CHANGE_SORT_BY: {
117 | return {
118 | ...state,
119 | sortBy: action.payload,
120 | after: null,
121 | posts: []
122 | }
123 | }
124 | case CHANGE_SORT_BY_INTERVAL: {
125 | return {
126 | ...state,
127 | sortByInterval: action.payload
128 | }
129 | }
130 | case CHANGE_SORT_COMMENTS_BY: {
131 | return {
132 | ...state,
133 | sortCommentsBy: action.payload
134 | }
135 | }
136 | case CHANGE_SEARCH_TERM: {
137 | return {
138 | ...state,
139 | searchTerm: action.payload
140 | }
141 | }
142 | case SET_AFTER: {
143 | return {
144 | ...state,
145 | after: action.payload
146 | }
147 | }
148 |
149 | default:
150 | return state
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/context/reddit/redditTypes.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'redux'
2 |
3 | export interface Props {
4 | children: React.ReactNode
5 | setLoading: () => void
6 | }
7 |
8 | export interface PostData {
9 | data: {
10 | author: string
11 | created_utc: number
12 | domain: string
13 | link_flair_text: string
14 | name: string
15 | num_comments: number
16 | preview: {
17 | images: {
18 | source: {
19 | url: string
20 | }
21 | }[]
22 | reddit_video_preview: any
23 | }
24 | permalink: string
25 | post_hint: string
26 | score: number
27 | selftext: string
28 | selftext_html: string
29 | secure_media: any
30 | subreddit: string
31 | stickied: boolean
32 | thumbnail: string
33 | title: string
34 | url: string
35 | }
36 | kind: 't3'
37 | }
38 |
39 | export interface CommentData {
40 | data: {
41 | author: string
42 | author_flair_text: string | null
43 | body: string
44 | body_html: string
45 | children: CommentData[] | string
46 | created_utc: number
47 | depth: number
48 | distinguished: string | null
49 | id: string
50 | is_submitter: boolean
51 | link_title: string
52 | link_permalink: string
53 | link_id: string
54 | name: string
55 | parent_id: string
56 | permalink: string
57 | replies: CommentData | string
58 | score: number
59 | score_hidden: boolean
60 |
61 | stickied: boolean
62 | subreddit: string
63 | }
64 | kind: 't1'
65 | }
66 |
67 | export interface CommentMore {
68 | data: {
69 | children: string[]
70 | count: number
71 | depth: number
72 | id: string
73 | name: string
74 | parent_id: string
75 | }
76 | kind: 'more'
77 | }
78 |
79 | export interface SubredditInfo {
80 | data: {
81 | accounts_active: number
82 | created_utc: number
83 | description_html: string
84 | display_name: string
85 | display_name_prefixed: string
86 | header_img: string
87 | icon_img: string
88 | primary_color: string
89 | subscribers: number
90 | title: string
91 | user_is_subscriber: boolean
92 | }
93 | kind: 't5'
94 | }
95 |
96 | export type State = {
97 | trendingSubreddits?: null | string[]
98 | searchTerm?: null | string
99 | subreddit?: null | string
100 | subredditInfo?: SubredditInfo | null
101 | sortBy?: string
102 | sortByInterval?: string | null
103 | sortCommentsBy?: string
104 | posts?: PostData[][]
105 | post?: PostData | null
106 | comments?: CommentData[] | null
107 | defaultSubreddits?: null | DefaultSubreddit[]
108 | autocompleteSubreddits?: null | DefaultSubreddit[]
109 | after?: string | null
110 | basicSubreddits?: string[]
111 | clearPostDetail?: () => void
112 | clearCommentDetail?: () => void
113 | tryTest?: () => void
114 | getPosts?: () => Promise
115 | clearPosts?: () => void
116 | getPostDetail?: (permalink: string, name: string) => void
117 | getMoreComments?: (linkId: string, children: string[]) => Promise
118 | setSubreddit?: (subreddit: string | null) => void
119 | getSubredditInfo?: () => void
120 | subredditAutocomplete?: (query: string) => void
121 | changeSortBy?: (sortBy: string, sortByInterval?: string) => void
122 | changeSortCommentsBy?: (sortCommentsBy: string) => void
123 | setLoading?: () => void
124 | changeSearchTerm?: (search: string) => void
125 | }
126 |
127 | type ActionInterface = {
128 | type: T
129 | payload: U
130 | }
131 |
132 | export type DefaultSubreddit = { name: string; icon: string }
133 |
134 | export type AllActions =
135 | | ActionInterface<'TEST_TYPE', null>
136 | | ActionInterface<'GET_POSTS', PostData[]>
137 | | ActionInterface<'GET_POST_DETAIL', CommentData[]>
138 | | ActionInterface<'GET_POST_ON_REFRESH', PostData>
139 | | ActionInterface<'SET_SUBREDDIT', string | null>
140 | | ActionInterface<'CHANGE_SORT_BY', string>
141 | | ActionInterface<'CHANGE_SORT_BY_INTERVAL', string | null>
142 | | ActionInterface<'CHANGE_SORT_COMMENTS_BY', string>
143 | | ActionInterface<'GET_DEFAULT_SUBREDDITS', DefaultSubreddit[]>
144 | | ActionInterface<'GET_SUBREDDIT_INFO', SubredditInfo>
145 | | ActionInterface<'SET_AFTER', string | null>
146 | | ActionInterface<'CLEAR_POST_DETAIL', null>
147 | | ActionInterface<'FILTER_POST_FROM_POSTS', string>
148 | | ActionInterface<'SUBREDDIT_AUTOCOMPLETE', DefaultSubreddit[] | null>
149 | | ActionInterface<'CLEAR_POSTS', null>
150 | | ActionInterface<'CLEAR_COMMENT_DETAIL', null>
151 | | ActionInterface<'CLEAR_SUBREDDIT_INFO', null>
152 | | ActionInterface<'GET_TRENDING_SUBREDDITS', string[]>
153 | | ActionInterface<'CHANGE_SEARCH_TERM', string | null>
154 |
--------------------------------------------------------------------------------
/src/context/types.ts:
--------------------------------------------------------------------------------
1 | // AUTH REDUCER TYPES
2 | export const TEST_TYPE = 'TEST_TYPE'
3 | export const AUTHENTICATE = 'AUTHENTICATE'
4 | // REDDIT REDUCER TYPES
5 | export const GET_POSTS = 'GET_POSTS'
6 | export const CLEAR_POSTS = 'CLEAR_POSTS'
7 | export const GET_POST_DETAIL = 'GET_POST_DETAIL'
8 | export const GET_POST_ON_REFRESH = 'GET_POST_ON_REFRESH'
9 | export const FILTER_POST_FROM_POSTS = 'FILTER_POST_FROM_POSTS'
10 | export const CLEAR_POST_DETAIL = 'CLEAR_POST_DETAIL'
11 | export const CLEAR_COMMENT_DETAIL = 'CLEAR_COMMENT_DETAIL'
12 | export const CLEAR_SUBREDDIT_INFO = 'CLEAR_SUBREDDIT_INFO'
13 | export const SET_SUBREDDIT = 'SET_SUBREDDIT'
14 | export const GET_DEFAULT_SUBREDDITS = 'GET_DEFAULT_SUBREDDITS'
15 | export const GET_TRENDING_SUBREDDITS = 'GET_TRENDING_SUBREDDITS'
16 | export const GET_SUBREDDIT_INFO = 'GET_SUBREDDIT_INFO'
17 | export const SUBREDDIT_AUTOCOMPLETE = 'SUBREDDIT_AUTOCOMPLETE'
18 | export const SET_LOADING = 'SET_LOADING'
19 | export const CHANGE_SORT_BY = 'CHANGE_SORT_BY'
20 | export const CHANGE_SORT_BY_INTERVAL = 'CHANGE_SORT_BY_INTERVAL'
21 | export const CHANGE_SORT_COMMENTS_BY = 'CHANGE_SORT_COMMENTS_BY'
22 | export const CHANGE_SEARCH_TERM = 'CHANGE_SEARCH_TERM'
23 | export const SET_AFTER = 'SET_AFTER'
24 | // USER REDUCER TYPES
25 | export const GET_USER_ABOUT = 'GET_USER_ABOUT'
26 | export const GET_USER_TROPHIES = 'GET_USER_TROPHIES'
27 | export const GET_USERNAME = 'GET_USERNAME'
28 | export const GET_USER_POSTS = 'GET_USER_POSTS'
29 | export const CLEAR_USER_POSTS = 'CLEAR_USER_POSTS'
30 | export const CLEAR_USER_INFO = 'CLEAR_USER_INFO'
31 | export const CHANGE_SORT_USER_CONTENT_BY = 'CHANGE_SORT_USER_CONTENT_BY'
32 |
--------------------------------------------------------------------------------
/src/context/user/UserState.tsx:
--------------------------------------------------------------------------------
1 | import React, { useReducer } from 'react'
2 |
3 | import UserContext from './userContext'
4 | import userReducer from './userReducer'
5 | import axios from 'axios'
6 | import {
7 | GET_USER_TROPHIES,
8 | GET_USER_ABOUT,
9 | GET_USERNAME,
10 | GET_USER_POSTS,
11 | CLEAR_USER_INFO,
12 | CHANGE_SORT_USER_CONTENT_BY,
13 | CLEAR_USER_POSTS,
14 | SET_AFTER
15 | } from '../types'
16 | import { setLoading } from '../../redux/actions/loadingActions'
17 | import { connect } from 'react-redux'
18 |
19 | interface Props {
20 | children: React.ReactNode
21 | setLoading: () => void
22 | }
23 |
24 | const UserState: React.FC = ({ children, setLoading }) => {
25 | const initialState = {
26 | after: null,
27 | loading: false,
28 | test: 'test',
29 | userName: null,
30 | userData: null,
31 | userTrophies: null,
32 | userPosts: [],
33 | sortUserContentBy: 'new'
34 | }
35 |
36 | const [state, dispatch] = useReducer(userReducer, initialState)
37 |
38 | const getUserTrophies = async (userName: string) => {
39 | try {
40 | const res = await axios.get(
41 | `https://oauth.reddit.com/api/v1/user/${userName}/trophies`
42 | )
43 | console.log(res)
44 | dispatch({ type: GET_USER_TROPHIES, payload: res.data })
45 | } catch (err) {
46 | throw err
47 | }
48 | }
49 |
50 | const getUserAbout = async (userName: string) => {
51 | try {
52 | const res = await axios.get(
53 | `https://oauth.reddit.com/user/${userName}/about`
54 | )
55 | console.log(res)
56 | dispatch({ type: GET_USER_ABOUT, payload: res.data })
57 | } catch (err) {
58 | throw err
59 | }
60 | }
61 |
62 | const getUserPosts = async (userName: string | null) => {
63 | // if there is an username fetch data else clear
64 | if (userName) {
65 | if (!state.after) {
66 | setLoading()
67 | }
68 | try {
69 | const res = await axios.get(
70 | `https://oauth.reddit.com/user/${userName}/overview?raw_json=1&sort=${state.sortUserContentBy}&after=${state.after}`
71 | )
72 |
73 | console.log(res)
74 |
75 | dispatch({ type: SET_AFTER, payload: res.data.data.after })
76 | dispatch({ type: GET_USER_POSTS, payload: res.data.data.children })
77 | } catch (err) {
78 | throw err
79 | }
80 | if (!state.after) {
81 | setLoading()
82 | }
83 | } else {
84 | dispatch({ type: CLEAR_USER_POSTS, payload: null })
85 | }
86 | }
87 |
88 | const getUserName = (userName: string) => {
89 | dispatch({ type: GET_USERNAME, payload: userName })
90 | }
91 |
92 | const clearUserInfo = () => {
93 | dispatch({ type: CLEAR_USER_INFO, payload: null })
94 | }
95 |
96 | const getUserInfo = (userName: string | null) => {
97 | // setLoading
98 | if (userName) {
99 | getUserName(userName)
100 | getUserAbout(userName)
101 | getUserTrophies(userName)
102 | } else {
103 | clearUserInfo()
104 | }
105 | }
106 |
107 | const changeSortUserContentBy = (sortBy: string) => {
108 | dispatch({ type: CHANGE_SORT_USER_CONTENT_BY, payload: sortBy })
109 | }
110 |
111 | return (
112 |
125 | {children}
126 |
127 | )
128 | }
129 |
130 | export default connect(null, { setLoading })(UserState)
131 |
--------------------------------------------------------------------------------
/src/context/user/userContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | import { State } from './userTypes'
3 |
4 | const activitiesContext = createContext({})
5 |
6 | export default activitiesContext
7 |
--------------------------------------------------------------------------------
/src/context/user/userReducer.ts:
--------------------------------------------------------------------------------
1 | import { State } from '../user/userTypes'
2 | import { AllActions } from '../user/userTypes'
3 | import {
4 | GET_USER_TROPHIES,
5 | GET_USER_ABOUT,
6 | GET_USERNAME,
7 | CLEAR_USER_INFO,
8 | CHANGE_SORT_USER_CONTENT_BY,
9 | GET_USER_POSTS,
10 | CLEAR_USER_POSTS,
11 | SET_AFTER
12 | } from '../types'
13 |
14 | export default (state: State, action: AllActions) => {
15 | switch (action.type) {
16 | case GET_USER_TROPHIES: {
17 | return {
18 | ...state,
19 | userTrophies: action.payload
20 | }
21 | }
22 | case GET_USER_ABOUT: {
23 | return {
24 | ...state,
25 | userData: action.payload
26 | }
27 | }
28 | case GET_USER_POSTS: {
29 | return {
30 | ...state,
31 | userPosts: [...state.userPosts!, action.payload]
32 | }
33 | }
34 | case GET_USERNAME: {
35 | return {
36 | ...state,
37 | userName: action.payload
38 | }
39 | }
40 | case CHANGE_SORT_USER_CONTENT_BY: {
41 | return {
42 | ...state,
43 | sortUserContentBy: action.payload,
44 | after: null,
45 | userPosts: []
46 | }
47 | }
48 | case CLEAR_USER_POSTS: {
49 | return {
50 | ...state,
51 | userPosts: [],
52 | after: null
53 | }
54 | }
55 | case CLEAR_USER_INFO: {
56 | return {
57 | ...state,
58 | after: null,
59 | userTrophies: null,
60 | userData: null,
61 | userName: null
62 | }
63 | }
64 | case SET_AFTER: {
65 | return {
66 | ...state,
67 | after: action.payload
68 | }
69 | }
70 | default:
71 | return state
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/context/user/userTypes.ts:
--------------------------------------------------------------------------------
1 | import { PostData, CommentData } from '../reddit/redditTypes'
2 |
3 | export type State = {
4 | after?: string | null
5 | test?: string
6 | loading?: boolean
7 | userData?: UserData | null
8 | userPosts?: (PostData | CommentData)[][]
9 | userTrophies?: TrophyList | null
10 | userName?: string | null
11 | sortUserContentBy?: string
12 | getUserInfo?: (userName: string | null) => void
13 | getUserPosts?: (userName: string | null) => void
14 | changeSortUserContentBy?: (sortBy: string) => void
15 | }
16 |
17 | export interface Trophy {
18 | data: {
19 | award_id: string | null
20 | description: string | null
21 | icon_40: string
22 | icon_70: string
23 | id: string
24 | name: string
25 | url: string | null
26 | }
27 | kind: 't6'
28 | }
29 |
30 | export interface TrophyList {
31 | data: {
32 | trophies: Trophy[]
33 | }
34 | kind: 'TrophyList'
35 | }
36 |
37 | export interface UserData {
38 | data: {
39 | comment_karma: number
40 | created_utc: number
41 | name: string
42 | link_karma: number
43 | subreddit: any
44 | }
45 | kind: 't2'
46 | }
47 |
48 | type ActionInterface = {
49 | type: T
50 | payload: U
51 | }
52 |
53 | export type AllActions =
54 | | ActionInterface<'TEST_TYPE', null>
55 | | ActionInterface<'GET_USER_TROPHIES', TrophyList>
56 | | ActionInterface<'GET_USER_ABOUT', UserData>
57 | | ActionInterface<'GET_USERNAME', string | null>
58 | | ActionInterface<'CLEAR_USER_INFO', null>
59 | | ActionInterface<'CHANGE_SORT_USER_CONTENT_BY', string>
60 | | ActionInterface<'GET_USER_POSTS', (CommentData | PostData)[]>
61 | | ActionInterface<'CLEAR_USER_POSTS', null>
62 | | ActionInterface<'SET_AFTER', string>
63 |
--------------------------------------------------------------------------------
/src/hooks/useDebouncedRippleCleanup.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from 'react'
2 |
3 | export const useDebouncedRippleCleanUp = (
4 | rippleCount: number,
5 | duration: number,
6 | cleanUpFunction: () => void
7 | ) => {
8 | useLayoutEffect(() => {
9 | let bounce: any = null
10 | if (rippleCount > 0) {
11 | clearTimeout(bounce)
12 |
13 | bounce = setTimeout(() => {
14 | cleanUpFunction()
15 | clearTimeout(bounce)
16 | }, duration * 4)
17 | }
18 |
19 | return () => clearTimeout(bounce)
20 | }, [rippleCount, duration, cleanUpFunction])
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById('root')
10 | )
11 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/redux/actions/loadingActions.ts:
--------------------------------------------------------------------------------
1 | import { SET_LOADING } from '../../context/types'
2 |
3 | export const setLoading = () => {
4 | return {
5 | type: SET_LOADING
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/redux/actions/themeActions.ts:
--------------------------------------------------------------------------------
1 | import { TOGGLE_THEME } from '../reducers/themeReducer'
2 |
3 | export const toggleTheme = () => {
4 | return {
5 | type: TOGGLE_THEME
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/redux/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { themeReducer } from './themeReducer'
3 | import { loadingReducer } from './loadingReducer'
4 |
5 | export const rootReducer = combineReducers({
6 | theme: themeReducer,
7 | loading: loadingReducer
8 | })
9 |
--------------------------------------------------------------------------------
/src/redux/reducers/loadingReducer.ts:
--------------------------------------------------------------------------------
1 | export type LoadingState = {
2 | loading: boolean
3 | }
4 |
5 | const initialState = {
6 | loading: false
7 | }
8 |
9 | export const SET_LOADING = 'SET_LOADING'
10 |
11 | export const loadingReducer = (
12 | state: LoadingState = initialState,
13 | action: { type: 'SET_LOADING' }
14 | ) => {
15 | switch (action.type) {
16 | case SET_LOADING:
17 | return {
18 | ...state,
19 | loading: state.loading ? false : true
20 | }
21 | default:
22 | return state
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/redux/reducers/themeReducer.ts:
--------------------------------------------------------------------------------
1 | export type ThemeState = {
2 | palette: string
3 | }
4 |
5 | const initialState = {
6 | palette: 'dark'
7 | }
8 |
9 | export const TOGGLE_THEME = 'TOGGLE_THEME'
10 |
11 | export const themeReducer = (
12 | state: ThemeState = initialState,
13 | action: { type: 'TOGGLE_THEME' }
14 | ) => {
15 | switch (action.type) {
16 | case TOGGLE_THEME:
17 | return {
18 | ...state,
19 | palette: state.palette === 'dark' ? 'light' : 'dark'
20 | }
21 | default:
22 | return state
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/redux/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import { composeWithDevTools } from 'redux-devtools-extension'
3 | import thunk from 'redux-thunk'
4 | import { rootReducer } from './reducers'
5 |
6 | import { ThemeState } from './reducers/themeReducer'
7 | import { LoadingState } from './reducers/loadingReducer'
8 |
9 | export type ReduxState = {
10 | theme: ThemeState
11 | loading: LoadingState
12 | }
13 |
14 | const initialState = {}
15 |
16 | const middleware = [thunk]
17 |
18 | export const store = createStore(
19 | rootReducer,
20 | initialState,
21 | composeWithDevTools(applyMiddleware(...middleware))
22 | )
23 |
--------------------------------------------------------------------------------
/src/styled.d.ts:
--------------------------------------------------------------------------------
1 | // import original module declarations
2 | import 'styled-components'
3 |
4 | // and extend them!
5 | declare module 'styled-components' {
6 | export interface DefaultTheme {
7 | colors: {
8 | backgroundColor: string
9 | textColor: string
10 | textColorFaded: string
11 | primaryColor: string
12 | secondaryColor: string
13 | subMenuColor: string
14 | navActive: string
15 | highlight: string
16 | }
17 | boxShadow: string
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/themes/MyThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import { ThemeProvider } from 'styled-components'
4 | import { darkTheme, lightTheme } from './my-theme'
5 | import { ReduxState } from '../redux/store'
6 |
7 | interface MyThemeProviderProps {
8 | children: React.ReactNode
9 | }
10 |
11 | export const MyThemeProvider: React.FC = ({
12 | children
13 | }) => {
14 | const state = useSelector((state: ReduxState) => state.theme)
15 |
16 | return (
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/themes/my-theme.ts:
--------------------------------------------------------------------------------
1 | // my-theme.ts
2 | import { DefaultTheme } from 'styled-components'
3 |
4 | const darkTheme: DefaultTheme = {
5 | colors: {
6 | backgroundColor: 'rgb(18, 18, 18)',
7 | textColor: 'rgb(255, 255, 255)',
8 | textColorFaded: 'rgba(255, 255, 255, 0.6)',
9 | primaryColor: '#FFB300',
10 | secondaryColor: '#6600ff',
11 | subMenuColor: 'rgb(42, 42, 42)',
12 | navActive: 'rgba(255,255,255,0.2)',
13 | highlight: '#264061'
14 | },
15 | boxShadow: 'none'
16 | }
17 |
18 | const lightTheme: DefaultTheme = {
19 | colors: {
20 | backgroundColor: 'rgb(255, 255, 255)',
21 | textColor: 'rgb(18, 18, 18)',
22 | textColorFaded: 'rgba(18, 18, 18, 0.6)',
23 | primaryColor: '#FFD54F',
24 | secondaryColor: '#b587ff',
25 | subMenuColor: 'rgb(255, 255, 255)',
26 | navActive: 'rgba(18,18,18,0.2)',
27 | highlight: '#65A6F9'
28 | },
29 | boxShadow: '0 0.4rem 0.4rem rgba(0,0,0,0.2)'
30 | }
31 |
32 | export { darkTheme, lightTheme }
33 |
--------------------------------------------------------------------------------
/src/utils/customEase.ts:
--------------------------------------------------------------------------------
1 | export const customEase = [0.6, 0.05, -0.01, 0.9]
2 |
--------------------------------------------------------------------------------
/src/utils/decodeHtml.ts:
--------------------------------------------------------------------------------
1 | export const decodeHTML = (str: string) => {
2 | let txt = document.createElement('textarea')
3 | txt.innerHTML = str
4 | return txt.value
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/defaultSubredditsParser.ts:
--------------------------------------------------------------------------------
1 | interface Ikeys {
2 | name: string
3 | icon: string
4 | }
5 |
6 | export const defaultSubredditsParser = (subreddits: []) => {
7 | return subreddits.map(
8 | (subreddit: any) =>
9 | ({
10 | name: subreddit.data.display_name,
11 | icon: subreddit.data.icon_img
12 | } as Ikeys)
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/setAuthToken.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosRequestConfig } from 'axios'
2 | import moment from 'moment'
3 | import qs from 'qs'
4 |
5 | export const setAuthToken = (token?: string) => {
6 | if (token) {
7 | // apply to every request
8 |
9 | axios.defaults.headers.common['Authorization'] = token
10 | } else {
11 | // Delete auth header
12 | delete axios.defaults.headers.common['Authorization']
13 | }
14 | }
15 |
16 | axios.interceptors.request.use(
17 | config => {
18 | let exp = localStorage.getItem('exp')
19 | let now = new Date()
20 |
21 | if (exp) {
22 | if (new Date(exp) < now) {
23 | console.log('expired')
24 | localStorage.clear()
25 | return applicationOnlyAuth().then((token: string) => {
26 | setAuthToken(token)
27 | return Promise.resolve(config)
28 | })
29 | }
30 | }
31 | return config
32 | },
33 | err => {
34 | return Promise.reject(err)
35 | }
36 | )
37 |
38 | const applicationOnlyAuth = async () => {
39 | const body = {
40 | grant_type: 'client_credentials'
41 | }
42 |
43 | const config: AxiosRequestConfig = {
44 | headers: {
45 | 'Content-Type': 'application/x-www-form-urlencoded'
46 | },
47 | auth: {
48 | username: process.env.REACT_APP_CLIENT_ID!,
49 | password: process.env.REACT_APP_SECRET_KEY!
50 | }
51 | }
52 |
53 | try {
54 | const res = await axios.post(
55 | 'https://www.reddit.com/api/v1/access_token',
56 | qs.stringify(body),
57 | config
58 | )
59 |
60 | const exp = moment().add(res.data.expires_in, 's')
61 |
62 | localStorage.setItem('token', res.data.access_token)
63 | localStorage.setItem('exp', exp.toString())
64 |
65 | return res.data.access_token
66 | } catch (err) {
67 | throw err
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils/subredditParser.ts:
--------------------------------------------------------------------------------
1 | // types of posts are self:text general:link, video:link
2 | // video: hosted or image
3 |
4 | interface PostData {
5 | author: string
6 | created_utc: number
7 | domain: string
8 | link_flair_text: string
9 | num_comments: number
10 | preview: {
11 | images: {
12 | source: {
13 | url: string
14 | }
15 | }[]
16 | reddit_video_preview: any
17 | }
18 | post_hint?: string
19 | score: number
20 | selftext: string
21 | secure_media?: any
22 | subreddit: string
23 | stickied: boolean
24 | thumbnail: string
25 | title: string
26 | url: string
27 | }
28 |
29 | export const getPostType = (postData: PostData): string => {
30 | const { post_hint, preview, selftext, secure_media, url } = postData
31 |
32 | // self post
33 | if (selftext.length > 0) {
34 | return 'self'
35 | // link with no image
36 | } else if (!preview && selftext.length === 0) {
37 | return 'link'
38 | // link with an image
39 | } else if (post_hint === 'link' && !preview.reddit_video_preview) {
40 | return 'link:preview'
41 | // gifycat,imgur, or redgif link
42 | } else if (preview.reddit_video_preview) {
43 | return 'video:outside'
44 | // reddit hosted video
45 | } else if (secure_media) {
46 | if (post_hint === 'rich:video') {
47 | return 'link:video'
48 | } else {
49 | return 'video:hosted'
50 | }
51 | } else if (url.split('.').slice(-1)[0] === 'gif') {
52 | return 'video:native'
53 | } else return 'image'
54 | }
55 |
56 | export const getMedia = (postData: PostData, type: string) => {
57 | if (type === 'self' || type === 'link') {
58 | return null
59 | } else if (
60 | type === 'image' ||
61 | type === 'link:video' ||
62 | type === 'link:preview'
63 | ) {
64 | return postData.preview.images[0].source.url
65 | } else if (type === 'video:native') {
66 | return postData.url
67 | } else if (type === 'video:hosted') {
68 | return postData.secure_media.reddit_video.fallback_url
69 | } else if (type === 'video:outside') {
70 | return postData.preview.reddit_video_preview.fallback_url
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/utils/variants.ts:
--------------------------------------------------------------------------------
1 | export const parentVariants = {
2 | hidden: {
3 | transition: {
4 | staggerChildren: 0.04
5 | }
6 | },
7 | visible: {
8 | transition: {
9 | staggerChildren: 0.04
10 | }
11 | }
12 | }
13 |
14 | export const childVariants = {
15 | hidden: {
16 | x: -1000
17 | },
18 | visible: {
19 | x: 0
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------