├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── index.html ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── app │ ├── hooks.ts │ ├── services │ │ ├── api.ts │ │ ├── commentsApi.ts │ │ ├── followApi.ts │ │ ├── likesApi.ts │ │ ├── postsApi.ts │ │ └── userApi.ts │ ├── store.ts │ └── types.ts ├── components │ ├── button │ │ └── index.tsx │ ├── card │ │ └── index.tsx │ ├── container │ │ └── index.tsx │ ├── count-info │ │ └── index.tsx │ ├── create-comment │ │ └── index.tsx │ ├── create-post │ │ └── index.tsx │ ├── edit-profile │ │ └── index.tsx │ ├── error-message │ │ └── index.tsx │ ├── go-back │ │ └── index.tsx │ ├── header │ │ └── index.tsx │ ├── input │ │ └── index.tsx │ ├── layout │ │ └── index.tsx │ ├── meta-info │ │ └── index.tsx │ ├── nav-bar │ │ └── index.tsx │ ├── nav-button │ │ └── index.tsx │ ├── profile-info │ │ └── index.tsx │ ├── profile │ │ └── index.tsx │ ├── theme-provider │ │ └── index.tsx │ ├── typography │ │ └── index.tsx │ └── user │ │ └── index.tsx ├── constants.ts ├── features │ └── user │ │ ├── authGuard.tsx │ │ ├── login.tsx │ │ ├── register.tsx │ │ └── userSlice.ts ├── hooks │ └── useAuthGuard.ts ├── index.css ├── logo.svg ├── main.tsx ├── middleware │ └── auth.ts ├── pages │ ├── auth │ │ └── index.tsx │ ├── current-post │ │ └── index.tsx │ ├── followers │ │ └── index.tsx │ ├── following │ │ └── index.tsx │ ├── posts │ │ └── index.tsx │ └── user-profile │ │ └── index.tsx ├── setupTests.ts ├── utils │ ├── format-to-client-date.ts │ └── has-error-field.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.donationalerts.com/r/maxroslow'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # Dependencies 11 | node_modules 12 | .yarn/* 13 | !.yarn/patches 14 | !.yarn/plugins 15 | !.yarn/releases 16 | !.yarn/sdks 17 | !.yarn/versions 18 | # Swap the comments on the following lines if you don't wish to use zero-installs 19 | # Documentation here: https://yarnpkg.com/features/zero-installs 20 | !.yarn/cache 21 | #.pnp.* 22 | 23 | # Testing 24 | coverage 25 | 26 | # Production 27 | build 28 | 29 | # Miscellaneous 30 | *.local 31 | 32 | vite.config.ts.*.mjs -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build React Application 2 | FROM node:14 AS build 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY package*.json ./ 7 | 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | # Stage 2: Serve React Application with Nginx 15 | FROM nginx:stable-alpine 16 | 17 | COPY --from=build /usr/src/app/build /usr/share/nginx/html 18 | 19 | COPY nginx.conf /etc/nginx/conf.d/default.conf 20 | 21 | EXPOSE 8000 22 | 23 | CMD ["nginx", "-g", "daemon off;"] 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Для запуска проекта, необходимо выполнить следующие шаги: 2 | 3 | 1. Склонировать репозиторий с клиентским приложением по ссылке https://github.com/brian7346/react-threads.git на свой компьютер. 4 | ``` 5 | git clone https://github.com/brian7346/react-threads.git 6 | ``` 7 | 8 | 2. Склонировать репозиторий с api по ссылке [https://github.com/brian7346/express-threads-api.git](https://github.com/brian7346/express-threads-api/tree/main) на свой компьютер. 9 | ``` 10 | git clone https://github.com/brian7346/express-threads-api.git 11 | ``` 12 | 13 | 3. Открыть терминал (или командную строку) и перейти в корневую директорию сервера. 14 | ``` 15 | cd express-threads-api 16 | ``` 17 | 18 | 4. Переименовать файл .env.local (убрать .local) 19 | ``` 20 | .env 21 | ``` 22 | 23 | 5. Запустить команду docker compose которая поднимет сервер, клиент и базу данных 24 | ``` 25 | docker compose up 26 | ``` 27 | 28 | 6. Открыть браузер и перейти по адресу http://localhost:80, чтобы увидеть запущенный проект. 29 | 30 | 31 | 32 | # Если вы хотите скачать образ базы данных MongoDB 33 | 34 | Запустите контейнер с образом MongoDB и настройками replica set (он автоматичиски скачает и запустит этот образ): 35 | 36 | ``` 37 | docker run --name mongo \ 38 | -p 27017:27017 \ 39 | -e MONGO_INITDB_ROOT_USERNAME="monty" \ 40 | -e MONGO_INITDB_ROOT_PASSWORD="pass" \ 41 | -d prismagraphql/mongo-single-replica:5.0.3 42 | ``` 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Social 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; # Путь к папке с собранными статическими файлами вашего приложения 7 | index index.html index.htm; 8 | try_files $uri $uri/ /index.html =404; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-template-redux", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "start": "vite", 9 | "build": "tsc && vite build", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "format": "prettier --write .", 13 | "lint": "eslint .", 14 | "type-check": "tsc" 15 | }, 16 | "dependencies": { 17 | "@nextui-org/react": "^2.1.13", 18 | "@reduxjs/toolkit": "^1.8.1", 19 | "framer-motion": "^10.16.4", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-hook-form": "^7.46.2", 23 | "react-icons": "^4.11.0", 24 | "react-redux": "^8.0.1", 25 | "react-router-dom": "^6.16.0" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/dom": "^9.2.0", 29 | "@testing-library/jest-dom": "^5.11.4", 30 | "@testing-library/react": "^14.0.0", 31 | "@testing-library/user-event": "^14.2.5", 32 | "@types/react": "^18.0.15", 33 | "@types/react-dom": "^18.0.6", 34 | "@types/testing-library__jest-dom": "^5.14.5", 35 | "@vitejs/plugin-react": "^4.0.0", 36 | "autoprefixer": "^10.4.15", 37 | "eslint": "^8.0.0", 38 | "eslint-config-react-app": "^7.0.1", 39 | "eslint-plugin-prettier": "^4.2.1", 40 | "jsdom": "^21.1.0", 41 | "postcss": "^8.4.29", 42 | "prettier": "^2.7.1", 43 | "prettier-config-nick": "^1.0.2", 44 | "tailwindcss": "^3.3.3", 45 | "typescript": "^5.0.2", 46 | "vite": "^4.0.0", 47 | "vitest": "^0.30.1" 48 | }, 49 | "eslintConfig": { 50 | "extends": [ 51 | "react-app", 52 | "react-app/jest" 53 | ], 54 | "plugins": [ 55 | "prettier" 56 | ], 57 | "rules": { 58 | "react/jsx-no-target-blank": "off" 59 | } 60 | }, 61 | "prettier": "prettier-config-nick" 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" 2 | import type { RootState, AppDispatch } from "./store" 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch: () => AppDispatch = useDispatch 6 | export const useAppSelector: TypedUseSelectorHook = useSelector 7 | -------------------------------------------------------------------------------- /src/app/services/api.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery, retry } from "@reduxjs/toolkit/query/react" 2 | import { RootState } from "../store" 3 | import { BASE_URL } from "../../constants" 4 | 5 | const baseQuery = fetchBaseQuery({ 6 | baseUrl: `${BASE_URL}/api`, 7 | prepareHeaders: (headers, { getState }) => { 8 | const token = 9 | (getState() as RootState).auth.token || localStorage.getItem("token") 10 | 11 | if (token) { 12 | headers.set("authorization", `Bearer ${token}`) 13 | } 14 | return headers 15 | }, 16 | }) 17 | 18 | const baseQueryWithRetry = retry(baseQuery, { maxRetries: 0 }) 19 | 20 | export const api = createApi({ 21 | reducerPath: "splitApi", 22 | baseQuery: baseQueryWithRetry, 23 | refetchOnMountOrArgChange: true, 24 | endpoints: () => ({}), 25 | }) 26 | -------------------------------------------------------------------------------- /src/app/services/commentsApi.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from "../types" 2 | import { api } from "./api" 3 | 4 | export const commentsApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | createComment: builder.mutation>({ 7 | query: (newComment) => ({ 8 | url: `/comments`, 9 | method: "POST", 10 | body: newComment, 11 | }), 12 | }), 13 | deleteComment: builder.mutation({ 14 | query: (commentId) => ({ 15 | url: `/comments/${commentId}`, 16 | method: "DELETE", 17 | }), 18 | }), 19 | }), 20 | }) 21 | 22 | export const { useCreateCommentMutation, useDeleteCommentMutation } = 23 | commentsApi 24 | 25 | export const { 26 | endpoints: { createComment, deleteComment }, 27 | } = commentsApi 28 | -------------------------------------------------------------------------------- /src/app/services/followApi.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api" 2 | 3 | export const followApi = api.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | followUser: builder.mutation({ 6 | query: (body) => ({ 7 | url: `/follow`, 8 | method: "POST", 9 | body, 10 | }), 11 | }), 12 | unfollowUser: builder.mutation({ 13 | query: (userId) => ({ 14 | url: `/unfollow/${userId}`, 15 | method: "DELETE", 16 | }), 17 | }), 18 | }), 19 | }) 20 | 21 | export const { useFollowUserMutation, useUnfollowUserMutation } = followApi 22 | 23 | export const { 24 | endpoints: { followUser, unfollowUser }, 25 | } = followApi 26 | -------------------------------------------------------------------------------- /src/app/services/likesApi.ts: -------------------------------------------------------------------------------- 1 | import { Like } from "../types" 2 | import { api } from "./api" 3 | 4 | export const likesApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | likePost: builder.mutation({ 7 | query: (body) => ({ 8 | url: "/likes", 9 | method: "POST", 10 | body, 11 | }), 12 | }), 13 | unlikePost: builder.mutation({ 14 | query: (postId) => ({ 15 | url: `/likes/${postId}`, 16 | method: "DELETE", 17 | }), 18 | }), 19 | }), 20 | }) 21 | 22 | export const { useLikePostMutation, useUnlikePostMutation } = likesApi 23 | 24 | export const { 25 | endpoints: { likePost, unlikePost }, 26 | } = likesApi 27 | -------------------------------------------------------------------------------- /src/app/services/postsApi.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "../types" 2 | import { api } from "./api" 3 | 4 | export const postApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | createPost: builder.mutation({ 7 | query: (postData) => ({ 8 | url: "/posts", 9 | method: "POST", 10 | body: postData, 11 | }), 12 | }), 13 | getAllPosts: builder.query({ 14 | query: () => ({ 15 | url: "/posts", 16 | method: "GET", 17 | }), 18 | }), 19 | getPostById: builder.query({ 20 | query: (id) => ({ 21 | url: `/posts/${id}`, 22 | method: "GET", 23 | }), 24 | }), 25 | deletePost: builder.mutation({ 26 | query: (id) => ({ 27 | url: `/posts/${id}`, 28 | method: "DELETE", 29 | }), 30 | }), 31 | }), 32 | }) 33 | 34 | export const { 35 | useCreatePostMutation, 36 | useGetAllPostsQuery, 37 | useGetPostByIdQuery, 38 | useDeletePostMutation, 39 | useLazyGetAllPostsQuery, 40 | useLazyGetPostByIdQuery, 41 | } = postApi 42 | 43 | export const { 44 | endpoints: { createPost, getAllPosts, getPostById, deletePost }, 45 | } = postApi 46 | -------------------------------------------------------------------------------- /src/app/services/userApi.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../types" 2 | import { api } from "./api" 3 | 4 | export const userApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | login: builder.mutation< 7 | { token: string }, 8 | { email: string; password: string } 9 | >({ 10 | query: (userData) => ({ 11 | url: "/login", 12 | method: "POST", 13 | body: userData, 14 | }), 15 | }), 16 | register: builder.mutation< 17 | { email: string; password: string; name: string }, 18 | { email: string; password: string; name: string } 19 | >({ 20 | query: (userData) => ({ 21 | url: "/register", 22 | method: "POST", 23 | body: userData, 24 | }), 25 | }), 26 | current: builder.query({ 27 | query: () => ({ 28 | url: "/current", 29 | method: "GET", 30 | }), 31 | }), 32 | getUserById: builder.query({ 33 | query: (id) => ({ 34 | url: `/users/${id}`, 35 | method: "GET", 36 | }), 37 | }), 38 | updateUser: builder.mutation({ 39 | query: ({ userData, id }) => ({ 40 | url: `/users/${id}`, 41 | method: "PUT", 42 | body: userData, 43 | }), 44 | }), 45 | }), 46 | }) 47 | 48 | export const { 49 | useRegisterMutation, 50 | useLoginMutation, 51 | useCurrentQuery, 52 | useLazyCurrentQuery, 53 | useGetUserByIdQuery, 54 | useLazyGetUserByIdQuery, 55 | useUpdateUserMutation, 56 | } = userApi 57 | 58 | export const { 59 | endpoints: { login, register, current, getUserById, updateUser }, 60 | } = userApi 61 | -------------------------------------------------------------------------------- /src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit" 2 | import { api } from "./services/api" 3 | import auth from "../features/user/userSlice" 4 | import { listenerMiddleware } from "../middleware/auth" 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | [api.reducerPath]: api.reducer, 9 | auth, 10 | }, 11 | middleware: (getDefaultMiddleware) => 12 | getDefaultMiddleware() 13 | .concat(api.middleware) 14 | .prepend(listenerMiddleware.middleware), 15 | }) 16 | 17 | export type AppDispatch = typeof store.dispatch 18 | export type RootState = ReturnType 19 | export type AppThunk = ThunkAction< 20 | ReturnType, 21 | RootState, 22 | unknown, 23 | Action 24 | > 25 | -------------------------------------------------------------------------------- /src/app/types.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string 3 | email: string 4 | password: string 5 | name?: string 6 | avatarUrl?: string 7 | dateOfBirth?: Date 8 | createdAt: Date 9 | updatedAt: Date 10 | bio?: string 11 | location?: string 12 | posts: Post[] 13 | following: Follows[] 14 | followers: Follows[] 15 | likes: Like[] 16 | comments: Comment[] 17 | isFollowing?: boolean 18 | } 19 | 20 | export type Follows = { 21 | id: string 22 | follower: User 23 | followerId: string 24 | following: User 25 | followingId: string 26 | } 27 | 28 | export type Post = { 29 | id: string 30 | content: string 31 | author: User 32 | authorId: string 33 | likes: Like[] 34 | comments: Comment[] 35 | likedByUser: boolean 36 | createdAt: Date 37 | updatedAt: Date 38 | } 39 | 40 | export type Like = { 41 | id: string 42 | user: User 43 | userId: string 44 | post: Post 45 | postId: string 46 | } 47 | 48 | export type Comment = { 49 | id: string 50 | content: string 51 | user: User 52 | userId: string 53 | post: Post 54 | postId: string 55 | } 56 | -------------------------------------------------------------------------------- /src/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button as NextButton } from "@nextui-org/react" 2 | import React from "react" 3 | 4 | type Props = { 5 | children: React.ReactNode 6 | icon?: JSX.Element 7 | className?: string 8 | type?: "button" | "submit" | "reset" 9 | fullWidth?: boolean 10 | color?: 11 | | "default" 12 | | "primary" 13 | | "secondary" 14 | | "success" 15 | | "warning" 16 | | "danger" 17 | | undefined 18 | } 19 | 20 | export const Button: React.FC = ({ 21 | children, 22 | icon, 23 | className, 24 | type, 25 | fullWidth, 26 | color, 27 | }) => { 28 | return ( 29 | 38 | {children} 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/card/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card as NextUiCard, 3 | CardHeader, 4 | CardBody, 5 | CardFooter, 6 | } from "@nextui-org/react" 7 | import { MetaInfo } from "../meta-info" 8 | import { Typography } from "../typography" 9 | import { User } from "../user" 10 | import { Link, useNavigate } from "react-router-dom" 11 | import { FaRegComment } from "react-icons/fa6" 12 | import { 13 | useUnlikePostMutation, 14 | useLikePostMutation, 15 | } from "../../app/services/likesApi" 16 | import { 17 | useDeletePostMutation, 18 | useLazyGetAllPostsQuery, 19 | useLazyGetPostByIdQuery, 20 | } from "../../app/services/postsApi" 21 | import { FcDislike } from "react-icons/fc" 22 | import { MdOutlineFavoriteBorder } from "react-icons/md" 23 | import { formatToClientDate } from "../../utils/format-to-client-date" 24 | import { RiDeleteBinLine } from "react-icons/ri" 25 | import { useSelector } from "react-redux" 26 | import { selectCurrent } from "../../features/user/userSlice" 27 | import { useDeleteCommentMutation } from "../../app/services/commentsApi" 28 | import { Spinner } from "@nextui-org/react" 29 | import { ErrorMessage } from "../error-message" 30 | import { useState } from "react" 31 | import { hasErrorField } from "../../utils/has-error-field" 32 | 33 | type Props = { 34 | avatarUrl: string 35 | name: string 36 | authorId: string 37 | content: string 38 | commentId?: string 39 | likesCount?: number 40 | commentsCount?: number 41 | createdAt?: Date 42 | id?: string 43 | cardFor: "comment" | "post" | "current-post" 44 | likedByUser?: boolean 45 | } 46 | 47 | export const Card = ({ 48 | avatarUrl = "", 49 | name = "", 50 | content = "", 51 | authorId = "", 52 | id = "", 53 | likesCount = 0, 54 | commentsCount = 0, 55 | cardFor = "post", 56 | likedByUser = false, 57 | createdAt, 58 | commentId = "", 59 | }: Props) => { 60 | const [likePost] = useLikePostMutation() 61 | const [unlikePost] = useUnlikePostMutation() 62 | const [triggerGetAllPosts] = useLazyGetAllPostsQuery() 63 | const [triggerGetPostById] = useLazyGetPostByIdQuery() 64 | const [deletePost, deletePostStatus] = useDeletePostMutation() 65 | const [deleteComment, deleteCommentStatus] = useDeleteCommentMutation() 66 | const [error, setError] = useState("") 67 | const navigate = useNavigate() 68 | const currentUser = useSelector(selectCurrent) 69 | 70 | const refetchPosts = async () => { 71 | switch (cardFor) { 72 | case "post": 73 | await triggerGetAllPosts().unwrap() 74 | break 75 | case "current-post": 76 | await triggerGetAllPosts().unwrap() 77 | break 78 | case "comment": 79 | await triggerGetPostById(id).unwrap() 80 | break 81 | default: 82 | throw new Error("Неверный аргумент cardFor") 83 | } 84 | } 85 | 86 | const handleClick = async () => { 87 | try { 88 | likedByUser 89 | ? await unlikePost(id).unwrap() 90 | : await likePost({ postId: id }).unwrap() 91 | 92 | await refetchPosts() 93 | } catch (err) { 94 | if (hasErrorField(err)) { 95 | setError(err.data.error) 96 | } else { 97 | setError(err as string) 98 | } 99 | } 100 | } 101 | 102 | const handleDelete = async () => { 103 | try { 104 | switch (cardFor) { 105 | case "post": 106 | await deletePost(id).unwrap() 107 | await refetchPosts() 108 | break 109 | case "current-post": 110 | await deletePost(id).unwrap() 111 | navigate('/') 112 | break 113 | case "comment": 114 | await deleteComment(commentId).unwrap() 115 | await refetchPosts() 116 | break 117 | default: 118 | throw new Error("Неверный аргумент cardFor") 119 | } 120 | 121 | } catch (err) { 122 | console.log(err) 123 | if (hasErrorField(err)) { 124 | setError(err.data.error) 125 | } else { 126 | setError(err as string) 127 | } 128 | } 129 | } 130 | 131 | return ( 132 | 133 | 134 | 135 | 141 | 142 | {authorId === currentUser?.id && ( 143 |
144 | {deletePostStatus.isLoading || deleteCommentStatus.isLoading ? ( 145 | 146 | ) : ( 147 | 148 | )} 149 |
150 | )} 151 |
152 | 153 | {content} 154 | 155 | {cardFor !== "comment" && ( 156 | 157 |
158 |
159 | 163 |
164 | 165 | 166 | 167 |
168 | 169 |
170 | )} 171 |
172 | ) 173 | } 174 | -------------------------------------------------------------------------------- /src/components/container/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | type Props = { 4 | children: React.ReactElement[] | React.ReactElement 5 | } 6 | 7 | export const Container: React.FC = ({ children }) => { 8 | return
{children}
9 | } 10 | -------------------------------------------------------------------------------- /src/components/count-info/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | count: number; 5 | title: string; 6 | } 7 | 8 | export const CountInfo: React.FC = ({ 9 | count, 10 | title, 11 | }) => { 12 | return ( 13 |
14 | {count} 15 | {title} 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/create-comment/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Textarea } from "@nextui-org/react" 2 | import { IoMdCreate } from "react-icons/io" 3 | import { useForm, Controller } from "react-hook-form" 4 | import { ErrorMessage } from "../error-message" 5 | import { useCreateCommentMutation } from "../../app/services/commentsApi" 6 | import { useParams } from "react-router-dom" 7 | import { useLazyGetPostByIdQuery } from "../../app/services/postsApi" 8 | 9 | export const CreateComment = () => { 10 | const { id } = useParams<{ id: string }>() 11 | const [createComment] = useCreateCommentMutation() 12 | const [getPostById] = useLazyGetPostByIdQuery() 13 | 14 | const { 15 | handleSubmit, 16 | control, 17 | formState: { errors }, 18 | setValue, 19 | } = useForm() 20 | 21 | const onSubmit = handleSubmit(async (data) => { 22 | try { 23 | if (id) { 24 | await createComment({ content: data.comment, postId: id }).unwrap() 25 | await getPostById(id).unwrap() 26 | setValue("comment", "") 27 | } 28 | } catch (error) { 29 | console.log("err", error) 30 | } 31 | }) 32 | 33 | const error = errors?.comment?.message as string 34 | 35 | return ( 36 |
37 | ( 45 |