├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/client/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/client/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 | Send
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 |
Write an article
34 |
35 |
40 | Log out
41 |
42 |
43 |
48 |
49 | >
50 | ) : (
51 | <>
52 |
53 |
Log in
54 |
55 |
56 |
Create Account
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 | {/* inputImageRef.current.click()}
107 | variant="outlined"
108 | size="large"
109 | >
110 | {isEditing ? "Change preview" : "Download previews"}
111 |
112 |
118 | {postObj.imageUrl && (
119 | <>
120 |
125 | Delete
126 |
127 |
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 |
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 |
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 |
132 | >
133 | )}
134 | {isEdit ? (
135 |
136 | SAVE
137 |
138 | ) : (
139 |
setEdit(true)}
143 | >
144 | EDIT
145 |
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 |
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 | ]
--------------------------------------------------------------------------------