├── .gitignore
├── webpack.config.js
├── .env.example
├── netlify.toml
├── public
├── weebook.png
└── index.html
├── src
├── index.css
├── redux
│ ├── reducers
│ │ ├── index.js
│ │ ├── auth.js
│ │ ├── posts.js
│ │ └── user.js
│ ├── constants
│ │ └── actionConstants.js
│ └── actions
│ │ └── posts.js
├── components
│ ├── Home
│ │ ├── FloatingActBtnStyles.js
│ │ ├── FloatingActBtn.js
│ │ ├── HomeStyles.js
│ │ └── Home.js
│ ├── Footer
│ │ ├── FooterStyles.js
│ │ └── Footer.js
│ ├── Feed
│ │ ├── PostsFeedStyles.js
│ │ ├── IndividualPostStyles.js
│ │ ├── PostsFeed.js
│ │ └── IndividualPost.js
│ ├── MdEditor
│ │ ├── ModalContainer.js
│ │ ├── ModalContainerStyles.js
│ │ ├── ModalContentStyles.js
│ │ └── ModalContent.js
│ ├── Navbar
│ │ ├── NavStyles.js
│ │ ├── Drawer.js
│ │ └── Navbar.js
│ └── Auth
│ │ ├── AuthStyles.js
│ │ └── Auth.js
├── index.js
├── Apis
│ └── Api.js
├── Hooks
│ └── WindowSize.js
└── App.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .env
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_CLIENT_ID = 'Your Goolge Client Id'
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "/*"
3 | to = "/index.html"
4 | status = 200
--------------------------------------------------------------------------------
/public/weebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikhilsourav/weebook-client/HEAD/public/weebook.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | }
6 | html {
7 | scroll-behavior: smooth;
8 | height: 100vh;
9 | }
10 | body {
11 | font-family: 'Roboto', Arial, Helvetica, sans-serif;
12 | }
13 |
--------------------------------------------------------------------------------
/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | // Redux import
2 | import { combineReducers } from 'redux';
3 |
4 | // reducers import
5 | import posts from './posts';
6 | import auth from './auth';
7 | import user from './user';
8 |
9 | // all reducers are combined and exported to root index.js
10 | export default combineReducers({ posts, auth, user });
11 |
--------------------------------------------------------------------------------
/src/components/Home/FloatingActBtnStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | Container: {
5 | position: 'fixed',
6 | bottom: '50px',
7 | right: '50px',
8 | display: 'none',
9 | [theme.breakpoints.down(500)]: {
10 | display: 'flex',
11 | },
12 | },
13 | }));
14 |
--------------------------------------------------------------------------------
/src/components/Footer/FooterStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | BottomGreet: {
5 | width: 'fit-content',
6 | display: 'block',
7 | margin: 'auto',
8 | ...theme.typography.button,
9 | backgroundColor: theme.palette.background.main,
10 | padding: theme.spacing(2),
11 | },
12 | }));
13 |
--------------------------------------------------------------------------------
/src/redux/constants/actionConstants.js:
--------------------------------------------------------------------------------
1 | export const CREATE = 'CREATE';
2 | export const UPDATE = 'UPDATE';
3 | export const DELETE = 'DELETE';
4 | export const FETCH_ALL = 'FETCH_ALL';
5 |
6 | export const AUTH = 'AUTH';
7 | export const LOGOUT = 'LOGOUT';
8 |
9 | export const SET_CURRENT_USER_ID = 'SET_CURRENT_USER_ID ';
10 | export const INITIATE_POST_EDIT = 'INITIATE_POST_EDIT';
11 | export const POST_CLICKED = 'POST_CLICKED';
12 | export const FAB_CLICKED = 'FAB_CLICKED';
13 |
--------------------------------------------------------------------------------
/src/components/Feed/PostsFeedStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | Posts: {
5 | backgroundColor: theme.palette.primary.main,
6 | padding: '15px',
7 | [theme.breakpoints.down('sm')]: {
8 | paddingLeft: theme.spacing(1),
9 | paddingRight: theme.spacing(1),
10 | },
11 | },
12 | Skeleton: {
13 | marginBottom: '40px',
14 | },
15 | SkeletonItem: {
16 | marginTop: '10px',
17 | },
18 | }));
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | import { Provider } from 'react-redux';
7 | import { configureStore } from '@reduxjs/toolkit';
8 | import reducers from './redux/reducers/';
9 |
10 | // create global store
11 | const store = configureStore({ reducer: reducers });
12 |
13 | ReactDOM.render(
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | );
19 |
--------------------------------------------------------------------------------
/src/redux/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { AUTH, LOGOUT } from '../constants/actionConstants';
2 |
3 | const authReducer = (state = { authData: null }, action) => {
4 | switch (action.type) {
5 | case AUTH:
6 | localStorage.setItem('profile', JSON.stringify({ ...action?.data }));
7 | return { ...state, authData: action?.data };
8 | case LOGOUT:
9 | localStorage.clear();
10 | return { ...state, authData: null };
11 | default:
12 | return state;
13 | }
14 | };
15 |
16 | export default authReducer;
17 |
--------------------------------------------------------------------------------
/src/components/MdEditor/ModalContainer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Container for ModalContent
3 | */
4 | import { Paper, Container } from '@material-ui/core';
5 | import useStyles from './ModalContainerStyles';
6 | import Form from './ModalContent';
7 |
8 | const ModalContainer = () => {
9 | const classes = useStyles();
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default ModalContainer;
21 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import { Paper, Typography } from '@material-ui/core';
2 | import { useLocation } from 'react-router-dom';
3 | import useStyles from './FooterStyles';
4 |
5 | const Footer = () => {
6 | const classes = useStyles();
7 | const location = useLocation();
8 | const greetingText =
9 | location.pathname === '/auth' ? 'Hey there friendly citizen!' : 'Wow! You scroll too much!';
10 |
11 | return (
12 |
13 |
14 | {greetingText}
15 |
16 |
17 | );
18 | };
19 |
20 | export default Footer;
21 |
--------------------------------------------------------------------------------
/src/redux/reducers/posts.js:
--------------------------------------------------------------------------------
1 | // import const action types
2 | import { FETCH_ALL, DELETE, CREATE, UPDATE } from '../constants/actionConstants';
3 |
4 | const postReducers = (posts = [], action) => {
5 | switch (action.type) {
6 | case FETCH_ALL:
7 | return action.payload;
8 | case CREATE:
9 | return [action.payload, ...posts];
10 | case UPDATE:
11 | return posts.map((post) => (post._id === action.payload._id ? action.payload : post));
12 | case DELETE:
13 | return posts.filter((post) => post._id !== action.payload);
14 | default:
15 | return posts;
16 | }
17 | };
18 |
19 | export default postReducers;
20 |
--------------------------------------------------------------------------------
/src/components/Navbar/NavStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | Heading: {
5 | flexGrow: 1,
6 | fontFamily: 'Roboto',
7 | color: 'rgba(255,255,250,1)',
8 | },
9 | Profile: {
10 | width: theme.spacing(3.5),
11 | height: theme.spacing(3.5),
12 | },
13 | NavItem: {
14 | display: 'flex',
15 | justifyContent: 'space-between',
16 | alignItems: 'center',
17 | width: '100%',
18 | minWidth: '160px',
19 | minHeight: '50px',
20 | },
21 | Modal: {
22 | display: 'flex',
23 | justifyContent: 'space-between',
24 | alignItems: 'center',
25 | },
26 | }));
27 |
--------------------------------------------------------------------------------
/src/components/MdEditor/ModalContainerStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | ModalContainer: {
5 | height: 'max(81vh, 550px)',
6 | width: '90%',
7 | padding: '15px',
8 | backgroundColor: theme.palette.primary.main,
9 | [theme.breakpoints.down('xs')]: {
10 | marginTop: theme.spacing(2),
11 | width: '100%',
12 | height: '550px',
13 | },
14 | [theme.breakpoints.down('sm')]: {
15 | paddingLeft: theme.spacing(1),
16 | paddingRight: theme.spacing(1),
17 | },
18 | position: 'relative',
19 | },
20 | Paper: {
21 | height: '100%',
22 | },
23 | }));
24 |
--------------------------------------------------------------------------------
/src/components/Home/FloatingActBtn.js:
--------------------------------------------------------------------------------
1 | import { Fab } from '@material-ui/core';
2 | import AddIcon from '@material-ui/icons/Add';
3 | import useStyles from './FloatingActBtnStyles';
4 | import { useDispatch } from 'react-redux';
5 | import { FAB_CLICKED } from '../../redux/constants/actionConstants';
6 |
7 | const FloatingActionButton = () => {
8 | const classes = useStyles();
9 | const dispatch = useDispatch();
10 |
11 | const handleClick = () => {
12 | dispatch({ type: FAB_CLICKED, payload: true });
13 | };
14 |
15 | return (
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default FloatingActionButton;
23 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | weebook
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/Home/HomeStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | Container: {
5 | height: '100%',
6 | outline: 'none',
7 | position: 'relative',
8 | maxWidth: '1100px',
9 | overflowX: 'hidden',
10 | [theme.breakpoints.down('xs')]: {
11 | padding: '0',
12 | },
13 | },
14 | GridContainer: {
15 | flexDirection: 'row',
16 | justifyContent: 'center',
17 | alignItems: 'flex-start',
18 | margin: '20px 0',
19 | padding: theme.spacing(1),
20 | [theme.breakpoints.down('xs')]: {
21 | flexDirection: 'column-reverse',
22 | alignItems: 'stretch',
23 | flexGrow: 1,
24 | },
25 | },
26 | }));
27 |
--------------------------------------------------------------------------------
/src/Apis/Api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API = axios.create({ baseURL: 'https://weebook-ager.onrender.com/' });
4 |
5 | // send req with headers
6 | API.interceptors.request.use((req) => {
7 | if (localStorage.getItem('profile')) {
8 | req.headers.authorization = `Bearer ${JSON.parse(localStorage.getItem('profile')).token}`;
9 | }
10 | return req;
11 | });
12 |
13 | export const getPosts = () => API.get('/posts');
14 | export const createPost = (newPost) => API.post('/posts', newPost);
15 | export const updatePost = (id, updatedPost) => API.patch(`/posts/${id}`, updatedPost);
16 | export const deletePost = (id) => API.delete(`/posts/${id}`);
17 | export const likePost = (id) => API.patch(`/posts/${id}/likePost`);
18 |
--------------------------------------------------------------------------------
/src/components/Auth/AuthStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | AuthBody: {
5 | display: 'flex',
6 | alignItems: 'center',
7 | justifyContent: 'center',
8 | height: '100vh',
9 | },
10 | paper: {
11 | width: '100%',
12 | height: 'auto',
13 | marginTop: theme.spacing(8),
14 | display: 'flex',
15 | flexDirection: 'column',
16 | alignItems: 'center',
17 | padding: theme.spacing(2),
18 | },
19 | avatar: {
20 | margin: theme.spacing(1),
21 | backgroundColor: theme.palette.primary.main,
22 | },
23 | form: {
24 | width: 'fit-content',
25 | marginTop: theme.spacing(4),
26 | marginBottom: theme.spacing(4),
27 | marginLeft: 'auto',
28 | marginRight: 'auto',
29 | },
30 | }));
31 |
--------------------------------------------------------------------------------
/src/components/MdEditor/ModalContentStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | root: {
5 | '& .MuiTextField-root': {
6 | marginBottom: theme.spacing(2),
7 | marginLeft: theme.spacing(0.7),
8 | marginRight: theme.spacing(0.7),
9 | },
10 | },
11 | Form: {
12 | display: 'flex',
13 | flexWrap: 'wrap',
14 | justifyContent: 'center',
15 | },
16 | FormHeading: {
17 | margin: '15px 0',
18 | },
19 | Clear: {
20 | position: 'absolute',
21 | top: '22px',
22 | right: '25px',
23 | },
24 | ButtonSubmit: {
25 | marginBottom: 10,
26 | },
27 | SignIn: {
28 | height: '100%',
29 | padding: '30px',
30 | display: 'flex',
31 | alignItems: 'center',
32 | justifyContent: 'center',
33 | },
34 | }));
35 |
--------------------------------------------------------------------------------
/src/components/Home/Home.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Container, Grid } from '@material-ui/core';
3 | import { useDispatch } from 'react-redux';
4 |
5 | import FloatingActBtn from './FloatingActBtn';
6 | import useStyles from './HomeStyles';
7 | import Posts from '../Feed/PostsFeed';
8 | import { getPosts } from '../../redux/actions/posts';
9 |
10 | const Home = () => {
11 | const classes = useStyles();
12 | const dispatch = useDispatch();
13 |
14 | useEffect(() => {
15 | dispatch(getPosts());
16 | }, [dispatch]);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Home;
31 |
--------------------------------------------------------------------------------
/src/redux/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_CURRENT_USER_ID,
3 | INITIATE_POST_EDIT,
4 | POST_CLICKED,
5 | FAB_CLICKED,
6 | } from '../constants/actionConstants';
7 |
8 | const initialUserState = {
9 | currentUserId: null,
10 | hasSelectedEditMode: false,
11 | hasClickedPostBtn: false,
12 | hasClickedFab: false,
13 | };
14 |
15 | const userReducers = (state = initialUserState, action) => {
16 | switch (action.type) {
17 | case SET_CURRENT_USER_ID: {
18 | return { ...state, currentUserId: action.payload };
19 | }
20 | case INITIATE_POST_EDIT: {
21 | return { ...state, hasSelectedEditMode: action.payload };
22 | }
23 | case POST_CLICKED: {
24 | return { ...state, hasClickedPostBtn: action.payload };
25 | }
26 | case FAB_CLICKED: {
27 | return { ...state, hasClickedFab: action.payload };
28 | }
29 | default:
30 | return state;
31 | }
32 | };
33 |
34 | export default userReducers;
35 |
--------------------------------------------------------------------------------
/src/Hooks/WindowSize.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | // Get window's dimension
4 | const useWindowDimensions = () => {
5 | const getWindowDimensions = () => {
6 | const { innerWidth: width, innerHeight: height } = window;
7 | return { width, height };
8 | };
9 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
10 | useEffect(() => {
11 | function handleResize() {
12 | setWindowDimensions(getWindowDimensions());
13 | }
14 | window.addEventListener('resize', handleResize);
15 | return () => window.removeEventListener('resize', handleResize);
16 | }, []);
17 |
18 | return windowDimensions;
19 | };
20 |
21 | // Resize textfield dynamically using useCalcRows
22 | const useCalcRows = () => {
23 | const { width: windowWidth, height: windowHeight } = useWindowDimensions();
24 |
25 | if (windowHeight > 760) {
26 | if (windowWidth < 600) {
27 | return { minRows: 14, maxRows: 14 };
28 | } else if (windowWidth < 960) {
29 | return { minRows: 27, maxRows: 27 };
30 | }
31 | return { minRows: 27, maxRows: 27 };
32 | } else {
33 | if (windowWidth < 600) {
34 | return { minRows: 15, maxRows: 15 };
35 | } else if (windowWidth < 960) {
36 | return { minRows: 18, maxRows: 18 };
37 | }
38 | return { minRows: 18, maxRows: 18 };
39 | }
40 | };
41 |
42 | export { useCalcRows, useWindowDimensions };
43 |
--------------------------------------------------------------------------------
/src/components/Feed/IndividualPostStyles.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from '@material-ui/core/styles';
2 |
3 | export default makeStyles((theme) => ({
4 | root: {
5 | height: '100%',
6 | padding: theme.spacing(1),
7 | marginBottom: theme.spacing(2),
8 | position: 'relative',
9 | '&:hover': {
10 | boxShadow: '7px 7px 15px -4px rgba(0,0,0,0.5)',
11 | },
12 | },
13 | HeadWrapper: {
14 | display: 'flex',
15 | justifyContent: 'space-between',
16 | alignItems: 'center',
17 | position: 'relative',
18 | },
19 | Info: {
20 | display: 'flex',
21 | justifyContent: 'flex-start',
22 | [theme.breakpoints.down('sm')]: {
23 | flexDirection: 'column',
24 | },
25 | },
26 | Name: {
27 | fontSize: 14,
28 | color: theme.palette.text.secondary,
29 | marginRight: '20px',
30 | },
31 | CreatedAt: {
32 | fontSize: 14,
33 | color: theme.palette.text.secondary,
34 | [theme.breakpoints.down('sm')]: {
35 | marginTop: '10px',
36 | },
37 | },
38 | Edit: {
39 | [theme.breakpoints.down('sm')]: {
40 | position: 'absolute',
41 | right: '-12px',
42 | top: '50%',
43 | transform: 'translateY(-50%)',
44 | },
45 | },
46 | Title: {
47 | margin: '25px 0',
48 | fontWeight: 'bold',
49 | },
50 | Content: {
51 | wordSpacing: '2px',
52 | },
53 | Actions: {
54 | display: 'flex',
55 | justifyContent: 'space-between',
56 | },
57 | }));
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.3",
7 | "@material-ui/icons": "^4.11.2",
8 | "@material-ui/lab": "^4.0.0-alpha.57",
9 | "@react-oauth/google": "^0.12.1",
10 | "@reduxjs/toolkit": "^1.9.7",
11 | "@testing-library/jest-dom": "^5.11.9",
12 | "@testing-library/react": "^11.2.5",
13 | "@testing-library/user-event": "^12.7.0",
14 | "axios": "^1.6.2",
15 | "dotenv": "^8.6.0",
16 | "jwt-decode": "^3.1.2",
17 | "moment": "^2.29.1",
18 | "react": "^17.0.1",
19 | "react-dom": "^17.0.1",
20 | "react-redux": "^8.1.3",
21 | "react-router-dom": "^5.2.0",
22 | "react-scripts": "^5.0.1",
23 | "redux-thunk": "^2.4.2",
24 | "web-vitals": "^1.1.0"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/Feed/PostsFeed.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Container for IndividualPost
3 | */
4 | import { Paper, Typography } from '@material-ui/core';
5 | import { useSelector } from 'react-redux';
6 | import Skeleton from '@material-ui/lab/Skeleton';
7 |
8 | import Post from './IndividualPost';
9 | import useStyles from './PostsFeedStyles';
10 |
11 | const SkeletonLoader = () => {
12 | const classes = useStyles();
13 |
14 | const renderSkeletonItem = () => (
15 | <>
16 |
17 |
18 |
19 |
20 |
21 | >
22 | );
23 |
24 | return (
25 |
26 | {[1, 2, 3].map((key) => (
27 |
28 | {renderSkeletonItem()}
29 |
30 | ))}
31 |
32 | );
33 | };
34 |
35 | const Posts = () => {
36 | const classes = useStyles();
37 | const posts = useSelector((state) => state.posts);
38 |
39 | return !posts.length ? (
40 |
41 | ) : (
42 |
43 | {posts.map((post) => (
44 |
45 | ))}
46 |
47 | );
48 | };
49 |
50 | export default Posts;
51 |
--------------------------------------------------------------------------------
/src/redux/actions/posts.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../Apis/Api.js'; // backend services
2 | import { FETCH_ALL, DELETE, UPDATE, CREATE } from '../constants/actionConstants';
3 |
4 | // Action creators
5 | export const getPosts = () => async (dispatch) => {
6 | try {
7 | const { data } = await api.getPosts();
8 | dispatch({ type: FETCH_ALL, payload: data });
9 | } catch (error) {
10 | console.error('Error in getPosts action creator:', error);
11 | }
12 | };
13 |
14 | export const createPost = (post) => async (dispatch) => {
15 | try {
16 | const { data } = await api.createPost(post);
17 | dispatch({ type: CREATE, payload: data });
18 | } catch (error) {
19 | console.error('Error in createPost action creator:', error);
20 | }
21 | };
22 |
23 | export const updatePost = (id, post) => async (dispatch) => {
24 | try {
25 | const { data } = await api.updatePost(id, post);
26 | dispatch({ type: UPDATE, payload: data });
27 | } catch (error) {
28 | console.error('Error in updatePost action creator:', error);
29 | }
30 | };
31 |
32 | export const deletePost = (id) => async (dispatch) => {
33 | try {
34 | await api.deletePost(id);
35 | dispatch({ type: DELETE, payload: id });
36 | } catch (error) {
37 | console.error('Error in deletePost action creator:', error);
38 | }
39 | };
40 |
41 | export const likePost = (id) => async (dispatch) => {
42 | try {
43 | const { data } = await api.likePost(id);
44 | dispatch({ type: UPDATE, payload: data });
45 | } catch (error) {
46 | console.error('Error in likePost action creator:', error);
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/Auth/Auth.js:
--------------------------------------------------------------------------------
1 | import jwt_decode from 'jwt-decode';
2 | import LockOutlined from '@material-ui/icons/LockOutlined';
3 | import { useHistory } from 'react-router-dom';
4 | import { Container, Avatar, Typography, Paper, Grid } from '@material-ui/core';
5 | import { GoogleLogin } from '@react-oauth/google';
6 | import { useDispatch } from 'react-redux';
7 |
8 | import useStyles from './AuthStyles';
9 | import { AUTH } from '../../redux/constants/actionConstants';
10 |
11 | const Auth = () => {
12 | const classes = useStyles();
13 | const dispatch = useDispatch();
14 | const history = useHistory();
15 |
16 | // successful login
17 | const googleSuccess = (res) => {
18 | const token = res?.credential;
19 | const result = jwt_decode(token);
20 | try {
21 | dispatch({ type: AUTH, data: { result, token } });
22 | history.push('/');
23 | } catch (error) {
24 | console.log(error);
25 | }
26 | };
27 |
28 | // login failure
29 | const googleFailure = () => {
30 | console.log(`Google sign in was unsuccessful`);
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | Sign in
40 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default Auth;
55 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { BrowserRouter, Switch, Route } from 'react-router-dom';
3 | import { GoogleOAuthProvider } from '@react-oauth/google';
4 | import { Paper, ThemeProvider, createTheme } from '@material-ui/core';
5 | import blue from '@material-ui/core/colors/blue';
6 |
7 | import Navbar from './components/Navbar/Navbar';
8 | import Home from './components/Home/Home';
9 | import Auth from './components/Auth/Auth';
10 | import Footer from './components/Footer/Footer';
11 |
12 | // Greetings
13 | const greetVisitor = () => {
14 | console.log(
15 | '%c Thanks for visiting!',
16 | 'font-weight: bold; font-size: 20px; background-color: #2196f3; border-radius: 5px; padding: 10px; text-shadow: 2px 2px rgba(0,0,0,0.5)'
17 | );
18 | };
19 |
20 | // Theme settings
21 | const useTheme = () => {
22 | const [themeMode, setThemeMode] = useState('dark');
23 | const theme = createTheme({ palette: { primary: blue, type: themeMode } });
24 | const toggleLightMode = () => setThemeMode('light');
25 | const toggleDarkMode = () => setThemeMode('dark');
26 | return { themeMode, theme, toggleLightMode, toggleDarkMode };
27 | };
28 |
29 | const App = () => {
30 | greetVisitor();
31 | const { themeMode, theme, toggleLightMode, toggleDarkMode } = useTheme();
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/src/components/Navbar/Drawer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import clsx from 'clsx';
4 | import Drawer from '@material-ui/core/Drawer';
5 | import List from '@material-ui/core/List';
6 | import ListItem from '@material-ui/core/ListItem';
7 | import ListItemText from '@material-ui/core/ListItemText';
8 | import MenuIcon from '@material-ui/icons/Menu';
9 | import IconButton from '@material-ui/core/IconButton';
10 |
11 | const useStyles = makeStyles((theme) => ({
12 | list: { width: 250 },
13 | fullList: { width: 'auto' },
14 | menu: { color: theme.palette.common.white },
15 | }));
16 |
17 | const TemporaryDrawer = ({ drawerElements }) => {
18 | const classes = useStyles();
19 | const [state, setState] = useState({ right: false });
20 |
21 | const toggleDrawer = (anchor, open) => (event) => {
22 | if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
23 | return;
24 | }
25 |
26 | setState({ ...state, [anchor]: open });
27 | };
28 |
29 | const renderListItems = () => (
30 |
36 |
37 | {drawerElements.map((text, index) => (
38 |
39 |
40 |
41 | ))}
42 |
43 |
44 | );
45 |
46 | return (
47 |
48 | {['right'].map((anchor) => (
49 |
50 |
51 |
52 |
53 |
54 | {renderListItems()}
55 |
56 |
57 | ))}
58 |
59 | );
60 | };
61 |
62 | export default TemporaryDrawer;
63 |
--------------------------------------------------------------------------------
/src/components/Feed/IndividualPost.js:
--------------------------------------------------------------------------------
1 | import { Card, Typography, Button, CardContent, CardActions, Tooltip } from '@material-ui/core';
2 | import FavoriteIcon from '@material-ui/icons/Favorite';
3 | import DeleteIcon from '@material-ui/icons/Delete';
4 | import EditIcon from '@material-ui/icons/Edit';
5 | import moment from 'moment';
6 |
7 | import useStyles from './IndividualPostStyles';
8 | import { useDispatch } from 'react-redux';
9 | import { deletePost, likePost } from '../../redux/actions/posts';
10 | import { INITIATE_POST_EDIT, SET_CURRENT_USER_ID } from '../../redux/constants/actionConstants';
11 |
12 | const Post = ({ post }) => {
13 | const classes = useStyles();
14 | const dispatch = useDispatch();
15 | const userFromLocalStoratge = JSON.parse(localStorage.getItem('profile'));
16 | const isCurrentUserCreator = userFromLocalStoratge?.result?.sub === post.creator;
17 |
18 | const extractDateFromObj = () => {
19 | return new Date(parseInt(post._id.substring(0, 8), 16) * 1000);
20 | };
21 | const handleEdit = () => {
22 | dispatch({ type: SET_CURRENT_USER_ID, payload: post._id });
23 | dispatch({ type: INITIATE_POST_EDIT, payload: true });
24 | };
25 | const handleLike = () => {
26 | dispatch(likePost(post._id));
27 | };
28 | const handleDelete = () => {
29 | dispatch(deletePost(post._id));
30 | dispatch({ type: SET_CURRENT_USER_ID, payload: null });
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 |
38 | {post.name}
39 |
40 | {moment(extractDateFromObj()).fromNow()}
41 |
42 |
43 |
44 | {isCurrentUserCreator && (
45 |
50 | )}
51 |
52 |
53 |
54 | {post.title}
55 |
56 | {post.content}
57 |
58 |
59 |
60 |
65 |
66 | {post.likes.length}
67 |
68 | {isCurrentUserCreator && (
69 |
74 | )}
75 |
76 |
77 | );
78 | };
79 |
80 | export default Post;
81 |
--------------------------------------------------------------------------------
/src/components/MdEditor/ModalContent.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { TextField, Button, Typography, Tooltip, IconButton, Paper } from '@material-ui/core';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import ClearAllIcon from '@material-ui/icons/ClearAll';
5 |
6 | import useStyles from './ModalContentStyles';
7 | import { createPost, updatePost } from '../../redux/actions/posts';
8 | import { POST_CLICKED, SET_CURRENT_USER_ID } from '../../redux/constants/actionConstants';
9 | import { useCalcRows } from '../../Hooks/WindowSize';
10 |
11 | const Form = () => {
12 | const { minRows, maxRows } = useCalcRows();
13 | const classes = useStyles();
14 | const dispatch = useDispatch();
15 | const userFromLocalStorage = JSON.parse(localStorage.getItem('profile'));
16 | const currentUserId = useSelector((state) => state.user.currentUserId);
17 | const [formData, setFormData] = useState({ title: '', content: '' });
18 |
19 | const selectedPost = useSelector((state) =>
20 | currentUserId ? state.posts.find((post) => post._id === currentUserId) : null
21 | );
22 |
23 | const handleSubmit = (e) => {
24 | e.preventDefault();
25 | const updatedData = { ...formData, name: userFromLocalStorage?.result?.name };
26 | currentUserId
27 | ? dispatch(updatePost(currentUserId, updatedData))
28 | : dispatch(createPost(updatedData));
29 | clearForm();
30 | dispatch({ type: POST_CLICKED, payload: true });
31 | };
32 |
33 | const clearForm = () => {
34 | setFormData({ title: '', content: '' });
35 | dispatch({ type: SET_CURRENT_USER_ID, payload: null });
36 | };
37 |
38 | useEffect(() => {
39 | if (selectedPost) setFormData(selectedPost);
40 | }, [selectedPost]);
41 |
42 | if (!userFromLocalStorage?.result?.name) {
43 | return (
44 |
45 | Please sign in
46 |
47 | );
48 | }
49 |
50 | return (
51 |
92 | );
93 | };
94 |
95 | export default Form;
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/src/components/Navbar/Navbar.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 | import { Link, useHistory, useLocation } from 'react-router-dom';
3 | import {
4 | AppBar,
5 | Toolbar,
6 | Typography,
7 | Container,
8 | Tooltip,
9 | IconButton,
10 | Button,
11 | Modal,
12 | Grid,
13 | } from '@material-ui/core';
14 |
15 | import Brightness4Icon from '@material-ui/icons/Brightness4';
16 | import Brightness7Icon from '@material-ui/icons/Brightness7';
17 | import GitHubIcon from '@material-ui/icons/GitHub';
18 | import ExitToAppIcon from '@material-ui/icons/ExitToApp';
19 | import AddBoxRoundedIcon from '@material-ui/icons/AddBoxRounded';
20 | import useStyles from './NavStyles';
21 | import MUIDrawer from './Drawer';
22 | import ModalContainer from '../MdEditor/ModalContainer';
23 |
24 | import decode from 'jwt-decode';
25 | import { useDispatch, useSelector } from 'react-redux';
26 | import {
27 | LOGOUT,
28 | INITIATE_POST_EDIT,
29 | POST_CLICKED,
30 | FAB_CLICKED,
31 | } from '../../redux/constants/actionConstants';
32 | import { useWindowDimensions } from '../../Hooks/WindowSize';
33 |
34 | const Navbar = ({ themeMode, lightMode, darkMode }) => {
35 | const classes = useStyles();
36 | const dispatch = useDispatch();
37 | const history = useHistory();
38 | const location = useLocation();
39 | const [isModalOpen, setIsModalOpen] = useState(false);
40 | const [hasRendered, setHasRendered] = useState(false);
41 | const { width: windowWidth } = useWindowDimensions();
42 | const { hasSelectedEditMode, hasClickedPostBtn, hasClickedFab } = useSelector(
43 | (state) => state.user
44 | );
45 | const [userFromLocalStorage, setUserFromLocalStorage] = useState(
46 | JSON.parse(localStorage.getItem('profile'))
47 | );
48 |
49 | // modal operations
50 | const handleOpen = useCallback(() => {
51 | setIsModalOpen(true);
52 | dispatch({ type: POST_CLICKED, payload: false });
53 | dispatch({ type: FAB_CLICKED, payload: false });
54 | }, [dispatch, setIsModalOpen]);
55 |
56 | const handleClose = useCallback(() => {
57 | setIsModalOpen(false);
58 | dispatch({ type: INITIATE_POST_EDIT, payload: false });
59 | }, [dispatch, setIsModalOpen]);
60 |
61 | // logout
62 | const logout = useCallback(() => {
63 | dispatch({ type: LOGOUT });
64 | history.push('/auth');
65 | setUserFromLocalStorage(null);
66 | }, [dispatch, history, setUserFromLocalStorage]);
67 |
68 | // render as soon as location changes
69 | useEffect(() => {
70 | const token = userFromLocalStorage?.token;
71 | if (token) {
72 | const decodedToken = decode(token);
73 | if (decodedToken.exp * 1000 < new Date().getTime()) {
74 | logout();
75 | }
76 | }
77 | setUserFromLocalStorage(JSON.parse(localStorage.getItem('profile')));
78 | }, [location, logout, userFromLocalStorage?.token]);
79 |
80 | // handle modal open/close as edit button is clicked by user
81 | useEffect(() => {
82 | if (hasRendered) hasSelectedEditMode ? handleOpen() : handleClose();
83 | else setHasRendered(true);
84 | }, [hasSelectedEditMode, handleOpen, handleClose, hasRendered]);
85 |
86 | // handle modal open/close as post/update btn is clicked by user
87 | useEffect(() => {
88 | if (hasClickedPostBtn) handleClose();
89 | else handleOpen();
90 | }, [hasClickedPostBtn, handleOpen, handleClose]);
91 |
92 | // handle modal open/close as floating action btn is clicked by user
93 | useEffect(() => {
94 | if (hasClickedFab) handleOpen();
95 | else handleOpen();
96 | }, [hasClickedFab, handleOpen, handleClose]);
97 |
98 | const renderSmallScreen = () => (
99 |
106 | Toggle Theme
107 | {themeMode === 'light' ? : }
108 | ,
109 | ,
116 | ,
120 | ]}
121 | />
122 | );
123 |
124 | const renderLargeScreen = () => (
125 | <>
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | {themeMode === 'light' ? : }
134 |
135 |
136 | window.open('https://github.com/nikhilsourav/weebook-client', '_blank')}
139 | >
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | >
150 | );
151 |
152 | const renderModal = () => (
153 |
160 |
161 |
162 |
163 |
164 | );
165 |
166 | return (
167 | <>
168 |
169 |
170 |
171 |
172 | weebook
173 |
174 | {userFromLocalStorage === null ? (
175 |
178 | ) : windowWidth < 500 ? (
179 | renderSmallScreen()
180 | ) : (
181 | renderLargeScreen()
182 | )}
183 |
184 |
185 |
186 |
187 | {renderModal()}
188 | >
189 | );
190 | };
191 |
192 | export default Navbar;
193 |
--------------------------------------------------------------------------------