├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json ├── index.html └── primitiveui.css ├── prettier.config.js ├── src ├── setupTests.js ├── features │ ├── posts │ │ ├── PostAuthor.js │ │ ├── TimeAgo.js │ │ ├── ReactionButton.js │ │ ├── SinglePost.js │ │ ├── UpdatePostForm.js │ │ ├── PostsList.js │ │ ├── AddPostForm.js │ │ └── postsSlice.js │ ├── users │ │ ├── UsersList.js │ │ ├── usersSlice.js │ │ └── UserPage.js │ └── notifications │ │ ├── notificationsSlice.js │ │ └── NotificationsList.js ├── app │ ├── store.js │ └── Navbar.js ├── index.js ├── api │ ├── client.js │ └── server.js ├── App.js └── index.css ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Huniko519/SocialApp-React-Redux/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Huniko519/SocialApp-React-Redux/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Huniko519/SocialApp-React-Redux/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/features/posts/PostAuthor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | 4 | export const PostAuthor = ({ userId }) => { 5 | const author = useSelector(state => 6 | state.users.find(user => user.id === userId) 7 | ) 8 | 9 | return by {author ? author.name : 'Unknown author'} 10 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | 3 | import postsReducer from '../features/posts/postsSlice' 4 | import usersReducer from '../features/users/usersSlice' 5 | import notificationsReducer from '../features/notifications/notificationsSlice' 6 | export default configureStore({ 7 | reducer: { 8 | posts: postsReducer, 9 | users: usersReducer, 10 | notifications: notificationsReducer 11 | } 12 | }) -------------------------------------------------------------------------------- /src/features/posts/TimeAgo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { parseISO, formatDistanceToNow } from 'date-fns' 3 | 4 | export const TimeAgo = ({ timestamp }) => { 5 | let timeAgo = '' 6 | if (timestamp) { 7 | const date = parseISO(timestamp) 8 | const timePeriod = formatDistanceToNow(date) 9 | timeAgo = `${timePeriod} ago` 10 | } 11 | 12 | return ( 13 | 14 |   {timeAgo} 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import store from './app/store' 6 | import { fetchUsers } from './features/users/usersSlice' 7 | import { fetchPosts } from './features/posts/postsSlice' 8 | import { Provider } from 'react-redux' 9 | 10 | import './api/server' 11 | 12 | store.dispatch(fetchUsers()) 13 | store.dispatch(fetchPosts()) 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ) 23 | -------------------------------------------------------------------------------- /src/features/users/UsersList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | import { selectAllUsers } from './usersSlice' 5 | 6 | export const UsersList = () => { 7 | const users = useSelector(selectAllUsers) 8 | 9 | const renderedUsers = users.map(user => ( 10 |
  • 11 | {user.name} 12 |
  • 13 | )) 14 | return ( 15 |
    16 |

    Users

    17 | 20 |
    21 | ) 22 | } -------------------------------------------------------------------------------- /src/features/posts/ReactionButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch } from 'react-redux' 3 | 4 | import { reactionAdded } from './postsSlice' 5 | 6 | const reactionEmoji = { 7 | thumbsUp: '👍', 8 | hooray: '🎉', 9 | } 10 | 11 | export const ReactionButtons = ({ post }) => { 12 | const dispatch = useDispatch() 13 | 14 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => { 15 | return ( 16 | 26 | ) 27 | }) 28 | 29 | return
    {reactionButtons}
    30 | } -------------------------------------------------------------------------------- /src/features/posts/SinglePost.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | import { PostAuthor } from './PostAuthor' 5 | import { TimeAgo } from './TimeAgo' 6 | import { selectPostById } from './postsSlice' 7 | 8 | export const SinglePostPage = ({ match }) => { 9 | const { postID } = match.params 10 | const post = useSelector(state => selectPostById(state, postID)) 11 | 12 | if (!post) { 13 | return ( 14 |
    15 | Not found! 16 |
    17 | ) 18 | } 19 | return ( 20 |
    21 |
    22 |

    {post.title}

    23 |

    {post.content}

    24 | 25 | 26 | Edit 27 |
    28 |
    29 | ) 30 | } -------------------------------------------------------------------------------- /src/features/users/usersSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit' 2 | import { client } from '../../api/client' 3 | import { selectAllPosts } from '../posts/postsSlice' 4 | 5 | const initialState = [] 6 | 7 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => { 8 | const response = await client.get('/fakeApi/users') 9 | return response.users 10 | }) 11 | 12 | const usersSlice = createSlice({ 13 | name: 'users', 14 | initialState, 15 | reducers: {}, 16 | extraReducers: { 17 | [fetchUsers.fulfilled]: (state, action) => { 18 | return action.payload 19 | } 20 | } 21 | }) 22 | export const selectAllUsers = state => state.users; 23 | export const selectUserById = (state, userId) => 24 | state.users.find( user => user.id === userId) 25 | export const selectPostByUser = createSelector( 26 | [selectAllPosts, (state, userId) => userId], 27 | (posts, userId) => posts.filter(post => post.user === userId) 28 | ) 29 | 30 | export default usersSlice.reducer -------------------------------------------------------------------------------- /src/api/client.js: -------------------------------------------------------------------------------- 1 | // A tiny wrapper around fetch(), borrowed from 2 | // https://kentcdodds.com/blog/replace-axios-with-a-simple-custom-fetch-wrapper 3 | 4 | export async function client(endpoint, { body, ...customConfig } = {}) { 5 | const headers = { 'Content-Type': 'application/json' } 6 | 7 | const config = { 8 | method: body ? 'POST' : 'GET', 9 | ...customConfig, 10 | headers: { 11 | ...headers, 12 | ...customConfig.headers, 13 | }, 14 | } 15 | 16 | if (body) { 17 | config.body = JSON.stringify(body) 18 | } 19 | 20 | let data 21 | try { 22 | const response = await window.fetch(endpoint, config) 23 | data = await response.json() 24 | if (response.ok) { 25 | return data 26 | } 27 | throw new Error(response.statusText) 28 | } catch (err) { 29 | return Promise.reject(err.message ? err.message : data) 30 | } 31 | } 32 | 33 | client.get = function (endpoint, customConfig = {}) { 34 | return client(endpoint, { ...customConfig, method: 'GET' }) 35 | } 36 | 37 | client.post = function (endpoint, body, customConfig = {}) { 38 | return client(endpoint, { ...customConfig, body }) 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-Social-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.4.0", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "classnames": "^2.2.6", 11 | "date-fns": "^2.12.0", 12 | "faker": "^4.1.0", 13 | "miragejs": "^0.1.35", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-redux": "^7.2.0", 17 | "react-router-dom": "^5.1.2", 18 | "react-scripts": "3.4.1", 19 | "redux-devtools-extension": "^2.13.8", 20 | "seedrandom": "^3.0.5", 21 | "txtgen": "^2.2.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "prettier": "^2.0.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | 5 | import { fetchNotifications, allNotificationsRead, selectAllNotifications } from '../features/notifications/notificationsSlice' 6 | 7 | export const Navbar = () => { 8 | const dispatch = useDispatch() 9 | const notifications = useSelector(selectAllNotifications) 10 | const numUnread = notifications.filter( n => !n.read).length 11 | 12 | const fetchNewNotifications = () => { 13 | dispatch(fetchNotifications()) 14 | // dispatch(allNotificationsRead()) 15 | } 16 | 17 | let unreadNotificationsBadge 18 | 19 | if(numUnread > 0) { 20 | unreadNotificationsBadge = ( 21 | {numUnread} 22 | ) 23 | } 24 | 25 | return ( 26 | 42 | ) 43 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Redirect, 7 | } from 'react-router-dom' 8 | 9 | import { Navbar } from './app/Navbar' 10 | import { PostsList } from './features/posts/PostsList' 11 | import { AddPostForm } from './features/posts/AddPostForm' 12 | import { SinglePostPage } from './features/posts/SinglePost' 13 | import { UpdatePostForm } from './features/posts/UpdatePostForm' 14 | import { UsersList } from './features/users/UsersList' 15 | import { UserPage } from './features/users/UserPage' 16 | import { NotificationsList } from './features/notifications/NotificationsList' 17 | function App() { 18 | return ( 19 | 20 | 21 |
    22 | 23 | ( 27 | 28 | 29 | 30 | 31 | )} 32 | /> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
    41 |
    42 | ) 43 | } 44 | 45 | export default App 46 | -------------------------------------------------------------------------------- /src/features/notifications/notificationsSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import { selectAllPosts } from "../posts/postsSlice"; 3 | import { client } from "../../api/client"; 4 | 5 | export const fetchNotifications = createAsyncThunk( 6 | 'notifications/fetchNotifications', 7 | async (_, { getState }) => { 8 | const allNotifications = selectAllNotifications(getState()) 9 | const [latestNotifications] = allNotifications 10 | const latestTimestamp = latestNotifications ? latestNotifications.date : "" 11 | const response = await client.get( 12 | `/fakeApi/notifications?since=${latestTimestamp}` 13 | ) 14 | return response.notifications 15 | } 16 | ) 17 | 18 | const notificationsSlice = createSlice({ 19 | name: 'notifications', 20 | initialState: [], 21 | reducers: { 22 | allNotificationsRead(state, action) { 23 | state.forEach( notification => { 24 | notification.read = true 25 | }) 26 | } 27 | }, 28 | extraReducers: { 29 | [fetchNotifications.fulfilled]: (state, action) => { 30 | state.forEach( notification => { 31 | notification.isNew = !notification.read 32 | }) 33 | state.push(...action.payload) 34 | state.sort((a, b) => b.date.localeCompare(a.date)) 35 | } 36 | } 37 | }) 38 | 39 | export const { allNotificationsRead } = notificationsSlice.actions 40 | export default notificationsSlice.reducer 41 | 42 | export const selectAllNotifications = state => state.notifications -------------------------------------------------------------------------------- /src/features/users/UserPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { selectAllPosts } from '../posts/postsSlice' 4 | import { Link } from 'react-router-dom' 5 | import { selectUserById, selectPostByUser } from './usersSlice' 6 | 7 | export const UserPage = ({ match }) => { 8 | const { userId } = match.params 9 | const user = useSelector(state => selectUserById(state, userId)) 10 | 11 | // Following code cause the UserPage re-render when any action dispatchs even if the posts data has'nt changed 12 | // Because useSelector forces the component to re-render when we return a new refrence value. 13 | // Here, allPosts.filter(...) returns a new refrence array so the UserPage re-render needlessly. 14 | // So we create selectPostByUser function to use memorized selector so that UserPage not re-render. 15 | 16 | // const postForUser = useSelector(state => { 17 | // const allPosts = selectAllPosts(state) 18 | // return allPosts.filter( post => post.user === userId) 19 | // }) 20 | 21 | const postForUser = useSelector(state => selectPostByUser(state, userId)) 22 | 23 | if(!user) { 24 | return ( 25 |
    26 | Not found! 27 |
    28 | ) 29 | } 30 | const postTitles = postForUser.map( post => ( 31 |
  • 32 | { post.title } 33 |
  • 34 | )) 35 | 36 | return ( 37 |
    38 |

    { user.name }

    39 | 42 |
    43 | ) 44 | } -------------------------------------------------------------------------------- /src/features/notifications/NotificationsList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { formatDistanceToNow, parseISO } from 'date-fns' 4 | import classnames from 'classnames' 5 | 6 | import { selectAllUsers } from '../users/usersSlice' 7 | import { selectAllNotifications, allNotificationsRead } from './notificationsSlice' 8 | 9 | export const NotificationsList = () => { 10 | const dispatch = useDispatch(); 11 | const notifications = useSelector(selectAllNotifications) 12 | const users = useSelector(selectAllUsers) 13 | useEffect(() => { 14 | dispatch(allNotificationsRead()) 15 | }) 16 | const renderedNotifications = notifications.map(notification => { 17 | const date = parseISO(notification.date) 18 | const timeAgo = formatDistanceToNow(date) 19 | const user = users.find(user => user.id === notification.user) || { 20 | name: 'Unknown User' 21 | } 22 | 23 | const notificationClassname = classnames('notification', { 24 | new: notification.isNew 25 | }) 26 | return ( 27 |
    28 |
    29 | {user.name} {notification.message} 30 |
    31 |
    32 | {timeAgo} ago 33 |
    34 |
    35 | ) 36 | }) 37 | 38 | return ( 39 |
    40 |

    Notifications

    41 | {renderedNotifications} 42 |
    43 | ) 44 | } -------------------------------------------------------------------------------- /src/features/posts/UpdatePostForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { postUpdated, selectPostById } from './postsSlice' 4 | import { useHistory } from 'react-router-dom' 5 | 6 | export const UpdatePostForm = ({match}) => { 7 | const { postID } = match.params 8 | const dispatch = useDispatch() 9 | const history = useHistory() 10 | 11 | var post = useSelector(state => selectPostById(state, postID)) 12 | if(!post) post = { title: null, content:null } 13 | const [title, setTitle] = useState(post.title) 14 | const [content, setContent] = useState(post.content) 15 | 16 | 17 | const titleChanged = e => setTitle(e.target.value) 18 | const contentChanged = e => setContent(e.target.value) 19 | const updateButtonClicked = () => { 20 | if(title && content) { 21 | dispatch( 22 | postUpdated({ 23 | id: postID, 24 | title, 25 | content 26 | }) 27 | ) 28 | } 29 | history.push(`/posts/${postID}`) 30 | } 31 | 32 | if(!post.title) { 33 | return ( 34 |
    35 | Not Found. 36 |
    37 | ) 38 | } 39 | 40 | return ( 41 |
    42 |
    43 |

    Edit Post

    44 | 45 | 46 | 47 |