├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.js ├── MainRouter.js ├── admin │ ├── Admin.js │ └── test.js ├── auth │ ├── PrivateRoute.js │ └── index.js ├── core │ ├── Home.js │ └── Menu.js ├── images │ ├── avatar.jpg │ └── mountains.jpg ├── index.js ├── post │ ├── Comment.js │ ├── EditPost.js │ ├── NewPost.js │ ├── Posts.js │ ├── SinglePost.js │ └── apiPost.js └── user │ ├── DeleteUser.js │ ├── EditProfile.js │ ├── FindPeople.js │ ├── FollowProfileButton.js │ ├── ForgotPassword.js │ ├── Profile.js │ ├── ProfileTabs.js │ ├── ResetPassword.js │ ├── Signin.js │ ├── Signup.js │ ├── SocialLogin.js │ ├── Users.js │ └── apiUser.js └── yarn.lock /.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 | .env 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Source code for the Udemy Course 2 | 3 | ### [React Node Social Network from Scratch to Deployment ](https://www.udemy.com/node-react/?couponCode=GITHUB) 4 | 5 | ### To run this project, do the following: 6 | 7 | ##### create .env with the following code (update credentials). Make sure to create .env in the root of the project, not inside /src. react-front/.env 8 | 9 | ``` 10 | REACT_APP_API_URL=http://localhost:8080/api 11 | REACT_APP_GOOGLE_CLIENT_ID=xxxxxx.apps.googleusercontent.com 12 | ``` 13 | 14 | ##### Then run the following commands to start up the app 15 | 16 | ``` 17 | cd react-front 18 | npm install 19 | npm start 20 | ``` 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "lodash": "^4.17.11", 7 | "react": "^16.7.0", 8 | "react-dom": "^16.7.0", 9 | "react-google-login": "^5.0.2", 10 | "react-google-recaptcha": "^1.0.5", 11 | "react-recaptcha": "^2.3.10", 12 | "react-router-dom": "^4.3.1", 13 | "react-scripts": "^3.2.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/react-front/be63af731d2c353460f6c3e12d54654855e617f2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | React App 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import MainRouter from "./MainRouter"; 4 | 5 | const App = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/MainRouter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import Home from "./core/Home"; 4 | import Menu from "./core/Menu"; 5 | import Signup from "./user/Signup"; 6 | import Signin from "./user/Signin"; 7 | import Profile from "./user/Profile"; 8 | import Users from "./user/Users"; 9 | import EditProfile from "./user/EditProfile"; 10 | import FindPeople from "./user/FindPeople"; 11 | import NewPost from "./post/NewPost"; 12 | import EditPost from "./post/EditPost"; 13 | import SinglePost from "./post/SinglePost"; 14 | import PrivateRoute from "./auth/PrivateRoute"; 15 | import ForgotPassword from "./user/ForgotPassword"; 16 | import ResetPassword from "./user/ResetPassword"; 17 | import Admin from "./admin/Admin"; 18 | 19 | const MainRouter = () => ( 20 |
21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 |
50 | ); 51 | 52 | export default MainRouter; 53 | -------------------------------------------------------------------------------- /src/admin/Admin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Posts from "../post/Posts"; 3 | import Users from "../user/Users"; 4 | import { isAuthenticated } from "../auth"; 5 | import { Redirect } from "react-router-dom"; 6 | 7 | class Admin extends Component { 8 | state = { 9 | redirectToHome: false 10 | }; 11 | 12 | componentDidMount() { 13 | if (isAuthenticated().user.role !== "admin") { 14 | this.setState({ redirectToHome: true }); 15 | } 16 | } 17 | 18 | render() { 19 | if (this.state.redirectToHome) { 20 | return ; 21 | } 22 | 23 | return ( 24 |
25 |
26 |

Admin Dashboard

27 |

Welcome to React Frontend

28 |
29 |
30 |
31 |
32 |

Posts

33 |
34 | 35 |
36 |
37 |

Users

38 |
39 | 40 |
41 |
42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | export default Admin; 49 | -------------------------------------------------------------------------------- /src/admin/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/react-front/be63af731d2c353460f6c3e12d54654855e617f2/src/admin/test.js -------------------------------------------------------------------------------- /src/auth/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | import { isAuthenticated } from "./index"; 4 | 5 | const PrivateRoute = ({ component: Component, ...rest }) => ( 6 | // props means components passed down to this pricate route component 7 | 10 | isAuthenticated() ? ( 11 | 12 | ) : ( 13 | 19 | ) 20 | } 21 | /> 22 | ); 23 | 24 | export default PrivateRoute; 25 | -------------------------------------------------------------------------------- /src/auth/index.js: -------------------------------------------------------------------------------- 1 | export const signup = user => { 2 | return fetch(`${process.env.REACT_APP_API_URL}/signup`, { 3 | method: 'POST', 4 | headers: { 5 | Accept: 'application/json', 6 | 'Content-Type': 'application/json' 7 | }, 8 | body: JSON.stringify(user) 9 | }) 10 | .then(response => { 11 | return response.json(); 12 | }) 13 | .catch(err => console.log(err)); 14 | }; 15 | 16 | export const signin = user => { 17 | return fetch(`${process.env.REACT_APP_API_URL}/signin`, { 18 | method: 'POST', 19 | headers: { 20 | Accept: 'application/json', 21 | 'Content-Type': 'application/json' 22 | }, 23 | body: JSON.stringify(user) 24 | }) 25 | .then(response => { 26 | return response.json(); 27 | }) 28 | .catch(err => console.log(err)); 29 | }; 30 | 31 | export const authenticate = (jwt, next) => { 32 | if (typeof window !== 'undefined') { 33 | localStorage.setItem('jwt', JSON.stringify(jwt)); 34 | next(); 35 | } 36 | }; 37 | 38 | export const setName = (name, next) => { 39 | if (typeof window !== 'undefined') { 40 | localStorage.setItem('username', JSON.stringify(name)); 41 | next(); 42 | } 43 | }; 44 | 45 | export const signout = next => { 46 | if (typeof window !== 'undefined') localStorage.removeItem('jwt'); 47 | next(); 48 | return fetch(`${process.env.REACT_APP_API_URL}/signout`, { 49 | method: 'GET' 50 | }) 51 | .then(response => { 52 | console.log('signout', response); 53 | return response.json(); 54 | }) 55 | .catch(err => console.log(err)); 56 | }; 57 | 58 | export const isAuthenticated = () => { 59 | if (typeof window == 'undefined') { 60 | return false; 61 | } 62 | 63 | if (localStorage.getItem('jwt')) { 64 | return JSON.parse(localStorage.getItem('jwt')); 65 | } else { 66 | return false; 67 | } 68 | }; 69 | 70 | export const forgotPassword = email => { 71 | console.log('email: ', email); 72 | return fetch(`${process.env.REACT_APP_API_URL}/forgot-password/`, { 73 | method: 'PUT', 74 | headers: { 75 | Accept: 'application/json', 76 | 'Content-Type': 'application/json' 77 | }, 78 | body: JSON.stringify({ email }) 79 | }) 80 | .then(response => { 81 | console.log('forgot password response: ', response); 82 | return response.json(); 83 | }) 84 | .catch(err => console.log(err)); 85 | }; 86 | 87 | export const resetPassword = resetInfo => { 88 | return fetch(`${process.env.REACT_APP_API_URL}/reset-password/`, { 89 | method: 'PUT', 90 | headers: { 91 | Accept: 'application/json', 92 | 'Content-Type': 'application/json' 93 | }, 94 | body: JSON.stringify(resetInfo) 95 | }) 96 | .then(response => { 97 | console.log('forgot password response: ', response); 98 | return response.json(); 99 | }) 100 | .catch(err => console.log(err)); 101 | }; 102 | 103 | export const socialLogin = user => { 104 | return fetch(`${process.env.REACT_APP_API_URL}/social-login/`, { 105 | method: 'POST', 106 | headers: { 107 | Accept: 'application/json', 108 | 'Content-Type': 'application/json' 109 | }, 110 | // credentials: "include", // works only in the same origin 111 | body: JSON.stringify(user) 112 | }) 113 | .then(response => { 114 | console.log('signin response: ', response); 115 | return response.json(); 116 | }) 117 | .catch(err => console.log(err)); 118 | }; 119 | -------------------------------------------------------------------------------- /src/core/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Posts from "../post/Posts"; 3 | 4 | const Home = () => ( 5 |
6 |
7 |

8 | React Node MongoDB Social Network App 9 |

10 |

11 | Node API, React web app, Authentication, User Profile, Follow/Unfollow, 12 | Like/Unlike, Comments, Social Login and more 13 |

14 |
15 |
16 | 17 |
18 |
19 | ); 20 | 21 | export default Home; 22 | -------------------------------------------------------------------------------- /src/core/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | import { signout, isAuthenticated } from '../auth'; 4 | 5 | const isActive = (history, path) => { 6 | if (history.location.pathname === path) return { color: '#ff9900' }; 7 | else return { color: '#ffffff' }; 8 | }; 9 | 10 | const Menu = ({ history }) => ( 11 |
12 |
    13 |
  • 14 | 15 | Home 16 | 17 |
  • 18 | 19 |
  • 20 | 24 | Users 25 | 26 |
  • 27 | 28 |
  • 29 | 30 | Create Post 31 | 32 |
  • 33 | 34 | {!isAuthenticated() && ( 35 | 36 |
  • 37 | 38 | Sign In 39 | 40 |
  • 41 |
  • 42 | 43 | Sign Up 44 | 45 |
  • 46 |
    47 | )} 48 | 49 | {isAuthenticated() && isAuthenticated().user.role === 'admin' && ( 50 |
  • 51 | 52 | Admin 53 | 54 |
  • 55 | )} 56 | 57 | {isAuthenticated() && ( 58 | 59 |
  • 60 | 61 | Find People 62 | 63 |
  • 64 | 65 |
  • 66 | 71 | {`${isAuthenticated().user.name}'s profile`} 72 | 73 |
  • 74 | 75 |
  • 76 | signout(() => history.push('/'))} 80 | > 81 | Sign Out 82 | 83 |
  • 84 |
    85 | )} 86 |
87 |
88 | ); 89 | 90 | export default withRouter(Menu); 91 | -------------------------------------------------------------------------------- /src/images/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/react-front/be63af731d2c353460f6c3e12d54654855e617f2/src/images/avatar.jpg -------------------------------------------------------------------------------- /src/images/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/react-front/be63af731d2c353460f6c3e12d54654855e617f2/src/images/mountains.jpg -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /src/post/Comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { comment, uncomment } from "./apiPost"; 3 | import { isAuthenticated } from "../auth"; 4 | import { Link } from "react-router-dom"; 5 | import DefaultProfile from "../images/avatar.jpg"; 6 | 7 | class Comment extends Component { 8 | state = { 9 | text: "", 10 | error: "" 11 | }; 12 | 13 | handleChange = event => { 14 | this.setState({ error: "" }); 15 | this.setState({ text: event.target.value }); 16 | }; 17 | 18 | isValid = () => { 19 | const { text } = this.state; 20 | if (!text.length > 0 || text.length > 150) { 21 | this.setState({ 22 | error: 23 | "Comment should not be empty and less than 150 characters long" 24 | }); 25 | return false; 26 | } 27 | return true; 28 | }; 29 | 30 | addComment = e => { 31 | e.preventDefault(); 32 | 33 | if (!isAuthenticated()) { 34 | this.setState({ error: "Please signin to leave a comment" }); 35 | return false; 36 | } 37 | 38 | if (this.isValid()) { 39 | const userId = isAuthenticated().user._id; 40 | const token = isAuthenticated().token; 41 | const postId = this.props.postId; 42 | 43 | comment(userId, token, postId, { text: this.state.text }).then( 44 | data => { 45 | if (data.error) { 46 | console.log(data.error); 47 | } else { 48 | this.setState({ text: "" }); 49 | // dispatch fresh list of coments to parent (SinglePost) 50 | this.props.updateComments(data.comments); 51 | } 52 | } 53 | ); 54 | } 55 | }; 56 | 57 | deleteComment = comment => { 58 | const userId = isAuthenticated().user._id; 59 | const token = isAuthenticated().token; 60 | const postId = this.props.postId; 61 | 62 | uncomment(userId, token, postId, comment).then(data => { 63 | if (data.error) { 64 | console.log(data.error); 65 | } else { 66 | this.props.updateComments(data.comments); 67 | } 68 | }); 69 | }; 70 | 71 | deleteConfirmed = comment => { 72 | let answer = window.confirm( 73 | "Are you sure you want to delete your comment?" 74 | ); 75 | if (answer) { 76 | this.deleteComment(comment); 77 | } 78 | }; 79 | 80 | render() { 81 | const { comments } = this.props; 82 | const { error } = this.state; 83 | 84 | return ( 85 |
86 |

Leave a comment

87 | 88 |
89 |
90 | 97 | 100 |
101 |
102 | 103 |
107 | {error} 108 |
109 | 110 |
111 |

{comments.length} Comments

112 |
113 | {comments.map((comment, i) => ( 114 |
115 |
116 | 117 | 126 | (i.target.src = `${DefaultProfile}`) 127 | } 128 | src={`${ 129 | process.env.REACT_APP_API_URL 130 | }/user/photo/${comment.postedBy._id}`} 131 | alt={comment.postedBy.name} 132 | /> 133 | 134 |
135 |

{comment.text}

136 |

137 | Posted by{" "} 138 | 141 | {comment.postedBy.name}{" "} 142 | 143 | on{" "} 144 | {new Date( 145 | comment.created 146 | ).toDateString()} 147 | 148 | {isAuthenticated().user && 149 | isAuthenticated().user._id === 150 | comment.postedBy._id && ( 151 | <> 152 | 154 | this.deleteConfirmed( 155 | comment 156 | ) 157 | } 158 | className="text-danger float-right mr-1" 159 | > 160 | Remove 161 | 162 | 163 | )} 164 | 165 |

166 |
167 |
168 |
169 | ))} 170 |
171 |
172 | ); 173 | } 174 | } 175 | 176 | export default Comment; 177 | -------------------------------------------------------------------------------- /src/post/EditPost.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { singlePost, update } from "./apiPost"; 3 | import { isAuthenticated } from "../auth"; 4 | import { Redirect } from "react-router-dom"; 5 | import DefaultPost from "../images/mountains.jpg"; 6 | 7 | class EditPost extends Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | id: "", 12 | title: "", 13 | body: "", 14 | redirectToProfile: false, 15 | error: "", 16 | fileSize: 0, 17 | loading: false 18 | }; 19 | } 20 | 21 | init = postId => { 22 | singlePost(postId).then(data => { 23 | if (data.error) { 24 | this.setState({ redirectToProfile: true }); 25 | } else { 26 | this.setState({ 27 | id: data.postedBy._id, 28 | title: data.title, 29 | body: data.body, 30 | error: "" 31 | }); 32 | } 33 | }); 34 | }; 35 | 36 | componentDidMount() { 37 | this.postData = new FormData(); 38 | const postId = this.props.match.params.postId; 39 | this.init(postId); 40 | } 41 | 42 | isValid = () => { 43 | const { title, body, fileSize } = this.state; 44 | if (fileSize > 1000000) { 45 | this.setState({ 46 | error: "File size should be less than 100kb", 47 | loading: false 48 | }); 49 | return false; 50 | } 51 | if (title.length === 0 || body.length === 0) { 52 | this.setState({ error: "All fields are required", loading: false }); 53 | return false; 54 | } 55 | return true; 56 | }; 57 | 58 | handleChange = name => event => { 59 | this.setState({ error: "" }); 60 | const value = 61 | name === "photo" ? event.target.files[0] : event.target.value; 62 | 63 | const fileSize = name === "photo" ? event.target.files[0].size : 0; 64 | this.postData.set(name, value); 65 | this.setState({ [name]: value, fileSize }); 66 | }; 67 | 68 | clickSubmit = event => { 69 | event.preventDefault(); 70 | this.setState({ loading: true }); 71 | 72 | if (this.isValid()) { 73 | const postId = this.props.match.params.postId; 74 | const token = isAuthenticated().token; 75 | 76 | update(postId, token, this.postData).then(data => { 77 | if (data.error) this.setState({ error: data.error }); 78 | else { 79 | this.setState({ 80 | loading: false, 81 | title: "", 82 | body: "", 83 | redirectToProfile: true 84 | }); 85 | } 86 | }); 87 | } 88 | }; 89 | 90 | editPostForm = (title, body) => ( 91 |
92 |
93 | 94 | 100 |
101 |
102 | 103 | 109 |
110 | 111 |
112 | 113 |