├── .gitignore ├── public ├── post1.jpg ├── post2.jpg ├── post1_comments.jpg └── post2-comments.jpg ├── client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── components │ │ ├── Slider │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── Header │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── Interactions │ │ │ ├── Interaction │ │ │ │ ├── index.scss │ │ │ │ └── index.js │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── Loader.js │ │ ├── Menu │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── Overlay │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── BottomDrawer │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── Comments │ │ │ ├── index.scss │ │ │ └── index.js │ │ ├── Display │ │ │ ├── index.scss │ │ │ └── index.js │ │ └── Views │ │ │ └── Home.js │ ├── setupTests.js │ ├── stories │ │ ├── 3-Menu.stories.js │ │ ├── 0-Welcome.stories.js │ │ ├── 4-BottomDrawer.stories.js │ │ ├── 1-Button.stories.js │ │ ├── 2-Display.stories.js │ │ └── 5-Comments.stories.js │ ├── App.test.js │ ├── utils │ │ ├── AppUtils.js │ │ └── PostUtils.js │ ├── store.js │ ├── index.scss │ ├── index.js │ ├── App.scss │ ├── actions │ │ ├── postsActions.js │ │ ├── types.js │ │ └── appActions.js │ ├── reducers │ │ ├── appReducer.js │ │ └── postsReducer.js │ ├── App.js │ └── serviceWorker.js ├── .gitignore ├── package.json └── README.md ├── package.json ├── server.js ├── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /public/post1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/public/post1.jpg -------------------------------------------------------------------------------- /public/post2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/public/post2.jpg -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /public/post1_comments.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/public/post1_comments.jpg -------------------------------------------------------------------------------- /public/post2-comments.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AWash227/reddit-but-its-tiktok/HEAD/public/post2-comments.jpg -------------------------------------------------------------------------------- /client/src/components/Slider/index.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/stories/3-Menu.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../index.scss"; 3 | import "tachyons"; 4 | import Menu from "../components/Menu"; 5 | 6 | export default { 7 | title: "Menu", 8 | component: Menu 9 | }; 10 | 11 | export const DefaultMenu = () => ; 12 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/Header/index.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | display: flex; 4 | top: 0; 5 | right: 0; 6 | left: 0; 7 | z-index: 2; 8 | justify-content: center; 9 | color: white; 10 | margin: 2rem; 11 | div { 12 | text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.4); 13 | padding: 0 0.25rem 0 0.25rem; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Interactions/Interaction/index.scss: -------------------------------------------------------------------------------- 1 | .interaction { 2 | box-sizing: border-box; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | cursor: pointer; 7 | .text { 8 | text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.7); 9 | } 10 | svg { 11 | filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.5)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/utils/AppUtils.js: -------------------------------------------------------------------------------- 1 | import { formatPost } from "./PostUtils"; 2 | 3 | export const formatResponse = data => ({ 4 | before: data.data.before, 5 | after: data.data.after, 6 | numPosts: data.data.dist, 7 | posts: data.data.children.map(post => formatPost(post.data)) 8 | }); 9 | 10 | export const DEV_URL = `http://localhost:5000`; 11 | export const PROD_URL = ``; 12 | -------------------------------------------------------------------------------- /client/src/components/Interactions/index.scss: -------------------------------------------------------------------------------- 1 | .interactions { 2 | pointer-events: all; 3 | overflow: hidden; 4 | position: fixed; 5 | z-index: 3; 6 | display: flex; 7 | flex-direction: column; 8 | box-sizing: border-box; 9 | top: 50%; 10 | right: 0; 11 | padding: 1rem; 12 | margin: 1rem; 13 | width: 20%; 14 | height: 50%; 15 | overflow: none; 16 | word-wrap: break-word; 17 | } 18 | -------------------------------------------------------------------------------- /client/src/stories/0-Welcome.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { linkTo } from '@storybook/addon-links'; 3 | import { Welcome } from '@storybook/react/demo'; 4 | 5 | export default { 6 | title: 'Welcome', 7 | component: Welcome, 8 | }; 9 | 10 | export const ToStorybook = () => ; 11 | 12 | ToStorybook.story = { 13 | name: 'to Storybook', 14 | }; 15 | -------------------------------------------------------------------------------- /client/src/stories/4-BottomDrawer.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../index.scss"; 3 | import "tachyons"; 4 | import BottomDrawer from "../components/BottomDrawer"; 5 | 6 | export default { 7 | title: "Bottom Drawer", 8 | component: BottomDrawer 9 | }; 10 | 11 | export const DefaultMenu = () => ( 12 | {}} /> 13 | ); 14 | -------------------------------------------------------------------------------- /client/src/components/Interactions/Interaction/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.scss"; 3 | 4 | const Interaction = ({ icon, text, onClick }) => { 5 | return ( 6 |
7 |
{icon}
8 |
{text}
9 |
10 | ); 11 | }; 12 | 13 | export default Interaction; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "axios": "^0.19.2", 8 | "cors": "^2.8.5", 9 | "express": "^4.17.1" 10 | }, 11 | "devDependencies": { 12 | "nodemon": "^2.0.2" 13 | }, 14 | "scripts": { 15 | "start": "node server.js", 16 | "heroku-postbuild": "cd client && npm install && npm run build" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /.storybook 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 | -------------------------------------------------------------------------------- /client/src/components/Slider/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Swipeable } from "react-swipeable"; 3 | import "./index.scss"; 4 | 5 | const Slider = ({ handleSwipeDown, handleSwipeUp, children }) => { 6 | return ( 7 | handleSwipeDown()} 9 | onSwipedUp={() => handleSwipeUp()} 10 | trackMouse 11 | className="slider w-100 h-100" 12 | > 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default Slider; 19 | -------------------------------------------------------------------------------- /client/src/stories/1-Button.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { action } from '@storybook/addon-actions'; 3 | import { Button } from '@storybook/react/demo'; 4 | 5 | export default { 6 | title: 'Button', 7 | component: Button, 8 | }; 9 | 10 | export const Text = () => ; 11 | 12 | export const Emoji = () => ( 13 | 18 | ); 19 | -------------------------------------------------------------------------------- /client/src/components/Interactions/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.scss"; 3 | import Interaction from "./Interaction"; 4 | 5 | const Interactions = ({ interactions }) => { 6 | return ( 7 |
8 | {interactions.map((interaction, i) => ( 9 | 15 | ))} 16 |
17 | ); 18 | }; 19 | 20 | export default Interactions; 21 | -------------------------------------------------------------------------------- /client/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from "redux"; 2 | import { postsReducer } from "./reducers/postsReducer"; 3 | import thunk from "redux-thunk"; 4 | import { appReducer } from "./reducers/appReducer"; 5 | 6 | const RootReducer = combineReducers({ posts: postsReducer, app: appReducer }); 7 | 8 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 9 | 10 | const store = createStore( 11 | RootReducer, 12 | composeEnhancers(applyMiddleware(thunk)) 13 | ); 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /client/src/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | #root, 3 | body { 4 | width: 100%; 5 | height: 100%; 6 | background-color: black; 7 | -webkit-tap-highlight-color: transparent; 8 | } 9 | body { 10 | margin: 0; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 12 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 13 | sans-serif; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 20 | monospace; 21 | } 22 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "rbitt", 3 | "name": "Reddit but it's TikTok", 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": "fullscreen", 23 | "theme_color": "#000000", 24 | "background_color": "#000000" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Loader } from "react-loaders"; 3 | 4 | export const FullScreenLoader = ({ active }) => { 5 | if (active) { 6 | return ( 7 |
21 | 22 |
23 | ); 24 | } 25 | return null; 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import "tachyons"; 5 | import "loaders.css"; 6 | import "./index.scss"; 7 | import App from "./App"; 8 | import * as serviceWorker from "./serviceWorker"; 9 | import store from "./store"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById("root") 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /client/src/components/Menu/index.scss: -------------------------------------------------------------------------------- 1 | .menu { 2 | width: 100%; 3 | height: 4rem; 4 | position: fixed; 5 | z-index: 3; 6 | bottom: 0; 7 | color: white; 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: space-evenly; 11 | align-items: center; 12 | padding: 1rem 0 0.5rem 0; 13 | border-top: 1px solid rgba(255, 255, 255, 0.5); 14 | } 15 | .menu-item { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | svg { 20 | color: rgba(255, 255, 255, 0.9); 21 | filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.45)); 22 | } 23 | a { 24 | font-size: 0.7rem; 25 | text-decoration: none; 26 | color: white; 27 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 1); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .App-logo { 8 | height: 40vmin; 9 | pointer-events: none; 10 | } 11 | 12 | @media (prefers-reduced-motion: no-preference) { 13 | .App-logo { 14 | animation: App-logo-spin infinite 20s linear; 15 | } 16 | } 17 | 18 | .App-header { 19 | background-color: #282c34; 20 | min-height: 100vh; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | font-size: calc(10px + 2vmin); 26 | color: white; 27 | } 28 | 29 | .App-link { 30 | color: #61dafb; 31 | } 32 | 33 | @keyframes App-logo-spin { 34 | from { 35 | transform: rotate(0deg); 36 | } 37 | to { 38 | transform: rotate(360deg); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/actions/postsActions.js: -------------------------------------------------------------------------------- 1 | import { postTypes as pt, appTypes as at } from "./types"; 2 | import axios from "axios"; 3 | import { PROD_URL, DEV_URL } from "../utils/AppUtils"; 4 | 5 | // Change me to DEV_URL if you are trying to run the app locally 6 | const URL = PROD_URL; 7 | 8 | export const setCount = (count = 0) => ({ 9 | type: pt.SET_COUNT, 10 | payload: count 11 | }); 12 | export const fetchCommentsFromPost = (postId, query) => dispatch => { 13 | dispatch({type:pt.FETCHING_COMMENTS}); 14 | axios 15 | .get(`${URL}/api/comments/${postId}${query}`) 16 | .then(({ data }) => { 17 | let comments = data[1].data.children.map(child => child.data); 18 | dispatch({ type: pt.FETCH_COMMENTS_FROM_POST, payload: comments }); 19 | dispatch({type:pt.FINISHED_FETCHING_COMMENTS}); 20 | }) 21 | .catch(err => console.error(err)); 22 | }; 23 | 24 | export const nextPost = () => ({ 25 | type: pt.LOAD_NEXT_POST 26 | }); 27 | -------------------------------------------------------------------------------- /client/src/components/Overlay/index.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position: fixed; 3 | z-index: 4; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | pointer-events: none; 9 | &-title { 10 | overflow: inherit; 11 | padding: 1rem; 12 | padding-top: 2rem; 13 | color: white; 14 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.8); 15 | position: absolute; 16 | bottom: 4rem; 17 | left: 0; 18 | background: rgb(0, 0, 0); 19 | background: linear-gradient( 20 | 180deg, 21 | rgba(0, 0, 0, 0) 0%, 22 | rgba(0, 0, 0, 0.2) 20%, 23 | rgba(0, 0, 0, 0.5) 50%, 24 | rgba(0, 0, 0, 0.8) 100% 25 | ); 26 | z-index: 4; 27 | } 28 | &-enter { 29 | opacity: 0; 30 | background-color: blue; 31 | } 32 | &-enter-active { 33 | opacity: 1; 34 | transition: opacity 200ms; 35 | } 36 | &-exit { 37 | opacity: 1; 38 | transition: opacity 200ms; 39 | } 40 | &-exit-active { 41 | opacity: 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const postTypes = (() => { 2 | const SET_POST = "SET_POST"; 3 | const SET_POSTS = "SET_POSTS"; 4 | const SET_COUNT = "SET_COUNT"; 5 | const FETCH_COMMENTS_FROM_POST = "FETCH_COMMENTS_FROM_POST"; 6 | const FETCHING_COMMENTS = "FETCHING_COMMENTS"; 7 | const FINISHED_FETCHING_COMMENTS = "FINISHED_FETCHING_COMMENTS"; 8 | const LOAD_NEXT_POST = "LOAD_NEXT_POST"; 9 | return { 10 | SET_POST, 11 | SET_POSTS, 12 | SET_COUNT, 13 | FETCH_COMMENTS_FROM_POST, 14 | FETCHING_COMMENTS, 15 | FINISHED_FETCHING_COMMENTS, 16 | LOAD_NEXT_POST 17 | }; 18 | })(); 19 | 20 | export const appTypes = (() => { 21 | const FETCH_POSTS_FROM_SUBREDDIT = "FETCH_POSTS_FROM_SUBREDDIT"; 22 | const FETCHING_POSTS = "FETCHING_POSTS"; 23 | const FINISHED_FETCHING_POSTS = "FINISHED_FETCHING_POSTS"; 24 | const SET_SUBREDDIT = "SET_SUBREDDIT"; 25 | return { 26 | FETCH_POSTS_FROM_SUBREDDIT, 27 | SET_SUBREDDIT, 28 | FETCHING_POSTS, 29 | FINISHED_FETCHING_POSTS 30 | }; 31 | })(); 32 | -------------------------------------------------------------------------------- /client/src/reducers/appReducer.js: -------------------------------------------------------------------------------- 1 | import { appTypes as at, postTypes as pt } from "../actions/types"; 2 | import { formatResponse } from "../utils/AppUtils"; 3 | import { postsReducer } from "./postsReducer"; 4 | const initialState = { 5 | subreddit: "all", 6 | fetchingPosts: false, 7 | data: {} 8 | }; 9 | 10 | export const appReducer = (state = initialState, action) => { 11 | switch (action.type) { 12 | case at.FETCHING_POSTS: 13 | return { 14 | ...state, 15 | fetchingPosts: true 16 | }; 17 | case at.FINISHED_FETCHING_POSTS: 18 | return { 19 | ...state, 20 | fetchingPosts: false 21 | }; 22 | case at.FETCH_POSTS_FROM_SUBREDDIT: 23 | console.log("FETCHING", action.payload); 24 | return { 25 | ...state, 26 | data: action.payload 27 | }; 28 | 29 | case at.SET_SUBREDDIT: 30 | return { 31 | ...state, 32 | subreddit: action.payload 33 | }; 34 | 35 | default: 36 | return state; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /client/src/components/BottomDrawer/index.scss: -------------------------------------------------------------------------------- 1 | .bottom-drawer { 2 | z-index: 10; 3 | position: fixed; 4 | bottom: 0; 5 | background-color: white; 6 | border-top-left-radius: 10px; 7 | border-top-right-radius: 10px; 8 | width: 100%; 9 | pointer-events: all; 10 | .bottom-drawer-header { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | height: 2rem; 15 | font-size: 0.9rem; 16 | color: black; 17 | .close { 18 | position: absolute; 19 | right: 0.5rem; 20 | top: 0.25rem; 21 | } 22 | } 23 | .body { 24 | background-color: white; 25 | height: 27.5rem; 26 | max-height: 60%; 27 | overflow-y: scroll; 28 | -webkit-overflow-scrolling: touch; 29 | } 30 | } 31 | 32 | .bottom-drawer-enter { 33 | bottom: -100%; 34 | } 35 | 36 | .bottom-drawer-enter-active { 37 | bottom: 0; 38 | transition: all 0.25s; 39 | } 40 | .bottom-drawer-exit { 41 | bottom: 0; 42 | } 43 | .bottom-drawer-exit-active { 44 | bottom: -100%; 45 | transition: all 0.25s; 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/Menu/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.scss"; 3 | import { 4 | IoIosHome, 5 | IoIosSearch, 6 | IoIosAddCircle, 7 | IoIosChatboxes, 8 | IoIosPerson 9 | } from "react-icons/io"; 10 | 11 | const menuItems = [ 12 | { icon: , text: "Home", link: "/home" }, 13 | { icon: , text: "Discover", link: "/discover" }, 14 | { icon: , text: "", link: "/add" }, 15 | { icon: , text: "Inbox", link: "/inbox" }, 16 | { icon: , text: "Me", link: "/me" } 17 | ]; 18 | 19 | const MenuItem = ({ selected, icon, text, link }) => { 20 | return ( 21 |
22 | {icon} 23 | {text} 24 |
25 | ); 26 | }; 27 | 28 | const Menu = () => { 29 | return ( 30 |
31 | {menuItems.map(item => ( 32 | 39 | ))} 40 |
41 | ); 42 | }; 43 | 44 | export default Menu; 45 | -------------------------------------------------------------------------------- /client/src/components/Comments/index.scss: -------------------------------------------------------------------------------- 1 | .comments { 2 | width: 100%; 3 | height: 100%; 4 | background-color: white; 5 | padding: 0.5rem; 6 | 7 | .comment-thread { 8 | margin: 0.5rem 0 0 0; 9 | } 10 | } 11 | 12 | .comment { 13 | font-size: 0.9rem; 14 | background-color: white; 15 | padding: 0.5rem 0 0 0.5rem; 16 | text-align: left; 17 | color: black; 18 | .comment-header { 19 | height: 1rem; 20 | font-size: 0.7rem; 21 | color: black; 22 | .comment-collapse { 23 | display: inline-flex; 24 | border-radius: 999px; 25 | width: 0.75rem; 26 | height: 0.75rem; 27 | justify-content: center; 28 | align-items: center; 29 | padding: 0.15rem; 30 | margin-right: 0.25rem; 31 | &:hover { 32 | background-color: rgba(0, 0, 0, 0.2); 33 | } 34 | } 35 | .comment-author { 36 | &:hover { 37 | text-decoration: underline; 38 | cursor: pointer; 39 | } 40 | } 41 | .comment-score { 42 | color: rgba(0, 0, 0, 0.5); 43 | } 44 | } 45 | .comment-body { 46 | margin: 0 0 0.25rem 0; 47 | padding: 0.5rem 0 0 0.5rem; 48 | line-height: 1.5; 49 | p { 50 | padding: 0; 51 | margin: 0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/src/actions/appActions.js: -------------------------------------------------------------------------------- 1 | import { appTypes as at, postTypes as pt } from "./types"; 2 | import axios from "axios"; 3 | import { formatResponse } from "../utils/AppUtils"; 4 | import { PROD_URL, DEV_URL } from "../utils/AppUtils"; 5 | 6 | // Change me to DEV_URL if you are trying to run the app locally 7 | const URL = PROD_URL; 8 | 9 | export const fetchPostsFromSubreddit = ( 10 | subreddit = "pics", 11 | query = "" 12 | ) => dispatch => { 13 | dispatch({ type: at.FETCHING_POSTS }); 14 | axios 15 | .get(`${URL}/api/r/${subreddit}${query}`) 16 | .then(res => { 17 | const data = formatResponse(res.data); 18 | let bOA = ""; 19 | if (query.includes("before")) { 20 | bOA = "BEFORE"; 21 | } else if (query.includes("after")) { 22 | bOA = "AFTER"; 23 | } else { 24 | bOA = ""; 25 | } 26 | dispatch({ type: at.FETCH_POSTS_FROM_SUBREDDIT, payload: data }); 27 | dispatch({ 28 | type: pt.SET_POSTS, 29 | payload: { posts: data.posts, numPosts: data.numPosts - 1, query: bOA } 30 | }); 31 | dispatch({ type: at.FINISHED_FETCHING_POSTS }); 32 | }) 33 | .catch(err => console.error(err)); 34 | }; 35 | 36 | export const setSubreddit = (sub = "") => ({ 37 | type: at.SET_SUBREDDIT, 38 | payload: sub 39 | }); 40 | -------------------------------------------------------------------------------- /client/src/components/Display/index.scss: -------------------------------------------------------------------------------- 1 | .display { 2 | overflow: hidden; 3 | height: 100%; 4 | width: 100%; 5 | &-img { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | align-content: center; 10 | overflow: inherit; 11 | position: relative; 12 | height: 100%; 13 | width: 100%; 14 | z-index: 2; 15 | img, 16 | video, 17 | p { 18 | max-height: 100vh; 19 | max-width: 100vw; 20 | } 21 | img { 22 | border-radius: 5px; 23 | pointer-events: none; 24 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2), 25 | 0px 0px 15px rgba(0, 0, 0, 0.5); 26 | } 27 | .text { 28 | padding: 1rem; 29 | margin: 2rem; 30 | border: 1px solid white; 31 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); 32 | background-color: rgba(0, 0, 0, 0.35); 33 | border-radius: 5px; 34 | } 35 | } 36 | &-bg { 37 | position: fixed; 38 | top: 0; 39 | bottom: 0; 40 | left: 0; 41 | right: 0; 42 | overflow: inherit; 43 | pointer-events: none; 44 | filter: blur(10px) opacity(0.5); 45 | -webkit-filter: blur(10px); 46 | background-position: center; 47 | background-repeat: no-repeat; 48 | background-size: cover; 49 | border-radius: 10px; 50 | z-index: 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/components/Views/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Header from "../Header"; 3 | import Display from "../Display"; 4 | import Menu from "../Menu"; 5 | import Slider from "../Slider"; 6 | 7 | const Home = ({ 8 | subreddit, 9 | post, 10 | nextPost, 11 | prevPost, 12 | fetchPosts, 13 | loadNextPost, 14 | loadPrevPost 15 | }) => { 16 | const [overlayActive, setOverlayActive] = useState(false); 17 | return ( 18 |
19 |
20 | {/* 21 | 28 | 29 | */} 30 | 36 | {/* 37 | 45 | 46 | */} 47 | 48 |
49 | ); 50 | }; 51 | 52 | export default Home; 53 | -------------------------------------------------------------------------------- /client/src/components/BottomDrawer/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import "./index.scss"; 3 | import { IoIosClose } from "react-icons/io"; 4 | import { Swipeable } from "react-swipeable"; 5 | import { CSSTransition, Transition } from "react-transition-group"; 6 | 7 | const BottomDrawer = ({ active, title, setActive, children }) => { 8 | const [isAtTop, setIsAtTop] = useState(false); 9 | const handleClose = () => { 10 | setActive(false); 11 | }; 12 | 13 | return ( 14 | 17 | 23 |
24 |
25 | {title} 26 | handleClose()} 28 | className="close" 29 | size={25} 30 | /> 31 |
32 |
{ 35 | if (e.currentTarget.scrollTop > 5) { 36 | setIsAtTop(false); 37 | } else { 38 | setIsAtTop(true); 39 | } 40 | }} 41 | > 42 | {isAtTop} 43 | {children} 44 |
45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default BottomDrawer; 52 | -------------------------------------------------------------------------------- /client/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./index.scss"; 3 | import { connect } from "react-redux"; 4 | import { setSubreddit } from "../../actions/appActions"; 5 | 6 | const Header = ({ subreddit, setSubreddit, fetchPosts }) => { 7 | const links = [ 8 | "Following", 9 | |, 10 | `/r/${subreddit}` 11 | ]; 12 | const [sub, setSub] = useState(""); 13 | return ( 14 |
15 |
16 | {links.map((link, i) => 17 | i === 2 ? ( 18 |
19 | {link} 20 |
21 | ) : ( 22 |
23 | {link} 24 |
25 | ) 26 | )} 27 |
28 |
{ 30 | e.preventDefault(); 31 | setSubreddit(sub); 32 | }} 33 | > 34 | { 41 | if (e.keyCode === 13) { 42 | setSubreddit(sub); 43 | fetchPosts(subreddit); 44 | } 45 | }} 46 | onChange={e => setSub(e.target.value)} 47 | /> 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default connect(null, { setSubreddit })(Header); 54 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const server = express(); 3 | const cors = require("cors"); 4 | const axios = require("axios").default; 5 | const port = process.env.PORT || 5000; 6 | const router = express.Router(); 7 | 8 | const BASE_URL = "https://reddit.com"; 9 | 10 | const isEmptyObject = object => { 11 | for (let key in object) { 12 | if (object.hasOwnProperty(key)) { 13 | return false; 14 | } 15 | return true; 16 | } 17 | }; 18 | 19 | const queryToString = query => { 20 | console.log(JSON.stringify(query)); 21 | if (query && !isEmptyObject(query)) { 22 | return Object.keys(query) 23 | .map((key, i) => 24 | i === 0 ? `?${key}=${query[key]}` : `&${key}=${query[key]}` 25 | ) 26 | .reduce((prev, curr) => prev + curr, ""); 27 | } 28 | }; 29 | 30 | router.route("/r/:subreddit").get((req, res) => { 31 | axios 32 | .get( 33 | `${BASE_URL}/r/${req.params.subreddit}.json${queryToString(req.query)}` 34 | ) 35 | .then(posts => { 36 | res.json(posts.data); 37 | }) 38 | .catch(err => console.log(err)); 39 | }); 40 | 41 | router.route("/comments/:id").get((req, res) => { 42 | axios 43 | .get( 44 | `${BASE_URL}/comments/${req.params.id}.json${queryToString(req.query)}` 45 | ) 46 | .then(comments => { 47 | res.json(comments.data); 48 | }) 49 | .catch(err => console.error(err)); 50 | }); 51 | 52 | server.use(express.json()); 53 | server.use(cors()); 54 | server.use("/api", router); 55 | server.use(express.static("./client/build/")); 56 | if (process.env.NODE_ENV === "production") { 57 | server.get("/", (req, res) => { 58 | res.sendFile(path.resolve(__dirname, "client", "build", "index.html")); 59 | }); 60 | } 61 | 62 | server.listen(port, () => console.log(`Reddit CORS fixer : Port ${port}`)); 63 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | rbitt 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/src/stories/2-Display.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../index.scss"; 3 | import "tachyons"; 4 | import Display from "../components/Display"; 5 | 6 | export default { 7 | title: "Display", 8 | component: Display 9 | }; 10 | 11 | export const FullImagePost = () => ( 12 | 25 | ); 26 | 27 | export const FullGIFPost = () => ( 28 | 40 | ); 41 | 42 | export const FullVideoEmbedPost = () => ( 43 | 56 | ); 57 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.19.2", 10 | "blurhash": "^1.1.3", 11 | "cors": "^2.8.5", 12 | "express": "^4.17.1", 13 | "loaders.css": "^0.1.2", 14 | "node-sass": "^4.13.1", 15 | "react": "^16.13.0", 16 | "react-blurhash": "^0.1.2", 17 | "react-dom": "^16.13.0", 18 | "react-icons": "^3.9.0", 19 | "react-loaders": "^3.0.1", 20 | "react-redux": "^7.2.0", 21 | "react-scripts": "3.4.0", 22 | "react-swipeable": "^5.5.1", 23 | "react-transition-group": "^4.3.0", 24 | "redux": "^4.0.5", 25 | "redux-thunk": "^2.3.0", 26 | "storybook": "^5.3.17", 27 | "tachyons": "^4.11.1" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject", 34 | "storybook": "start-storybook -p 9009 -s public", 35 | "build-storybook": "build-storybook -s public" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@storybook/addon-actions": "^5.3.17", 54 | "@storybook/addon-links": "^5.3.17", 55 | "@storybook/addons": "^5.3.17", 56 | "@storybook/preset-create-react-app": "^2.1.1", 57 | "@storybook/preset-scss": "^1.0.2", 58 | "@storybook/react": "^5.3.17", 59 | "css-loader": "^3.4.2", 60 | "sass-loader": "^8.0.2", 61 | "style-loader": "^1.1.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/src/components/Overlay/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { IoIosHeart, IoIosChatbubbles, IoIosRedo } from "react-icons/io"; 3 | import { comments } from "../../stories/5-Comments.stories"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { numToString, getChildren } from "../../utils/PostUtils"; 6 | import Interactions from "../Interactions"; 7 | import BottomDrawer from "../BottomDrawer"; 8 | import "./index.scss"; 9 | import Comments from "../Comments"; 10 | import { fetchCommentsFromPost } from "../../actions/postsActions"; 11 | 12 | const Overlay = ({ post, active }) => { 13 | const [commentsActive, setCommentsActive] = useState(false); 14 | const {count, posts, fetchingComment} = useSelector((state)=>state.posts); 15 | const dispatch = useDispatch(); 16 | const interactions = [ 17 | { icon: , stat: numToString(post.score) }, 18 | { 19 | icon: , 20 | stat: numToString(post.num_comments), 21 | onClick: () => setCommentsActive(true) 22 | }, 23 | { icon: , stat: "4763" } 24 | ]; 25 | 26 | useEffect(() => { 27 | if(commentsActive){ 28 | if (posts.length) { 29 | dispatch(fetchCommentsFromPost(posts[count].id, "")); 30 | } 31 | } 32 | }, [dispatch, commentsActive]); 33 | 34 | if (active) { 35 | return ( 36 |
37 | 38 | 43 | {fetchingComment ?

Loading

: } 44 |
45 | {post.type !== "TEXT" ? ( 46 |
{post.title}
47 | ) : null} 48 |
49 | ); 50 | } else { 51 | return null; 52 | } 53 | }; 54 | 55 | export default Overlay; 56 | -------------------------------------------------------------------------------- /client/src/reducers/postsReducer.js: -------------------------------------------------------------------------------- 1 | import { postTypes as pt } from "../actions/types"; 2 | import { formatResponse } from "../utils/AppUtils"; 3 | import { fetchPostsFromSubreddit } from "../actions/appActions"; 4 | import { 5 | mapOutChildren, 6 | recursiveMapOutChildren, 7 | getAllChildren 8 | } from "../utils/PostUtils"; 9 | 10 | const initialState = { 11 | data: {}, 12 | count: 3, 13 | posts: [], 14 | numPosts: 0, 15 | post: {}, 16 | fetchingComment: false, 17 | }; 18 | 19 | export const postsReducer = (state = initialState, action) => { 20 | switch (action.type) { 21 | case pt.SET_POSTS: 22 | console.log(`Setting posts to: `, action.payload); 23 | switch (action.payload.query) { 24 | case "BEFORE": 25 | return { 26 | ...state, 27 | posts: action.payload.posts, 28 | count: action.payload.posts.length - 1 29 | }; 30 | case "AFTER": 31 | return { 32 | ...state, 33 | posts: action.payload.posts, 34 | count: 0 35 | }; 36 | default: 37 | return { 38 | ...state, 39 | posts: action.payload.posts, 40 | count: 0 41 | }; 42 | } 43 | case pt.SET_COUNT: 44 | console.log(state.posts[state.count]); 45 | return { 46 | ...state, 47 | count: action.payload 48 | }; 49 | case pt.FETCHING_COMMENTS: 50 | return { 51 | ...state, 52 | fetchingComment: true 53 | } 54 | case pt.FETCH_COMMENTS_FROM_POST: 55 | // console.log("COMMENTS:", action.payload); 56 | let newPosts = [...state.posts]; 57 | newPosts[state.count] = { 58 | ...newPosts[state.count], 59 | comments: action.payload 60 | }; 61 | return { 62 | ...state, 63 | posts: newPosts 64 | }; 65 | case pt.FINISHED_FETCHING_COMMENTS: 66 | return { 67 | ...state, 68 | fetchingComment: false 69 | } 70 | case pt.LOAD_NEXT_POST: 71 | default: 72 | return state; 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import "./App.scss"; 3 | import { setCount } from "./actions/postsActions"; 4 | import { fetchPostsFromSubreddit } from "./actions/appActions"; 5 | import { nextPost, fetchCommentsFromPost } from "./actions/postsActions"; 6 | import Home from "./components/Views/Home"; 7 | import { connect } from "react-redux"; 8 | import { FullScreenLoader } from "./components/Loader"; 9 | import { getMediaSrc } from "./utils/PostUtils"; 10 | 11 | function App({ 12 | app, 13 | data, 14 | posts, 15 | count, 16 | fetchPostsFromSubreddit, 17 | fetchCommentsFromPost, 18 | setCount 19 | }) { 20 | const loadNextPost = useCallback(() => { 21 | if (count === posts.length - 1) { 22 | fetchPostsFromSubreddit( 23 | app.subreddit, 24 | `?limit=25&after=${app.data.after}&count=${count}` 25 | ); 26 | } else if (count < posts.length) { 27 | setCount(count + 1); 28 | } 29 | }, [count, app.subreddit]); 30 | 31 | const loadPreviousPost = useCallback(() => { 32 | if (count === 0) { 33 | if (app.data.before) { 34 | fetchPostsFromSubreddit( 35 | app.subreddit, 36 | `?limit=25&before=${app.data.before}&count=${posts.length - 37 | 1}` 38 | ); 39 | } 40 | } else if (count > 0) { 41 | setCount(count - 1); 42 | } 43 | }, [count, app.subreddit]); 44 | // Initial Load 45 | useEffect(() => { 46 | fetchPostsFromSubreddit(app.subreddit); 47 | }, []); 48 | 49 | useEffect(() => { 50 | // Preload all posts 51 | posts.map(post => { 52 | new Image().src = post.thumbnail; 53 | }); 54 | }, [posts]); 55 | 56 | // useEffect(() => { 57 | // if (posts.length) { 58 | // fetchCommentsFromPost(posts[count].id, ""); 59 | // } 60 | // }, [count]); 61 | 62 | console.log(app.subreddit); 63 | return ( 64 |
65 | 66 | loadNextPost()} 71 | loadPrevPost={() => loadPreviousPost()} 72 | /> 73 |
74 | ); 75 | } 76 | 77 | const mapStateToProps = state => ({ 78 | posts: state.posts.posts, 79 | post: state.posts.post, 80 | count: state.posts.count, 81 | data: state.posts.data, 82 | app: state.app 83 | }); 84 | 85 | const mapDispatchToProps = { 86 | fetchPostsFromSubreddit, 87 | fetchCommentsFromPost, 88 | setCount, 89 | nextPost 90 | }; 91 | 92 | export default connect(mapStateToProps, mapDispatchToProps)(App); 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reddit (but it's TikTok) 2 | 3 | I have long been a viewer of Reddit content. But now there is a new Social Media out there: TikTok. Now, I am not a fan of the content on there, but the app layout is not bad. 4 | So I did what anyone would do and took all of the content on Reddit and put it into Tiktok. 5 | 6 | 7 | 8 | 9 | 10 | 11 | ## How it works 12 | 13 | Client: React, Redux, Axios 14 | Server: Express 15 | 16 | The client side has been custom-made (and programmed) to look as close to TikTok as possible with some features altered to be more user-friendly w/ Reddit content. 17 | Currently, not every feature has been implemented. I'm not sure I intend to continue working on it but on the plus side you have the two most important features working (sorta). 18 | 19 | - Viewing Posts! 20 | - You also have the ability to switch subreddits using the search bar. (You have to click enter twice for some reason though...) 21 | - Comments! 22 | 23 | The server side is essentially just a wrapper over the Reddit API w/ two endpoints that enables you to fetch posts and comments. That's all I need right now. 24 | You're probably wondering why we need a wrapper if we're just doing API requests to reddit in the end anyways... Well, it's because of default browser rules on different devices. 25 | They seem to not like it when a website automatically sends requests to another server (it's a CORS error) which is solved by just fetching the data from a server that can disable that rule. 26 | So, the client needs to be bundled with the server to work on any device that is not a development machine. 27 | 28 | ## Further Improvements 29 | 30 | There is a lot more that can be done with this. Right now, the comments system only displays my username right now (this would be the first thing to fix). 31 | Here are some potential further improvements: 32 | 33 | - Sign in with your reddit account 34 | - Like posts 35 | - Comment on posts 36 | - Actually post something 37 | - Themes 38 | - Caching and pre-fetching content to make infinite scroll better. 39 | - Fixing the bug where swiping backwards on a post messes up the ordering of posts 40 | - Making the site design desktop friendly (please only use it on mobile right now :) ) 41 | 42 | ## How to run? 43 | 44 | 1. Clone the repo into a directory 45 | 2. Run npm install on both the client and root folders 46 | 3. Then execute 'npm start' on both folders (root first, then client) 47 | 4. Navigate to appActions.js and postActions.js and change the URL variable in each to DEV_URL 48 | 5. Navigate to localhost:5000 and it should be working! 49 | -------------------------------------------------------------------------------- /client/src/components/Display/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import "./index.scss"; 3 | import { removeEncoding } from "../../utils/PostUtils"; 4 | import Overlay from "../Overlay"; 5 | import Slider from "../Slider"; 6 | 7 | const Display = ({ post, autoplay, loadPrevPost, loadNextPost }) => { 8 | const [active, setActive] = useState(true); 9 | 10 | const handleKeyPress = useCallback(event => { 11 | const { key } = event; 12 | if(key === 'ArrowDown'){ 13 | loadNextPost(); 14 | } 15 | if(key === 'ArrowUp'){ 16 | loadPrevPost(); 17 | } 18 | }, [post]); 19 | 20 | useEffect(() => { 21 | window.addEventListener('keydown', handleKeyPress); 22 | return () => { 23 | window.removeEventListener('keydown', handleKeyPress); 24 | } 25 | }, [handleKeyPress]) 26 | 27 | return ( 28 |
{}}> 29 | 30 |
34 | 35 | 36 |
37 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | const MediaDisplay = ({ type, src, thumbSrc, title, autoplay }) => { 51 | const [fullSrc, setFullSrc] = useState(thumbSrc); 52 | useEffect(() => { 53 | setFullSrc(thumbSrc); 54 | if (type === "IMAGE") { 55 | let image = new Image(); 56 | image.src = src; 57 | // Ensure it is loading the correct image for the post 58 | if (image.src === src) { 59 | image.onload = () => setFullSrc(src); 60 | } 61 | } 62 | }, [src]); 63 | switch (type) { 64 | case "VIDEO": 65 | return ( 66 |