├── .idea ├── .gitignore ├── Web-Blog.iml ├── modules.xml └── vcs.xml ├── README.md ├── client ├── .gitignore ├── .idea │ ├── .gitignore │ ├── inspectionProfiles │ │ └── Project_Default.xml │ ├── modules.xml │ ├── prettier.xml │ ├── react-blog.iml │ └── vcs.xml ├── package-lock.json ├── package.json ├── public │ └── index.html └── src │ ├── App.js │ ├── axios.js │ ├── components │ ├── AddComment │ │ ├── AddComment.module.scss │ │ └── index.jsx │ ├── CommentsBlock.jsx │ ├── Header │ │ ├── Header.module.scss │ │ └── index.jsx │ ├── Post │ │ ├── Post.module.scss │ │ ├── Skeleton.jsx │ │ └── index.jsx │ ├── SideBlock │ │ ├── SideBlock.module.scss │ │ └── index.jsx │ ├── TagsBlock.jsx │ ├── UserInfo │ │ ├── UserInfo.module.scss │ │ └── index.jsx │ └── index.js │ ├── index.js │ ├── index.scss │ ├── pages │ ├── AddPost │ │ ├── AddPost.module.scss │ │ └── index.jsx │ ├── FullPost.jsx │ ├── Home.jsx │ ├── Login │ │ ├── Login.module.scss │ │ └── index.jsx │ ├── Profile │ │ ├── Profile.css │ │ └── index.jsx │ ├── Registration │ │ ├── Login.module.scss │ │ └── index.jsx │ └── index.js │ ├── redux │ ├── slices │ │ ├── auth.js │ │ └── posts.js │ └── store.js │ └── theme.js └── server ├── .gitignore ├── controllers ├── PostController.js ├── UserController.js └── index.js ├── index.js ├── models ├── Post.js └── User.js ├── package-lock.json ├── package.json ├── uploads └── wp8420305-genshin-impact-hd-desktop-wallpapers.jpg ├── utils ├── chekAuth.js ├── handleValidationErrors.js └── index.js └── validations ├── auth.js └── post.js /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/Web-Blog.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Blog 2 | 3 | ### Demo: https://web-blog-by-beka.vercel.app/ 4 | 5 | ## About 📓 6 | 7 | The main idea was to create your own communication platform. The end result was a platform with minimal functionality, but the idea of writing, deleting and editing posts/articles was achieved🎯. The whole project was written by me (Back-end & Front-end). I used MongoDB as a database. Back-end development used Node.js & Express.js. And in Front-end development I used React.js & Redux/Toolkit. 8 | 9 | ### Note: 10 | 11 | _The project has not yet been adapted for mobile versions_ 12 | 13 | ## How it works ⚙️ 14 | 15 | ### Back-end 🗄️ 16 | 17 | In the server folder, the index.js file runs a local server on port 5000. The same file prescribes routers for the type of requests, and depending on the request will trigger a certain function prescribed in the file PostController.js for posts/articles and UserController.js for users. The Models folder contains models of posts and users. The validations folder contains all validations for post creation, registration, and editing of posts. In the folder prloads are stored all the images that were uploaded to the server. 😎👌🔥 18 | 19 | ### Front-end 🖥️ 20 | 21 | All client part is in the client folder, all files are in the folder src. The main files are App.js, axios.js, redux and pages. In the App.js folder all the routers are prescribed, and depending on the router the component I specified will be rendered. In axios.js I wrote a function so that api server is always embedded, you can read a bit about it here **(https://axios-http.com/docs/instance)**. In the folder redux prescribed methods to change the state in the project. The pages folder contains all the pages that are present in the project. 💯🔥 22 | 23 |
24 | 25 | ### How to start locally 26 | 27 | ### Note: 28 | 29 | It is possible to run the project locally, but it will not work correctly because there is a mongodb uri in the project, and I can not cover this🥺. So you can use this as a template to your projects, in places where api for front and mongodb uri are inserted. 30 | 31 | ## How to start Front-end side : 32 | 33 | 1. You must enter the client folder 34 | 35 | ```bash 36 | cd client/ 37 | ``` 38 | 39 | 2. Next, you should install all the dependencies 40 | 41 | ```bash 42 | npm install 43 | ``` 44 | 45 | 3. And at the end, start the server 46 | 47 | ```bash 48 | npm start 49 | ``` 50 | 51 |
52 | 53 | ## How to start Back-end side : 54 | 55 | 1. You must enter the server folder 56 | 57 | ```bash 58 | cd server/ 59 | ``` 60 | 61 | 2. Next, you should install all the dependencies 62 | 63 | ```bash 64 | npm install 65 | ``` 66 | 67 | 3. And at the end, start the server 68 | 69 | ```bash 70 | npm run dev 71 | ``` 72 | 73 | ### Technologies used 74 | 75 | - React.js 76 | - Redux Toolkit 77 | - Node.js 78 | - Express.js 79 | - JWT 80 | - MongoDB / Mongoose 81 | - Multer storage 82 | - Mui 83 | - SCSS 84 | -------------------------------------------------------------------------------- /client/.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 | -------------------------------------------------------------------------------- /client/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /client/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /client/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /client/.idea/react-blog.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.5", 7 | "@emotion/styled": "^11.10.5", 8 | "@material-ui/core": "^4.12.4", 9 | "@mui/icons-material": "^5.8.0", 10 | "@mui/material": "^5.11.7", 11 | "@reduxjs/toolkit": "^1.8.2", 12 | "@testing-library/jest-dom": "^5.16.4", 13 | "@testing-library/react": "^13.2.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "axios": "^0.27.2", 16 | "clsx": "^1.1.1", 17 | "dotenv": "^16.0.3", 18 | "easymde": "^2.16.1", 19 | "prettier": "^2.6.2", 20 | "react": "^18.1.0", 21 | "react-dom": "^18.1.0", 22 | "react-hook-form": "^7.32.0", 23 | "react-markdown": "^8.0.3", 24 | "react-redux": "^8.0.2", 25 | "react-router-dom": "^6.3.0", 26 | "react-scripts": "5.0.1", 27 | "react-simplemde-editor": "^5.0.2", 28 | "sass": "^1.58.0", 29 | "web-vitals": "^2.1.4" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Blog 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import Container from "@mui/material/Container"; 2 | import { Header } from "./components"; 3 | import { Home, FullPost, Registration, AddPost, Login } from "./pages"; 4 | import { Route, Routes } from "react-router-dom"; 5 | import { useDispatch } from "react-redux"; 6 | import { useEffect } from "react"; 7 | import { fetchAuthMe } from "./redux/slices/auth"; 8 | import Profile from "./pages/Profile"; 9 | 10 | function App() { 11 | 12 | const dispatch = useDispatch() 13 | 14 | useEffect(()=> { 15 | dispatch(fetchAuthMe()) 16 | },[]) 17 | 18 | return ( 19 | <> 20 |
21 | 22 | 23 | }/> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /client/src/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | const axiosBaseUrl = axios.create({baseURL: process.env.REACT_APP_API_URL}); 3 | 4 | axiosBaseUrl.interceptors.request.use((config)=> { 5 | config.headers.Authorization = window.localStorage.getItem("token"); 6 | 7 | return config; 8 | }) 9 | 10 | export default axiosBaseUrl -------------------------------------------------------------------------------- /client/src/components/AddComment/AddComment.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | margin-top: 10px; 4 | padding-bottom: 20px; 5 | margin-right: 20px; 6 | margin-left: 17px; 7 | } 8 | 9 | .avatar { 10 | margin-right: 15px; 11 | } 12 | 13 | .form { 14 | width: 100%; 15 | 16 | button { 17 | margin-top: 10px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/components/AddComment/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import styles from "./AddComment.module.scss"; 4 | 5 | import TextField from "@mui/material/TextField"; 6 | import Avatar from "@mui/material/Avatar"; 7 | import Button from "@mui/material/Button"; 8 | 9 | export const Index = () => { 10 | return ( 11 | <> 12 |
13 | 17 |
18 | 25 | 26 |
27 |
28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/components/CommentsBlock.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { SideBlock } from "./SideBlock"; 4 | import ListItem from "@mui/material/ListItem"; 5 | import ListItemAvatar from "@mui/material/ListItemAvatar"; 6 | import Avatar from "@mui/material/Avatar"; 7 | import ListItemText from "@mui/material/ListItemText"; 8 | import Divider from "@mui/material/Divider"; 9 | import List from "@mui/material/List"; 10 | import Skeleton from "@mui/material/Skeleton"; 11 | 12 | export const CommentsBlock = ({ items, children, isLoading = true }) => { 13 | return ( 14 | 15 | 16 | {(isLoading ? [...Array(5)] : items).map((obj, index) => ( 17 | 18 | 19 | 20 | {isLoading ? ( 21 | 22 | ) : ( 23 | 24 | )} 25 | 26 | {isLoading ? ( 27 |
28 | 29 | 30 |
31 | ) : ( 32 | 36 | )} 37 |
38 | 39 |
40 | ))} 41 |
42 | {children} 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /client/src/components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: #fff; 3 | padding: 10px 0; 4 | border-bottom: 1px solid #e0e0e0; 5 | margin-bottom: 30px; 6 | } 7 | 8 | .inner { 9 | display: flex; 10 | justify-content: space-between; 11 | } 12 | 13 | .logo { 14 | background-color: rgb(28, 18, 53); 15 | color: #fff; 16 | font-weight: 700; 17 | line-height: 35px; 18 | text-transform: uppercase; 19 | letter-spacing: 0.15px; 20 | border-radius: 5px; 21 | padding: 0 10px; 22 | text-decoration: none; 23 | transition: all .5s; 24 | 25 | &:hover { 26 | background-color: #ecebea; 27 | color: #000; 28 | 29 | } 30 | } 31 | 32 | .buttons { 33 | button { 34 | margin-left: 10px; 35 | background-color: rgb(28, 18, 53) !important; 36 | color: #fff; 37 | font-weight: bold; 38 | } 39 | 40 | button:hover{ 41 | background-color: #ecebea !important; 42 | color: #000; 43 | 44 | } 45 | a { 46 | text-decoration: none; 47 | } 48 | 49 | display: flex; 50 | align-items: center; 51 | 52 | .avatarLogo{ 53 | margin-left: 10px; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/src/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@mui/material/Button"; 3 | import { Link } from "react-router-dom"; 4 | import styles from "./Header.module.scss"; 5 | import Container from "@mui/material/Container"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { logout, selectIsAuth } from "../../redux/slices/auth"; 8 | import { Avatar } from "@mui/material"; 9 | 10 | export const Header = () => { 11 | const dispatch = useDispatch(); 12 | const isAuth = useSelector(selectIsAuth); 13 | const userData = useSelector((state) => state.auth.data); 14 | 15 | const onClickLogout = () => { 16 | if (window.confirm("Are you sure you want to get out?")) { 17 | dispatch(logout()); 18 | window.localStorage.removeItem("token"); 19 | } 20 | }; 21 | 22 | return ( 23 |
24 | 25 |
26 | 27 |
WEB-BLOG
28 | 29 |
30 | {isAuth ? ( 31 | <> 32 | 33 | 34 | 35 | 42 | 43 | 48 | 49 | 50 | ) : ( 51 | <> 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | )} 60 |
61 |
62 |
63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /client/src/components/Post/Post.module.scss: -------------------------------------------------------------------------------- 1 | $color_text: #181d1d; 2 | 3 | .root { 4 | background-color: #ecebea; 5 | border: 1px solid #dedede; 6 | border-radius: 6px; 7 | overflow: hidden; 8 | margin-bottom: 15px; 9 | position: relative; 10 | 11 | &:hover { 12 | border: 1px solid $color_text; 13 | box-shadow: 0 0 0 1px $color_text; 14 | 15 | .editButtons { 16 | opacity: 1; 17 | } 18 | } 19 | 20 | &Full { 21 | &:hover { 22 | background-color: #fff; 23 | border: 1px solid #dedede; 24 | box-shadow: none; 25 | } 26 | } 27 | } 28 | 29 | .image { 30 | width: 100%; 31 | height: 300px; 32 | object-fit: cover; 33 | 34 | &Full { 35 | min-height: 300px; 36 | height: 100%; 37 | } 38 | } 39 | 40 | .wrapper { 41 | padding: 10px 20px 20px; 42 | } 43 | 44 | .content { 45 | margin: 30px 0 50px; 46 | 47 | p { 48 | font-size: 22px; 49 | line-height: 36px; 50 | } 51 | } 52 | 53 | .indention { 54 | padding-left: 40px; 55 | } 56 | 57 | .title { 58 | font-size: 28px; 59 | margin: 0; 60 | 61 | a { 62 | text-decoration: none; 63 | color: #1f2833; 64 | 65 | &:hover { 66 | color: $color_text; 67 | } 68 | } 69 | 70 | &Full { 71 | font-size: 42px; 72 | font-weight: 900; 73 | } 74 | } 75 | 76 | .tags { 77 | list-style: none; 78 | padding: 0; 79 | margin: 5px 0 0 0; 80 | 81 | li { 82 | display: inline-block; 83 | font-size: 14px; 84 | margin-right: 15px; 85 | opacity: 0.5; 86 | 87 | &:hover { 88 | opacity: 1; 89 | } 90 | 91 | a { 92 | text-decoration: none; 93 | color: #000; 94 | } 95 | } 96 | } 97 | 98 | .postDetails { 99 | list-style: none; 100 | padding: 0; 101 | margin: 20px 0 0 0; 102 | 103 | li { 104 | display: inline-flex; 105 | align-items: center; 106 | font-size: 14px; 107 | margin-right: 20px; 108 | opacity: 0.5; 109 | 110 | svg { 111 | font-size: 18px; 112 | margin-right: 5px; 113 | } 114 | } 115 | } 116 | 117 | .skeleton { 118 | background-color: #fff; 119 | border: 1px solid #dedede; 120 | border-radius: 6px; 121 | overflow: hidden; 122 | margin-bottom: 15px; 123 | 124 | &Content { 125 | padding: 20px; 126 | } 127 | 128 | &Info { 129 | margin-left: 50px; 130 | } 131 | } 132 | 133 | .skeletonUser { 134 | display: flex; 135 | 136 | &Details { 137 | display: flex; 138 | flex-direction: column; 139 | } 140 | } 141 | 142 | .skeletonTags { 143 | display: flex; 144 | 145 | span { 146 | margin-right: 15px; 147 | } 148 | } 149 | 150 | .editButtons { 151 | position: absolute; 152 | right: 15px; 153 | top: 15px; 154 | background-color: rgba(255, 255, 255, 1); 155 | border-radius: 10px; 156 | opacity: 0; 157 | transition: all 0.15s ease-in-out; 158 | } 159 | -------------------------------------------------------------------------------- /client/src/components/Post/Skeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Stack from "@mui/material/Stack"; 3 | import Skeleton from "@mui/material/Skeleton"; 4 | 5 | import styles from "./Post.module.scss"; 6 | 7 | export const PostSkeleton = () => { 8 | return ( 9 |
10 | 11 | 12 |
13 |
14 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /client/src/components/Post/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import IconButton from "@mui/material/IconButton"; 4 | import DeleteIcon from "@mui/icons-material/Clear"; 5 | import EditIcon from "@mui/icons-material/Edit"; 6 | import EyeIcon from "@mui/icons-material/RemoveRedEyeOutlined"; 7 | // import CommentIcon from "@mui/icons-material/ChatBubbleOutlineOutlined"; 8 | import { Link } from "react-router-dom"; 9 | import styles from "./Post.module.scss"; 10 | import { UserInfo } from "../UserInfo"; 11 | import { PostSkeleton } from "./Skeleton"; 12 | import { useDispatch } from "react-redux"; 13 | import { fetchRemovePost } from "../../redux/slices/posts"; 14 | 15 | export const Post = ({ 16 | id, 17 | title, 18 | createdAt, 19 | imageUrl, 20 | user, 21 | viewsCount, 22 | commentsCount, 23 | tags, 24 | children, 25 | isFullPost, 26 | isLoading, 27 | isEditable, 28 | }) => { 29 | const dispatch = useDispatch(); 30 | 31 | if (isLoading) { 32 | return ; 33 | } 34 | 35 | const onClickRemove = () => { 36 | if (window.confirm("Do you really want to delete this article?")) { 37 | dispatch(fetchRemovePost(id)); 38 | } 39 | }; 40 | 41 | return ( 42 |
43 | {isEditable && ( 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | )} 55 |
56 | 57 |
58 |

61 | {isFullPost ? title : {title}} 62 |

63 |
    64 | {tags.map((name) => ( 65 |
  • # {name}
  • 66 | ))} 67 |
68 | {children &&
{children}
} 69 |
    70 |
  • 71 | 72 | {viewsCount} 73 |
  • 74 |
75 |
76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /client/src/components/SideBlock/SideBlock.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: 20px; 3 | } 4 | 5 | .title { 6 | padding: 15px 15px 0 15px; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/components/SideBlock/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./SideBlock.module.scss"; 3 | import Typography from "@mui/material/Typography"; 4 | import Paper from "@mui/material/Paper"; 5 | 6 | export const SideBlock = ({ title, children }) => { 7 | return ( 8 | 9 | 10 | {title} 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/TagsBlock.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import List from "@mui/material/List"; 4 | import ListItem from "@mui/material/ListItem"; 5 | import ListItemButton from "@mui/material/ListItemButton"; 6 | import ListItemIcon from "@mui/material/ListItemIcon"; 7 | import TagIcon from "@mui/icons-material/Tag"; 8 | import ListItemText from "@mui/material/ListItemText"; 9 | import Skeleton from "@mui/material/Skeleton"; 10 | 11 | import { SideBlock } from "./SideBlock"; 12 | 13 | export const TagsBlock = ({ items, isLoading = true }) => { 14 | return ( 15 | 16 | 17 | {(isLoading ? [...Array(5)] : items).map((name, i) => ( 18 | 22 | 23 | 24 | 25 | 26 | 27 | {isLoading ? ( 28 | 29 | ) : ( 30 | 31 | )} 32 | 33 | 34 | 35 | ))} 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /client/src/components/UserInfo/UserInfo.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .avatar { 7 | width: 40px; 8 | height: 40px; 9 | image-rendering: optimizeQuality ; 10 | border-radius: 30px; 11 | margin-right: 10px; 12 | } 13 | 14 | .userName { 15 | font-weight: 500; 16 | font-size: 14px; 17 | } 18 | 19 | .userDetails { 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | .additional { 25 | font-size: 12px; 26 | opacity: 0.6; 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/UserInfo/index.jsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@mui/material"; 2 | import React from "react"; 3 | import styles from "./UserInfo.module.scss"; 4 | 5 | export const UserInfo = ({ avatarUrl, fullName, additionalText }) => { 6 | return ( 7 |
8 | 13 |
14 | {fullName} 15 | {additionalText} 16 |
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./TagsBlock"; 2 | export * from "./CommentsBlock"; 3 | export * from "./Post"; 4 | export * from "./AddComment"; 5 | export * from "./SideBlock"; 6 | export * from "./UserInfo"; 7 | export * from "./Header"; 8 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import {BrowserRouter} from "react-router-dom" 4 | import App from "./App"; 5 | import CssBaseline from "@mui/material/CssBaseline"; 6 | import { Provider } from "react-redux"; 7 | import "./index.scss"; 8 | import { ThemeProvider } from "@mui/material"; 9 | import { theme } from "./theme"; 10 | import store from "./redux/store"; 11 | 12 | const root = ReactDOM.createRoot(document.getElementById("root")); 13 | 14 | root.render( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /client/src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Roboto'; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | background-color: #F5F5F5 !important; 7 | } -------------------------------------------------------------------------------- /client/src/pages/AddPost/AddPost.module.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | input { 3 | font-size: 42px; 4 | font-weight: 900; 5 | } 6 | 7 | div { 8 | &:before, 9 | &:after { 10 | display: none; 11 | } 12 | } 13 | } 14 | 15 | .image { 16 | width: 100%; 17 | } 18 | 19 | .tags { 20 | margin: 15px 0; 21 | } 22 | 23 | .editor { 24 | margin: 30px -30px; 25 | 26 | :global { 27 | .cm-s-easymde { 28 | border: 0; 29 | font-size: 22px; 30 | } 31 | .editor-toolbar { 32 | border: 0; 33 | background-color: rgb(0 0 0 / 2%); 34 | } 35 | } 36 | } 37 | 38 | .buttons { 39 | display: flex; 40 | 41 | button { 42 | margin-right: 15px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/src/pages/AddPost/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import TextField from "@mui/material/TextField"; 3 | import Paper from "@mui/material/Paper"; 4 | import Button from "@mui/material/Button"; 5 | import SimpleMDE from "react-simplemde-editor"; 6 | import axiosBaseUrl from "../../axios"; 7 | import "easymde/dist/easymde.min.css"; 8 | import styles from "./AddPost.module.scss"; 9 | import { useSelector } from "react-redux"; 10 | import { selectIsAuth } from "../../redux/slices/auth"; 11 | import { Navigate, useNavigate, useParams } from "react-router-dom"; 12 | 13 | export const AddPost = () => { 14 | const { id } = useParams(); 15 | const isAuth = useSelector(selectIsAuth); 16 | const navigate = useNavigate(); 17 | const [isLoading, setLoading] = useState(false); 18 | const [text, setText] = useState(""); 19 | const isEditing = Boolean(id); 20 | const [postObj, setPostObj] = React.useState({ 21 | title: "", 22 | tags: "", 23 | imageUrl: "", 24 | }); 25 | 26 | const inputImageRef = useRef(null); 27 | 28 | const handleChangeFile = async (event) => { 29 | try { 30 | const formData = new FormData(); 31 | const file = event.target.files[0]; 32 | formData.append("image", file); 33 | const { data } = await axiosBaseUrl.post("/upload", formData); 34 | setPostObj({ ...postObj, imageUrl: data.url }); 35 | } catch (error) { 36 | console.warn(error); 37 | alert("Error downloading the file"); 38 | } 39 | }; 40 | 41 | const onClickRemoveImage = () => { 42 | setPostObj({ ...postObj, imageUrl: "" }); 43 | }; 44 | 45 | const onChange = React.useCallback((value) => { 46 | setText(value); 47 | }, []); 48 | 49 | const onSubmit = async () => { 50 | try { 51 | setLoading(true); 52 | const obj = { ...postObj, text }; 53 | console.log(obj); 54 | const { data } = isEditing 55 | ? await axiosBaseUrl.patch(`/posts/${id}`, obj) 56 | : await axiosBaseUrl.post("/posts", obj); 57 | 58 | const _id = isEditing ? id : data._id; 59 | navigate(`/posts/${_id}`); 60 | } catch (error) { 61 | console.warn("Error in article creation!"); 62 | } 63 | }; 64 | 65 | useEffect(() => { 66 | if (id) { 67 | axiosBaseUrl 68 | .get(`/posts/${id}`) 69 | .then(({ data }) => { 70 | setPostObj({ 71 | title: data.title, 72 | tags: data.tags, 73 | imageUrl: data.imageUrl, 74 | }); 75 | setText(data.text); 76 | }) 77 | .catch((err) => { 78 | console.warn(err); 79 | alert(err); 80 | }); 81 | } 82 | }, []); 83 | 84 | const options = React.useMemo( 85 | () => ({ 86 | spellChecker: false, 87 | maxHeight: "400px", 88 | autofocus: true, 89 | placeholder: "Enter text...", 90 | status: false, 91 | autosave: { 92 | enabled: true, 93 | delay: 1000, 94 | }, 95 | }), 96 | [] 97 | ); 98 | 99 | if (!window.localStorage.getItem("token") && !isAuth) { 100 | return ; 101 | } 102 | 103 | return ( 104 | 105 | {/* 112 | 118 | {postObj.imageUrl && ( 119 | <> 120 | 127 | Uploaded 132 | 133 | )} 134 |
135 |
*/} 136 | { 142 | setPostObj({ ...postObj, title: e.target.value }); 143 | }} 144 | fullWidth 145 | /> 146 | { 152 | setPostObj({ ...postObj, tags: e.target.value }); 153 | }} 154 | fullWidth 155 | /> 156 | 162 |
163 | 166 | 167 | 168 | 169 |
170 |
171 | ); 172 | }; 173 | -------------------------------------------------------------------------------- /client/src/pages/FullPost.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { Post } from "../components/Post"; 4 | import axiosBaseUrl from "../axios"; 5 | import ReactMarkdown from "react-markdown"; 6 | 7 | export const FullPost = () => { 8 | const [data, setData] = useState(); 9 | const [isLoading, setLoading] = useState(true); 10 | const { id } = useParams(); 11 | 12 | useEffect(() => { 13 | axiosBaseUrl 14 | .get(`/posts/${id}`) 15 | .then((res) => { 16 | setData(res.data); 17 | setLoading(false); 18 | }) 19 | .catch((err) => { 20 | console.warn(err); 21 | alert("Error in obtaining an article"); 22 | }); 23 | }, []); 24 | 25 | if (isLoading) { 26 | return ; 27 | } 28 | 29 | return ( 30 | <> 31 | 46 | 47 | 48 | {/* 67 | 68 | */} 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /client/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Tabs from "@mui/material/Tabs"; 3 | import Tab from "@mui/material/Tab"; 4 | import Grid from "@mui/material/Grid"; 5 | import { Post } from "../components/Post"; 6 | import { TagsBlock } from "../components/TagsBlock"; 7 | import { useDispatch, useSelector } from "react-redux"; 8 | import { fetchPosts, fetchTags } from "../redux/slices/posts"; 9 | 10 | export const Home = () => { 11 | const dispatch = useDispatch(); 12 | const userData = useSelector((state) => state.auth.data); 13 | const { posts, tags } = useSelector((state) => state.posts); 14 | 15 | const isPostLoading = posts.status === "loading"; 16 | const isTagLoading = tags.status === "loading"; 17 | useEffect(() => { 18 | dispatch(fetchPosts()); 19 | dispatch(fetchTags()); 20 | }, []); 21 | 22 | return ( 23 | <> 24 | 29 | 30 | 31 | 32 | 33 | {(isPostLoading ? [...Array(5)] : posts.items).map((obj, index) => 34 | isPostLoading ? ( 35 | 36 | ) : ( 37 | 52 | ) 53 | )} 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /client/src/pages/Login/Login.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 400px; 3 | padding: 50px; 4 | border: 1px solid #dedede; 5 | margin: 50px auto; 6 | } 7 | 8 | .field { 9 | margin-bottom: 20px !important; 10 | } 11 | 12 | .title { 13 | text-align: center !important; 14 | font-weight: bold !important; 15 | margin-bottom: 30px !important; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/pages/Login/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@mui/material/Typography"; 3 | import TextField from "@mui/material/TextField"; 4 | import Paper from "@mui/material/Paper"; 5 | import Button from "@mui/material/Button"; 6 | import { useForm } from "react-hook-form"; 7 | import styles from "./Login.module.scss"; 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { fetchAuth, selectIsAuth } from "../../redux/slices/auth"; 10 | import { Navigate } from "react-router-dom"; 11 | 12 | export const Login = () => { 13 | const isAuth = useSelector(selectIsAuth); 14 | const dispatch = useDispatch(); 15 | 16 | const { 17 | register, 18 | handleSubmit, 19 | formState: { errors, isValid }, 20 | } = useForm({ 21 | mode: "onChange", 22 | }); 23 | 24 | const onSubmit = async (values) => { 25 | const data = await dispatch(fetchAuth(values)); 26 | 27 | if (!data.payload) { 28 | alert("Failed to log in!"); 29 | } 30 | 31 | if ("token" in data.payload) { 32 | window.localStorage.setItem("token", data.payload.token); 33 | } 34 | }; 35 | 36 | if (isAuth) { 37 | return ; 38 | } 39 | 40 | return ( 41 | 42 | 43 | Log in 44 | 45 |
46 | 55 | 64 | 73 | 74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /client/src/pages/Profile/Profile.css: -------------------------------------------------------------------------------- 1 | .ava { 2 | width: 100%; 3 | text-align: center; 4 | } 5 | .info { 6 | width: 70%; 7 | margin-left: 85px; 8 | padding: 15px; 9 | display: flex; 10 | justify-content: space-between; 11 | flex-wrap: wrap; 12 | align-items: center; 13 | } 14 | 15 | .info > input{ 16 | background-color: transparent; 17 | border: 1px solid gray; 18 | padding: 10px; 19 | margin: 20px; 20 | 21 | } 22 | 23 | .info span{ 24 | display: block; 25 | padding: 10px; 26 | } 27 | .row { 28 | margin: 30px; 29 | } 30 | 31 | .main{ 32 | width: 50%; 33 | margin: auto; 34 | text-align: center; 35 | border-radius: 10px; 36 | box-shadow: 6px 4px 13px 2px rgba(0,0,0,0.64); 37 | padding: 15px; 38 | background-color: #fff; 39 | } 40 | 41 | .nick{ 42 | font-size: 20px; 43 | font-weight: 500; 44 | } -------------------------------------------------------------------------------- /client/src/pages/Profile/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect /* useRef */ } from "react"; 2 | import { Avatar, Button } from "@material-ui/core"; 3 | import axiosBaseUrl from "../../axios"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | import "./Profile.css"; 6 | import { useSelector } from "react-redux"; 7 | const useStyles = makeStyles((theme) => ({ 8 | root: { 9 | display: "flex", 10 | "& > *": { 11 | margin: theme.spacing(1), 12 | }, 13 | }, 14 | small: { 15 | width: theme.spacing(3), 16 | height: theme.spacing(3), 17 | }, 18 | large: { 19 | width: theme.spacing(17), 20 | height: theme.spacing(17), 21 | margin: "auto", 22 | }, 23 | inp: { 24 | margin: "15px", 25 | color: "#000", 26 | }, 27 | })); 28 | 29 | const Profile = () => { 30 | // const inputImageRef = useRef(null); 31 | 32 | const userData = useSelector((state) => state.auth.data); 33 | const classes = useStyles(); 34 | const [user, setUser] = useState({}); 35 | const [isEdit, setEdit] = useState(false); 36 | 37 | async function showProfile() { 38 | const { data } = await axiosBaseUrl(`/auth/profile`); 39 | setUser(data); 40 | } 41 | 42 | // const onClickRemoveImage = () => { 43 | // setUser({ ...user, avatarUrl: "" }); 44 | // }; 45 | 46 | // const handleChangeFile = async (event) => { 47 | // try { 48 | // const formData = new FormData(); 49 | // const file = event.target.files[0]; 50 | // formData.append("image", file); 51 | // const { data } = await axiosBaseUrl.post("/upload", formData); 52 | // setUser({ ...user, avatarUrl: data.url }); 53 | // } catch (error) { 54 | // console.warn(error); 55 | // alert("Ошибка при загрузке файла"); 56 | // } 57 | // }; 58 | 59 | useEffect(() => { 60 | showProfile(); 61 | }, []); 62 | 63 | async function saveDb() { 64 | await axiosBaseUrl.patch(`/auth/profile/${userData._id}`, user); 65 | console.log(user); 66 | setEdit(false); 67 | console.log("Save db"); 68 | } 69 | console.log(user); 70 | return ( 71 |
72 |
73 |
74 | {!isEdit ? ( 75 | <> 76 |
77 |
78 | 83 | {user.name} 84 |
85 |
86 | Full Name: 87 | 88 | Email: 89 | 90 |
91 |
92 | 93 | ) : ( 94 | <> 95 |
96 |
97 | 102 | {/* 107 | {user.avatarUrl ? ( 108 | 109 | ) : ( 110 | "" 111 | )} */} 112 |
113 |
114 | Full Name: 115 | { 118 | setUser({ ...user, fullName: e.target.value }); 119 | }} 120 | value={user.fullName} 121 | /> 122 | Email: 123 | { 126 | setUser({ ...user, email: e.target.value }); 127 | }} 128 | value={user.email} 129 | /> 130 |
131 |
132 | 133 | )} 134 | {isEdit ? ( 135 | 138 | ) : ( 139 | 146 | )} 147 |
148 |
149 |
150 | ); 151 | }; 152 | 153 | export default Profile; 154 | -------------------------------------------------------------------------------- /client/src/pages/Registration/Login.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 400px; 3 | padding: 50px; 4 | border: 1px solid #dedede; 5 | margin: 50px auto; 6 | } 7 | 8 | .field { 9 | margin-bottom: 20px !important; 10 | } 11 | 12 | .title { 13 | text-align: center !important; 14 | font-weight: bold !important; 15 | margin-bottom: 30px !important; 16 | } 17 | 18 | .avatar { 19 | display: flex; 20 | justify-content: center; 21 | flex-wrap: wrap; 22 | align-items: baseline; 23 | margin-bottom: 30px; 24 | text-align: center; 25 | 26 | & > .avatarBlock > *{ 27 | margin: 10px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/pages/Registration/index.jsx: -------------------------------------------------------------------------------- 1 | import React /* useRef, useState */ from "react"; 2 | import Typography from "@mui/material/Typography"; 3 | import TextField from "@mui/material/TextField"; 4 | import Paper from "@mui/material/Paper"; 5 | import Button from "@mui/material/Button"; 6 | // import Avatar from "@mui/material/Avatar"; 7 | import styles from "./Login.module.scss"; 8 | import { fetchRegister, selectIsAuth } from "../../redux/slices/auth"; 9 | import { useDispatch, useSelector } from "react-redux"; 10 | import { useForm } from "react-hook-form"; 11 | import { Navigate } from "react-router-dom"; 12 | // import axiosBaseUrl from "../../axios"; 13 | 14 | export const Registration = () => { 15 | // const inputImageRef = useRef(null); 16 | const isAuth = useSelector(selectIsAuth); 17 | const dispatch = useDispatch(); 18 | // const [avatar, setAvatar] = useState(""); 19 | const { 20 | register, 21 | handleSubmit, 22 | formState: { errors, isValid }, 23 | } = useForm({ 24 | mode: "onChange", 25 | }); 26 | 27 | // 28 | // const onClickRemoveImage = () => { 29 | // setAvatar(""); 30 | // }; 31 | 32 | // const handleChangeFile = async (event) => { 33 | // try { 34 | // const formData = new FormData(); 35 | // const file = event.target.files[0]; 36 | // formData.append("image", file); 37 | // const { data } = await axiosBaseUrl.post("/upload", formData); 38 | // setAvatar(data.url); 39 | // } catch (error) { 40 | // console.warn(error); 41 | // alert("Ошибка при загрузке файла"); 42 | // } 43 | // }; 44 | 45 | const onSubmit = async (values) => { 46 | // const data = { ...values, avatarUrl: avatar }; 47 | // console.log(data); 48 | const user_data = await dispatch(fetchRegister(values)); 49 | if (!user_data.payload) { 50 | alert("Failed to register!"); 51 | } 52 | 53 | if ("token" in user_data.payload) { 54 | window.localStorage.setItem("token", user_data.payload.token); 55 | } 56 | }; 57 | 58 | if (isAuth) { 59 | return ; 60 | } 61 | 62 | return ( 63 | 64 | 65 | Creating an account 66 | 67 | 68 |
69 | {/*
70 | 75 | {avatar && ( 76 |
77 | 81 | 88 |
89 | )} 90 |
*/} 91 | 99 | 108 | 117 | 126 | 127 |
128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { Home } from "./Home"; 2 | export { FullPost } from "./FullPost"; 3 | export { AddPost } from "./AddPost"; 4 | export { Registration } from "./Registration"; 5 | export { Login } from "./Login"; 6 | -------------------------------------------------------------------------------- /client/src/redux/slices/auth.js: -------------------------------------------------------------------------------- 1 | import {createSlice, createAsyncThunk} from "@reduxjs/toolkit" 2 | import axiosBaseUrl from "../../axios"; 3 | 4 | export const fetchAuth = createAsyncThunk("auth/fetchAuth", async (params) => { 5 | const {data} = await axiosBaseUrl.post("/auth/login", params); 6 | return data; 7 | }) 8 | 9 | export const fetchAuthMe = createAsyncThunk("auth/fetchAuthMe", async () => { 10 | const {data} = await axiosBaseUrl.get("/auth/profile"); 11 | console.log(data); 12 | return data; 13 | }) 14 | 15 | export const fetchRegister = createAsyncThunk("auth/fetchRegister", async (params) => { 16 | const {data} = await axiosBaseUrl.post("/auth/register", params); 17 | return data; 18 | }) 19 | 20 | const initialState = { 21 | data: null, 22 | status: "loading" 23 | } 24 | 25 | const authSlice = createSlice({ 26 | name: "auth", 27 | initialState, 28 | reducers : { 29 | logout: (state) => { 30 | state.data = null; 31 | } 32 | }, 33 | extraReducers:{ 34 | [fetchAuth.pending]: (state)=> { 35 | state.status = "loading" 36 | state.data = null 37 | }, 38 | [fetchAuth.fulfilled]: (state,action)=> { 39 | state.status = "laoded" 40 | state.data = action.payload 41 | }, 42 | [fetchAuth.rejected]: (state)=> { 43 | state.status = "error" 44 | state.data = null 45 | }, 46 | [fetchAuthMe.pending]: (state)=> { 47 | state.status = "loading" 48 | state.data = null 49 | }, 50 | [fetchAuthMe.fulfilled]: (state,action)=> { 51 | state.status = "laoded" 52 | state.data = action.payload 53 | }, 54 | [fetchAuthMe.rejected]: (state)=> { 55 | state.status = "error" 56 | state.data = null 57 | }, 58 | [fetchRegister.pending]: (state)=> { 59 | state.status = "loading" 60 | state.data = null 61 | }, 62 | [fetchRegister.fulfilled]: (state,action)=> { 63 | state.status = "laoded" 64 | state.data = action.payload 65 | }, 66 | [fetchRegister.rejected]: (state)=> { 67 | state.status = "error" 68 | state.data = null 69 | }, 70 | } 71 | }) 72 | 73 | export const authReducer = authSlice.reducer 74 | export const selectIsAuth = state => Boolean(state.auth.data) 75 | export const { logout } = authSlice.actions -------------------------------------------------------------------------------- /client/src/redux/slices/posts.js: -------------------------------------------------------------------------------- 1 | import {createSlice, createAsyncThunk} from "@reduxjs/toolkit" 2 | import axiosBaseUrl from "../../axios"; 3 | 4 | export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => { 5 | const {data} = await axiosBaseUrl.get("/posts"); 6 | return data; 7 | }) 8 | 9 | export const fetchTags = createAsyncThunk("posts/fetchTags", async () => { 10 | const {data} = await axiosBaseUrl.get("/tags"); 11 | return data; 12 | }) 13 | 14 | export const fetchRemovePost = createAsyncThunk("posts/fetchRemovePost", async (id) => { await axiosBaseUrl.delete(`/posts/${id}`);}) 15 | 16 | const initialState = { 17 | posts: { 18 | items: [], 19 | status: "loading" 20 | }, 21 | tags: { 22 | items: [], 23 | status: "loading" 24 | } 25 | } 26 | 27 | const postSlice = createSlice({ 28 | name: "posts", 29 | initialState, 30 | reducers: {}, 31 | extraReducers: { 32 | // Get Posts 33 | [fetchPosts.pending]: (state)=> { 34 | state.posts.items = [] 35 | state.posts.status = "loading" 36 | }, 37 | [fetchPosts.fulfilled]: (state,action)=> { 38 | state.posts.items = action.payload 39 | state.posts.status = "loaded" 40 | }, 41 | [fetchPosts.rejected]: (state)=> { 42 | state.posts.items = [] 43 | state.posts.status = "error" 44 | }, 45 | // Get Tags 46 | [fetchTags.pending]: (state)=> { 47 | state.tags.items = [] 48 | state.tags.status = "loading" 49 | }, 50 | [fetchTags.fulfilled]: (state,action)=> { 51 | state.tags.items = action.payload 52 | state.tags.status = "loaded" 53 | }, 54 | [fetchTags.rejected]: (state)=> { 55 | state.tags.items = [] 56 | state.tags.status = "error" 57 | }, 58 | // Delete Post 59 | [fetchRemovePost.pending]: (state, action)=> { 60 | state.posts.items = state.posts.items.filter(obj => obj._id !== action.meta.arg) 61 | }, 62 | } 63 | }) 64 | 65 | export const postsReducer = postSlice.reducer; -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import {configureStore} from "@reduxjs/toolkit" 2 | import { postsReducer } from "./slices/posts"; 3 | import { authReducer } from "./slices/auth"; 4 | const store = configureStore({ 5 | reducer: { 6 | posts: postsReducer, 7 | auth: authReducer 8 | } 9 | }) 10 | 11 | export default store; -------------------------------------------------------------------------------- /client/src/theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@mui/material/styles"; 2 | 3 | export const theme = createTheme({ 4 | shadows: ["none"], 5 | palette: { 6 | primary: { 7 | main: "#4361ee", 8 | }, 9 | }, 10 | typography: { 11 | button: { 12 | textTransform: "none", 13 | fontWeight: 400, 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules -------------------------------------------------------------------------------- /server/controllers/PostController.js: -------------------------------------------------------------------------------- 1 | import PostModel from "../models/Post.js" 2 | 3 | export const getAll = async(req,res)=> { 4 | try { 5 | const posts = await PostModel.find().populate("user").exec(); 6 | res.json(posts) 7 | } catch (error) { 8 | console.log(error); 9 | res.status(500).json({ 10 | message: "Не удалось создать статьи" 11 | }) 12 | } 13 | } 14 | 15 | export const getLastTags = async (req,res)=> { 16 | try { 17 | const posts = await PostModel.find().limit(5).exec(); 18 | 19 | const tags = posts.map(obj => obj.tags.slice(0,5)) 20 | 21 | const tags_filter = tags.filter((el)=> { 22 | return (el.length != 0) 23 | }).flat() 24 | 25 | res.json(tags_filter) 26 | } catch (error) { 27 | console.log(error); 28 | res.status(500).json({ 29 | message: "Не удалось создать статьи" 30 | }) 31 | } 32 | } 33 | 34 | export const getOne = async(req,res)=> { 35 | try { 36 | const postId = req.params.id; 37 | 38 | PostModel.findOneAndUpdate({ 39 | _id:postId 40 | },{ 41 | $inc: { viewsCount : 1} 42 | },{ 43 | returnDocument: "after" 44 | }, 45 | (err,doc)=> { 46 | if(err){ 47 | console.log(error); 48 | return res.status(500).json({ 49 | message: "Не удалось вернуть статью" 50 | }) 51 | } 52 | 53 | if(!doc){ 54 | return res.status(404).json({ 55 | message: "Статья не найдена" 56 | }) 57 | } 58 | 59 | res.json(doc) 60 | } 61 | ).populate("user") 62 | 63 | } catch (error) { 64 | console.log(error); 65 | res.status(500).json({ 66 | message: "Не удалось создать статью" 67 | }) 68 | } 69 | } 70 | 71 | export const remove = async(req,res)=> { 72 | try { 73 | const postId = req.params.id; 74 | 75 | PostModel.findOneAndDelete({ 76 | _id:postId 77 | }, (err,doc)=>{ 78 | if(err){ 79 | console.log(error); 80 | return res.status(500).json({ 81 | message: "Не удалось удалить статью" 82 | }) 83 | } 84 | 85 | if(!doc){ 86 | return res.status(404).json({ 87 | message: "Статья не найдена" 88 | }) 89 | } 90 | 91 | res.json({ 92 | success: true 93 | }) 94 | }) 95 | 96 | } catch (error) { 97 | console.log(error); 98 | res.status(500).json({ 99 | message: "Не удалось создать статью" 100 | }) 101 | } 102 | } 103 | 104 | export const create = async (req,res)=> { 105 | try { 106 | const doc = new PostModel({ 107 | title: req.body.title, 108 | text: req.body.text, 109 | imageUrl: req.body.imageUrl, 110 | tags: req.body.tags.split(","), 111 | user: req.userId 112 | }); 113 | 114 | const post = await doc.save(); 115 | 116 | res.json(post) 117 | 118 | } catch (error) { 119 | console.log(error); 120 | res.status(500).json({ 121 | message: "Не удалось создать статью" 122 | }) 123 | } 124 | } 125 | 126 | export const update = async (req,res)=> { 127 | try { 128 | const postId = req.params.id; 129 | 130 | await PostModel.updateOne({ 131 | _id:postId 132 | },{ 133 | title: req.body.title, 134 | text: req.body.text, 135 | imageUrl: req.body.imageUrl, 136 | user: req.userId, 137 | tags: req.body.tags.split(" "), 138 | }) 139 | 140 | res.json({ 141 | success:true 142 | }) 143 | } catch (error) { 144 | console.log(error); 145 | res.status(500).json({ 146 | message: "Не удалось обновить статью" 147 | }) 148 | } 149 | } -------------------------------------------------------------------------------- /server/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | 2 | import jwt from "jsonwebtoken" 3 | import UserModel from "../models/User.js" 4 | import bcrypt from "bcrypt"; 5 | 6 | export const register =async (req,res)=> { 7 | 8 | try{ 9 | 10 | 11 | const password = req.body.password; 12 | const salt = await bcrypt.genSalt(10) 13 | const hash = await bcrypt.hash(password, salt) 14 | 15 | const doc = new UserModel({ 16 | email: req.body.email, 17 | fullName: req.body.fullName, 18 | avatarUrl: req.body.avatarUrl, 19 | passwordHash : hash 20 | }) 21 | 22 | const user = await doc.save(); 23 | 24 | const token = jwt.sign({ 25 | _id: user._id 26 | },"hash",{ 27 | expiresIn: "30d" 28 | }) 29 | 30 | const {passwordHash, ...userData } = user._doc 31 | 32 | res.json({ 33 | ...userData, 34 | token 35 | }) 36 | }catch(err){ 37 | console.log(err); 38 | res.status(500).json({ 39 | message: "Не удалось зарегистрироваться", 40 | }) 41 | } 42 | } 43 | 44 | export const login = async (req,res)=> { 45 | try{ 46 | const user = await UserModel.findOne({email: req.body.email}); 47 | 48 | if(!user){ 49 | return req.status(404).json({ 50 | message: "Пользователь не найден" 51 | }) 52 | } 53 | 54 | const isValidPass = await bcrypt.compare(req.body.password, user._doc.passwordHash); 55 | 56 | if(!isValidPass){ 57 | return res.status(400).json({ 58 | message: "Неверный логин или пароль" 59 | }) 60 | } 61 | 62 | const token = jwt.sign({ 63 | _id: user._id 64 | },"hash",{ 65 | expiresIn: "30d" 66 | }) 67 | 68 | const {passwordHash, ...userData } = user._doc 69 | 70 | res.json({ 71 | ...userData, 72 | token 73 | }) 74 | }catch(err){ 75 | console.log(err); 76 | res.status(500).json({ 77 | message: "Не удалось авторизоваться", 78 | }) 79 | } 80 | } 81 | 82 | export const getUserInfo = async (req,res)=> { 83 | try { 84 | 85 | const user = await UserModel.findById(req.userId) 86 | if(!user){ 87 | return res.status(404)({ 88 | message: "Пользователь не надйен" 89 | }) 90 | } 91 | 92 | const {passwordHash, ...userData } = user._doc 93 | 94 | res.json(userData) 95 | 96 | } catch (error) { 97 | console.log(error); 98 | res.status(500).json({ 99 | message: "Нет доступа" 100 | }) 101 | } 102 | } 103 | 104 | export const update = async (req,res) => { 105 | try { 106 | const userId = req.params.id; 107 | await UserModel.updateOne({ 108 | _id:userId 109 | },{ 110 | fullName: req.body.fullName, 111 | email: req.body.email, 112 | avatarUrl: req.body.avatarUrl, 113 | 114 | }) 115 | 116 | res.json({ 117 | success:true 118 | }) 119 | } catch (error) { 120 | console.log(error); 121 | res.status(500).json({ 122 | message: "Не удалось изменить пользовательские данные" 123 | }) 124 | } 125 | } -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | 2 | export * as UserController from "./UserController.js" 3 | export * as PostController from "./PostController.js" 4 | 5 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import mongoose from "mongoose" 3 | import multer from "multer" 4 | import fs from "fs" 5 | import { registerValidation, loginValidation, updateValidation } from "./validations/auth.js" 6 | import {UserController, PostController} from "./controllers/index.js" 7 | import { postCreateValidation } from "./validations/post.js"; 8 | import cors from "cors" 9 | import {handleValidationErrors,chekAuth} from "./utils/index.js" 10 | mongoose.connect(process.env.MONGODB_URI, { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true 13 | }) 14 | .then(()=> { 15 | console.log("DB is connected"); 16 | }) 17 | .catch((err)=> { 18 | console.log("DB error"+err); 19 | }) 20 | 21 | const app = express() 22 | 23 | const storage = multer.diskStorage({ 24 | destination: (_, file, cb)=> { 25 | if(!fs.existsSync("uploads")){ 26 | fs.mkdirSync("uploads") 27 | } 28 | cb(null, "uploads"); 29 | }, 30 | filename: (_, file, cb)=> { 31 | cb(null, file.originalname) 32 | } 33 | }) 34 | 35 | const upload = multer({storage}) 36 | 37 | app.use(express.json()) 38 | app.use(cors()) 39 | app.use("/uploads", express.static("uploads")) 40 | 41 | app.post("/auth/login",loginValidation, handleValidationErrors,UserController.login ) 42 | app.post("/auth/register", registerValidation,handleValidationErrors, UserController.register ) 43 | app.get("/auth/profile",chekAuth, UserController.getUserInfo) 44 | app.patch("/auth/profile/:id",chekAuth, updateValidation, handleValidationErrors, UserController.update) 45 | 46 | app.post("/upload", upload.single("image"),(req,res)=>{ 47 | res.json({ 48 | url:`/uploads/${req.file.originalname}` 49 | }) 50 | }) 51 | 52 | app.get("/tags",PostController.getLastTags) 53 | app.get("/posts",PostController.getAll) 54 | app.get("/posts/tags",PostController.getLastTags) 55 | app.get("/posts/:id", PostController.getOne) 56 | app.post("/posts", chekAuth, postCreateValidation,handleValidationErrors, PostController.create) 57 | app.delete("/posts/:id", chekAuth, PostController.remove) 58 | app.patch("/posts/:id",chekAuth, postCreateValidation,handleValidationErrors,PostController.update) 59 | 60 | 61 | app.listen(5000, (err)=>{ 62 | if(err){ 63 | return console.log(err); 64 | } 65 | console.log("Server started without problems"); 66 | }) -------------------------------------------------------------------------------- /server/models/Post.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const PostSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true 7 | }, 8 | text: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | tags:{ 14 | type: Array, 15 | default: [] 16 | }, 17 | viewsCount: { 18 | type:Number, 19 | default: 0 20 | }, 21 | user: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | ref: "User", 24 | required: true 25 | }, 26 | imageUrl: String 27 | }, { 28 | timestamps: true, 29 | 30 | }); 31 | 32 | export default mongoose.model("Post", PostSchema) 33 | 34 | -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | fullName: { 5 | type: String, 6 | required: true 7 | }, 8 | email: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | passwordHash:{ 14 | type: String, 15 | required: true 16 | }, 17 | avatarUrl: String 18 | }, { 19 | timestamps: true, 20 | 21 | }); 22 | 23 | export default mongoose.model("User", UserSchema) 24 | 25 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "nodemon index.js", 9 | "index": "node index.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bcrypt": "^5.1.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.0.3", 18 | "express": "^4.18.2", 19 | "express-validator": "^6.14.2", 20 | "jsonwebtoken": "^9.0.0", 21 | "mongoose": "^6.8.3", 22 | "multer": "^1.4.5-lts.1", 23 | "nodemon": "^2.0.20" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/uploads/wp8420305-genshin-impact-hd-desktop-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bekturmsv/Web-Blog/7e279a49149ca2718710bb0f3c32654b2baed72b/server/uploads/wp8420305-genshin-impact-hd-desktop-wallpapers.jpg -------------------------------------------------------------------------------- /server/utils/chekAuth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export default (req,res,next)=>{ 4 | 5 | const token = ( req.headers.authorization || "").replace(/Bearer\s?/, ""); 6 | 7 | if(token){ 8 | try { 9 | const decoded = jwt.verify(token, "hash"); 10 | 11 | req.userId = decoded._id; 12 | 13 | next() 14 | } catch (error) { 15 | return res.status(403).json({ 16 | message: 'Нет доступа' 17 | }) 18 | } 19 | }else{ 20 | return res.status(403).json({ 21 | message: 'Нет доступа' 22 | }) 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /server/utils/handleValidationErrors.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from "express-validator" 2 | 3 | export default (req,res,next) => { 4 | const errors = validationResult(req); 5 | 6 | if(!errors.isEmpty()){ 7 | return res.status(400).json(errors.array()); 8 | } 9 | 10 | next(); 11 | } -------------------------------------------------------------------------------- /server/utils/index.js: -------------------------------------------------------------------------------- 1 | export {default as handleValidationErrors} from "./handleValidationErrors.js"; 2 | export {default as chekAuth} from "./chekAuth.js"; 3 | -------------------------------------------------------------------------------- /server/validations/auth.js: -------------------------------------------------------------------------------- 1 | import {body} from "express-validator" 2 | 3 | export const registerValidation =[ 4 | body("email", "Неверный формат почты").isEmail(), 5 | body("password","Пароль должен быть минимум 7 символов").isLength({min: 7}), 6 | body("fullName","Укажите имя").isLength({min: 3}), 7 | body("avatarUrl","Неверная ссылка на аватарку").optional().isString(), 8 | ] 9 | 10 | export const loginValidation =[ 11 | body("email", "Неверный формат почты").isEmail(), 12 | body("password","Пароль должен быть минимум 7 символов").isLength({min: 7}), 13 | ] 14 | 15 | export const updateValidation = [ 16 | body("fullName", "Укажите новое имя").isLength({min: 3}), 17 | body("email", "Неверный формат почты").isEmail(), 18 | body("avatarUrl","Неверная ссылка на аватарку").optional().isString(), 19 | 20 | ] -------------------------------------------------------------------------------- /server/validations/post.js: -------------------------------------------------------------------------------- 1 | import {body} from "express-validator" 2 | 3 | export const postCreateValidation =[ 4 | body("title", "Введите заголовок статьи").isLength({min:3}).isString(), 5 | body("text","Введите текст статьи").isLength({min: 5}).isString(), 6 | body("tags","Неверный формат тэгов (укажите массив)").optional().isString(), 7 | body("imageUrl","Неверная ссылка на изображение").optional().isString(), 8 | ] --------------------------------------------------------------------------------