├── src ├── vite-env.d.ts ├── setupTests.ts ├── constants.ts ├── utils │ ├── format-to-client-date.ts │ └── has-error-field.ts ├── components │ ├── error-message │ │ └── index.tsx │ ├── typography │ │ └── index.tsx │ ├── container │ │ └── index.tsx │ ├── count-info │ │ └── index.tsx │ ├── profile-info │ │ └── index.tsx │ ├── nav-button │ │ └── index.tsx │ ├── go-back │ │ └── index.tsx │ ├── meta-info │ │ └── index.tsx │ ├── user │ │ └── index.tsx │ ├── nav-bar │ │ └── index.tsx │ ├── button │ │ └── index.tsx │ ├── theme-provider │ │ └── index.tsx │ ├── input │ │ └── index.tsx │ ├── layout │ │ └── index.tsx │ ├── profile │ │ └── index.tsx │ ├── create-post │ │ └── index.tsx │ ├── header │ │ └── index.tsx │ ├── create-comment │ │ └── index.tsx │ ├── edit-profile │ │ └── index.tsx │ └── card │ │ └── index.tsx ├── features │ └── user │ │ ├── authGuard.tsx │ │ ├── userSlice.ts │ │ ├── register.tsx │ │ └── login.tsx ├── app │ ├── hooks.ts │ ├── services │ │ ├── followApi.ts │ │ ├── likesApi.ts │ │ ├── api.ts │ │ ├── commentsApi.ts │ │ ├── postsApi.ts │ │ └── userApi.ts │ ├── store.ts │ └── types.ts ├── hooks │ └── useAuthGuard.ts ├── middleware │ └── auth.ts ├── index.css ├── logo.svg ├── pages │ ├── followers │ │ └── index.tsx │ ├── following │ │ └── index.tsx │ ├── auth │ │ └── index.tsx │ ├── posts │ │ └── index.tsx │ ├── current-post │ │ └── index.tsx │ └── user-profile │ │ └── index.tsx └── main.tsx ├── .vscode └── settings.json ├── postcss.config.js ├── tsconfig.node.json ├── nginx.conf ├── tailwind.config.js ├── vite.config.ts ├── Dockerfile ├── index.html ├── .gitignore ├── tsconfig.json ├── .github └── FUNDING.yml ├── README.md └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import "@testing-library/jest-dom" 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = 2 | process.env.NODE_ENV === "production" ? "http://localhost:3000" : "http://localhost:3000" 3 | -------------------------------------------------------------------------------- /src/utils/format-to-client-date.ts: -------------------------------------------------------------------------------- 1 | export const formatToClientDate = (date?: Date) => { 2 | if (!date) { 3 | return '' 4 | } 5 | 6 | return new Date(date).toLocaleDateString() 7 | } 8 | -------------------------------------------------------------------------------- /src/components/error-message/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export const ErrorMessage = ({ error = "" }: { error: string }) => { 4 | return error &&

{error}

5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/components/typography/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | type Props = { 4 | children: string 5 | size?: string 6 | } 7 | 8 | export const Typography: React.FC = ({ children, size = "text-xl" }) => { 9 | return

{children}

10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/utils/has-error-field.ts: -------------------------------------------------------------------------------- 1 | export function hasErrorField( 2 | err: unknown, 3 | ): err is { data: { error: string } } { 4 | return ( 5 | typeof err === "object" && 6 | err !== null && 7 | "data" in err && 8 | typeof err.data === "object" && 9 | err.data !== null && 10 | "error" in err.data 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/features/user/authGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useCurrentQuery } from "../../app/services/userApi" 2 | import { Spinner } from "@nextui-org/react" 3 | 4 | export const AuthGuard = ({ children }: { children: JSX.Element }) => { 5 | const { isLoading } = useCurrentQuery() 6 | 7 | if (isLoading) { 8 | return 9 | } 10 | 11 | return children 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { nextui } = require("@nextui-org/theme") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "./index.html", 7 | "./src/**/*.{js,ts,jsx,tsx}", 8 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: "class", 14 | plugins: [nextui()], 15 | } 16 | -------------------------------------------------------------------------------- /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/profile-info/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | title: string; 5 | info?: string; 6 | } 7 | 8 | export const ProfileInfo: React.FC = ({ 9 | title, 10 | info, 11 | }) => { 12 | 13 | if (!info) { 14 | return null; 15 | } 16 | 17 | return ( 18 |

19 | {title}{info} 20 |

21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | import react from "@vitejs/plugin-react" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | open: true, 9 | }, 10 | build: { 11 | outDir: "build", 12 | sourcemap: true, 13 | }, 14 | test: { 15 | globals: true, 16 | environment: "jsdom", 17 | setupFiles: "src/setupTests", 18 | mockReset: true, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/hooks/useAuthGuard.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux" 2 | import { selectIsAuthenticated } from "../features/user/userSlice" 3 | import { useNavigate } from "react-router-dom" 4 | import { useEffect } from "react" 5 | 6 | export const useAuthGuard = () => { 7 | const isAuthenticated = useSelector(selectIsAuthenticated) 8 | const navigate = useNavigate() 9 | 10 | useEffect(() => { 11 | if (isAuthenticated) { 12 | navigate("/") 13 | } 14 | }, []) 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Social 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/nav-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "react-router-dom" 3 | import { Button } from "../button" 4 | 5 | type Props = { 6 | children: React.ReactNode 7 | icon: JSX.Element 8 | href: string 9 | } 10 | 11 | export const NavButton: React.FC = ({ children, icon, href }) => { 12 | return ( 13 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { createListenerMiddleware } from "@reduxjs/toolkit" 2 | import { userApi } from "../app/services/userApi" 3 | 4 | export const listenerMiddleware = createListenerMiddleware() 5 | 6 | listenerMiddleware.startListening({ 7 | matcher: userApi.endpoints.login.matchFulfilled, 8 | effect: async (action, listenerApi) => { 9 | listenerApi.cancelActiveListeners() 10 | 11 | if (action.payload.token) { 12 | localStorage.setItem("token", action.payload.token) 13 | } 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/go-back/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { FaRegArrowAltCircleLeft } from "react-icons/fa" 3 | import { useNavigate } from "react-router-dom" 4 | 5 | export const GoBack = () => { 6 | const navigate = useNavigate() 7 | 8 | const handleGoBack = () => { 9 | navigate(-1) 10 | } 11 | 12 | return ( 13 |
17 | 18 | Назад 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/meta-info/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { IconType } from "react-icons" 3 | 4 | type Props = { 5 | count: number 6 | Icon: IconType 7 | } 8 | 9 | export const MetaInfo: React.FC = ({ count, Icon }) => { 10 | return ( 11 |
12 | {count > 0 && ( 13 |

{count}

14 | )} 15 |

16 | 17 |

18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "module": "ESNext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "types": ["testing-library__jest-dom"] 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/user/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { User as NextUiUser } from "@nextui-org/react" 3 | import { BASE_URL } from "../../constants" 4 | 5 | type Props = { 6 | name: string 7 | avatarUrl: string 8 | description?: string 9 | className?: string 10 | } 11 | 12 | export const User: React.FC = ({ 13 | name = "", 14 | description = "", 15 | avatarUrl = "", 16 | className = "", 17 | }) => { 18 | return ( 19 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@500;600;700&family=Noto+Sans:wght@500;700;900&family=Roboto&display=swap"); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | body { 7 | margin: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 9 | "Noto Sans", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 10 | "Helvetica Neue", sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | main { 16 | min-height: 100vh; 17 | } 18 | 19 | code { 20 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 21 | monospace; 22 | } 23 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/nav-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { BsPostcard } from "react-icons/bs" 3 | import { FaUsers } from "react-icons/fa" 4 | import { FiUsers } from "react-icons/fi" 5 | import { NavButton } from "../nav-button" 6 | 7 | export const NavBar: React.FC = () => { 8 | return ( 9 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /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/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/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/followers/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux" 2 | import { selectCurrent } from "../../features/user/userSlice" 3 | import { Link } from "react-router-dom" 4 | import { Card, CardBody } from "@nextui-org/react" 5 | import { User } from "../../components/user" 6 | 7 | export const Followers = () => { 8 | const currentUser = useSelector(selectCurrent) 9 | 10 | if (!currentUser) { 11 | return null 12 | } 13 | 14 | return currentUser.followers.length > 0 ? ( 15 |
16 | {currentUser.followers.map((user) => ( 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | ))} 29 |
30 | ) : ( 31 |

У вас нет подписчиков

32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/following/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux" 2 | import { selectCurrent } from "../../features/user/userSlice" 3 | import { Link } from "react-router-dom" 4 | import { Card, CardBody } from "@nextui-org/react" 5 | import { User } from "../../components/user" 6 | 7 | export const Following = () => { 8 | const currentUser = useSelector(selectCurrent) 9 | 10 | if (!currentUser) { 11 | return null 12 | } 13 | 14 | return currentUser.following.length > 0 ? ( 15 |
16 | {currentUser.following.map((user) => ( 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | ))} 29 |
30 | ) : ( 31 |

Вы не подписаны ни на кого

32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/theme-provider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | type ThemeContextType = { 4 | theme: "dark" | "light" 5 | toggleTheme: () => void 6 | } 7 | 8 | export const ThemeContext = React.createContext({ 9 | theme: "dark", 10 | toggleTheme: () => null, 11 | }) 12 | 13 | export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { 14 | const storedTheme = localStorage.getItem('theme'); 15 | const currentTheme = storedTheme ? storedTheme as 'dark' | 'light' : 'dark'; 16 | 17 | const [theme, setTheme] = React.useState<"dark" | "light">(currentTheme) 18 | 19 | console.log('theme', theme, currentTheme) 20 | 21 | const toggleTheme = () => { 22 | setTheme((prevTheme) => { 23 | const newTheme = prevTheme === "light" ? "dark" : "light"; 24 | localStorage.setItem('theme', newTheme); 25 | 26 | return newTheme 27 | }) 28 | } 29 | 30 | return ( 31 | 32 |
33 | {children} 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/input/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Control, useController } from "react-hook-form" 3 | import { Input as NextInput } from "@nextui-org/react" 4 | 5 | type Props = { 6 | name: string 7 | label: string 8 | placeholder?: string 9 | type?: string 10 | control: Control 11 | required?: string 12 | endContent?: JSX.Element 13 | } 14 | 15 | export const Input: React.FC = ({ 16 | name, 17 | label, 18 | placeholder, 19 | type, 20 | control, 21 | required = "", 22 | endContent, 23 | }) => { 24 | const { 25 | field, 26 | fieldState: { invalid }, 27 | formState: { errors }, 28 | } = useController({ 29 | name, 30 | control, 31 | rules: { required }, 32 | }) 33 | 34 | return ( 35 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import { Container } from "../container" 3 | import { NavBar } from "../nav-bar" 4 | import { Link, Outlet, useNavigate } from "react-router-dom" 5 | import { Profile } from "../profile" 6 | import { useSelector } from "react-redux" 7 | import { 8 | selectUser, 9 | selectIsAuthenticated, 10 | } from "../../features/user/userSlice" 11 | import { Header } from "../header" 12 | 13 | export const Layout = () => { 14 | const isAuthenticated = useSelector(selectIsAuthenticated) 15 | const user = useSelector(selectUser) 16 | const navigate = useNavigate() 17 | 18 | useEffect(() => { 19 | if (!isAuthenticated) { 20 | navigate("/auth") 21 | } 22 | }, []) 23 | 24 | return ( 25 | <> 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 |
{!user && }
36 |
37 |
38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /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/pages/auth/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, Tab, Tabs } from "@nextui-org/react" 2 | import { useState } from "react" 3 | import { Login } from "../../features/user/login" 4 | import { Register } from "../../features/user/register" 5 | import { useAuthGuard } from "../../hooks/useAuthGuard" 6 | 7 | export const Auth = () => { 8 | const [selected, setSelected] = useState("login") 9 | 10 | useAuthGuard() 11 | 12 | return ( 13 |
14 |
15 | 16 | 17 | setSelected(key as string)} 22 | > 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/components/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardHeader, CardBody, Image } from "@nextui-org/react" 2 | import { useSelector } from "react-redux" 3 | import { selectCurrent } from "../../features/user/userSlice" 4 | import { MdAlternateEmail } from "react-icons/md" 5 | import { BASE_URL } from "../../constants" 6 | import { Link } from "react-router-dom" 7 | 8 | export const Profile = () => { 9 | const current = useSelector(selectCurrent) 10 | 11 | if (!current) { 12 | return null 13 | } 14 | 15 | const { name, email, avatarUrl, id } = current 16 | 17 | return ( 18 | 19 | 20 | Card background 26 | 27 | 28 | 29 |

{name}

30 | 31 |

32 | 33 | {email} 34 |

35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "../../components/card" 2 | import { CreatePost } from "../../components/create-post" 3 | import { useGetAllPostsQuery } from "../../app/services/postsApi" 4 | 5 | export const Posts = () => { 6 | const { data } = useGetAllPostsQuery() 7 | 8 | return ( 9 | <> 10 |
11 | 12 |
13 | {data && data.length > 0 14 | ? data.map( 15 | ({ 16 | content, 17 | author, 18 | id, 19 | authorId, 20 | comments, 21 | likes, 22 | likedByUser, 23 | createdAt, 24 | }) => ( 25 | 38 | ), 39 | ) 40 | : null} 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/components/create-post/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Textarea } from "@nextui-org/react" 2 | import { IoMdCreate } from "react-icons/io" 3 | import { 4 | useCreatePostMutation, 5 | useLazyGetAllPostsQuery, 6 | } from "../../app/services/postsApi" 7 | import { useForm, Controller } from "react-hook-form" 8 | import { ErrorMessage } from "../error-message" 9 | 10 | export const CreatePost = () => { 11 | const [createPost] = useCreatePostMutation() 12 | const [triggerGetAllPosts] = useLazyGetAllPostsQuery() 13 | const { 14 | handleSubmit, 15 | control, 16 | formState: { errors }, 17 | setValue, 18 | } = useForm() 19 | 20 | const onSubmit = handleSubmit(async (data) => { 21 | try { 22 | await createPost({ content: data.post }).unwrap() 23 | setValue("post", "") 24 | await triggerGetAllPosts().unwrap() 25 | } catch (error) { 26 | console.log("err", error) 27 | } 28 | }) 29 | const error = errors?.post?.message as string 30 | 31 | return ( 32 |
33 | ( 41 |