├── .firebase └── hosting.YnVpbGQ.cache ├── .firebaserc ├── .gitignore ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── public ├── icon.png ├── index.html └── manifest.json └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── layout │ ├── Navbar.js │ └── Notifications.js ├── profile │ ├── EditDetails.js │ ├── Profile.js │ └── StaticProfile.js └── scream │ ├── CommentForm.js │ ├── Comments.js │ ├── DeleteScream.js │ ├── LikeButton.js │ ├── PostScream.js │ ├── Scream.js │ └── ScreamDialog.js ├── images ├── icon.png └── no-img.png ├── index.js ├── pages ├── home.js ├── login.js ├── signup.js └── user.js ├── redux ├── actions │ ├── dataActions.js │ └── userActions.js ├── reducers │ ├── dataReducer.js │ ├── uiReducer.js │ └── userReducer.js ├── store.js └── types.js ├── serviceWorker.js └── util ├── AuthRoute.js ├── MyButton.js ├── ProfileSkeleton.js ├── ScreamSkeleton.js └── theme.js /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1554208452151,16476039344dce16615b6056cff9a669826183cbc58677dcd498d56a2b4c457c 2 | icon.png,1553167685204,a03f5cb4ee1c75e98174db767944ce0ac320c044b14d25bd574c357c1dd081f7 3 | manifest.json,499162500000,a40a4294484385ec155814f7d72caf5967a19f5efcbedf7a62b2cdff07e42711 4 | index.html,1554208716882,3faede4d20d2ccfcce96ab8d739cbd813fb4bff808e5297dc39aec97bab33f50 5 | precache-manifest.f96c7c78f2d0003f4cbaf45f333f187a.js,1554208452151,3df9a622dd8514b8026d36a41bdb2900c24600a34ceb2bbbf85be2d237784cb3 6 | service-worker.js,1554208452151,313591192fff38ad7673e307c0d5257d2e72ae905434b8b36b1a15b0b30951ea 7 | static/css/main.af07096d.chunk.css,1554208452157,b97f8f97a846641516abd6a75d5924e6255d7f4b6d387742008172be7075e670 8 | static/css/main.af07096d.chunk.css.map,1554208452170,57cb89bfe117ccb3c5d2110e5ef53021957747a3c4e35b72ec2655e724626023 9 | static/js/main.352e3899.chunk.js,1554208452157,bd2dcd42f9852dfdd162121a41ec7c6bb11fcc12152ac38ab24637a0c0091adf 10 | static/js/runtime~main.a8a9905a.js,1554208452170,27518aed75eb917ee7575e8c911b596e850b5265b9e5694a7681cba901419f4d 11 | static/js/runtime~main.a8a9905a.js.map,1554208452170,d13b46ae2acf0d2863e1c6449a51f64702fa5b464c475c0ec20d6e8de9970ef9 12 | static/media/no-img.6732bd42.png,1554208452157,da10c4c8280892a5795d71218c238a46bc196aa4fe36d4cafd34954338a262d2 13 | static/js/main.352e3899.chunk.js.map,1554208452172,670227688380230394ea438d7f743cb75c90f6ae31ad1d12919c9e6e1772abde 14 | static/js/2.1fdac75c.chunk.js,1554208452170,8764ef6acc7295e81dd846a6d80b6906ae919864275a113e73b6f93becf068ac 15 | static/js/2.1fdac75c.chunk.js.map,1554208452172,2685a3a585899dd74d74be1547beb45383e43c9d02cbf3c0b6f72bb49c7441ba 16 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "socialape-d081e" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the code base for the React app for my [Full Stack React & Firebase series](https://www.youtube.com/watch?v=RkBfu-W7tt0&list=PLMhAeHCz8S38ryyeMiBPPUnFAiWnoPvWP) 2 | 3 | ## 1: API Base URL 4 | 5 | Add https://europe-west1-socialape-d081e.cloudfunctions.net/api as the 'proxy' value in package.json 6 | 7 | ## 2: Install packages 8 | 9 | run `npm install` 10 | 11 | ## 3: Run project 12 | 13 | run `npm start` 14 | 15 | ## 4: Open it 16 | 17 | go to [http://localhost:3000](http://localhost:3000) 18 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialape-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^3.9.2", 7 | "@material-ui/icons": "^3.0.2", 8 | "axios": "^0.18.0", 9 | "dayjs": "^1.8.11", 10 | "jwt-decode": "^2.2.0", 11 | "react": "^16.8.4", 12 | "react-dom": "^16.8.4", 13 | "react-redux": "^6.0.1", 14 | "react-router-dom": "^5.0.0", 15 | "react-scripts": "2.1.8", 16 | "redux": "^4.0.1", 17 | "redux-thunk": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ], 34 | "proxy": "https://europe-west1-socialape-d081e.cloudfunctions.net/api" 35 | } 36 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidjou/classsed-react-firebase-client/7cd46138dfd5f33f54b74c140a445ac2a9174e5f/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | SocialApe 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | background-color: rgb(245, 245, 245); 7 | } 8 | .container { 9 | margin: 80px auto 0 auto; 10 | max-width: 1200px; 11 | } 12 | .nav-container { 13 | margin: auto; 14 | } 15 | .nav-container svg { 16 | color: #fff; 17 | } 18 | a { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import './App.css'; 4 | import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider'; 5 | import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; 6 | import jwtDecode from 'jwt-decode'; 7 | // Redux 8 | import { Provider } from 'react-redux'; 9 | import store from './redux/store'; 10 | import { SET_AUTHENTICATED } from './redux/types'; 11 | import { logoutUser, getUserData } from './redux/actions/userActions'; 12 | // Components 13 | import Navbar from './components/layout/Navbar'; 14 | import themeObject from './util/theme'; 15 | import AuthRoute from './util/AuthRoute'; 16 | // Pages 17 | import home from './pages/home'; 18 | import login from './pages/login'; 19 | import signup from './pages/signup'; 20 | import user from './pages/user'; 21 | 22 | import axios from 'axios'; 23 | 24 | const theme = createMuiTheme(themeObject); 25 | 26 | axios.defaults.baseURL = 27 | 'https://europe-west1-socialape-d081e.cloudfunctions.net/api'; 28 | 29 | const token = localStorage.FBIdToken; 30 | if (token) { 31 | const decodedToken = jwtDecode(token); 32 | if (decodedToken.exp * 1000 < Date.now()) { 33 | store.dispatch(logoutUser()); 34 | window.location.href = '/login'; 35 | } else { 36 | store.dispatch({ type: SET_AUTHENTICATED }); 37 | axios.defaults.headers.common['Authorization'] = token; 38 | store.dispatch(getUserData()); 39 | } 40 | } 41 | 42 | class App extends Component { 43 | render() { 44 | return ( 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 |
62 |
63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import MyButton from '../../util/MyButton'; 6 | import PostScream from '../scream/PostScream'; 7 | import Notifications from './Notifications'; 8 | // MUI stuff 9 | import AppBar from '@material-ui/core/AppBar'; 10 | import Toolbar from '@material-ui/core/Toolbar'; 11 | import Button from '@material-ui/core/Button'; 12 | // Icons 13 | import HomeIcon from '@material-ui/icons/Home'; 14 | 15 | class Navbar extends Component { 16 | render() { 17 | const { authenticated } = this.props; 18 | return ( 19 | 20 | 21 | {authenticated ? ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) : ( 32 | 33 | 36 | 39 | 42 | 43 | )} 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | Navbar.propTypes = { 51 | authenticated: PropTypes.bool.isRequired 52 | }; 53 | 54 | const mapStateToProps = (state) => ({ 55 | authenticated: state.user.authenticated 56 | }); 57 | 58 | export default connect(mapStateToProps)(Navbar); 59 | -------------------------------------------------------------------------------- /src/components/layout/Notifications.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import dayjs from 'dayjs'; 4 | import relativeTime from 'dayjs/plugin/relativeTime'; 5 | import PropTypes from 'prop-types'; 6 | // MUI stuff 7 | import Menu from '@material-ui/core/Menu'; 8 | import MenuItem from '@material-ui/core/MenuItem'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import Tooltip from '@material-ui/core/Tooltip'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import Badge from '@material-ui/core/Badge'; 13 | // Icons 14 | import NotificationsIcon from '@material-ui/icons/Notifications'; 15 | import FavoriteIcon from '@material-ui/icons/Favorite'; 16 | import ChatIcon from '@material-ui/icons/Chat'; 17 | // Redux 18 | import { connect } from 'react-redux'; 19 | import { markNotificationsRead } from '../../redux/actions/userActions'; 20 | 21 | class Notifications extends Component { 22 | state = { 23 | anchorEl: null 24 | }; 25 | handleOpen = (event) => { 26 | this.setState({ anchorEl: event.target }); 27 | }; 28 | handleClose = () => { 29 | this.setState({ anchorEl: null }); 30 | }; 31 | onMenuOpened = () => { 32 | let unreadNotificationsIds = this.props.notifications 33 | .filter((not) => !not.read) 34 | .map((not) => not.notificationId); 35 | this.props.markNotificationsRead(unreadNotificationsIds); 36 | }; 37 | render() { 38 | const notifications = this.props.notifications; 39 | const anchorEl = this.state.anchorEl; 40 | 41 | dayjs.extend(relativeTime); 42 | 43 | let notificationsIcon; 44 | if (notifications && notifications.length > 0) { 45 | notifications.filter((not) => not.read === false).length > 0 46 | ? (notificationsIcon = ( 47 | not.read === false).length 50 | } 51 | color="secondary" 52 | > 53 | 54 | 55 | )) 56 | : (notificationsIcon = ); 57 | } else { 58 | notificationsIcon = ; 59 | } 60 | let notificationsMarkup = 61 | notifications && notifications.length > 0 ? ( 62 | notifications.map((not) => { 63 | const verb = not.type === 'like' ? 'liked' : 'commented on'; 64 | const time = dayjs(not.createdAt).fromNow(); 65 | const iconColor = not.read ? 'primary' : 'secondary'; 66 | const icon = 67 | not.type === 'like' ? ( 68 | 69 | ) : ( 70 | 71 | ); 72 | 73 | return ( 74 | 75 | {icon} 76 | 82 | {not.sender} {verb} your scream {time} 83 | 84 | 85 | ); 86 | }) 87 | ) : ( 88 | 89 | You have no notifications yet 90 | 91 | ); 92 | return ( 93 | 94 | 95 | 100 | {notificationsIcon} 101 | 102 | 103 | 109 | {notificationsMarkup} 110 | 111 | 112 | ); 113 | } 114 | } 115 | 116 | Notifications.propTypes = { 117 | markNotificationsRead: PropTypes.func.isRequired, 118 | notifications: PropTypes.array.isRequired 119 | }; 120 | 121 | const mapStateToProps = (state) => ({ 122 | notifications: state.user.notifications 123 | }); 124 | 125 | export default connect( 126 | mapStateToProps, 127 | { markNotificationsRead } 128 | )(Notifications); 129 | -------------------------------------------------------------------------------- /src/components/profile/EditDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import MyButton from '../../util/MyButton'; 5 | // Redux stuff 6 | import { connect } from 'react-redux'; 7 | import { editUserDetails } from '../../redux/actions/userActions'; 8 | // MUI Stuff 9 | import Button from '@material-ui/core/Button'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Dialog from '@material-ui/core/Dialog'; 12 | import DialogActions from '@material-ui/core/DialogActions'; 13 | import DialogContent from '@material-ui/core/DialogContent'; 14 | import DialogTitle from '@material-ui/core/DialogTitle'; 15 | // Icons 16 | import EditIcon from '@material-ui/icons/Edit'; 17 | 18 | const styles = (theme) => ({ 19 | ...theme, 20 | button: { 21 | float: 'right' 22 | } 23 | }); 24 | 25 | class EditDetails extends Component { 26 | state = { 27 | bio: '', 28 | website: '', 29 | location: '', 30 | open: false 31 | }; 32 | mapUserDetailsToState = (credentials) => { 33 | this.setState({ 34 | bio: credentials.bio ? credentials.bio : '', 35 | website: credentials.website ? credentials.website : '', 36 | location: credentials.location ? credentials.location : '' 37 | }); 38 | }; 39 | handleOpen = () => { 40 | this.setState({ open: true }); 41 | this.mapUserDetailsToState(this.props.credentials); 42 | }; 43 | handleClose = () => { 44 | this.setState({ open: false }); 45 | }; 46 | componentDidMount() { 47 | const { credentials } = this.props; 48 | this.mapUserDetailsToState(credentials); 49 | } 50 | 51 | handleChange = (event) => { 52 | this.setState({ 53 | [event.target.name]: event.target.value 54 | }); 55 | }; 56 | handleSubmit = () => { 57 | const userDetails = { 58 | bio: this.state.bio, 59 | website: this.state.website, 60 | location: this.state.location 61 | }; 62 | this.props.editUserDetails(userDetails); 63 | this.handleClose(); 64 | }; 65 | render() { 66 | const { classes } = this.props; 67 | return ( 68 | 69 | 74 | 75 | 76 | 82 | Edit your details 83 | 84 |
85 | 97 | 107 | 117 | 118 |
119 | 120 | 123 | 126 | 127 |
128 |
129 | ); 130 | } 131 | } 132 | 133 | EditDetails.propTypes = { 134 | editUserDetails: PropTypes.func.isRequired, 135 | classes: PropTypes.object.isRequired 136 | }; 137 | 138 | const mapStateToProps = (state) => ({ 139 | credentials: state.user.credentials 140 | }); 141 | 142 | export default connect( 143 | mapStateToProps, 144 | { editUserDetails } 145 | )(withStyles(styles)(EditDetails)); 146 | -------------------------------------------------------------------------------- /src/components/profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import { Link } from 'react-router-dom'; 5 | import dayjs from 'dayjs'; 6 | import EditDetails from './EditDetails'; 7 | import MyButton from '../../util/MyButton'; 8 | import ProfileSkeleton from '../../util/ProfileSkeleton'; 9 | // MUI stuff 10 | import Button from '@material-ui/core/Button'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import MuiLink from '@material-ui/core/Link'; 13 | import Paper from '@material-ui/core/Paper'; 14 | // Icons 15 | import LocationOn from '@material-ui/icons/LocationOn'; 16 | import LinkIcon from '@material-ui/icons/Link'; 17 | import CalendarToday from '@material-ui/icons/CalendarToday'; 18 | import EditIcon from '@material-ui/icons/Edit'; 19 | import KeyboardReturn from '@material-ui/icons/KeyboardReturn'; 20 | //Redux 21 | import { connect } from 'react-redux'; 22 | import { logoutUser, uploadImage } from '../../redux/actions/userActions'; 23 | 24 | const styles = (theme) => ({ 25 | ...theme 26 | }); 27 | 28 | class Profile extends Component { 29 | handleImageChange = (event) => { 30 | const image = event.target.files[0]; 31 | const formData = new FormData(); 32 | formData.append('image', image, image.name); 33 | this.props.uploadImage(formData); 34 | }; 35 | handleEditPicture = () => { 36 | const fileInput = document.getElementById('imageInput'); 37 | fileInput.click(); 38 | }; 39 | handleLogout = () => { 40 | this.props.logoutUser(); 41 | }; 42 | render() { 43 | const { 44 | classes, 45 | user: { 46 | credentials: { handle, createdAt, imageUrl, bio, website, location }, 47 | loading, 48 | authenticated 49 | } 50 | } = this.props; 51 | 52 | let profileMarkup = !loading ? ( 53 | authenticated ? ( 54 | 55 |
56 |
57 | profile 58 | 64 | 69 | 70 | 71 |
72 |
73 |
74 | 80 | @{handle} 81 | 82 |
83 | {bio && {bio}} 84 |
85 | {location && ( 86 | 87 | {location} 88 |
89 |
90 | )} 91 | {website && ( 92 | 93 | 94 | 95 | {' '} 96 | {website} 97 | 98 |
99 |
100 | )} 101 | {' '} 102 | Joined {dayjs(createdAt).format('MMM YYYY')} 103 |
104 | 105 | 106 | 107 | 108 |
109 |
110 | ) : ( 111 | 112 | 113 | No profile found, please login again 114 | 115 |
116 | 124 | 132 |
133 |
134 | ) 135 | ) : ( 136 | 137 | ); 138 | 139 | return profileMarkup; 140 | } 141 | } 142 | 143 | const mapStateToProps = (state) => ({ 144 | user: state.user 145 | }); 146 | 147 | const mapActionsToProps = { logoutUser, uploadImage }; 148 | 149 | Profile.propTypes = { 150 | logoutUser: PropTypes.func.isRequired, 151 | uploadImage: PropTypes.func.isRequired, 152 | user: PropTypes.object.isRequired, 153 | classes: PropTypes.object.isRequired 154 | }; 155 | 156 | export default connect( 157 | mapStateToProps, 158 | mapActionsToProps 159 | )(withStyles(styles)(Profile)); 160 | -------------------------------------------------------------------------------- /src/components/profile/StaticProfile.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import dayjs from 'dayjs'; 5 | import { Link } from 'react-router-dom'; 6 | // MUI 7 | import MuiLink from '@material-ui/core/Link'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Typography from '@material-ui/core/Typography'; 10 | // Icons 11 | import LocationOn from '@material-ui/icons/LocationOn'; 12 | import LinkIcon from '@material-ui/icons/Link'; 13 | import CalendarToday from '@material-ui/icons/CalendarToday'; 14 | 15 | const styles = (theme) => ({ 16 | ...theme 17 | }); 18 | 19 | const StaticProfile = (props) => { 20 | const { 21 | classes, 22 | profile: { handle, createdAt, imageUrl, bio, website, location } 23 | } = props; 24 | 25 | return ( 26 | 27 |
28 |
29 | profile 30 |
31 |
32 |
33 | 39 | @{handle} 40 | 41 |
42 | {bio && {bio}} 43 |
44 | {location && ( 45 | 46 | {location} 47 |
48 |
49 | )} 50 | {website && ( 51 | 52 | 53 | 54 | {' '} 55 | {website} 56 | 57 |
58 |
59 | )} 60 | {' '} 61 | Joined {dayjs(createdAt).format('MMM YYYY')} 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | StaticProfile.propTypes = { 69 | profile: PropTypes.object.isRequired, 70 | classes: PropTypes.object.isRequired 71 | }; 72 | 73 | export default withStyles(styles)(StaticProfile); 74 | -------------------------------------------------------------------------------- /src/components/scream/CommentForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | // MUI Stuff 5 | import Button from '@material-ui/core/Button'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import TextField from '@material-ui/core/TextField'; 8 | // Redux stuff 9 | import { connect } from 'react-redux'; 10 | import { submitComment } from '../../redux/actions/dataActions'; 11 | 12 | const styles = (theme) => ({ 13 | ...theme 14 | }); 15 | 16 | class CommentForm extends Component { 17 | state = { 18 | body: '', 19 | errors: {} 20 | }; 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if (nextProps.UI.errors) { 24 | this.setState({ errors: nextProps.UI.errors }); 25 | } 26 | if (!nextProps.UI.errors && !nextProps.UI.loading) { 27 | this.setState({ body: '' }); 28 | } 29 | } 30 | 31 | handleChange = (event) => { 32 | this.setState({ [event.target.name]: event.target.value }); 33 | }; 34 | handleSubmit = (event) => { 35 | event.preventDefault(); 36 | this.props.submitComment(this.props.screamId, { body: this.state.body }); 37 | }; 38 | 39 | render() { 40 | const { classes, authenticated } = this.props; 41 | const errors = this.state.errors; 42 | 43 | const commentFormMarkup = authenticated ? ( 44 | 45 |
46 | 57 | 65 | 66 |
67 |
68 | ) : null; 69 | return commentFormMarkup; 70 | } 71 | } 72 | 73 | CommentForm.propTypes = { 74 | submitComment: PropTypes.func.isRequired, 75 | UI: PropTypes.object.isRequired, 76 | classes: PropTypes.object.isRequired, 77 | screamId: PropTypes.string.isRequired, 78 | authenticated: PropTypes.bool.isRequired 79 | }; 80 | 81 | const mapStateToProps = (state) => ({ 82 | UI: state.UI, 83 | authenticated: state.user.authenticated 84 | }); 85 | 86 | export default connect( 87 | mapStateToProps, 88 | { submitComment } 89 | )(withStyles(styles)(CommentForm)); 90 | -------------------------------------------------------------------------------- /src/components/scream/Comments.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import { Link } from 'react-router-dom'; 5 | import dayjs from 'dayjs'; 6 | // MUI 7 | import Grid from '@material-ui/core/Grid'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | const styles = (theme) => ({ 11 | ...theme, 12 | commentImage: { 13 | maxWidth: '100%', 14 | height: 100, 15 | objectFit: 'cover', 16 | borderRadius: '50%' 17 | }, 18 | commentData: { 19 | marginLeft: 20 20 | } 21 | }); 22 | 23 | class Comments extends Component { 24 | render() { 25 | const { comments, classes } = this.props; 26 | return ( 27 | 28 | {comments.map((comment, index) => { 29 | const { body, createdAt, userImage, userHandle } = comment; 30 | return ( 31 | 32 | 33 | 34 | 35 | comment 40 | 41 | 42 |
43 | 49 | {userHandle} 50 | 51 | 52 | {dayjs(createdAt).format('h:mm a, MMMM DD YYYY')} 53 | 54 |
55 | {body} 56 |
57 |
58 |
59 |
60 | {index !== comments.length - 1 && ( 61 |
62 | )} 63 |
64 | ); 65 | })} 66 |
67 | ); 68 | } 69 | } 70 | 71 | Comments.propTypes = { 72 | comments: PropTypes.array.isRequired 73 | }; 74 | 75 | export default withStyles(styles)(Comments); 76 | -------------------------------------------------------------------------------- /src/components/scream/DeleteScream.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import PropTypes from 'prop-types'; 4 | import MyButton from '../../util/MyButton'; 5 | 6 | // MUI Stuff 7 | import Button from '@material-ui/core/Button'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogTitle from '@material-ui/core/DialogTitle'; 10 | import DialogActions from '@material-ui/core/DialogActions'; 11 | import DeleteOutline from '@material-ui/icons/DeleteOutline'; 12 | 13 | import { connect } from 'react-redux'; 14 | import { deleteScream } from '../../redux/actions/dataActions'; 15 | 16 | const styles = { 17 | deleteButton: { 18 | position: 'absolute', 19 | left: '90%', 20 | top: '10%' 21 | } 22 | }; 23 | 24 | class DeleteScream extends Component { 25 | state = { 26 | open: false 27 | }; 28 | handleOpen = () => { 29 | this.setState({ open: true }); 30 | }; 31 | handleClose = () => { 32 | this.setState({ open: false }); 33 | }; 34 | deleteScream = () => { 35 | this.props.deleteScream(this.props.screamId); 36 | this.setState({ open: false }); 37 | }; 38 | render() { 39 | const { classes } = this.props; 40 | 41 | return ( 42 | 43 | 48 | 49 | 50 | 56 | 57 | Are you sure you want to delete this scream ? 58 | 59 | 60 | 63 | 66 | 67 | 68 | 69 | ); 70 | } 71 | } 72 | 73 | DeleteScream.propTypes = { 74 | deleteScream: PropTypes.func.isRequired, 75 | classes: PropTypes.object.isRequired, 76 | screamId: PropTypes.string.isRequired 77 | }; 78 | 79 | export default connect( 80 | null, 81 | { deleteScream } 82 | )(withStyles(styles)(DeleteScream)); 83 | -------------------------------------------------------------------------------- /src/components/scream/LikeButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import MyButton from '../../util/MyButton'; 3 | import { Link } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | // Icons 6 | import FavoriteIcon from '@material-ui/icons/Favorite'; 7 | import FavoriteBorder from '@material-ui/icons/FavoriteBorder'; 8 | // REdux 9 | import { connect } from 'react-redux'; 10 | import { likeScream, unlikeScream } from '../../redux/actions/dataActions'; 11 | 12 | export class LikeButton extends Component { 13 | likedScream = () => { 14 | if ( 15 | this.props.user.likes && 16 | this.props.user.likes.find( 17 | (like) => like.screamId === this.props.screamId 18 | ) 19 | ) 20 | return true; 21 | else return false; 22 | }; 23 | likeScream = () => { 24 | this.props.likeScream(this.props.screamId); 25 | }; 26 | unlikeScream = () => { 27 | this.props.unlikeScream(this.props.screamId); 28 | }; 29 | render() { 30 | const { authenticated } = this.props.user; 31 | const likeButton = !authenticated ? ( 32 | 33 | 34 | 35 | 36 | 37 | ) : this.likedScream() ? ( 38 | 39 | 40 | 41 | ) : ( 42 | 43 | 44 | 45 | ); 46 | return likeButton; 47 | } 48 | } 49 | 50 | LikeButton.propTypes = { 51 | user: PropTypes.object.isRequired, 52 | screamId: PropTypes.string.isRequired, 53 | likeScream: PropTypes.func.isRequired, 54 | unlikeScream: PropTypes.func.isRequired 55 | }; 56 | 57 | const mapStateToProps = (state) => ({ 58 | user: state.user 59 | }); 60 | 61 | const mapActionsToProps = { 62 | likeScream, 63 | unlikeScream 64 | }; 65 | 66 | export default connect( 67 | mapStateToProps, 68 | mapActionsToProps 69 | )(LikeButton); 70 | -------------------------------------------------------------------------------- /src/components/scream/PostScream.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import MyButton from '../../util/MyButton'; 5 | // MUI Stuff 6 | import Button from '@material-ui/core/Button'; 7 | import TextField from '@material-ui/core/TextField'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogContent from '@material-ui/core/DialogContent'; 10 | import DialogTitle from '@material-ui/core/DialogTitle'; 11 | import CircularProgress from '@material-ui/core/CircularProgress'; 12 | import AddIcon from '@material-ui/icons/Add'; 13 | import CloseIcon from '@material-ui/icons/Close'; 14 | // Redux stuff 15 | import { connect } from 'react-redux'; 16 | import { postScream, clearErrors } from '../../redux/actions/dataActions'; 17 | 18 | const styles = (theme) => ({ 19 | ...theme, 20 | submitButton: { 21 | position: 'relative', 22 | float: 'right', 23 | marginTop: 10 24 | }, 25 | progressSpinner: { 26 | position: 'absolute' 27 | }, 28 | closeButton: { 29 | position: 'absolute', 30 | left: '91%', 31 | top: '6%' 32 | } 33 | }); 34 | 35 | class PostScream extends Component { 36 | state = { 37 | open: false, 38 | body: '', 39 | errors: {} 40 | }; 41 | componentWillReceiveProps(nextProps) { 42 | if (nextProps.UI.errors) { 43 | this.setState({ 44 | errors: nextProps.UI.errors 45 | }); 46 | } 47 | if (!nextProps.UI.errors && !nextProps.UI.loading) { 48 | this.setState({ body: '', open: false, errors: {} }); 49 | } 50 | } 51 | handleOpen = () => { 52 | this.setState({ open: true }); 53 | }; 54 | handleClose = () => { 55 | this.props.clearErrors(); 56 | this.setState({ open: false, errors: {} }); 57 | }; 58 | handleChange = (event) => { 59 | this.setState({ [event.target.name]: event.target.value }); 60 | }; 61 | handleSubmit = (event) => { 62 | event.preventDefault(); 63 | this.props.postScream({ body: this.state.body }); 64 | }; 65 | render() { 66 | const { errors } = this.state; 67 | const { 68 | classes, 69 | UI: { loading } 70 | } = this.props; 71 | return ( 72 | 73 | 74 | 75 | 76 | 82 | 87 | 88 | 89 | Post a new scream 90 | 91 |
92 | 105 | 120 | 121 |
122 |
123 |
124 | ); 125 | } 126 | } 127 | 128 | PostScream.propTypes = { 129 | postScream: PropTypes.func.isRequired, 130 | clearErrors: PropTypes.func.isRequired, 131 | UI: PropTypes.object.isRequired 132 | }; 133 | 134 | const mapStateToProps = (state) => ({ 135 | UI: state.UI 136 | }); 137 | 138 | export default connect( 139 | mapStateToProps, 140 | { postScream, clearErrors } 141 | )(withStyles(styles)(PostScream)); 142 | -------------------------------------------------------------------------------- /src/components/scream/Scream.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import { Link } from 'react-router-dom'; 4 | import dayjs from 'dayjs'; 5 | import relativeTime from 'dayjs/plugin/relativeTime'; 6 | import PropTypes from 'prop-types'; 7 | import MyButton from '../../util/MyButton'; 8 | import DeleteScream from './DeleteScream'; 9 | import ScreamDialog from './ScreamDialog'; 10 | import LikeButton from './LikeButton'; 11 | // MUI Stuff 12 | import Card from '@material-ui/core/Card'; 13 | import CardContent from '@material-ui/core/CardContent'; 14 | import CardMedia from '@material-ui/core/CardMedia'; 15 | import Typography from '@material-ui/core/Typography'; 16 | // Icons 17 | import ChatIcon from '@material-ui/icons/Chat'; 18 | // Redux 19 | import { connect } from 'react-redux'; 20 | 21 | const styles = { 22 | card: { 23 | position: 'relative', 24 | display: 'flex', 25 | marginBottom: 20 26 | }, 27 | image: { 28 | minWidth: 200 29 | }, 30 | content: { 31 | padding: 25, 32 | objectFit: 'cover' 33 | } 34 | }; 35 | 36 | class Scream extends Component { 37 | render() { 38 | dayjs.extend(relativeTime); 39 | const { 40 | classes, 41 | scream: { 42 | body, 43 | createdAt, 44 | userImage, 45 | userHandle, 46 | screamId, 47 | likeCount, 48 | commentCount 49 | }, 50 | user: { 51 | authenticated, 52 | credentials: { handle } 53 | } 54 | } = this.props; 55 | 56 | const deleteButton = 57 | authenticated && userHandle === handle ? ( 58 | 59 | ) : null; 60 | return ( 61 | 62 | 67 | 68 | 74 | {userHandle} 75 | 76 | {deleteButton} 77 | 78 | {dayjs(createdAt).fromNow()} 79 | 80 | {body} 81 | 82 | {likeCount} Likes 83 | 84 | 85 | 86 | {commentCount} comments 87 | 92 | 93 | 94 | ); 95 | } 96 | } 97 | 98 | Scream.propTypes = { 99 | user: PropTypes.object.isRequired, 100 | scream: PropTypes.object.isRequired, 101 | classes: PropTypes.object.isRequired, 102 | openDialog: PropTypes.bool 103 | }; 104 | 105 | const mapStateToProps = (state) => ({ 106 | user: state.user 107 | }); 108 | 109 | export default connect(mapStateToProps)(withStyles(styles)(Scream)); 110 | -------------------------------------------------------------------------------- /src/components/scream/ScreamDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import MyButton from '../../util/MyButton'; 5 | import LikeButton from './LikeButton'; 6 | import Comments from './Comments'; 7 | import CommentForm from './CommentForm'; 8 | import dayjs from 'dayjs'; 9 | import { Link } from 'react-router-dom'; 10 | // MUI Stuff 11 | import Dialog from '@material-ui/core/Dialog'; 12 | import DialogContent from '@material-ui/core/DialogContent'; 13 | import CircularProgress from '@material-ui/core/CircularProgress'; 14 | import Grid from '@material-ui/core/Grid'; 15 | import Typography from '@material-ui/core/Typography'; 16 | // Icons 17 | import CloseIcon from '@material-ui/icons/Close'; 18 | import UnfoldMore from '@material-ui/icons/UnfoldMore'; 19 | import ChatIcon from '@material-ui/icons/Chat'; 20 | // Redux stuff 21 | import { connect } from 'react-redux'; 22 | import { getScream, clearErrors } from '../../redux/actions/dataActions'; 23 | 24 | const styles = (theme) => ({ 25 | ...theme, 26 | profileImage: { 27 | maxWidth: 200, 28 | height: 200, 29 | borderRadius: '50%', 30 | objectFit: 'cover' 31 | }, 32 | dialogContent: { 33 | padding: 20 34 | }, 35 | closeButton: { 36 | position: 'absolute', 37 | left: '90%' 38 | }, 39 | expandButton: { 40 | position: 'absolute', 41 | left: '90%' 42 | }, 43 | spinnerDiv: { 44 | textAlign: 'center', 45 | marginTop: 50, 46 | marginBottom: 50 47 | } 48 | }); 49 | 50 | class ScreamDialog extends Component { 51 | state = { 52 | open: false, 53 | oldPath: '', 54 | newPath: '' 55 | }; 56 | componentDidMount() { 57 | if (this.props.openDialog) { 58 | this.handleOpen(); 59 | } 60 | } 61 | handleOpen = () => { 62 | let oldPath = window.location.pathname; 63 | 64 | const { userHandle, screamId } = this.props; 65 | const newPath = `/users/${userHandle}/scream/${screamId}`; 66 | 67 | if (oldPath === newPath) oldPath = `/users/${userHandle}`; 68 | 69 | window.history.pushState(null, null, newPath); 70 | 71 | this.setState({ open: true, oldPath, newPath }); 72 | this.props.getScream(this.props.screamId); 73 | }; 74 | handleClose = () => { 75 | window.history.pushState(null, null, this.state.oldPath); 76 | this.setState({ open: false }); 77 | this.props.clearErrors(); 78 | }; 79 | 80 | render() { 81 | const { 82 | classes, 83 | scream: { 84 | screamId, 85 | body, 86 | createdAt, 87 | likeCount, 88 | commentCount, 89 | userImage, 90 | userHandle, 91 | comments 92 | }, 93 | UI: { loading } 94 | } = this.props; 95 | 96 | const dialogMarkup = loading ? ( 97 |
98 | 99 |
100 | ) : ( 101 | 102 | 103 | Profile 104 | 105 | 106 | 112 | @{userHandle} 113 | 114 |
115 | 116 | {dayjs(createdAt).format('h:mm a, MMMM DD YYYY')} 117 | 118 |
119 | {body} 120 | 121 | {likeCount} likes 122 | 123 | 124 | 125 | {commentCount} comments 126 |
127 |
128 | 129 | 130 |
131 | ); 132 | return ( 133 | 134 | 139 | 140 | 141 | 147 | 152 | 153 | 154 | 155 | {dialogMarkup} 156 | 157 | 158 | 159 | ); 160 | } 161 | } 162 | 163 | ScreamDialog.propTypes = { 164 | clearErrors: PropTypes.func.isRequired, 165 | getScream: PropTypes.func.isRequired, 166 | screamId: PropTypes.string.isRequired, 167 | userHandle: PropTypes.string.isRequired, 168 | scream: PropTypes.object.isRequired, 169 | UI: PropTypes.object.isRequired 170 | }; 171 | 172 | const mapStateToProps = (state) => ({ 173 | scream: state.data.scream, 174 | UI: state.UI 175 | }); 176 | 177 | const mapActionsToProps = { 178 | getScream, 179 | clearErrors 180 | }; 181 | 182 | export default connect( 183 | mapStateToProps, 184 | mapActionsToProps 185 | )(withStyles(styles)(ScreamDialog)); 186 | -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidjou/classsed-react-firebase-client/7cd46138dfd5f33f54b74c140a445ac2a9174e5f/src/images/icon.png -------------------------------------------------------------------------------- /src/images/no-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidjou/classsed-react-firebase-client/7cd46138dfd5f33f54b74c140a445ac2a9174e5f/src/images/no-img.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /src/pages/home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Grid from '@material-ui/core/Grid'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import Scream from '../components/scream/Scream'; 6 | import Profile from '../components/profile/Profile'; 7 | import ScreamSkeleton from '../util/ScreamSkeleton'; 8 | 9 | import { connect } from 'react-redux'; 10 | import { getScreams } from '../redux/actions/dataActions'; 11 | 12 | class home extends Component { 13 | componentDidMount() { 14 | this.props.getScreams(); 15 | } 16 | render() { 17 | const { screams, loading } = this.props.data; 18 | let recentScreamsMarkup = !loading ? ( 19 | screams.map((scream) => ) 20 | ) : ( 21 | 22 | ); 23 | return ( 24 | 25 | 26 | {recentScreamsMarkup} 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | home.propTypes = { 37 | getScreams: PropTypes.func.isRequired, 38 | data: PropTypes.object.isRequired 39 | }; 40 | 41 | const mapStateToProps = (state) => ({ 42 | data: state.data 43 | }); 44 | 45 | export default connect( 46 | mapStateToProps, 47 | { getScreams } 48 | )(home); 49 | -------------------------------------------------------------------------------- /src/pages/login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import PropTypes from 'prop-types'; 4 | import AppIcon from '../images/icon.png'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | // MUI Stuff 8 | import Grid from '@material-ui/core/Grid'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Button from '@material-ui/core/Button'; 12 | import CircularProgress from '@material-ui/core/CircularProgress'; 13 | // Redux stuff 14 | import { connect } from 'react-redux'; 15 | import { loginUser } from '../redux/actions/userActions'; 16 | 17 | const styles = (theme) => ({ 18 | ...theme 19 | }); 20 | 21 | class login extends Component { 22 | constructor() { 23 | super(); 24 | this.state = { 25 | email: '', 26 | password: '', 27 | errors: {} 28 | }; 29 | } 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.UI.errors) { 32 | this.setState({ errors: nextProps.UI.errors }); 33 | } 34 | } 35 | handleSubmit = (event) => { 36 | event.preventDefault(); 37 | const userData = { 38 | email: this.state.email, 39 | password: this.state.password 40 | }; 41 | this.props.loginUser(userData, this.props.history); 42 | }; 43 | handleChange = (event) => { 44 | this.setState({ 45 | [event.target.name]: event.target.value 46 | }); 47 | }; 48 | render() { 49 | const { 50 | classes, 51 | UI: { loading } 52 | } = this.props; 53 | const { errors } = this.state; 54 | 55 | return ( 56 | 57 | 58 | 59 | monkey 60 | 61 | Login 62 | 63 |
64 | 76 | 88 | {errors.general && ( 89 | 90 | {errors.general} 91 | 92 | )} 93 | 105 |
106 | 107 | dont have an account ? sign up here 108 | 109 | 110 |
111 | 112 | 113 | ); 114 | } 115 | } 116 | 117 | login.propTypes = { 118 | classes: PropTypes.object.isRequired, 119 | loginUser: PropTypes.func.isRequired, 120 | user: PropTypes.object.isRequired, 121 | UI: PropTypes.object.isRequired 122 | }; 123 | 124 | const mapStateToProps = (state) => ({ 125 | user: state.user, 126 | UI: state.UI 127 | }); 128 | 129 | const mapActionsToProps = { 130 | loginUser 131 | }; 132 | 133 | export default connect( 134 | mapStateToProps, 135 | mapActionsToProps 136 | )(withStyles(styles)(login)); 137 | -------------------------------------------------------------------------------- /src/pages/signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import PropTypes from 'prop-types'; 4 | import AppIcon from '../images/icon.png'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | // MUI Stuff 8 | import Grid from '@material-ui/core/Grid'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Button from '@material-ui/core/Button'; 12 | import CircularProgress from '@material-ui/core/CircularProgress'; 13 | // Redux stuff 14 | import { connect } from 'react-redux'; 15 | import { signupUser } from '../redux/actions/userActions'; 16 | 17 | const styles = (theme) => ({ 18 | ...theme 19 | }); 20 | 21 | class signup extends Component { 22 | constructor() { 23 | super(); 24 | this.state = { 25 | email: '', 26 | password: '', 27 | confirmPassword: '', 28 | handle: '', 29 | errors: {} 30 | }; 31 | } 32 | componentWillReceiveProps(nextProps) { 33 | if (nextProps.UI.errors) { 34 | this.setState({ errors: nextProps.UI.errors }); 35 | } 36 | } 37 | handleSubmit = (event) => { 38 | event.preventDefault(); 39 | this.setState({ 40 | loading: true 41 | }); 42 | const newUserData = { 43 | email: this.state.email, 44 | password: this.state.password, 45 | confirmPassword: this.state.confirmPassword, 46 | handle: this.state.handle 47 | }; 48 | this.props.signupUser(newUserData, this.props.history); 49 | }; 50 | handleChange = (event) => { 51 | this.setState({ 52 | [event.target.name]: event.target.value 53 | }); 54 | }; 55 | render() { 56 | const { 57 | classes, 58 | UI: { loading } 59 | } = this.props; 60 | const { errors } = this.state; 61 | 62 | return ( 63 | 64 | 65 | 66 | monkey 67 | 68 | SignUp 69 | 70 |
71 | 83 | 95 | 107 | 119 | {errors.general && ( 120 | 121 | {errors.general} 122 | 123 | )} 124 | 136 |
137 | 138 | Already have an account ? Login here 139 | 140 | 141 |
142 | 143 | 144 | ); 145 | } 146 | } 147 | 148 | signup.propTypes = { 149 | classes: PropTypes.object.isRequired, 150 | user: PropTypes.object.isRequired, 151 | UI: PropTypes.object.isRequired, 152 | signupUser: PropTypes.func.isRequired 153 | }; 154 | 155 | const mapStateToProps = (state) => ({ 156 | user: state.user, 157 | UI: state.UI 158 | }); 159 | 160 | export default connect( 161 | mapStateToProps, 162 | { signupUser } 163 | )(withStyles(styles)(signup)); 164 | -------------------------------------------------------------------------------- /src/pages/user.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import axios from 'axios'; 4 | import Scream from '../components/scream/Scream'; 5 | import StaticProfile from '../components/profile/StaticProfile'; 6 | import Grid from '@material-ui/core/Grid'; 7 | 8 | import ScreamSkeleton from '../util/ScreamSkeleton'; 9 | import ProfileSkeleton from '../util/ProfileSkeleton'; 10 | 11 | import { connect } from 'react-redux'; 12 | import { getUserData } from '../redux/actions/dataActions'; 13 | 14 | class user extends Component { 15 | state = { 16 | profile: null, 17 | screamIdParam: null 18 | }; 19 | componentDidMount() { 20 | const handle = this.props.match.params.handle; 21 | const screamId = this.props.match.params.screamId; 22 | 23 | if (screamId) this.setState({ screamIdParam: screamId }); 24 | 25 | this.props.getUserData(handle); 26 | axios 27 | .get(`/user/${handle}`) 28 | .then((res) => { 29 | this.setState({ 30 | profile: res.data.user 31 | }); 32 | }) 33 | .catch((err) => console.log(err)); 34 | } 35 | render() { 36 | const { screams, loading } = this.props.data; 37 | const { screamIdParam } = this.state; 38 | 39 | const screamsMarkup = loading ? ( 40 | 41 | ) : screams === null ? ( 42 |

No screams from this user

43 | ) : !screamIdParam ? ( 44 | screams.map((scream) => ) 45 | ) : ( 46 | screams.map((scream) => { 47 | if (scream.screamId !== screamIdParam) 48 | return ; 49 | else return ; 50 | }) 51 | ); 52 | 53 | return ( 54 | 55 | 56 | {screamsMarkup} 57 | 58 | 59 | {this.state.profile === null ? ( 60 | 61 | ) : ( 62 | 63 | )} 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | user.propTypes = { 71 | getUserData: PropTypes.func.isRequired, 72 | data: PropTypes.object.isRequired 73 | }; 74 | 75 | const mapStateToProps = (state) => ({ 76 | data: state.data 77 | }); 78 | 79 | export default connect( 80 | mapStateToProps, 81 | { getUserData } 82 | )(user); 83 | -------------------------------------------------------------------------------- /src/redux/actions/dataActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SCREAMS, 3 | LOADING_DATA, 4 | LIKE_SCREAM, 5 | UNLIKE_SCREAM, 6 | DELETE_SCREAM, 7 | SET_ERRORS, 8 | POST_SCREAM, 9 | CLEAR_ERRORS, 10 | LOADING_UI, 11 | SET_SCREAM, 12 | STOP_LOADING_UI, 13 | SUBMIT_COMMENT 14 | } from '../types'; 15 | import axios from 'axios'; 16 | 17 | // Get all screams 18 | export const getScreams = () => (dispatch) => { 19 | dispatch({ type: LOADING_DATA }); 20 | axios 21 | .get('/screams') 22 | .then((res) => { 23 | dispatch({ 24 | type: SET_SCREAMS, 25 | payload: res.data 26 | }); 27 | }) 28 | .catch((err) => { 29 | dispatch({ 30 | type: SET_SCREAMS, 31 | payload: [] 32 | }); 33 | }); 34 | }; 35 | export const getScream = (screamId) => (dispatch) => { 36 | dispatch({ type: LOADING_UI }); 37 | axios 38 | .get(`/scream/${screamId}`) 39 | .then((res) => { 40 | dispatch({ 41 | type: SET_SCREAM, 42 | payload: res.data 43 | }); 44 | dispatch({ type: STOP_LOADING_UI }); 45 | }) 46 | .catch((err) => console.log(err)); 47 | }; 48 | // Post a scream 49 | export const postScream = (newScream) => (dispatch) => { 50 | dispatch({ type: LOADING_UI }); 51 | axios 52 | .post('/scream', newScream) 53 | .then((res) => { 54 | dispatch({ 55 | type: POST_SCREAM, 56 | payload: res.data 57 | }); 58 | dispatch(clearErrors()); 59 | }) 60 | .catch((err) => { 61 | dispatch({ 62 | type: SET_ERRORS, 63 | payload: err.response.data 64 | }); 65 | }); 66 | }; 67 | // Like a scream 68 | export const likeScream = (screamId) => (dispatch) => { 69 | axios 70 | .get(`/scream/${screamId}/like`) 71 | .then((res) => { 72 | dispatch({ 73 | type: LIKE_SCREAM, 74 | payload: res.data 75 | }); 76 | }) 77 | .catch((err) => console.log(err)); 78 | }; 79 | // Unlike a scream 80 | export const unlikeScream = (screamId) => (dispatch) => { 81 | axios 82 | .get(`/scream/${screamId}/unlike`) 83 | .then((res) => { 84 | dispatch({ 85 | type: UNLIKE_SCREAM, 86 | payload: res.data 87 | }); 88 | }) 89 | .catch((err) => console.log(err)); 90 | }; 91 | // Submit a comment 92 | export const submitComment = (screamId, commentData) => (dispatch) => { 93 | axios 94 | .post(`/scream/${screamId}/comment`, commentData) 95 | .then((res) => { 96 | dispatch({ 97 | type: SUBMIT_COMMENT, 98 | payload: res.data 99 | }); 100 | dispatch(clearErrors()); 101 | }) 102 | .catch((err) => { 103 | dispatch({ 104 | type: SET_ERRORS, 105 | payload: err.response.data 106 | }); 107 | }); 108 | }; 109 | export const deleteScream = (screamId) => (dispatch) => { 110 | axios 111 | .delete(`/scream/${screamId}`) 112 | .then(() => { 113 | dispatch({ type: DELETE_SCREAM, payload: screamId }); 114 | }) 115 | .catch((err) => console.log(err)); 116 | }; 117 | 118 | export const getUserData = (userHandle) => (dispatch) => { 119 | dispatch({ type: LOADING_DATA }); 120 | axios 121 | .get(`/user/${userHandle}`) 122 | .then((res) => { 123 | dispatch({ 124 | type: SET_SCREAMS, 125 | payload: res.data.screams 126 | }); 127 | }) 128 | .catch(() => { 129 | dispatch({ 130 | type: SET_SCREAMS, 131 | payload: null 132 | }); 133 | }); 134 | }; 135 | 136 | export const clearErrors = () => (dispatch) => { 137 | dispatch({ type: CLEAR_ERRORS }); 138 | }; 139 | -------------------------------------------------------------------------------- /src/redux/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_USER, 3 | SET_ERRORS, 4 | CLEAR_ERRORS, 5 | LOADING_UI, 6 | SET_UNAUTHENTICATED, 7 | LOADING_USER, 8 | MARK_NOTIFICATIONS_READ 9 | } from '../types'; 10 | import axios from 'axios'; 11 | 12 | export const loginUser = (userData, history) => (dispatch) => { 13 | dispatch({ type: LOADING_UI }); 14 | axios 15 | .post('/login', userData) 16 | .then((res) => { 17 | setAuthorizationHeader(res.data.token); 18 | dispatch(getUserData()); 19 | dispatch({ type: CLEAR_ERRORS }); 20 | history.push('/'); 21 | }) 22 | .catch((err) => { 23 | dispatch({ 24 | type: SET_ERRORS, 25 | payload: err.response.data 26 | }); 27 | }); 28 | }; 29 | 30 | export const signupUser = (newUserData, history) => (dispatch) => { 31 | dispatch({ type: LOADING_UI }); 32 | axios 33 | .post('/signup', newUserData) 34 | .then((res) => { 35 | setAuthorizationHeader(res.data.token); 36 | dispatch(getUserData()); 37 | dispatch({ type: CLEAR_ERRORS }); 38 | history.push('/'); 39 | }) 40 | .catch((err) => { 41 | dispatch({ 42 | type: SET_ERRORS, 43 | payload: err.response.data 44 | }); 45 | }); 46 | }; 47 | 48 | export const logoutUser = () => (dispatch) => { 49 | localStorage.removeItem('FBIdToken'); 50 | delete axios.defaults.headers.common['Authorization']; 51 | dispatch({ type: SET_UNAUTHENTICATED }); 52 | }; 53 | 54 | export const getUserData = () => (dispatch) => { 55 | dispatch({ type: LOADING_USER }); 56 | axios 57 | .get('/user') 58 | .then((res) => { 59 | dispatch({ 60 | type: SET_USER, 61 | payload: res.data 62 | }); 63 | }) 64 | .catch((err) => console.log(err)); 65 | }; 66 | 67 | export const uploadImage = (formData) => (dispatch) => { 68 | dispatch({ type: LOADING_USER }); 69 | axios 70 | .post('/user/image', formData) 71 | .then(() => { 72 | dispatch(getUserData()); 73 | }) 74 | .catch((err) => console.log(err)); 75 | }; 76 | 77 | export const editUserDetails = (userDetails) => (dispatch) => { 78 | dispatch({ type: LOADING_USER }); 79 | axios 80 | .post('/user', userDetails) 81 | .then(() => { 82 | dispatch(getUserData()); 83 | }) 84 | .catch((err) => console.log(err)); 85 | }; 86 | 87 | export const markNotificationsRead = (notificationIds) => (dispatch) => { 88 | axios 89 | .post('/notifications', notificationIds) 90 | .then((res) => { 91 | dispatch({ 92 | type: MARK_NOTIFICATIONS_READ 93 | }); 94 | }) 95 | .catch((err) => console.log(err)); 96 | }; 97 | 98 | const setAuthorizationHeader = (token) => { 99 | const FBIdToken = `Bearer ${token}`; 100 | localStorage.setItem('FBIdToken', FBIdToken); 101 | axios.defaults.headers.common['Authorization'] = FBIdToken; 102 | }; 103 | -------------------------------------------------------------------------------- /src/redux/reducers/dataReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SCREAMS, 3 | LIKE_SCREAM, 4 | UNLIKE_SCREAM, 5 | LOADING_DATA, 6 | DELETE_SCREAM, 7 | POST_SCREAM, 8 | SET_SCREAM, 9 | SUBMIT_COMMENT 10 | } from '../types'; 11 | 12 | const initialState = { 13 | screams: [], 14 | scream: {}, 15 | loading: false 16 | }; 17 | 18 | export default function(state = initialState, action) { 19 | switch (action.type) { 20 | case LOADING_DATA: 21 | return { 22 | ...state, 23 | loading: true 24 | }; 25 | case SET_SCREAMS: 26 | return { 27 | ...state, 28 | screams: action.payload, 29 | loading: false 30 | }; 31 | case SET_SCREAM: 32 | return { 33 | ...state, 34 | scream: action.payload 35 | }; 36 | case LIKE_SCREAM: 37 | case UNLIKE_SCREAM: 38 | let index = state.screams.findIndex( 39 | (scream) => scream.screamId === action.payload.screamId 40 | ); 41 | state.screams[index] = action.payload; 42 | if (state.scream.screamId === action.payload.screamId) { 43 | state.scream = action.payload; 44 | } 45 | return { 46 | ...state 47 | }; 48 | case DELETE_SCREAM: 49 | index = state.screams.findIndex( 50 | (scream) => scream.screamId === action.payload 51 | ); 52 | state.screams.splice(index, 1); 53 | return { 54 | ...state 55 | }; 56 | case POST_SCREAM: 57 | return { 58 | ...state, 59 | screams: [action.payload, ...state.screams] 60 | }; 61 | case SUBMIT_COMMENT: 62 | return { 63 | ...state, 64 | scream: { 65 | ...state.scream, 66 | comments: [action.payload, ...state.scream.comments] 67 | } 68 | }; 69 | default: 70 | return state; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/redux/reducers/uiReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ERRORS, 3 | CLEAR_ERRORS, 4 | LOADING_UI, 5 | STOP_LOADING_UI 6 | } from '../types'; 7 | 8 | const initialState = { 9 | loading: false, 10 | errors: null 11 | }; 12 | 13 | export default function(state = initialState, action) { 14 | switch (action.type) { 15 | case SET_ERRORS: 16 | return { 17 | ...state, 18 | loading: false, 19 | errors: action.payload 20 | }; 21 | case CLEAR_ERRORS: 22 | return { 23 | ...state, 24 | loading: false, 25 | errors: null 26 | }; 27 | case LOADING_UI: 28 | return { 29 | ...state, 30 | loading: true 31 | }; 32 | case STOP_LOADING_UI: 33 | return { 34 | ...state, 35 | loading: false 36 | }; 37 | default: 38 | return state; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/redux/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_USER, 3 | SET_AUTHENTICATED, 4 | SET_UNAUTHENTICATED, 5 | LOADING_USER, 6 | LIKE_SCREAM, 7 | UNLIKE_SCREAM, 8 | MARK_NOTIFICATIONS_READ 9 | } from '../types'; 10 | 11 | const initialState = { 12 | authenticated: false, 13 | loading: false, 14 | credentials: {}, 15 | likes: [], 16 | notifications: [] 17 | }; 18 | 19 | export default function(state = initialState, action) { 20 | switch (action.type) { 21 | case SET_AUTHENTICATED: 22 | return { 23 | ...state, 24 | authenticated: true 25 | }; 26 | case SET_UNAUTHENTICATED: 27 | return initialState; 28 | case SET_USER: 29 | return { 30 | authenticated: true, 31 | loading: false, 32 | ...action.payload 33 | }; 34 | case LOADING_USER: 35 | return { 36 | ...state, 37 | loading: true 38 | }; 39 | case LIKE_SCREAM: 40 | return { 41 | ...state, 42 | likes: [ 43 | ...state.likes, 44 | { 45 | userHandle: state.credentials.handle, 46 | screamId: action.payload.screamId 47 | } 48 | ] 49 | }; 50 | case UNLIKE_SCREAM: 51 | return { 52 | ...state, 53 | likes: state.likes.filter( 54 | (like) => like.screamId !== action.payload.screamId 55 | ) 56 | }; 57 | case MARK_NOTIFICATIONS_READ: 58 | state.notifications.forEach((not) => (not.read = true)); 59 | return { 60 | ...state 61 | }; 62 | default: 63 | return state; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import userReducer from './reducers/userReducer'; 5 | import dataReducer from './reducers/dataReducer'; 6 | import uiReducer from './reducers/uiReducer'; 7 | 8 | const initialState = {}; 9 | 10 | const middleware = [thunk]; 11 | 12 | const reducers = combineReducers({ 13 | user: userReducer, 14 | data: dataReducer, 15 | UI: uiReducer 16 | }); 17 | 18 | const composeEnhancers = 19 | typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 20 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) 21 | : compose; 22 | 23 | const enhancer = composeEnhancers(applyMiddleware(...middleware)); 24 | const store = createStore(reducers, initialState, enhancer); 25 | 26 | export default store; 27 | -------------------------------------------------------------------------------- /src/redux/types.js: -------------------------------------------------------------------------------- 1 | // User reducer types 2 | export const SET_AUTHENTICATED = 'SET_AUTHENTICATED'; 3 | export const SET_UNAUTHENTICATED = 'SET_UNAUTHENTICATED'; 4 | export const SET_USER = 'SET_USER'; 5 | export const LOADING_USER = 'LOADING_USER'; 6 | export const MARK_NOTIFICATIONS_READ = 'MARK_NOTIFICATIONS_READ'; 7 | // UI reducer types 8 | export const SET_ERRORS = 'SET_ERRORS'; 9 | export const LOADING_UI = 'LOADING_UI'; 10 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 11 | export const LOADING_DATA = 'LOADING_DATA'; 12 | export const STOP_LOADING_UI = 'STOP_LOADING_UI'; 13 | // Data reducer types 14 | export const SET_SCREAMS = 'SET_SCREAMS'; 15 | export const SET_SCREAM = 'SET_SCREAM'; 16 | export const LIKE_SCREAM = 'LIKE_SCREAM'; 17 | export const UNLIKE_SCREAM = 'UNLIKE_SCREAM'; 18 | export const DELETE_SCREAM = 'DELETE_SCREAM'; 19 | export const POST_SCREAM = 'POST_SCREAM'; 20 | export const SUBMIT_COMMENT = 'SUBMIT_COMMENT'; 21 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/util/AuthRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const AuthRoute = ({ component: Component, authenticated, ...rest }) => ( 7 | 10 | authenticated === true ? : 11 | } 12 | /> 13 | ); 14 | 15 | const mapStateToProps = (state) => ({ 16 | authenticated: state.user.authenticated 17 | }); 18 | 19 | AuthRoute.propTypes = { 20 | user: PropTypes.object 21 | }; 22 | 23 | export default connect(mapStateToProps)(AuthRoute); 24 | -------------------------------------------------------------------------------- /src/util/MyButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Tooltip from '@material-ui/core/Tooltip'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | 6 | export default ({ children, onClick, tip, btnClassName, tipClassName }) => ( 7 | 8 | 9 | {children} 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/util/ProfileSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import NoImg from '../images/no-img.png'; 5 | // MUI 6 | import Paper from '@material-ui/core/Paper'; 7 | // Icons 8 | import LocationOn from '@material-ui/icons/LocationOn'; 9 | import LinkIcon from '@material-ui/icons/Link'; 10 | import CalendarToday from '@material-ui/icons/CalendarToday'; 11 | 12 | const styles = (theme) => ({ 13 | ...theme, 14 | handle: { 15 | height: 20, 16 | backgroundColor: theme.palette.primary.main, 17 | width: 60, 18 | margin: '0 auto 7px auto' 19 | }, 20 | fullLine: { 21 | height: 15, 22 | backgroundColor: 'rgba(0,0,0,0.6)', 23 | width: '100%', 24 | marginBottom: 10 25 | }, 26 | halfLine: { 27 | height: 15, 28 | backgroundColor: 'rgba(0,0,0,0.6)', 29 | width: '50%', 30 | marginBottom: 10 31 | } 32 | }); 33 | 34 | const ProfileSkeleton = (props) => { 35 | const { classes } = props; 36 | return ( 37 | 38 |
39 |
40 | profile 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Location 50 |
51 | https://website.com 52 |
53 | Joined date 54 |
55 |
56 | 57 | ); 58 | }; 59 | 60 | ProfileSkeleton.propTypes = { 61 | classes: PropTypes.object.isRequired 62 | }; 63 | 64 | export default withStyles(styles)(ProfileSkeleton); 65 | -------------------------------------------------------------------------------- /src/util/ScreamSkeleton.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import NoImg from '../images/no-img.png'; 3 | import PropTypes from 'prop-types'; 4 | // MUI 5 | import Card from '@material-ui/core/Card'; 6 | import CardMedia from '@material-ui/core/CardMedia'; 7 | import CardContent from '@material-ui/core/CardContent'; 8 | 9 | import withStyles from '@material-ui/core/styles/withStyles'; 10 | 11 | const styles = (theme) => ({ 12 | ...theme, 13 | card: { 14 | display: 'flex', 15 | marginBottom: 20 16 | }, 17 | cardContent: { 18 | width: '100%', 19 | flexDirection: 'column', 20 | padding: 25 21 | }, 22 | cover: { 23 | minWidth: 200, 24 | objectFit: 'cover' 25 | }, 26 | handle: { 27 | width: 60, 28 | height: 18, 29 | backgroundColor: theme.palette.primary.main, 30 | marginBottom: 7 31 | }, 32 | date: { 33 | height: 14, 34 | width: 100, 35 | backgroundColor: 'rgba(0,0,0, 0.3)', 36 | marginBottom: 10 37 | }, 38 | fullLine: { 39 | height: 15, 40 | width: '90%', 41 | backgroundColor: 'rgba(0,0,0, 0.6)', 42 | marginBottom: 10 43 | }, 44 | halfLine: { 45 | height: 15, 46 | width: '50%', 47 | backgroundColor: 'rgba(0,0,0, 0.6)', 48 | marginBottom: 10 49 | } 50 | }); 51 | 52 | const ScreamSkeleton = (props) => { 53 | const { classes } = props; 54 | 55 | const content = Array.from({ length: 5 }).map((item, index) => ( 56 | 57 | 58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | )); 67 | 68 | return {content}; 69 | }; 70 | 71 | ScreamSkeleton.propTypes = { 72 | classes: PropTypes.object.isRequired 73 | }; 74 | 75 | export default withStyles(styles)(ScreamSkeleton); 76 | -------------------------------------------------------------------------------- /src/util/theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | palette: { 3 | primary: { 4 | light: '#33c9dc', 5 | main: '#00bcd4', 6 | dark: '#008394', 7 | contrastText: '#fff' 8 | }, 9 | secondary: { 10 | light: '#ff6333', 11 | main: '#ff3d00', 12 | dark: '#b22a00', 13 | contrastText: '#fff' 14 | } 15 | }, 16 | typography: { 17 | useNextVariants: true 18 | }, 19 | form: { 20 | textAlign: 'center' 21 | }, 22 | image: { 23 | margin: '20px auto 20px auto' 24 | }, 25 | pageTitle: { 26 | margin: '10px auto 10px auto' 27 | }, 28 | textField: { 29 | margin: '10px auto 10px auto' 30 | }, 31 | button: { 32 | marginTop: 20, 33 | position: 'relative' 34 | }, 35 | customError: { 36 | color: 'red', 37 | fontSize: '0.8rem', 38 | marginTop: 10 39 | }, 40 | progress: { 41 | position: 'absolute' 42 | }, 43 | invisibleSeparator: { 44 | border: 'none', 45 | margin: 4 46 | }, 47 | visibleSeparator: { 48 | width: '100%', 49 | borderBottom: '1px solid rgba(0,0,0,0.1)', 50 | marginBottom: 20 51 | }, 52 | paper: { 53 | padding: 20 54 | }, 55 | profile: { 56 | '& .image-wrapper': { 57 | textAlign: 'center', 58 | position: 'relative', 59 | '& button': { 60 | position: 'absolute', 61 | top: '80%', 62 | left: '70%' 63 | } 64 | }, 65 | '& .profile-image': { 66 | width: 200, 67 | height: 200, 68 | objectFit: 'cover', 69 | maxWidth: '100%', 70 | borderRadius: '50%' 71 | }, 72 | '& .profile-details': { 73 | textAlign: 'center', 74 | '& span, svg': { 75 | verticalAlign: 'middle' 76 | }, 77 | '& a': { 78 | color: '#00bcd4' 79 | } 80 | }, 81 | '& hr': { 82 | border: 'none', 83 | margin: '0 0 10px 0' 84 | }, 85 | '& svg.button': { 86 | '&:hover': { 87 | cursor: 'pointer' 88 | } 89 | } 90 | }, 91 | buttons: { 92 | textAlign: 'center', 93 | '& a': { 94 | margin: '20px 10px' 95 | } 96 | } 97 | }; 98 | --------------------------------------------------------------------------------