├── src ├── helpers │ ├── Confetti │ │ ├── index.js │ │ └── Confetti.jsx │ ├── EasterEgg │ │ ├── index.js │ │ ├── image.png │ │ ├── EasterEgg.jsx │ │ └── EasterEgg.module.css │ ├── login.js │ ├── cut-string.js │ └── time.js ├── assets │ ├── cities.json │ ├── images.json │ └── users.json ├── redux │ ├── counter │ │ ├── counter.init-state.js │ │ ├── counter.selector.js │ │ ├── counter.action.js │ │ └── counter.reducer.js │ ├── auth │ │ ├── auth.selector.js │ │ ├── auth.init-state.js │ │ ├── auth.thunk.js │ │ └── auth.slice.js │ ├── posts │ │ ├── posts.init-state.js │ │ ├── posts.thunk.js │ │ └── posts.slice.js │ ├── profile │ │ ├── profile.init-state.js │ │ ├── profile.thunk.js │ │ └── profile.slice.js │ ├── users │ │ ├── users.init-state.js │ │ ├── users.selector.js │ │ └── users.slice.js │ ├── root.reducer.js │ ├── root.init-state.js │ ├── store.js │ └── rtk-posts │ │ └── rtk-posts.api.js ├── components │ ├── NotFound │ │ ├── 713761_big_8857bc5285.jpeg │ │ └── NotFound.jsx │ ├── Layout │ │ ├── Header │ │ │ └── Header.jsx │ │ ├── Sidebar │ │ │ ├── Sidebar.jsx │ │ │ └── Navigation │ │ │ │ └── Navigation.jsx │ │ └── Layout.jsx │ ├── Skeleton │ │ ├── Skeleton.jsx │ │ └── Skeleton.module.css │ ├── AuthRouts │ │ ├── PrivateRoute.jsx │ │ └── PublicRoute.jsx │ ├── Loader │ │ └── Loader.jsx │ ├── Button │ │ └── Button.jsx │ ├── Posts │ │ ├── SearchPosts.jsx │ │ ├── PostsLoader.jsx │ │ └── PostsItem.jsx │ ├── Modal │ │ └── Modal.jsx │ └── Confetti │ │ └── Confetti.jsx ├── constants │ └── status.constants.js ├── pages │ ├── NotFoundPage │ │ ├── pulp-fiction-john-travolta.gif │ │ └── NotFoundPage.jsx │ ├── ExercisesPage │ │ ├── CounterPageTwo │ │ │ ├── reducer.js │ │ │ └── CounterPageTwo.jsx │ │ ├── RerenderPage │ │ │ └── RerenderPage.jsx │ │ ├── LongRequestPage │ │ │ └── LongRequestPage.jsx │ │ ├── UsersPage │ │ │ ├── UsersItem.jsx │ │ │ └── UsersPage.jsx │ │ ├── TimerPage │ │ │ └── TimerPage.jsx │ │ ├── ExercisesPage.jsx │ │ └── CounterPage │ │ │ └── CounterPage.jsx │ ├── SinglePostPage │ │ ├── CommentsPage │ │ │ ├── CommentsPage.jsx │ │ │ ├── CommentForm │ │ │ │ └── CommentForm.jsx │ │ │ └── CommentList │ │ │ │ └── CommentList.jsx │ │ └── SinglePostPage.jsx │ ├── HomePage │ │ └── HomePage.jsx │ ├── LoginPage │ │ └── LoginPage.jsx │ ├── RTKPostsListPage │ │ └── RTKPostsListPage.jsx │ ├── PostsListPage │ │ └── PostsListPage.jsx │ ├── JoinPage │ │ └── JoinPage.jsx │ └── NewPostPage │ │ └── NewPostPage.jsx ├── http │ └── http.js ├── index.jsx ├── index.css └── App.jsx ├── public ├── img.jpg ├── user.png ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json ├── 404.html └── index.html ├── .prettierrc ├── .gitignore ├── .github └── workflows │ └── deploy.yml ├── package.json ├── .eslintrc └── README.md /src/helpers/Confetti/index.js: -------------------------------------------------------------------------------- 1 | export * from './Confetti'; 2 | -------------------------------------------------------------------------------- /src/assets/cities.json: -------------------------------------------------------------------------------- 1 | ["Київ", "Варшава", "Лондон", "Берлін"] 2 | -------------------------------------------------------------------------------- /src/helpers/EasterEgg/index.js: -------------------------------------------------------------------------------- 1 | export { EasterEgg } from './EasterEgg'; 2 | -------------------------------------------------------------------------------- /src/redux/counter/counter.init-state.js: -------------------------------------------------------------------------------- 1 | export const counterInitState = 0; 2 | -------------------------------------------------------------------------------- /public/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/public/img.jpg -------------------------------------------------------------------------------- /public/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/public/user.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/redux/counter/counter.selector.js: -------------------------------------------------------------------------------- 1 | export const selectCounter = state => state.counter; 2 | -------------------------------------------------------------------------------- /src/helpers/EasterEgg/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/src/helpers/EasterEgg/image.png -------------------------------------------------------------------------------- /src/helpers/login.js: -------------------------------------------------------------------------------- 1 | import { confetti } from 'components/Service/Confetti'; 2 | 3 | export const login = confetti.run; 4 | -------------------------------------------------------------------------------- /src/redux/counter/counter.action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | 3 | export const counterAction = createAction('COUNTER'); 4 | -------------------------------------------------------------------------------- /src/components/NotFound/713761_big_8857bc5285.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/src/components/NotFound/713761_big_8857bc5285.jpeg -------------------------------------------------------------------------------- /src/constants/status.constants.js: -------------------------------------------------------------------------------- 1 | export const STATUS = { 2 | idle: 'idle', 3 | loading: 'loading', 4 | success: 'success', 5 | error: 'error', 6 | }; 7 | -------------------------------------------------------------------------------- /src/redux/auth/auth.selector.js: -------------------------------------------------------------------------------- 1 | export const selectAuthStatus = (state) => state.auth.status; 2 | export const selectAuthToken = (state) => state.auth.data; 3 | -------------------------------------------------------------------------------- /src/pages/NotFoundPage/pulp-fiction-john-travolta.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMykhailychenko/goit-59-fs/HEAD/src/pages/NotFoundPage/pulp-fiction-john-travolta.gif -------------------------------------------------------------------------------- /src/helpers/cut-string.js: -------------------------------------------------------------------------------- 1 | export const cutString = (string, maxLength) => { 2 | return string.length > maxLength ? string.slice(0, maxLength) + '...' : string; 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "arrowParens": "avoid", 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /src/redux/auth/auth.init-state.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from '../../constants/status.constants'; 2 | 3 | export const authInitState = { 4 | status: STATUS.idle, 5 | data: null, 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | export const Header = ({ title }) => { 2 | return ( 3 |
4 |

{title}

5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/redux/posts/posts.init-state.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from '../../constants/status.constants'; 2 | 3 | export const postsInitState = { 4 | posts: null, 5 | status: STATUS.idle, 6 | }; 7 | -------------------------------------------------------------------------------- /src/redux/profile/profile.init-state.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from '../../constants/status.constants'; 2 | 3 | export const profileInitState = { 4 | status: STATUS.idle, 5 | data: null, 6 | }; 7 | -------------------------------------------------------------------------------- /src/redux/users/users.init-state.js: -------------------------------------------------------------------------------- 1 | import usersJson from '../../assets/users.json'; 2 | 3 | export const userInitState = { 4 | search: '', 5 | data: usersJson, 6 | isModalOpen: false, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Skeleton/Skeleton.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | import styles from './Skeleton.module.css'; 4 | 5 | export const Skeleton = ({ className, ...props }) => { 6 | return
; 7 | }; 8 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/CounterPageTwo/reducer.js: -------------------------------------------------------------------------------- 1 | export const counterReducer = (state, { type }) => { 2 | switch (type) { 3 | case 'MINUS': 4 | return state - 1; 5 | 6 | case 'PLUS': 7 | return state + 1; 8 | 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/redux/posts/posts.thunk.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | import { publicApi } from '../../http/http'; 4 | 5 | export const getPostsThunk = createAsyncThunk('posts', async params => { 6 | const { data } = await publicApi.get('/posts', { 7 | params, 8 | }); 9 | 10 | return data; 11 | }); 12 | -------------------------------------------------------------------------------- /src/redux/auth/auth.thunk.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | import { publicApi, token } from '../../http/http'; 4 | 5 | export const authLoginThunk = createAsyncThunk('login', async (values) => { 6 | const { data } = await publicApi.post('/users/login', values); 7 | token.set(data); 8 | return data; 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/AuthRouts/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate, Outlet } from 'react-router-dom'; 3 | 4 | import { selectAuthToken } from '../../redux/auth/auth.selector'; 5 | 6 | export const PrivateRoute = () => { 7 | const token = useSelector(selectAuthToken); 8 | 9 | return token ? : ; 10 | }; 11 | -------------------------------------------------------------------------------- /src/redux/counter/counter.reducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | 3 | import { counterAction } from './counter.action'; 4 | import { counterInitState } from './counter.init-state'; 5 | 6 | export const counterReducer = createReducer(counterInitState, builder => { 7 | builder.addCase(counterAction, (state, { payload }) => { 8 | return state + payload; 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import image from './713761_big_8857bc5285.jpeg'; 2 | 3 | export const NotFound = () => { 4 | return ( 5 |
6 | not found 7 |

Произошол отрицательний поиск. Потерь нет!

8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Skeleton/Skeleton.module.css: -------------------------------------------------------------------------------- 1 | @keyframes skeleton { 2 | 0% { 3 | background-position: 0 50%; 4 | } 5 | 50% { 6 | background-position: 100% 50%; 7 | } 8 | } 9 | 10 | .skeleton { 11 | width: 100%; 12 | min-height: 8px; 13 | border-radius: 5px; 14 | background: linear-gradient(90deg, #eee, #a9a7a7, #eee); 15 | background-size: 400% 400%; 16 | animation: skeleton 2s ease infinite; 17 | } 18 | -------------------------------------------------------------------------------- /.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 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | .vscode 27 | .idea -------------------------------------------------------------------------------- /src/components/AuthRouts/PublicRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate, Outlet, useLocation } from 'react-router-dom'; 3 | 4 | import { selectAuthToken } from '../../redux/auth/auth.selector'; 5 | 6 | export const PublicRoute = () => { 7 | const token = useSelector(selectAuthToken); 8 | const location = useLocation(); 9 | 10 | return token ? : ; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Layout/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import { Navigation } from './Navigation/Navigation'; 2 | 3 | export const Sidebar = () => { 4 | return ( 5 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | export const Loader = () => { 2 | return ( 3 | <> 4 |
5 |
9 |
10 | Loading... 11 |
12 |
13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/pages/SinglePostPage/CommentsPage/CommentsPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { CommentForm } from './CommentForm/CommentForm'; 4 | import { CommentList } from './CommentList/CommentList'; 5 | 6 | const CommentsPage = () => { 7 | const [comments, setComments] = useState(null); 8 | 9 | return ( 10 | <> 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default CommentsPage; 18 | -------------------------------------------------------------------------------- /src/helpers/time.js: -------------------------------------------------------------------------------- 1 | const concat = (...args) => { 2 | return args.join(':'); 3 | }; 4 | 5 | export const formatTime = seconds => { 6 | const milliseconds = seconds % 1000; 7 | 8 | const secondsOfset = seconds / 1000; 9 | const secondsPassed = Math.floor(secondsOfset) % 60; 10 | const minutesPassed = Math.floor(secondsOfset / 60) % 60; 11 | 12 | return concat( 13 | String(minutesPassed).padStart(2, '0'), 14 | String(secondsPassed).padStart(2, '0'), 15 | String(milliseconds).padStart(3, '0'), 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/redux/root.reducer.js: -------------------------------------------------------------------------------- 1 | import { authInitState } from './auth/auth.init-state'; 2 | import { counterInitState } from './counter/counter.init-state'; 3 | import { postsInitState } from './posts/posts.init-state'; 4 | import { profileInitState } from './profile/profile.init-state'; 5 | import { userInitState } from './users/users.init-state'; 6 | 7 | export const initState = { 8 | counter: counterInitState, 9 | users: userInitState, 10 | posts: postsInitState, 11 | auth: authInitState, 12 | profile: profileInitState, 13 | }; 14 | -------------------------------------------------------------------------------- /src/http/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const publicApi = axios.create({ 4 | baseURL: 'https://taupe-croissant-c4162a.netlify.app/api', 5 | }); 6 | 7 | export const privateApi = axios.create({ 8 | baseURL: 'https://taupe-croissant-c4162a.netlify.app/api', 9 | }); 10 | 11 | export const token = { 12 | set: (data) => { 13 | privateApi.defaults.headers.Authorization = `${data.token_type} ${data.access_token}`; 14 | }, 15 | 16 | remove: () => { 17 | privateApi.defaults.headers.Authorization = null; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/helpers/EasterEgg/EasterEgg.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import classNames from 'classnames'; 4 | 5 | import css from './EasterEgg.module.css'; 6 | import image from './image.png'; 7 | 8 | export const EasterEgg = () => { 9 | const [isOpen, setIsOpen] = useState(true); 10 | 11 | const handleOpen = () => setIsOpen(false); 12 | 13 | return ( 14 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/redux/profile/profile.thunk.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | import { privateApi, token } from '../../http/http'; 4 | import { selectAuthToken } from '../auth/auth.selector'; 5 | 6 | export const getProfileThunk = createAsyncThunk('profile', async (_, { getState, rejectWithValue }) => { 7 | const stateToken = selectAuthToken(getState()); 8 | 9 | if (!stateToken) { 10 | return rejectWithValue(); 11 | } 12 | 13 | token.set(stateToken); 14 | const { data } = await privateApi.get('/users/profile'); 15 | return data; 16 | }); 17 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v2.3.1 13 | 14 | - name: Install, lint, build 🔧 15 | run: | 16 | npm install 17 | npm run lint:js 18 | npm run build 19 | - name: Deploy 🚀 20 | uses: JamesIves/github-pages-deploy-action@4.1.0 21 | with: 22 | branch: gh-pages 23 | folder: build -------------------------------------------------------------------------------- /src/pages/NotFoundPage/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | import image from './pulp-fiction-john-travolta.gif'; 4 | 5 | const NotFoundPage = () => { 6 | return ( 7 |
8 | not found 9 |

Opsss! This page doesn't exist

10 | 11 | 12 | Open home page 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default NotFoundPage; 19 | -------------------------------------------------------------------------------- /src/redux/root.init-state.js: -------------------------------------------------------------------------------- 1 | import { authReducer } from './auth/auth.slice'; 2 | import { counterReducer } from './counter/counter.reducer'; 3 | import { postsReducer } from './posts/posts.slice'; 4 | import { profileReducer } from './profile/profile.slice'; 5 | import { postsApi } from './rtk-posts/rtk-posts.api'; 6 | import { usersReducer } from './users/users.slice'; 7 | 8 | export const rootReducer = { 9 | counter: counterReducer, 10 | users: usersReducer, 11 | posts: postsReducer, 12 | auth: authReducer, 13 | profile: profileReducer, 14 | 15 | [postsApi.reducerPath]: postsApi.reducer, 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/helpers/EasterEgg/EasterEgg.module.css: -------------------------------------------------------------------------------- 1 | .wrp { 2 | position: fixed; 3 | right: 0; 4 | bottom: 0; 5 | height: 20vh; 6 | width: 10vw; 7 | overflow: hidden; 8 | } 9 | 10 | @keyframes slide { 11 | 0% { 12 | transform: translateX(100%); 13 | } 14 | 100% { 15 | transform: translateX(30%) rotate(-10deg); 16 | } 17 | } 18 | 19 | .img { 20 | width: 100%; 21 | height: auto; 22 | transform: translateX(30%) rotate(-10deg); 23 | } 24 | 25 | .open .img { 26 | animation: slide 8s ease-in-out; 27 | } 28 | 29 | .close .img { 30 | transform: translateX(100%); 31 | transition: 1s ease-in-out; 32 | } 33 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactDOM from 'react-dom/client'; 4 | import { Provider } from 'react-redux'; 5 | import { PersistGate } from 'redux-persist/integration/react'; 6 | 7 | import { App } from './App'; 8 | import { store, persistor } from './redux/store'; 9 | 10 | import 'react-toastify/dist/ReactToastify.css'; 11 | import './index.css'; 12 | 13 | const root = ReactDOM.createRoot(document.getElementById('root')); 14 | root.render( 15 | 16 | 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | export const Button = ({ type = 'button', className = 'btn-primary', isLoading, children, disabled, ...props }) => { 4 | return ( 5 | // eslint-disable-next-line react/button-has-type 6 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/assets/images.json: -------------------------------------------------------------------------------- 1 | [ 2 | "https://images.unsplash.com/photo-1659368076528-399473cf1b48?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=4140&q=80", 3 | "https://images.unsplash.com/photo-1659574087501-92ef4aa7b2d8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2574&q=80", 4 | "https://images.unsplash.com/photo-1660579232151-f12f71c76cd2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2940&q=80", 5 | "https://images.unsplash.com/photo-1656904889109-e59d094ab030?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2942&q=80" 6 | ] 7 | -------------------------------------------------------------------------------- /src/redux/users/users.selector.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | 3 | export const selectIsModalOpen = state => state.users.isModalOpen; 4 | export const selectUsers = state => state.users.data; 5 | export const selectUsersSearch = state => state.users.search; 6 | 7 | export const selectFilteredUsers = createSelector( 8 | [selectUsersSearch, selectUsers], 9 | 10 | (search, users) => 11 | users.filter(user => 12 | user.name.toLowerCase().includes(search.toLowerCase()), 13 | ), 14 | ); 15 | 16 | export const selectTotalOpenToWork = createSelector( 17 | [selectUsers], 18 | users => users.filter(user => user.isOpenToWork).length, 19 | ); 20 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.jsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from 'react-toastify'; 2 | 3 | import { ConfettiContainer } from '../../helpers/Confetti'; 4 | 5 | import { Sidebar } from './Sidebar/Sidebar'; 6 | 7 | export const Layout = ({ children }) => { 8 | return ( 9 | <> 10 |
11 | 12 | 13 |
17 |
{children}
18 |
19 |
20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { 3 | persistStore, 4 | FLUSH, 5 | REHYDRATE, 6 | PAUSE, 7 | PERSIST, 8 | PURGE, 9 | REGISTER, 10 | } from 'redux-persist'; 11 | 12 | import { rootReducer } from './root.init-state'; 13 | import { initState } from './root.reducer'; 14 | import { postsApi } from './rtk-posts/rtk-posts.api'; 15 | 16 | export const store = configureStore({ 17 | preloadedState: initState, 18 | devTools: true, 19 | reducer: rootReducer, 20 | 21 | middleware: getDefaultMiddleware => 22 | getDefaultMiddleware({ 23 | serializableCheck: { 24 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 25 | }, 26 | }).concat([postsApi.middleware]), 27 | }); 28 | 29 | export const persistor = persistStore(store); 30 | -------------------------------------------------------------------------------- /src/redux/profile/profile.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | import { STATUS } from '../../constants/status.constants'; 4 | 5 | import { profileInitState } from './profile.init-state'; 6 | import { getProfileThunk } from './profile.thunk'; 7 | 8 | const profileSlice = createSlice({ 9 | name: 'profile', 10 | initialState: profileInitState, 11 | extraReducers: builder => { 12 | builder.addCase(getProfileThunk.pending, (state) => { 13 | state.status = STATUS.loading; 14 | }).addCase(getProfileThunk.fulfilled, (state, { payload }) => { 15 | state.status = STATUS.success; 16 | state.data = payload; 17 | }).addCase(getProfileThunk.rejected, (state) => { 18 | state.status = STATUS.error; 19 | }); 20 | }, 21 | }); 22 | 23 | export const profileReducer = profileSlice.reducer; 24 | -------------------------------------------------------------------------------- /src/redux/posts/posts.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | import { STATUS } from '../../constants/status.constants'; 4 | 5 | import { postsInitState } from './posts.init-state'; 6 | import { getPostsThunk } from './posts.thunk'; 7 | 8 | const postsSlice = createSlice({ 9 | name: 'posts', 10 | initialState: postsInitState, 11 | extraReducers: builder => { 12 | builder 13 | .addCase(getPostsThunk.pending, state => { 14 | state.status = STATUS.loading; 15 | }) 16 | .addCase(getPostsThunk.fulfilled, (state, { payload }) => { 17 | state.status = STATUS.success; 18 | state.posts = payload; 19 | }) 20 | .addCase(getPostsThunk.rejected, state => { 21 | state.status = STATUS.error; 22 | }); 23 | }, 24 | }); 25 | 26 | export const postsReducer = postsSlice.reducer; 27 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/RerenderPage/RerenderPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, memo } from 'react'; 2 | 3 | const Button = memo(({ label, onClick }) => { 4 | console.log('Button'); 5 | return ( 6 | 9 | ); 10 | }); 11 | 12 | Button.displayName = 'Button'; 13 | 14 | const RerenderPage = () => { 15 | const [counter, setCounter] = useState(0); 16 | 17 | const handleCount = useCallback(() => { 18 | setCounter(prev => prev + 1); 19 | }, []); 20 | 21 | console.log('Rerender'); 22 | 23 | return ( 24 |
25 |

{counter}

26 | 27 |
29 | ); 30 | }; 31 | 32 | export default RerenderPage; 33 | -------------------------------------------------------------------------------- /src/components/Posts/SearchPosts.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | export class SearchPosts extends Component { 4 | state = { 5 | value: '', 6 | }; 7 | 8 | handleChange = event => { 9 | const { value } = event.target; 10 | this.setState({ value }); 11 | }; 12 | 13 | handleSubmit = event => { 14 | event.preventDefault(); 15 | 16 | this.props.onSearch(this.state.value); 17 | }; 18 | 19 | render() { 20 | const { value } = this.state; 21 | 22 | return ( 23 |
28 | 35 | 36 | 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/redux/users/users.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { persistReducer } from 'redux-persist'; 3 | import storage from 'redux-persist/lib/storage'; 4 | 5 | import { userInitState } from './users.init-state'; 6 | 7 | const userSlice = createSlice({ 8 | name: 'users', 9 | initialState: userInitState, 10 | reducers: { 11 | usersSearchAction: (state, { payload }) => { 12 | state.search = payload; 13 | }, 14 | 15 | deleteUserAction: (state, { payload }) => { 16 | state.data = state.data.filter(user => user.id !== payload); 17 | }, 18 | 19 | toggleModalAction: state => { 20 | state.isModalOpen = !state.isModalOpen; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { usersSearchAction, deleteUserAction, toggleModalAction } = 26 | userSlice.actions; 27 | 28 | const persistConfig = { 29 | key: 'goit', 30 | storage, 31 | whitelist: ['data'], 32 | }; 33 | 34 | export const usersReducer = persistReducer(persistConfig, userSlice.reducer); 35 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/LongRequestPage/LongRequestPage.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class LongRequestPage extends Component { 4 | state = { 5 | isError: false, 6 | isLoading: false, 7 | isDone: false, 8 | }; 9 | 10 | async componentDidMount() { 11 | this.setState({ isLoading: true, isError: false }); 12 | 13 | // TODO fetch http://70.34.201.18:4444/long 14 | // if ok -> this.setState({ isDone: true, isLoading: false }); 15 | // if error -> this.setState({ isError: true, isLoading: false }); 16 | } 17 | 18 | componentWillUnmount() { 19 | // TODO 20 | } 21 | 22 | render() { 23 | const { isError, isLoading, isDone } = this.state; 24 | 25 | return ( 26 |
27 |

28 | {isDone && 'Success - request ended'} 29 | {isError && 'Error - no data'} 30 | {isLoading && 'Loading ...'} 31 |

32 | 33 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | export default LongRequestPage; 42 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import-normalize; 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | body { 9 | min-height: 100vh; 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 11 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 12 | sans-serif; 13 | } 14 | 15 | a, 16 | button { 17 | display: block; 18 | background: none; 19 | border: none; 20 | } 21 | 22 | svg { 23 | display: block; 24 | height: 1em; 25 | width: 1em; 26 | } 27 | 28 | @keyframes sale { 29 | 0% { 30 | color: red; 31 | } 32 | 30% { 33 | color: green; 34 | } 35 | 70% { 36 | color: blue; 37 | } 38 | } 39 | 40 | .content { 41 | min-height: 100vh; 42 | } 43 | 44 | .card-title { 45 | overflow: hidden; 46 | white-space: nowrap; 47 | text-overflow: ellipsis; 48 | } 49 | 50 | .sidebar { 51 | position: sticky; 52 | top: 0; 53 | left: 0; 54 | } 55 | 56 | .pagination { 57 | position: fixed; 58 | bottom: 0; 59 | left: 0; 60 | width: 100%; 61 | background: #fff; 62 | border-top: 1px solid #000; 63 | } 64 | 65 | .nav-btn { 66 | margin-left: -10px; 67 | text-align: left; 68 | } 69 | -------------------------------------------------------------------------------- /src/redux/rtk-posts/rtk-posts.api.js: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; 2 | 3 | export const postsApi = createApi({ 4 | reducerPath: 'rtk-posts', 5 | tagTypes: ['Posts'], 6 | baseQuery: fetchBaseQuery({ baseUrl: 'http://70.34.201.18:4444' }), 7 | 8 | endpoints: builder => ({ 9 | getPosts: builder.query({ 10 | query: params => ({ 11 | url: '/posts', 12 | params, 13 | }), 14 | 15 | providesTags: ({ data }) => { 16 | if (data) { 17 | return [ 18 | ...data.map(({ id }) => ({ type: 'Posts', id })), 19 | { type: 'Posts', id: 'LIST' }, 20 | ]; 21 | } 22 | 23 | return [{ type: 'Posts', id: 'LIST' }]; 24 | }, 25 | }), 26 | 27 | deletePosts: builder.mutation({ 28 | query: id => ({ 29 | url: '/posts/' + id, 30 | method: 'DELETE', 31 | }), 32 | 33 | invalidatesTags: (result, error, id) => [{ type: 'Posts', id }], 34 | }), 35 | }), 36 | }); 37 | 38 | export const { 39 | useGetPostsQuery, 40 | useLazyGetPostsQuery, 41 | useDeletePostsMutation, 42 | } = postsApi; 43 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/CounterPageTwo/CounterPageTwo.jsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | 3 | import { counterReducer } from './reducer'; 4 | 5 | const CounterPageTwo = () => { 6 | const [state, dispatch] = useReducer(counterReducer, 0); 7 | return ( 8 | <> 9 |
10 |

Counter

11 |

12 | {state} 13 |

14 | 15 |
16 | 23 | 24 | 31 |
32 |
33 | 34 | ); 35 | }; 36 | 37 | export default CounterPageTwo; 38 | -------------------------------------------------------------------------------- /src/redux/auth/auth.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { persistReducer } from 'redux-persist'; 3 | import storage from 'redux-persist/lib/storage'; 4 | 5 | import { STATUS } from '../../constants/status.constants'; 6 | import { getProfileThunk } from '../profile/profile.thunk'; 7 | 8 | import { authInitState } from './auth.init-state'; 9 | import { authLoginThunk } from './auth.thunk'; 10 | 11 | const authSlice = createSlice({ 12 | name: 'auth', 13 | initialState: authInitState, 14 | reducers: { 15 | logoutAction: () => authInitState, 16 | }, 17 | extraReducers: builder => { 18 | builder.addCase(authLoginThunk.pending, state => { 19 | state.status = STATUS.loading; 20 | }).addCase(authLoginThunk.fulfilled, (state, { payload }) => { 21 | state.status = STATUS.success; 22 | state.data = payload; 23 | }).addCase(authLoginThunk.rejected, state => { 24 | state.status = STATUS.error; 25 | }).addCase(getProfileThunk.rejected, () => authInitState); 26 | }, 27 | }); 28 | 29 | export const { logoutAction } = authSlice.actions; 30 | 31 | export const authReducer = persistReducer({ 32 | key: 'auth', 33 | storage, 34 | }, authSlice.reducer); 35 | -------------------------------------------------------------------------------- /src/components/Posts/PostsLoader.jsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '../Skeleton/Skeleton'; 2 | 3 | export const PostsLoader = ({ amount = 9 }) => { 4 | return ( 5 |
6 |
7 | {[...Array(amount)].map((_, index) => ( 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 |
Delete post
24 |
Read post
25 |
26 |
27 |
28 |
29 | ))} 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/UsersPage/UsersItem.jsx: -------------------------------------------------------------------------------- 1 | export const UsersItem = ({ user, onDelete }) => { 2 | const { id, name, email, bio, skills, isOpenToWork } = user; 3 | 4 | const handleDelete = () => onDelete(id); 5 | 6 | return ( 7 |
8 |
9 |
10 | {name} 11 | {isOpenToWork && ( 12 |

Open to work

13 | )} 14 |
15 | 16 |
{email}
17 |

{bio}

18 | 19 |
20 | {skills.map(skil => ( 21 | 22 | {skil} 23 | 24 | ))} 25 |
26 | 27 |
28 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/TimerPage/TimerPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react'; 2 | 3 | import { Button } from '../../../components/Button/Button'; 4 | import { formatTime } from '../../../helpers/time'; 5 | 6 | const TimerPage = () => { 7 | const ref = useRef(null); 8 | const [time, setTime] = useState(0); 9 | 10 | const handleStartTimer = () => { 11 | if (!ref.current) { 12 | ref.current = setInterval(() => { 13 | setTime(prev => prev + 1); 14 | console.log('setInterval'); 15 | }, 0); 16 | } 17 | }; 18 | 19 | const handleStopTimer = () => { 20 | if (ref.current) { 21 | clearInterval(ref.current); 22 | ref.current = null; 23 | } 24 | }; 25 | 26 | useEffect(() => { 27 | return () => { 28 | handleStopTimer(); 29 | }; 30 | }, []); 31 | 32 | return ( 33 | <> 34 |

{formatTime(time)}

35 | 36 |
37 | 40 | 41 | 44 |
45 | 46 | ); 47 | }; 48 | 49 | export default TimerPage; 50 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const Modal = ({ children, onClose }) => { 4 | useEffect(() => { 5 | const handleKeyClose = event => { 6 | if (event.code === 'Escape') { 7 | onClose(); 8 | } 9 | }; 10 | 11 | window.addEventListener('keydown', handleKeyClose); 12 | 13 | return () => { 14 | window.removeEventListener('keydown', handleKeyClose); 15 | }; 16 | }, [onClose]); 17 | 18 | const handleBackdropClick = event => { 19 | if (event.target === event.currentTarget) { 20 | onClose(); 21 | } 22 | }; 23 | 24 | return ( 25 | <> 26 |
27 |
32 |
33 |
34 |
35 |
Modal title
36 |
43 |
{children}
44 |
45 |
46 |
47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/ExercisesPage.jsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import { NavLink, Outlet } from 'react-router-dom'; 4 | 5 | const ExercisesPage = () => { 6 | return ( 7 | <> 8 |
    9 |
  • 10 | 11 | Timer 12 | 13 |
  • 14 | 15 |
  • 16 | 17 | Long Request 18 | 19 |
  • 20 | 21 |
  • 22 | 23 | Counter 24 | 25 |
  • 26 | 27 |
  • 28 | 29 | Counter Two 30 | 31 |
  • 32 | 33 |
  • 34 | 35 | Re-render 36 | 37 |
  • 38 | 39 |
  • 40 | 41 | Users list 42 | 43 |
  • 44 |
45 | 46 | Loading inside ExercisesPage...

}> 47 | 48 |
49 | 50 | ); 51 | }; 52 | 53 | export default ExercisesPage; 54 | -------------------------------------------------------------------------------- /src/pages/ExercisesPage/CounterPage/CounterPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | 5 | import { counterAction } from '../../../redux/counter/counter.action'; 6 | import { selectCounter } from '../../../redux/counter/counter.selector'; 7 | 8 | const CounterPage = () => { 9 | const dispatch = useDispatch(); 10 | const counter = useSelector(selectCounter); 11 | 12 | const handleMinus = () => { 13 | dispatch(counterAction(-1)); 14 | }; 15 | 16 | const handlePlus = () => { 17 | dispatch(counterAction(1)); 18 | }; 19 | 20 | const [isOpen, setIsOpen] = useState(false); 21 | 22 | return ( 23 | <> 24 | 31 | 32 |
33 |

Counter

34 |

35 | {counter} 36 |

37 | 38 |
39 | 46 | 47 | 54 |
55 |
56 | 57 | ); 58 | }; 59 | 60 | export default CounterPage; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "goit-59-fs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://iMykhailychenko.github.io/goit-59-fs/", 6 | "dependencies": { 7 | "@reduxjs/toolkit": "^1.9.2", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "axios": "^1.2.2", 12 | "classnames": "^2.3.2", 13 | "date-fns": "^2.29.3", 14 | "lodash": "^4.17.21", 15 | "prop-types": "^15.8.1", 16 | "react": "^18.2.0", 17 | "react-confetti": "^6.1.0", 18 | "react-dom": "^18.2.0", 19 | "react-redux": "^8.0.5", 20 | "react-router-dom": "^6.6.2", 21 | "react-scripts": "5.0.1", 22 | "react-toastify": "^9.1.1", 23 | "redux-persist": "^6.0.0", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject", 31 | "lint:js": "eslint src/**/*.{js,jsx}" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "eslint": "^8.20.0", 53 | "eslint-config-airbnb": "^19.0.4", 54 | "eslint-plugin-import": "^2.26.0", 55 | "eslint-plugin-jsx-a11y": "^6.6.0", 56 | "eslint-plugin-react": "^7.30.1", 57 | "eslint-plugin-react-hooks": "^4.6.0", 58 | "prettier": "^2.7.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Posts/PostsItem.jsx: -------------------------------------------------------------------------------- 1 | import formatDistanceToNow from 'date-fns/formatDistanceToNow'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | 4 | import { cutString } from '../../helpers/cut-string'; 5 | 6 | export const PostsItem = ({ post, onDelete }) => { 7 | const location = useLocation(); 8 | 9 | const deletePost = () => { 10 | onDelete?.(post.id); 11 | }; 12 | 13 | return ( 14 |
15 |
16 | {post.title} 23 | 24 |
25 |
{post.title}
26 | 27 |

{cutString(post.content, 60)}

28 | 29 |
    30 |
  • Views: {post.views}
  • 31 |
  • 32 | Created: {formatDistanceToNow(new Date(post.created_at))} 33 |
  • 34 |
35 | 36 |
37 | 44 | 45 | 50 | Read post 51 | 52 |
53 |
54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/pages/SinglePostPage/SinglePostPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import axios from 'axios'; 4 | import { useParams, Link, Outlet, useLocation } from 'react-router-dom'; 5 | import { toast } from 'react-toastify'; 6 | 7 | import { Loader } from '../../components/Loader/Loader'; 8 | 9 | const SinglePostPage = () => { 10 | const { postId } = useParams(); 11 | const location = useLocation(); 12 | console.log(location); 13 | 14 | const [post, setPost] = useState(null); 15 | const [isLoading, setIsLoading] = useState(true); 16 | 17 | useEffect(() => { 18 | setIsLoading(true); 19 | 20 | axios 21 | .get('http://70.34.201.18:4444/posts/' + postId) 22 | .then(({ data }) => setPost(data)) 23 | .catch(() => { 24 | toast.error('Something went wrong!'); 25 | }) 26 | .finally(() => setIsLoading(false)); 27 | }, [postId]); 28 | 29 | if (isLoading) { 30 | return ; 31 | } 32 | 33 | return ( 34 | post && ( 35 | <> 36 | 37 | Back 38 | 39 | 40 | {post.title} 46 |

{post.title}

47 | 48 |
'), 51 | }} 52 | /> 53 | 54 | 59 | Vew post comments 60 | 61 | 62 | 63 | 64 | ) 65 | ); 66 | }; 67 | 68 | export default SinglePostPage; 69 | -------------------------------------------------------------------------------- /src/pages/SinglePostPage/CommentsPage/CommentForm/CommentForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import axios from 'axios'; 4 | import classNames from 'classnames'; 5 | import { toast } from 'react-toastify'; 6 | 7 | export const CommentForm = ({ setComments }) => { 8 | // TODO change to dynamic value 9 | const postId = 10; 10 | 11 | const [isLoading, setIsLoading] = useState(false); 12 | const [content, setContent] = useState(''); 13 | 14 | const handleChange = event => setContent(event.target.value); 15 | const handleReset = () => setContent(''); 16 | 17 | const handleSubmit = event => { 18 | event.preventDefault(); 19 | 20 | if (!content.trim()) { 21 | toast.error('Fill all required fields!'); 22 | return; 23 | } 24 | 25 | setIsLoading(true); 26 | axios 27 | .post(`http://70.34.201.18:4444/posts/${postId}/comments`, { 28 | content, 29 | }) 30 | .then(data => { 31 | toast.success('You have successfully created a new comment!'); 32 | setComments(prev => ({ ...prev, data: [data, ...prev.data] })); 33 | handleReset(); 34 | }) 35 | .catch(() => { 36 | toast.error('Something went wrong!'); 37 | }) 38 | .finally(() => setIsLoading(false)); 39 | }; 40 | 41 | return ( 42 |
43 |