├── .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 |
85 | );
86 | })
87 | ) : (
88 |
91 | );
92 | return (
93 |
94 |
95 |
100 | {notificationsIcon}
101 |
102 |
103 |
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 |
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 |

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 |

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 |
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 |
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 |
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 |
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 |
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 |
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 |
60 |
61 | Login
62 |
63 |
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 |
67 |
68 | SignUp
69 |
70 |
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 |

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 |
--------------------------------------------------------------------------------