├── .dockerignore
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── Dockerfile
├── README.md
├── components.json
├── docker-compose.yml
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── errors
│ └── not-found.png
├── file.svg
├── globe.svg
├── logo.svg
├── next.svg
├── vercel.svg
└── window.svg
├── src
├── app
│ ├── (auth)
│ │ ├── auth
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (root)
│ │ ├── bookmarks
│ │ │ └── page.tsx
│ │ ├── friends
│ │ │ └── [id]
│ │ │ │ └── page.tsx
│ │ ├── im
│ │ │ ├── [id]
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── likes
│ │ │ └── page.tsx
│ │ ├── new
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── post
│ │ │ └── [id]
│ │ │ │ └── page.tsx
│ │ ├── profile
│ │ │ └── settings
│ │ │ │ ├── edit
│ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ └── user
│ │ │ └── [id]
│ │ │ ├── followers
│ │ │ └── page.tsx
│ │ │ ├── following
│ │ │ └── page.tsx
│ │ │ └── page.tsx
│ ├── actions.ts
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistMonoVF.woff
│ │ └── GeistVF.woff
│ ├── globals.css
│ └── layout.tsx
├── components
│ ├── shared
│ │ ├── aside-chats
│ │ │ ├── aside-chats-wrapper.tsx
│ │ │ └── index.ts
│ │ ├── bookmarks
│ │ │ ├── bookmarks-wrapper.tsx
│ │ │ └── index.ts
│ │ ├── chat-list
│ │ │ ├── chat-item
│ │ │ │ ├── chat-actions.tsx
│ │ │ │ ├── chat-item.tsx
│ │ │ │ ├── chat-preview.tsx
│ │ │ │ ├── constants
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── message-type-data.ts
│ │ │ │ └── index.ts
│ │ │ ├── chat-list-share-mode.tsx
│ │ │ ├── chat-list.tsx
│ │ │ ├── index.ts
│ │ │ └── schemas
│ │ │ │ ├── chat-list-share-mode-schema.ts
│ │ │ │ └── index.ts
│ │ ├── chat
│ │ │ ├── chat-head
│ │ │ │ ├── chat-head.tsx
│ │ │ │ └── index.ts
│ │ │ ├── chat-main
│ │ │ │ ├── chat-body
│ │ │ │ │ ├── chat-body.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── message
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── message-actions.tsx
│ │ │ │ │ │ ├── message-main-content.tsx
│ │ │ │ │ │ └── message-shared-content.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── message.tsx
│ │ │ │ ├── chat-fields
│ │ │ │ │ ├── chat-input.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── schemas
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── message-schema.ts
│ │ │ │ ├── chat-main.tsx
│ │ │ │ └── index.ts
│ │ │ ├── chat-wrapper.tsx
│ │ │ └── index.ts
│ │ ├── comments
│ │ │ ├── comment
│ │ │ │ ├── comment-actions.tsx
│ │ │ │ ├── comment-item.tsx
│ │ │ │ └── index.ts
│ │ │ ├── comments-list.tsx
│ │ │ └── index.ts
│ │ ├── edit
│ │ │ ├── edit-block
│ │ │ │ ├── edit-block.tsx
│ │ │ │ └── index.ts
│ │ │ ├── edit-wrapper.tsx
│ │ │ └── index.ts
│ │ ├── empty-state.tsx
│ │ ├── feed
│ │ │ ├── index.ts
│ │ │ ├── new
│ │ │ │ ├── index.ts
│ │ │ │ └── new-feed-wrapper.tsx
│ │ │ └── popular
│ │ │ │ ├── index.ts
│ │ │ │ └── popular-feed-wrapper.tsx
│ │ ├── follows
│ │ │ ├── followers
│ │ │ │ ├── followers-list.tsx
│ │ │ │ ├── followers-wrapper.tsx
│ │ │ │ └── index.ts
│ │ │ ├── following
│ │ │ │ ├── following-list.tsx
│ │ │ │ ├── following-wrapper.tsx
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── forms
│ │ │ ├── auth-forms
│ │ │ │ ├── code-form.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── login-form.tsx
│ │ │ │ └── register-form.tsx
│ │ │ ├── index.ts
│ │ │ └── update-user-form.tsx
│ │ ├── friend
│ │ │ ├── friend-actions.tsx
│ │ │ ├── friend.tsx
│ │ │ ├── friends-list.tsx
│ │ │ └── index.ts
│ │ ├── friends
│ │ │ ├── friends-wrapper.tsx
│ │ │ └── index.ts
│ │ ├── header
│ │ │ ├── components
│ │ │ │ ├── header-menu.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── profile-button
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── profile-button-menu.tsx
│ │ │ │ │ └── profile-button.tsx
│ │ │ │ └── search
│ │ │ │ │ ├── components
│ │ │ │ │ ├── empty-state.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── search-results.tsx
│ │ │ │ │ └── searched-users-list.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── search.tsx
│ │ │ ├── header.tsx
│ │ │ └── index.ts
│ │ ├── im
│ │ │ ├── im-head
│ │ │ │ ├── im-head.tsx
│ │ │ │ └── index.ts
│ │ │ ├── im-wrapper.tsx
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── liked-posts
│ │ │ ├── index.ts
│ │ │ └── liked-posts-wrapper.tsx
│ │ ├── logout-button.tsx
│ │ ├── modals
│ │ │ ├── auth-modals
│ │ │ │ ├── auth-modal.tsx
│ │ │ │ ├── auth-tabs.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── register-actions.tsx
│ │ │ ├── index.ts
│ │ │ └── shared-modal
│ │ │ │ ├── index.ts
│ │ │ │ └── shared-modal.tsx
│ │ ├── post-item
│ │ │ ├── components
│ │ │ │ ├── index.ts
│ │ │ │ ├── post-body
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── post-body.tsx
│ │ │ │ │ ├── post-content.tsx
│ │ │ │ │ ├── post-gallery
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── post-gallery-button.tsx
│ │ │ │ │ │ ├── post-gallery-item.tsx
│ │ │ │ │ │ └── post-gallery.tsx
│ │ │ │ │ ├── post-images.tsx
│ │ │ │ │ └── post-tags.tsx
│ │ │ │ ├── post-bottom-actions
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── post-bottom-actions-item.tsx
│ │ │ │ │ ├── post-bottom-actions.tsx
│ │ │ │ │ └── shared-body.tsx
│ │ │ │ └── post-top-actions.tsx
│ │ │ ├── index.ts
│ │ │ └── post-item.tsx
│ │ ├── post
│ │ │ ├── comment-section.tsx
│ │ │ ├── index.ts
│ │ │ └── post-wrapper.tsx
│ │ ├── posts-list.tsx
│ │ ├── profile-button.tsx
│ │ ├── profile-card.tsx
│ │ ├── providers
│ │ │ ├── auth-guard.tsx
│ │ │ ├── index.ts
│ │ │ ├── providers.tsx
│ │ │ ├── root-providers.tsx
│ │ │ ├── socket-provider.tsx
│ │ │ └── theme-provider.tsx
│ │ ├── search-chats.tsx
│ │ ├── select-image.tsx
│ │ ├── sidebar
│ │ │ ├── index.ts
│ │ │ ├── section-title.tsx
│ │ │ ├── sidebar-drawer.tsx
│ │ │ ├── sidebar-item.tsx
│ │ │ └── sidebar.tsx
│ │ ├── user
│ │ │ ├── index.ts
│ │ │ ├── profile
│ │ │ │ ├── content-tabs
│ │ │ │ │ ├── content-tabs.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── tabs
│ │ │ │ │ │ ├── comments-tab.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── posts-tab.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── user-actions.tsx
│ │ │ │ └── user-info.tsx
│ │ │ └── user-wrapper.tsx
│ │ ├── write-comment.tsx
│ │ └── write-post
│ │ │ ├── index.ts
│ │ │ ├── selected-images.tsx
│ │ │ ├── write-post-actions.tsx
│ │ │ └── write-post.tsx
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── checkbox.tsx
│ │ ├── command.tsx
│ │ ├── container.tsx
│ │ ├── dark-light-block.tsx
│ │ ├── dialog.tsx
│ │ ├── editable-message.tsx
│ │ ├── form.tsx
│ │ ├── index.ts
│ │ ├── label.tsx
│ │ ├── line.tsx
│ │ ├── list-nav-element.tsx
│ │ ├── loader.tsx
│ │ ├── logo.tsx
│ │ ├── motion-div.tsx
│ │ ├── multiple-selector-creatable.tsx
│ │ ├── opacity-animate-block.tsx
│ │ ├── shadcn-expendsions.tsx
│ │ ├── sheet.tsx
│ │ ├── sonner.tsx
│ │ └── tabs.tsx
├── constants
│ ├── index.ts
│ └── navigation
│ │ ├── index.ts
│ │ ├── nav-categories.ts
│ │ └── nav-menu-items.ts
├── lib
│ ├── hooks
│ │ ├── bookmarks
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── index.ts
│ │ │ │ └── toggle-bookmark.ts
│ │ │ ├── use-add-bookmark.ts
│ │ │ └── use-remove-bookmark.ts
│ │ ├── chat
│ │ │ ├── index.ts
│ │ │ ├── message
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-message-actions.ts
│ │ │ │ ├── use-message-handlers.ts
│ │ │ │ └── use-message-notifications.ts
│ │ │ ├── use-create-chat.tsx
│ │ │ └── use-infinity-scroll-messages.tsx
│ │ ├── comments
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── handle-delete-comment-on-post-page.ts
│ │ │ │ ├── handle-delete-comment-on-user-page.ts
│ │ │ │ ├── handle-post-comment-action.ts
│ │ │ │ ├── handle-update-comment-on-post-page.ts
│ │ │ │ └── index.ts
│ │ │ ├── use-create-comment.ts
│ │ │ ├── use-delete-comment.ts
│ │ │ └── use-update-comment.ts
│ │ ├── follows
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── index.ts
│ │ │ │ └── toggle-user-follow.ts
│ │ │ ├── use-follow-user.ts
│ │ │ └── use-un-follow-user.ts
│ │ ├── index.ts
│ │ ├── likes
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── index.ts
│ │ │ │ └── toggle-post-like.ts
│ │ │ ├── use-like-post.ts
│ │ │ └── use-unlike-post.ts
│ │ ├── posts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── handle-delete-post.ts
│ │ │ │ └── index.ts
│ │ │ ├── use-create-post.ts
│ │ │ ├── use-delete-post.tsx
│ │ │ ├── use-infinity-scroll-popular-posts.tsx
│ │ │ └── use-infinity-scroll-posts.tsx
│ │ ├── socket
│ │ │ ├── index.ts
│ │ │ ├── use-socket-connection.ts
│ │ │ ├── use-socket-events.ts
│ │ │ └── use-socket.ts
│ │ └── user
│ │ │ ├── bookmarks
│ │ │ ├── index.ts
│ │ │ └── use-infinity-scroll-user-bookmarks.tsx
│ │ │ ├── comments
│ │ │ ├── index.ts
│ │ │ └── use-infinity-scroll-user-comments.tsx
│ │ │ ├── follows
│ │ │ ├── index.ts
│ │ │ ├── use-infinity-scroll-user-followers.tsx
│ │ │ ├── use-infinity-scroll-user-following.tsx
│ │ │ └── use-infinity-scroll-user-friends.tsx
│ │ │ ├── index.ts
│ │ │ ├── posts
│ │ │ ├── index.ts
│ │ │ ├── use-infinity-scroll-posts-by-userId.tsx
│ │ │ └── use-infinity-scroll-user-liked-posts.tsx
│ │ │ ├── use-get-me-data.ts
│ │ │ ├── use-get-me.ts
│ │ │ └── use-update-profile.ts
│ └── utils
│ │ ├── auth
│ │ ├── index.ts
│ │ └── save-auth-cookies.ts
│ │ ├── chat
│ │ ├── helpers
│ │ │ ├── index.ts
│ │ │ ├── sort-chats-by-last-message.ts
│ │ │ ├── update-and-sort-chats.ts
│ │ │ ├── update-chats-with-new-message.ts
│ │ │ └── update-chats-with-updated-message.ts
│ │ └── index.ts
│ │ ├── index.ts
│ │ ├── shared
│ │ ├── format-to-client-date.ts
│ │ ├── get-absolute-url.ts
│ │ ├── get-relative-time.ts
│ │ ├── handle-api-error.ts
│ │ ├── has-error-field.ts
│ │ ├── index.ts
│ │ └── utils.ts
│ │ └── socket
│ │ ├── index.ts
│ │ └── socket-context.ts
├── middleware.ts
├── services
│ ├── api-client.ts
│ ├── bookmark-api.ts
│ ├── chat-api.ts
│ ├── comment-api.ts
│ ├── constants.ts
│ ├── follow-api.ts
│ ├── instance.ts
│ ├── like-api.ts
│ ├── post-api.ts
│ └── user-api.ts
├── store
│ ├── index.ts
│ └── slices
│ │ ├── index.ts
│ │ ├── new-mark-slice.ts
│ │ └── shared-post-slice.ts
└── types
│ ├── auth.ts
│ ├── chat.ts
│ ├── constants.ts
│ ├── dto.ts
│ ├── index.ts
│ └── response.ts
├── tailwind.config.ts
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 |
4 | .env.local
5 | .env.*.local
6 | .env.development
7 | .env.test
8 |
9 | !.env.production
10 | !.env
11 |
12 | .idea
13 | .vscode
14 | *.suo
15 | *.ntvs*
16 | *.njsproj
17 | *.sln
18 | .DS_Store
19 | *.pem
20 |
21 | # Version control
22 | .git
23 | .gitignore
24 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Create a copy of this file and rename it to .env
2 | NEXT_PUBLIC_API_URL="http://localhost:3001/api"
3 | NEXT_PUBLIC_SOCKET_API_URL="ws://localhost:3001/chat"
4 | NEXT_PUBLIC_BASE_API_URL="http://localhost:3001"
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"],
3 | "rules": {
4 | "@next/next/no-img-element": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # env files (can opt-in for committing if needed)
33 | .env
34 | .env.local
35 | .env.*.local
36 | !.env.example
37 |
38 | # vercel
39 | .vercel
40 |
41 | # typescript
42 | *.tsbuildinfo
43 | next-env.d.ts
44 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "bracketSameLine": true,
10 | "arrowParens": "always",
11 | "endOfLine": "lf",
12 | "proseWrap": "preserve",
13 | "htmlWhitespaceSensitivity": "css",
14 | "vueIndentScriptAndStyle": false,
15 | "embeddedLanguageFormatting": "auto",
16 | "singleAttributePerLine": false
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules\\typescript\\lib"
3 | }
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine AS base
2 |
3 | FROM base AS deps
4 |
5 | RUN apk add --no-cache libc6-compat
6 | WORKDIR /app
7 |
8 | COPY package.json package-lock.json* ./
9 | RUN npm install --legacy-peer-deps
10 |
11 | FROM base AS builder
12 | WORKDIR /app
13 | COPY --from=deps /app/node_modules ./node_modules
14 | COPY . .
15 |
16 | RUN npm run build
17 |
18 | FROM base AS runner
19 | WORKDIR /app
20 |
21 | ENV NODE_ENV=production
22 |
23 | RUN addgroup --system --gid 1001 nodejs
24 | RUN adduser --system --uid 1001 nextjs
25 |
26 | COPY --from=builder /app/public ./public
27 |
28 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
29 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
30 |
31 | USER nextjs
32 |
33 | EXPOSE 3000
34 |
35 | ENV PORT=3000
36 |
37 |
38 | ENV HOSTNAME="0.0.0.0"
39 | CMD ["node", "server.js"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chime Frontend
2 |
3 | Фронтенд часть социальной платформы Chime.
4 |
5 | ## Возможности
6 |
7 | - Посты с медиа
8 | - Личные интерактивные чаты
9 | - Лайки и комментарии
10 | - Подписчики и друзья
11 | - Репосты и закладки
12 |
13 | ## Технологии
14 |
15 | - Next.js
16 | - TypeScript
17 | - NextUI + Shadcn/ui
18 | - TanStack Query
19 | - Zustand
20 | - Tailwind CSS
21 | - Socket IO
22 |
23 | ## Запуск приложения
24 |
25 | ### Требования:
26 |
27 | - Node.js 18+
28 | - Docker (опционально)
29 |
30 | ### Локальная разработка:
31 |
32 | 1. Клонируйте репозиторий:
33 |
34 | ```bash
35 | git clone https://github.com/Leroyalle/chime-frontend.git
36 | cd chime-frontend
37 | ```
38 |
39 | 2. Создайте и заполните файл `.env`:
40 |
41 | ```bash
42 | cp .env.example .env
43 | ```
44 |
45 | 3. Установите зависимости:
46 |
47 | ```bash
48 | npm install --legacy-peer-deps
49 | ```
50 |
51 | 4. Запустите сервер разработки:
52 |
53 | ```bash
54 | npm dev dev --turbopack
55 | ```
56 |
57 | ### Продакшн:
58 |
59 | ```bash
60 | npm run build
61 | npm run start
62 | ```
63 |
64 | ### Docker:
65 |
66 | Проект включает Dockerfile и docker-compose.yml для контейнеризации.
67 |
68 | ```bash
69 | # Сборка и запуск
70 | docker compose up
71 | ```
72 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks",
19 | "types": "@/types",
20 | "constants": "@/constants"
21 | },
22 | "iconLibrary": "lucide"
23 | }
24 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | frontend:
5 | build: .
6 | ports:
7 | - '3000:3000'
8 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = { output: 'standalone' };
3 |
4 | module.exports = nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chime-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.10.0",
13 | "@nextui-org/input-otp": "^2.1.6",
14 | "@nextui-org/react": "^2.4.8",
15 | "@radix-ui/react-checkbox": "^1.1.3",
16 | "@radix-ui/react-dialog": "^1.1.4",
17 | "@radix-ui/react-label": "^2.1.1",
18 | "@radix-ui/react-slot": "^1.1.1",
19 | "@radix-ui/react-tabs": "^1.1.2",
20 | "@tanstack/react-query": "^5.62.2",
21 | "@tanstack/react-query-devtools": "^5.62.8",
22 | "@types/js-cookie": "^3.0.6",
23 | "axios": "^1.7.9",
24 | "class-variance-authority": "^0.7.1",
25 | "clsx": "^2.1.1",
26 | "cmdk": "^1.0.0",
27 | "dayjs": "^1.11.13",
28 | "framer-motion": "^11.13.1",
29 | "js-cookie": "^3.0.5",
30 | "lucide-react": "^0.464.0",
31 | "motion": "^11.17.0",
32 | "next": "15.0.3",
33 | "next-themes": "^0.4.4",
34 | "nextjs-toploader": "^3.7.15",
35 | "react": "19.0.0-rc-66855b96-20241106",
36 | "react-dom": "19.0.0-rc-66855b96-20241106",
37 | "react-hook-form": "^7.54.2",
38 | "react-intersection-observer": "^9.13.1",
39 | "react-toastify": "^11.0.1",
40 | "socket.io-client": "^4.8.1",
41 | "sonner": "^1.7.2",
42 | "tailwind-merge": "^2.5.5",
43 | "tailwindcss-animate": "^1.0.7",
44 | "vaul": "^1.1.2",
45 | "zod": "^3.24.1",
46 | "zustand": "^5.0.2"
47 | },
48 | "devDependencies": {
49 | "@types/node": "^20",
50 | "@types/react": "^18",
51 | "@types/react-dom": "^18",
52 | "eslint": "^8",
53 | "eslint-config-next": "15.0.3",
54 | "postcss": "^8",
55 | "tailwindcss": "^3.4.1",
56 | "typescript": "^5"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/errors/not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leroyalle/chime-frontend/17641bf79f34b59be20096cdac357e9307b1557f/public/errors/not-found.png
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(auth)/auth/page.tsx:
--------------------------------------------------------------------------------
1 | import { AuthModal } from '@/components/shared/modals';
2 |
3 | export default async function Auth() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from '@/components/shared';
2 | import type { Metadata } from 'next';
3 |
4 | export const metadata: Metadata = {
5 | title: 'Chime | Авторизация',
6 | description: 'Социальная сеть Chime',
7 | };
8 |
9 | export default function AuthLayout({
10 | children,
11 | }: Readonly<{
12 | children: React.ReactNode;
13 | }>) {
14 | return (
15 | <>
16 |
17 | {children}
18 |
19 | >
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/(root)/bookmarks/page.tsx:
--------------------------------------------------------------------------------
1 | import { BookmarksWrapper } from '@/components/shared';
2 | import { AxiosError, AxiosHeaders } from 'axios';
3 | import { cookies } from 'next/headers';
4 | import { TokensEnum } from '../../../types';
5 | import { Api } from '@/services/api-client';
6 |
7 | export default async function Bookmarks() {
8 | const cookiesStore = await cookies();
9 | const headers = new AxiosHeaders({
10 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
11 | });
12 | try {
13 | const bookmarks = await Api.bookmark.findAllBookmarks({ headers });
14 | return ;
15 | } catch (error) {
16 | if (error instanceof AxiosError && error.response?.status === 401) {
17 | console.log(error);
18 | }
19 | console.log(error);
20 | return
Error occurred
;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/(root)/friends/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { FriendsWrapper } from '@/components/shared/friends';
2 | import { AxiosHeaders } from 'axios';
3 | import { cookies } from 'next/headers';
4 | import { TokensEnum } from '../../../../types';
5 | import { Api } from '@/services/api-client';
6 | import { handleApiError } from '@/lib/utils/shared/handle-api-error';
7 |
8 | export default async function Friends({ params }: { params: Promise<{ id: string }> }) {
9 | const id = (await params).id;
10 | const cookiesStore = await cookies();
11 | const headers = new AxiosHeaders({
12 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
13 | });
14 |
15 | const friends = await Api.follow.getFriends({ userId: id, headers }).catch(handleApiError);
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(root)/im/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { ChatWrapper } from '@/components/shared';
2 | import { Api } from '@/services/api-client';
3 | import { TokensEnum } from '../../../../types';
4 | import { AxiosHeaders } from 'axios';
5 | import { cookies } from 'next/headers';
6 | import { handleApiError } from '@/lib/utils';
7 |
8 | export default async function InstantMessagingCurrent({
9 | params,
10 | }: {
11 | params: Promise<{ id: string }>;
12 | }) {
13 | const id = (await params).id;
14 | const cookiesStore = await cookies();
15 | const headers = new AxiosHeaders({
16 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
17 | });
18 |
19 | const chat = await Api.chat.getChatById({ id, headers }).catch(handleApiError);
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/(root)/im/page.tsx:
--------------------------------------------------------------------------------
1 | import { ImWrapper } from '@/components/shared';
2 |
3 | export default async function InstantMessaging() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Container,
3 | ProfileCard,
4 | Sidebar,
5 | AsideChatsWrapper,
6 | RootProviders,
7 | Header,
8 | } from '@/components/shared';
9 | import { Line } from '@/components/ui';
10 | import { cn } from '@/lib/utils';
11 | import type { Metadata } from 'next';
12 |
13 | export const metadata: Metadata = {
14 | title: 'Chime',
15 | description: 'Социальная сеть Chime',
16 | };
17 |
18 | export default function HomeLayout({
19 | children,
20 | }: Readonly<{
21 | children: React.ReactNode;
22 | }>) {
23 | return (
24 |
25 |
26 |
27 |
28 |
37 | {children}
38 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/(root)/likes/page.tsx:
--------------------------------------------------------------------------------
1 | import { LikedPostsWrapper } from '@/components/shared';
2 | import { AxiosError, AxiosHeaders } from 'axios';
3 | import { cookies } from 'next/headers';
4 | import { TokensEnum } from '../../../types';
5 | import { Api } from '@/services/api-client';
6 |
7 | export default async function Likes() {
8 | const cookiesStore = await cookies();
9 | const headers = new AxiosHeaders({
10 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
11 | });
12 | try {
13 | const likedPosts = await Api.posts.getUserLikedPosts({ headers });
14 | return ;
15 | } catch (error) {
16 | if (error instanceof AxiosError && error.response?.status === 401) {
17 | console.log(error);
18 | }
19 | console.log(error);
20 | return Error occurred
;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/(root)/new/page.tsx:
--------------------------------------------------------------------------------
1 | import { NewFeedWrapper } from '@/components/shared/feed';
2 | import { Api } from '@/services/api-client';
3 | import { AxiosHeaders } from 'axios';
4 | import { cookies } from 'next/headers';
5 | import { TokensEnum } from '../../../types';
6 | import { handleApiError } from '@/lib/utils';
7 |
8 | export default async function New() {
9 | const cookiesStore = await cookies();
10 | const headers = new AxiosHeaders({
11 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
12 | });
13 | const posts = await Api.posts
14 | .getAllPosts({ page: 1, perPage: 10, headers })
15 | .catch(handleApiError);
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(root)/page.tsx:
--------------------------------------------------------------------------------
1 | import { PopularFeedWrapper } from '@/components/shared/feed';
2 | import { Api } from '@/services/api-client';
3 | import { AxiosHeaders } from 'axios';
4 | import { cookies } from 'next/headers';
5 | import { TokensEnum } from '../../types';
6 | import { handleApiError } from '@/lib/utils';
7 |
8 | export default async function Popular() {
9 | const cookiesStore = await cookies();
10 | const headers = new AxiosHeaders({
11 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
12 | });
13 | const posts = await Api.posts
14 | .getAllPopularPosts({ page: 1, perPage: 10, headers })
15 | .catch(handleApiError);
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(root)/post/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { AxiosHeaders } from 'axios';
2 | import { cookies } from 'next/headers';
3 | import { PostWrapper } from '@/components/shared/post';
4 | import { Api } from '@/services/api-client';
5 | import { TokensEnum } from '../../../../types';
6 | import { handleApiError } from '@/lib/utils';
7 |
8 | export default async function Post({ params }: { params: Promise<{ id: string }> }) {
9 | const id = (await params).id;
10 | const cookiesStore = await cookies();
11 | const headers = new AxiosHeaders({
12 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
13 | });
14 |
15 | const post = await Api.posts.getPostById({ id, headers }).catch(handleApiError);
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(root)/profile/settings/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { AxiosHeaders } from 'axios';
3 | import { cookies } from 'next/headers';
4 | import { notFound } from 'next/navigation';
5 | import { TokensEnum } from '../../../../../types/constants';
6 | import { EditWrapper } from '@/components/shared';
7 | import { handleApiError } from '@/lib/utils';
8 |
9 | export default async function Edit() {
10 | const cookiesStore = await cookies();
11 | const headers = new AxiosHeaders({
12 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
13 | });
14 |
15 | const user = await Api.users.current(headers).catch(handleApiError);
16 |
17 | if (!user) {
18 | return notFound();
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/(root)/profile/settings/page.tsx:
--------------------------------------------------------------------------------
1 | export default async function Settings() {
2 | return (
3 |
4 |
Settings profile
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/(root)/user/[id]/followers/page.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { AxiosHeaders } from 'axios';
3 | import { cookies } from 'next/headers';
4 | import { TokensEnum } from '../../../../../types';
5 | import { FollowersWrapper } from '@/components/shared';
6 | import { handleApiError } from '@/lib/utils';
7 |
8 | export default async function Followers({ params }: { params: Promise<{ id: string }> }) {
9 | const id = (await params).id;
10 | const cookiesStore = await cookies();
11 | const headers = new AxiosHeaders({
12 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
13 | });
14 |
15 | const user = await Api.users.getUserById({ id, headers }).catch(handleApiError);
16 |
17 | const followers = await Api.follow
18 | .getFollowers({ userId: user.user.id, headers })
19 | .catch(handleApiError);
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/(root)/user/[id]/following/page.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { AxiosHeaders } from 'axios';
3 | import { cookies } from 'next/headers';
4 | import { TokensEnum } from '../../../../../types';
5 | import { FollowingWrapper } from '@/components/shared';
6 | import { handleApiError } from '@/lib/utils';
7 |
8 | export default async function Following({ params }: { params: Promise<{ id: string }> }) {
9 | const id = (await params).id;
10 | const cookiesStore = await cookies();
11 | const headers = new AxiosHeaders({
12 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
13 | });
14 |
15 | const user = await Api.users.getUserById({ id, headers }).catch(handleApiError);
16 |
17 | const following = await Api.follow
18 | .getFollowing({ userId: user.user.id, headers })
19 | .catch(handleApiError);
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/(root)/user/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { UserWrapper } from '@/components/shared';
2 | import { Api } from '@/services/api-client';
3 | import { AxiosHeaders } from 'axios';
4 | import { cookies } from 'next/headers';
5 | import { notFound } from 'next/navigation';
6 | import { TokensEnum } from '../../../../types/constants';
7 | import { handleApiError } from '@/lib/utils';
8 |
9 | export default async function User({ params }: { params: Promise<{ id: string }> }) {
10 | const id = (await params).id;
11 | const cookiesStore = await cookies();
12 | const headers = new AxiosHeaders({
13 | Authorization: `Bearer ${cookiesStore.get(TokensEnum.JWT)?.value}`,
14 | });
15 |
16 | const user = await Api.users.getUserById({ id, headers }).catch(handleApiError);
17 |
18 | if (!user.user) {
19 | return notFound();
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { cookies } from 'next/headers';
4 | import { TokensEnum } from '../types';
5 |
6 | export async function deleteCookie() {
7 | (await cookies()).delete(TokensEnum.JWT);
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leroyalle/chime-frontend/17641bf79f34b59be20096cdac357e9307b1557f/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leroyalle/chime-frontend/17641bf79f34b59be20096cdac357e9307b1557f/src/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/src/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leroyalle/chime-frontend/17641bf79f34b59be20096cdac357e9307b1557f/src/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { Nunito } from 'next/font/google';
3 | import './globals.css';
4 | import { Providers } from '@/components/shared';
5 | import type { Viewport } from 'next';
6 | const inter = Nunito({
7 | subsets: ['cyrillic'],
8 | variable: '--font-nunito',
9 | weight: ['400', '500', '600', '700', '800', '900'],
10 | });
11 |
12 | export const viewport: Viewport = {
13 | width: 'device-width',
14 | initialScale: 1,
15 | maximumScale: 1,
16 | userScalable: false,
17 | };
18 |
19 | export default function RootLayout({
20 | children,
21 | }: Readonly<{
22 | children: React.ReactNode;
23 | }>) {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/shared/aside-chats/aside-chats-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import { DarkLightBlock, Line } from '../../ui';
4 | import { cn } from '@/lib/utils';
5 | import { useQuery } from '@tanstack/react-query';
6 | import { Api } from '@/services/api-client';
7 | import { Skeleton } from '@nextui-org/react';
8 | import { ChatList } from '../chat-list';
9 | import { SearchChats } from '../search-chats';
10 | import { useDebounce } from '@/components/ui/shadcn-expendsions';
11 |
12 | interface Props {
13 | className?: string;
14 | }
15 |
16 | export const AsideChatsWrapper: React.FC = ({ className }) => {
17 | const isMounted = React.useRef(false);
18 | const [searchValue, setSearchValue] = useState('');
19 | const searchQuery = useDebounce(searchValue, 500);
20 | const { data: chats, isFetching: isFetchingChats } = useQuery(
21 | Api.chat.getUserChatsQueryOptions(searchQuery),
22 | );
23 |
24 | useEffect(() => {
25 | // каждый перезапрос при изменении searchValue триггерит загрузку
26 | if (!isMounted.current) {
27 | isMounted.current = true;
28 | }
29 | }, []);
30 |
31 | if (isFetchingChats && !isMounted.current) {
32 | return ;
33 | }
34 |
35 | return (
36 |
37 |
38 | Сообщения
39 |
45 |
46 | {chats && chats.length ? (
47 |
48 | ) : (
49 | нет чатов
50 | )}
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/shared/aside-chats/index.ts:
--------------------------------------------------------------------------------
1 | export { AsideChatsWrapper } from './aside-chats-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/bookmarks/bookmarks-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { useInfinityScrollUserBookmarks } from '@/lib/hooks';
4 | import { PostsList } from '../posts-list';
5 | import { Spinner } from '@nextui-org/react';
6 | import { EmptyState } from '../empty-state';
7 | import { InfinityResponse } from '../../../types/response';
8 | import { Post } from '../../../types/dto';
9 | import { Loader } from '@/components/ui';
10 |
11 | interface Props {
12 | initialData: InfinityResponse;
13 | }
14 |
15 | export const BookmarksWrapper: React.FC = ({ initialData }) => {
16 | const {
17 | data: bookmarks,
18 | isFetching,
19 |
20 | cursor,
21 | isFetchingNextPage,
22 | } = useInfinityScrollUserBookmarks(initialData);
23 |
24 | if (isFetching) {
25 | return ;
26 | }
27 |
28 | if (!bookmarks || bookmarks.length === 0) {
29 | return ;
30 | }
31 |
32 | return (
33 | <>
34 |
35 | {cursor}
36 | {isFetchingNextPage && }
37 | >
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/shared/bookmarks/index.ts:
--------------------------------------------------------------------------------
1 | export { BookmarksWrapper } from './bookmarks-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-item/chat-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
3 | import { Ellipsis } from 'lucide-react';
4 |
5 | interface Props {
6 | className?: string;
7 | }
8 |
9 | export const ChatActions: React.FC = ({ className }) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | alert(key)}>
17 | Закрепить
18 |
19 | Удалить
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-item/chat-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { ChatPreview as Preview } from './chat-preview';
4 | import Link from 'next/link';
5 | import { ChatActions as Actions } from './chat-actions';
6 | import { ChatWithMembers, RoutesEnum, UserResponse } from '@/types';
7 |
8 | interface Props {
9 | chat: ChatWithMembers;
10 | me: UserResponse;
11 | hasActions?: boolean;
12 | className?: string;
13 | }
14 |
15 | export const ChatItem: React.FC = ({ chat, me, hasActions = true, className }) => {
16 | const correspondent = chat.members.find((member) => member.id !== me.user.id);
17 | const messageAuthor = chat.lastMessage?.UserBase?.name === me.user.name ? 'Вы:' : null;
18 |
19 | return (
20 |
21 |
22 |
30 |
31 | {hasActions &&
}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-item/chat-preview.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn, getAbsoluteUrl } from '@/lib/utils';
3 | import { MessageDto } from '@/types';
4 | import { getRelativeTime } from '@/lib/utils';
5 | import { messageTypeData } from './constants';
6 | import { Avatar } from '@/components/ui';
7 |
8 | interface Props {
9 | avatar?: string | null;
10 | name?: string;
11 | lastMessage: MessageDto | null;
12 | lastMessageAuthor: string | null;
13 | className?: string;
14 | }
15 |
16 | export const ChatPreview: React.FC = ({
17 | avatar,
18 | name,
19 | lastMessage,
20 | lastMessageAuthor,
21 | className,
22 | }) => {
23 | const messageType = lastMessage && messageTypeData[lastMessage.type];
24 |
25 | return (
26 |
27 |
30 |
31 |
32 | {name}
33 |
34 |
35 |
36 | {lastMessageAuthor &&
{lastMessageAuthor}}
37 |
{lastMessage?.content}
38 | {messageType &&
{messageType}}
39 |
40 |
41 | {getRelativeTime(lastMessage?.createdAt)}
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-item/constants/index.ts:
--------------------------------------------------------------------------------
1 | export { messageTypeData } from './message-type-data';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-item/constants/message-type-data.ts:
--------------------------------------------------------------------------------
1 | import { MessageTypeEnum } from '@/types';
2 |
3 | export const messageTypeData = {
4 | [MessageTypeEnum.POST]: 'Запись',
5 | [MessageTypeEnum.TEXT]: null,
6 | };
7 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-item/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatItem } from './chat-item';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/chat-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { ChatItem } from './chat-item';
4 | import { DarkLightBlock } from '../../ui/dark-light-block';
5 | import { ChatWithMembers } from '@/types';
6 | import { useGetMe } from '@/lib/hooks';
7 |
8 | interface Props {
9 | hasActions?: boolean;
10 | items?: ChatWithMembers[];
11 | itemsStyles?: string;
12 | className?: string;
13 | }
14 |
15 | export const ChatList: React.FC = ({ items, itemsStyles, hasActions, className }) => {
16 | const { data: me } = useGetMe();
17 |
18 | if (!items || items.length === 0 || !me) {
19 | return null;
20 | }
21 |
22 | return (
23 |
24 | {items.map((chat) => (
25 |
32 | ))}
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatList } from './chat-list';
2 | export { ChatListShareMode } from './chat-list-share-mode';
3 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/schemas/chat-list-share-mode-schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const ChatListShareModeSchema = z.object({
4 | chats: z.array(z.string()).refine((value) => value.some((item) => item), {
5 | message: 'You have to select at least one chat.',
6 | }),
7 | message: z.string().optional(),
8 | });
9 | export type ChatListShareModeSchemaType = z.infer;
10 |
--------------------------------------------------------------------------------
/src/components/shared/chat-list/schemas/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chat-list-share-mode-schema';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-head/chat-head.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn, getAbsoluteUrl } from '@/lib/utils';
3 | import { Avatar } from '@/components/ui';
4 | import { ArrowLeft } from 'lucide-react';
5 | import { RoutesEnum, User } from '@/types';
6 | import Link from 'next/link';
7 |
8 | interface Props {
9 | correspondent?: User;
10 | className?: string;
11 | }
12 |
13 | export const ChatHead: React.FC = ({ correspondent, className }) => {
14 | if (!correspondent) {
15 | return null;
16 | }
17 |
18 | return (
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {correspondent.name}
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-head/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatHead } from './chat-head';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-body/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatBody } from './chat-body';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-body/message/components/index.ts:
--------------------------------------------------------------------------------
1 | export { MessageSharedContent as SharedContent } from './message-shared-content';
2 | export { MessageMainContent as MainContent } from './message-main-content';
3 | export { MessageActions as Actions } from './message-actions';
4 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-body/message/components/message-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropdownItem, DropdownMenu } from '@nextui-org/react';
3 |
4 | interface Props {
5 | isSender: boolean;
6 | onUpdate: VoidFunction;
7 | deleteMessage: VoidFunction;
8 | }
9 |
10 | export const MessageActions: React.FC = ({ isSender, onUpdate, deleteMessage }) => {
11 | return (
12 |
13 | Закрепить
14 | <>
15 | {isSender && (
16 | <>
17 |
18 | Изменить
19 |
20 |
25 | Удалить
26 |
27 | >
28 | )}
29 | >
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-body/message/components/message-main-content.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { RoutesEnum } from '@/types';
4 | import Link from 'next/link';
5 |
6 | interface Props {
7 | userId: string;
8 | author: string;
9 | content: string | null;
10 | className?: string;
11 | }
12 |
13 | export const MessageMainContent: React.FC = ({ userId, author, content, className }) => {
14 | return (
15 |
16 |
17 | {author}
18 |
19 |
20 | {content}
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-body/message/components/message-shared-content.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { User } from '@nextui-org/react';
4 | import Link from 'next/link';
5 | import { getAbsoluteUrl, getRelativeTime } from '@/lib/utils';
6 | import { Author, RoutesEnum } from '@/types';
7 |
8 | interface Props {
9 | contentPost: string | null;
10 | imagePreview: string | null;
11 | postId: string | null;
12 | postAuthor: Author | null;
13 | postCreatedAt: Date | null;
14 | className?: string;
15 | }
16 |
17 | export const MessageSharedContent: React.FC = ({
18 | contentPost,
19 | imagePreview,
20 | postId,
21 | postAuthor,
22 | postCreatedAt,
23 | className,
24 | }) => {
25 | const handleOpenPost = (e: React.MouseEvent) => {
26 | e.preventDefault();
27 | e.stopPropagation();
28 | };
29 |
30 | return (
31 |
32 |
{postAuthor?.name}}
34 | description={getRelativeTime(postCreatedAt)}
35 | avatarProps={{
36 | src: getAbsoluteUrl(postAuthor?.avatar),
37 | size: 'sm',
38 | }}
39 | className="justify-start"
40 | />
41 | {imagePreview && (
42 |
47 | )}
48 | {contentPost}
49 |
54 |
55 | Открыть пост
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-body/message/index.ts:
--------------------------------------------------------------------------------
1 | export { Message } from './message';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-fields/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatInput } from './chat-input';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-fields/schemas/index.ts:
--------------------------------------------------------------------------------
1 | export * from './message-schema';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-fields/schemas/message-schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const messageSchema = z.object({
4 | message: z
5 | .string()
6 | .min(1, { message: 'Текст обязателен' })
7 | .max(1000, { message: 'Не более 1000' }),
8 | });
9 |
10 | export type TMessageSchema = z.infer;
11 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/chat-main.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ChatBody } from './chat-body';
3 | import { ChatInput } from './chat-fields';
4 | import { MessageDto } from '@/types';
5 | import { Spinner } from '@nextui-org/react';
6 |
7 | interface Props {
8 | chatId: string;
9 | messages?: MessageDto[];
10 | chatRef: React.RefObject;
11 | cursor?: JSX.Element;
12 | isFetchingNextPage: boolean;
13 | }
14 |
15 | export const ChatMain: React.FC = ({
16 | chatId,
17 | chatRef,
18 | messages,
19 | cursor,
20 | isFetchingNextPage,
21 | }) => {
22 | const [editableMessage, setEditableMessage] = useState(null);
23 |
24 | return (
25 | <>
26 | : undefined
34 | }
35 | />
36 | setEditableMessage(null)}
41 | />
42 | >
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-main/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatMain } from './chat-main';
2 |
--------------------------------------------------------------------------------
/src/components/shared/chat/chat-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useRef } from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { ChatHead as Header } from './chat-head';
5 | import { DarkLightBlock, Loader } from '../../ui';
6 | import { useGetMe, useInfinityScrollMessages } from '@/lib/hooks';
7 | import { ChatWithMembers } from '../../../types/chat';
8 | import { ChatMain as Main } from './chat-main';
9 |
10 | interface Props {
11 | chatId: string;
12 | chat: ChatWithMembers;
13 | className?: string;
14 | }
15 |
16 | export const ChatWrapper: React.FC = ({ chatId, chat, className }) => {
17 | const chatRef = useRef(null);
18 | const { data: me } = useGetMe();
19 | const correspondent = chat.members.find((m) => m.id !== me?.user.id);
20 |
21 | const {
22 | data: messages,
23 | isPending,
24 | isFetchingNextPage,
25 | cursor,
26 | } = useInfinityScrollMessages({
27 | chatId,
28 | chatRef,
29 | });
30 |
31 | if (isPending) {
32 | return ;
33 | }
34 |
35 | return (
36 |
41 |
42 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/shared/chat/index.ts:
--------------------------------------------------------------------------------
1 | export { ChatWrapper } from './chat-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/comments/comment/comment-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
3 | import { Ellipsis } from 'lucide-react';
4 | import { useDeleteComment } from '@/lib/hooks';
5 | import { PageType } from '../comments-list';
6 | import { useRouter } from 'next/navigation';
7 | import { RoutesEnum } from '../../../../types';
8 |
9 | interface Props {
10 | postId: string;
11 | commentId: string;
12 | userId: string;
13 | isOwner: boolean;
14 | pageType: PageType;
15 | onUpdate?: VoidFunction;
16 | }
17 |
18 | export const CommentActions: React.FC = ({
19 | postId,
20 | commentId,
21 | userId,
22 | isOwner,
23 | pageType,
24 | onUpdate,
25 | }) => {
26 | const { deleteComment } = useDeleteComment({ postId, commentId, userId });
27 | const router = useRouter();
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | Пожаловаться
35 |
36 | {pageType === 'user' ? (
37 | router.push(`${RoutesEnum.POST}/${postId}`)}>
40 | Перейти к посту
41 |
42 | ) : null}
43 |
44 | {pageType === 'post' && isOwner ? (
45 | <>
46 |
47 | Изменить
48 |
49 | deleteComment()}>
54 | Удалить
55 |
56 | >
57 | ) : null}
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/src/components/shared/comments/comment/comment-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { User } from '@nextui-org/react';
3 | import { CommentActions } from './comment-actions';
4 | import Link from 'next/link';
5 | import { RoutesEnum, User as TUser } from '../../../../types';
6 | import dayjs from 'dayjs';
7 | import relativeTime from 'dayjs/plugin/relativeTime';
8 | import { DarkLightBlock, OpacityAnimateBlock } from '@/components/ui';
9 | import { PageType } from '../comments-list';
10 | import { getAbsoluteUrl } from '@/lib/utils';
11 |
12 | interface Props {
13 | id: string;
14 | postId: string;
15 | author: TUser;
16 | content: string;
17 | createdAt: Date;
18 | isOwner: boolean;
19 | pageType: PageType;
20 | onUpdate?: VoidFunction;
21 | className?: string;
22 | }
23 |
24 | export const CommentItem: React.FC = ({
25 | id,
26 | postId,
27 | author,
28 | content,
29 | createdAt,
30 | isOwner,
31 | onUpdate,
32 | pageType,
33 | className,
34 | }) => {
35 | return (
36 |
37 |
38 |
39 |
40 |
46 |
47 |
55 |
56 |
57 |
{content}
58 |
59 | {(dayjs.extend(relativeTime), dayjs(createdAt).fromNow())}
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/shared/comments/comment/index.ts:
--------------------------------------------------------------------------------
1 | export { CommentItem } from './comment-item';
2 |
--------------------------------------------------------------------------------
/src/components/shared/comments/comments-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Comment } from '@/types';
4 | import { CommentItem } from './comment';
5 | import { useGetMe } from '@/lib/hooks';
6 |
7 | export type PageType = 'user' | 'post';
8 |
9 | interface Props {
10 | items: Comment[] | null;
11 | onClickEditComment?: (comment: Comment) => void;
12 | pageType: PageType;
13 | className?: string;
14 | }
15 |
16 | export const CommentsList: React.FC = ({
17 | items,
18 | onClickEditComment,
19 | pageType,
20 | className,
21 | }) => {
22 | const { data: userData } = useGetMe();
23 |
24 | if (!items || items.length === 0) {
25 | return null;
26 | }
27 |
28 | return (
29 |
30 | {items.map((item) => (
31 | {
40 | if (onClickEditComment) onClickEditComment(item);
41 | }}
42 | pageType={pageType}
43 | className="mb-3"
44 | />
45 | ))}
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/shared/comments/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leroyalle/chime-frontend/17641bf79f34b59be20096cdac357e9307b1557f/src/components/shared/comments/index.ts
--------------------------------------------------------------------------------
/src/components/shared/edit/edit-block/edit-block.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { DarkLightBlock } from '../../../ui';
4 | import { ArrowLeft } from 'lucide-react';
5 | import { useRouter } from 'next/navigation';
6 | import { UpdateUserForm } from '../../forms';
7 |
8 | interface Props {
9 | id: string;
10 | name: string;
11 | email: string;
12 | about: string;
13 | avatar: string;
14 | className?: string;
15 | }
16 |
17 | export const EditBlock: React.FC = ({ id, name, email, about, avatar, className }) => {
18 | const router = useRouter();
19 |
20 | return (
21 |
22 |
23 |
router.back()} /> Блог
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/shared/edit/edit-block/index.ts:
--------------------------------------------------------------------------------
1 | export { EditBlock } from './edit-block';
2 |
--------------------------------------------------------------------------------
/src/components/shared/edit/edit-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { Api } from '@/services/api-client';
5 | import { useQuery } from '@tanstack/react-query';
6 | import { UserResponse } from '../../../types/response';
7 | import { EditBlock } from './edit-block';
8 |
9 | interface Props {
10 | initialData: UserResponse;
11 | className?: string;
12 | }
13 |
14 | export const EditWrapper: React.FC = ({ initialData, className }) => {
15 | const { data } = useQuery({
16 | ...Api.users.getUserQueryOptions(initialData.user.id),
17 | initialData,
18 | });
19 |
20 | return (
21 |
22 | Редактирование профиля
23 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/shared/edit/index.ts:
--------------------------------------------------------------------------------
1 | export { EditWrapper } from './edit-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/empty-state.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | title: string;
6 | text?: string;
7 | className?: string;
8 | }
9 |
10 | export const EmptyState: React.FC = ({ title, text, className }) => {
11 | return (
12 |
13 |
{title}
14 |
{text}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/shared/feed/index.ts:
--------------------------------------------------------------------------------
1 | export { NewFeedWrapper } from './new';
2 | export { PopularFeedWrapper } from './popular';
3 |
--------------------------------------------------------------------------------
/src/components/shared/feed/new/index.ts:
--------------------------------------------------------------------------------
1 | export { NewFeedWrapper } from './new-feed-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/feed/new/new-feed-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect } from 'react';
3 | import { InfinityResponse } from '../../../../types/response';
4 | import { Post } from '../../../../types/dto';
5 | import { useGetMe, useInfinityScrollPosts } from '@/lib/hooks';
6 | import { WritePost } from '../../write-post';
7 | import { PostsList } from '../../posts-list';
8 | import { Spinner } from '@nextui-org/react';
9 | import { EmptyState } from '../../empty-state';
10 | import { useNewMarkSlice } from '@/store';
11 |
12 | interface Props {
13 | initialPosts: InfinityResponse;
14 | className?: string;
15 | }
16 |
17 | export const NewFeedWrapper: React.FC = ({ initialPosts, className }) => {
18 | const { data: posts, cursor, isFetchingNextPage } = useInfinityScrollPosts({ initialPosts });
19 | const { data: me } = useGetMe();
20 | const setNewMark = useNewMarkSlice((state) => state.setNewMark);
21 |
22 | useEffect(() => {
23 | setNewMark(false);
24 | }, []);
25 |
26 | return (
27 |
28 |
29 | {posts ? (
30 | <>
31 |
32 | {cursor}
33 | {isFetchingNextPage &&
}
34 | >
35 | ) : (
36 |
37 | )}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/shared/feed/popular/index.ts:
--------------------------------------------------------------------------------
1 | export { PopularFeedWrapper } from './popular-feed-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/feed/popular/popular-feed-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { InfinityResponse } from '../../../../types/response';
4 | import { Post } from '../../../../types/dto';
5 | import { useGetMe, useInfinityScrollPopularPosts } from '@/lib/hooks';
6 | import { WritePost } from '../../write-post';
7 | import { PostsList } from '../../posts-list';
8 | import { Spinner } from '@nextui-org/react';
9 | import { EmptyState } from '../../empty-state';
10 |
11 | interface Props {
12 | initialPosts: InfinityResponse;
13 | className?: string;
14 | }
15 |
16 | export const PopularFeedWrapper: React.FC = ({ initialPosts, className }) => {
17 | const {
18 | data: posts,
19 | cursor,
20 | isFetchingNextPage,
21 | } = useInfinityScrollPopularPosts({ initialPosts });
22 | const { data: me } = useGetMe();
23 | return (
24 |
25 |
26 | {posts ? (
27 | <>
28 |
29 | {cursor}
30 | {isFetchingNextPage &&
}
31 | >
32 | ) : (
33 |
34 | )}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/shared/follows/followers/followers-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Follows } from '@/types';
4 | import { Friend } from '../../friend';
5 | import { DarkLightBlock } from '../../../ui';
6 |
7 | interface Props {
8 | items: Omit[];
9 | className?: string;
10 | }
11 |
12 | export const FollowersList: React.FC = ({ items, className }) => {
13 | return (
14 |
15 | {items.map((item) => (
16 |
17 |
23 |
24 | ))}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/shared/follows/followers/followers-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { useInfinityScrollUserFollowers } from '@/lib/hooks';
5 | import { FollowersList } from './followers-list';
6 | import { Spinner } from '@nextui-org/react';
7 | import { EmptyState } from '../../empty-state';
8 | import { InfinityResponse, Follows } from '@/types';
9 | import { Loader } from '@/components/ui';
10 |
11 | interface Props {
12 | userId: string;
13 | initialData: InfinityResponse[]>;
14 | className?: string;
15 | }
16 |
17 | export const FollowersWrapper: React.FC = ({ userId, initialData, className }) => {
18 | const { data, isFetching, cursor, isFetchingNextPage } = useInfinityScrollUserFollowers({
19 | userId,
20 | initialData,
21 | });
22 |
23 | if (isFetching) {
24 | return ;
25 | }
26 |
27 | if (!data || data.length === 0) {
28 | return ;
29 | }
30 |
31 | return (
32 |
33 | Подписчики
34 |
35 | {cursor}
36 | {isFetchingNextPage && }
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/shared/follows/followers/index.ts:
--------------------------------------------------------------------------------
1 | export { FollowersWrapper } from './followers-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/follows/following/following-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Follows } from '@/types';
4 | import { Friend } from '../../friend';
5 | import { DarkLightBlock } from '../../../ui';
6 |
7 | interface Props {
8 | items: Omit[];
9 | className?: string;
10 | }
11 |
12 | export const FollowingList: React.FC = ({ items, className }) => {
13 | return (
14 |
15 | {items.map((item) => (
16 |
17 |
23 |
24 | ))}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/shared/follows/following/following-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { InfinityResponse, Follows } from '@/types';
5 | import { useInfinityScrollUserFollowing } from '@/lib/hooks';
6 | import { FollowingList } from './following-list';
7 | import { Spinner } from '@nextui-org/react';
8 | import { EmptyState } from '../../empty-state';
9 | import { Loader } from '@/components/ui';
10 |
11 | interface Props {
12 | userId: string;
13 | initialData: InfinityResponse[]>;
14 | className?: string;
15 | }
16 |
17 | export const FollowingWrapper: React.FC = ({ userId, initialData, className }) => {
18 | const { data, isFetching, cursor, isFetchingNextPage } = useInfinityScrollUserFollowing({
19 | userId,
20 | initialData,
21 | });
22 |
23 | if (isFetching) {
24 | return ;
25 | }
26 |
27 | if (!data || data.length === 0) {
28 | return ;
29 | }
30 |
31 | return (
32 |
33 | Подписки
34 |
35 | {cursor}
36 | {isFetchingNextPage && }
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/shared/follows/following/index.ts:
--------------------------------------------------------------------------------
1 | export { FollowingWrapper } from './following-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/follows/index.ts:
--------------------------------------------------------------------------------
1 | export { FollowersWrapper } from './followers';
2 | export { FollowingWrapper } from './following';
3 |
--------------------------------------------------------------------------------
/src/components/shared/forms/auth-forms/index.ts:
--------------------------------------------------------------------------------
1 | export { RegisterForm } from './register-form';
2 | export { LoginForm } from './login-form';
3 | export { CodeForm } from './code-form';
4 |
--------------------------------------------------------------------------------
/src/components/shared/forms/index.ts:
--------------------------------------------------------------------------------
1 | export { RegisterForm, LoginForm, CodeForm } from './auth-forms';
2 | export { UpdateUserForm } from './update-user-form';
3 |
--------------------------------------------------------------------------------
/src/components/shared/friend/friend-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
4 | import { Ellipsis } from 'lucide-react';
5 |
6 | export const FriendActions: React.FC = () => {
7 | return (
8 |
9 |
10 |
11 |
12 | alert(key)}>
13 | Пожаловаться
14 |
15 | Удалить
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/shared/friend/friend.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn, getAbsoluteUrl } from '@/lib/utils';
3 | import { User } from '@nextui-org/react';
4 | import { FriendActions } from './friend-actions';
5 | import { RoutesEnum } from '../../../types';
6 | import Link from 'next/link';
7 |
8 | interface Props {
9 | friendId: string;
10 | name: string;
11 | alias: string;
12 | avatar: string | null;
13 | className?: string;
14 | }
15 |
16 | export const Friend: React.FC = ({ friendId, name, alias, avatar, className }) => {
17 | return (
18 |
19 | {name}}
21 | description={
22 |
23 | @{alias}
24 |
25 | }
26 | avatarProps={{
27 | src: getAbsoluteUrl(avatar),
28 | }}
29 | />
30 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/shared/friend/friends-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Friend } from './friend';
4 | import { DarkLightBlock } from '@/components/ui';
5 | import { Friend as TFriend } from '../../../types/dto';
6 |
7 | interface Props {
8 | items: TFriend[];
9 | className?: string;
10 | }
11 |
12 | export const FriendsList: React.FC = ({ items, className }) => {
13 | return (
14 |
15 | {items.map((item) => (
16 |
17 |
18 |
19 | ))}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/shared/friend/index.ts:
--------------------------------------------------------------------------------
1 | export { FriendsList } from './friends-list';
2 | export { Friend } from './friend';
3 |
--------------------------------------------------------------------------------
/src/components/shared/friends/friends-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { FriendsList } from '../friend';
5 | import { useInfinityScrollUserFriends } from '@/lib/hooks';
6 | import { Spinner } from '@nextui-org/react';
7 | import { InfinityResponse } from '../../../types/response';
8 | import { EmptyState } from '../empty-state';
9 | import { Friend } from '../../../types/dto';
10 | import { Loader } from '@/components/ui';
11 |
12 | interface Props {
13 | userId: string;
14 | initialData: InfinityResponse;
15 | className?: string;
16 | }
17 |
18 | export const FriendsWrapper: React.FC = ({ userId, initialData, className }) => {
19 | const { data, cursor, isFetching, isPending, isError, isFetchingNextPage } =
20 | useInfinityScrollUserFriends({
21 | userId,
22 | initialData,
23 | });
24 |
25 | if (isPending || isFetching) {
26 | return ;
27 | }
28 |
29 | if (isError) {
30 | return Error
;
31 | }
32 |
33 | if (!data || data.length === 0) {
34 | return ;
35 | }
36 |
37 | return (
38 |
39 | Друзья
40 |
41 | {cursor}
42 | {isFetchingNextPage && }
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/shared/friends/index.ts:
--------------------------------------------------------------------------------
1 | export { FriendsWrapper } from './friends-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/header-menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavbarMenu, NavbarMenuItem } from '@nextui-org/react';
3 | import { navMenuItems } from '@/constants';
4 | import Link from 'next/link';
5 |
6 | interface Props {
7 | userId: string;
8 | setIsMenuOpen: (value: boolean) => void;
9 | className?: string;
10 | }
11 |
12 | export const HeaderMenu: React.FC = ({ userId, setIsMenuOpen, className }) => {
13 | return (
14 |
15 | {navMenuItems.map((item, i) => (
16 |
17 | setIsMenuOpen(false)}
19 | className="w-full"
20 | href={item.appendUserId ? `${item.href}/${userId}` : item.href}>
21 | {item.title}
22 |
23 |
24 | ))}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/index.ts:
--------------------------------------------------------------------------------
1 | export { HeaderMenu } from './header-menu';
2 | export * from './profile-button';
3 | export * from './search';
4 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/profile-button/index.ts:
--------------------------------------------------------------------------------
1 | export { ProfileButton } from './profile-button';
2 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/profile-button/profile-button-menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropdownItem, DropdownMenu } from '@nextui-org/react';
3 | import { RoutesEnum } from '@/types';
4 | import Link from 'next/link';
5 | import { LogoutButton } from '@/components/shared';
6 |
7 | interface Props {
8 | userId: string;
9 | email: string;
10 | className?: string;
11 | }
12 |
13 | export const ProfileButtonMenu: React.FC = ({ userId, email, className }) => {
14 | return (
15 |
16 |
21 | Аккаунт
22 | {email}
23 |
24 |
25 | Настройки
26 |
27 |
28 | Выйти
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/profile-button/profile-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getAbsoluteUrl } from '@/lib/utils';
3 | import { ProfileButtonMenu } from './profile-button-menu';
4 | import { Dropdown, DropdownTrigger } from '@nextui-org/react';
5 | import { Avatar } from '@/components/ui';
6 |
7 | interface Props {
8 | name: string;
9 | avatar: string | null;
10 | email: string;
11 | userId: string;
12 | }
13 |
14 | export const ProfileButton: React.FC = ({ userId, name, avatar, email }) => {
15 | return (
16 |
17 |
18 |
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/search/components/empty-state.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | className?: string;
6 | }
7 |
8 | export const EmptyState: React.FC = ({ className }) => (
9 |
10 |
Пользователи с таким именем не найдены
11 |

12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/search/components/index.ts:
--------------------------------------------------------------------------------
1 | export { SearchResults } from './search-results';
2 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/search/components/search-results.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { EmptyState } from './empty-state';
3 | import { Spinner } from '@nextui-org/react';
4 | import { SearchedUsersList } from './searched-users-list';
5 | import { User } from '@/types';
6 |
7 | interface Props {
8 | isFetching: boolean;
9 | searchQuery: string;
10 | users: User[] | undefined;
11 | onClickItem: VoidFunction;
12 | className?: string;
13 | }
14 |
15 | export const SearchResults: React.FC = ({ isFetching, searchQuery, users, onClickItem }) => {
16 | if (isFetching) return ;
17 | if (!searchQuery) return null;
18 | if (!users?.length) return ;
19 | return ;
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/search/components/searched-users-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn, getAbsoluteUrl } from '@/lib/utils';
3 | import { RoutesEnum, User } from '@/types';
4 | import { Avatar } from '@nextui-org/react';
5 | import Link from 'next/link';
6 |
7 | interface Props {
8 | users: User[];
9 | onClickItem: VoidFunction;
10 | className?: string;
11 | }
12 |
13 | export const SearchedUsersList: React.FC = ({ users, onClickItem, className }) => (
14 | <>
15 | Люди
16 | {users.map((user) => (
17 |
25 |
26 | {user.name}
27 |
28 | ))}
29 | >
30 | );
31 |
--------------------------------------------------------------------------------
/src/components/shared/header/components/search/index.ts:
--------------------------------------------------------------------------------
1 | export { Search } from './search';
2 |
--------------------------------------------------------------------------------
/src/components/shared/header/header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import { Navbar, NavbarBrand, NavbarContent, NavbarMenuToggle } from '@nextui-org/react';
4 | import { useGetMe } from '@/lib/hooks';
5 | import { Logo } from '@/components/ui';
6 | import { HeaderMenu, ProfileButton, Search } from './components';
7 |
8 | interface Props {
9 | className?: string;
10 | }
11 |
12 | export const Header: React.FC = ({ className }) => {
13 | const [isMenuOpen, setIsMenuOpen] = useState(false);
14 | const { data: me } = useGetMe();
15 |
16 | if (!me) {
17 | return null;
18 | }
19 |
20 | return (
21 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/shared/header/index.ts:
--------------------------------------------------------------------------------
1 | export { Header } from './header';
2 |
--------------------------------------------------------------------------------
/src/components/shared/im/im-head/im-head.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | className?: string;
6 | }
7 |
8 | export const ImHead: React.FC = ({ className }) => {
9 | return (
10 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/shared/im/im-head/index.ts:
--------------------------------------------------------------------------------
1 | export { ImHead } from './im-head';
2 |
--------------------------------------------------------------------------------
/src/components/shared/im/im-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { ChatList } from '../chat-list';
5 | import { ImHead as Header } from './im-head';
6 | import { EmptyState } from '../empty-state';
7 | import { useQuery } from '@tanstack/react-query';
8 | import { Api } from '@/services/api-client';
9 | import { Spinner } from '@nextui-org/react';
10 | import { SearchChats } from '../search-chats';
11 | import { useDebounce } from '@/components/ui/shadcn-expendsions';
12 |
13 | interface Props {
14 | className?: string;
15 | }
16 |
17 | export const ImWrapper: React.FC = ({ className }) => {
18 | const isMounted = React.useRef(false);
19 | const [searchValue, setSearchValue] = useState('');
20 | const searchQuery = useDebounce(searchValue, 500);
21 | const { data: chats, isFetching } = useQuery(Api.chat.getUserChatsQueryOptions(searchQuery));
22 |
23 | useEffect(() => {
24 | if (!isMounted.current) {
25 | isMounted.current = true;
26 | }
27 | }, []);
28 |
29 | if (isFetching && !isMounted.current) {
30 | return (
31 |
35 | );
36 | }
37 |
38 | if ((!chats || chats.length === 0) && !isMounted.current) {
39 | return ;
40 | }
41 |
42 | return (
43 |
44 | Ваши чаты
45 |
46 |
47 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/shared/im/index.ts:
--------------------------------------------------------------------------------
1 | export { ImWrapper } from './im-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { Container } from '../ui/container';
2 | export { Providers, RootProviders } from './providers';
3 | export { ProfileCard } from './profile-card';
4 | export { Sidebar } from './sidebar';
5 | export { ProfileButton } from './profile-button';
6 | export { NewFeedWrapper, PopularFeedWrapper } from './feed';
7 | export { UserWrapper } from './user';
8 | export { ImWrapper } from './im';
9 | export { ChatWrapper } from './chat';
10 | export { EditWrapper } from './edit';
11 | export { FollowersWrapper } from './follows';
12 | export { FollowingWrapper } from './follows';
13 | export { Header } from './header';
14 | export { AsideChatsWrapper } from './aside-chats';
15 | export { LikedPostsWrapper } from './liked-posts';
16 | export { BookmarksWrapper } from './bookmarks';
17 | export { LogoutButton } from './logout-button';
18 |
--------------------------------------------------------------------------------
/src/components/shared/liked-posts/index.ts:
--------------------------------------------------------------------------------
1 | export { LikedPostsWrapper } from './liked-posts-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/liked-posts/liked-posts-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { useInfinityScrollUserLikedPosts } from '@/lib/hooks';
4 | import { PostsList } from '../posts-list';
5 | import { Spinner } from '@nextui-org/react';
6 | import { EmptyState } from '../empty-state';
7 | import { InfinityResponse } from '../../../types/response';
8 | import { Post } from '../../../types/dto';
9 |
10 | interface Props {
11 | initialData: InfinityResponse;
12 | }
13 |
14 | export const LikedPostsWrapper: React.FC = ({ initialData }) => {
15 | const {
16 | data: likedPosts,
17 | isFetching,
18 | cursor,
19 | isFetchingNextPage,
20 | } = useInfinityScrollUserLikedPosts(initialData);
21 |
22 | if (!likedPosts || likedPosts.length === 0) {
23 | return ;
24 | }
25 |
26 | if (isFetching) {
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | return (
35 | <>
36 |
37 | {cursor}
38 | {isFetchingNextPage && }
39 | >
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/shared/logout-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { Button } from '@nextui-org/react';
5 | import Cookies from 'js-cookie';
6 | import { TokensEnum } from '@/types';
7 | import { useRouter } from 'next/navigation';
8 |
9 | interface Props {
10 | className?: string;
11 | }
12 |
13 | export const LogoutButton: React.FC = ({ className }) => {
14 | const router = useRouter();
15 |
16 | const handleLogout = () => {
17 | Cookies.remove(TokensEnum.JWT);
18 | router.push('/auth');
19 | };
20 |
21 | return (
22 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/shared/modals/auth-modals/auth-modal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect } from 'react';
3 | import { Modal, ModalContent, ModalBody } from '@nextui-org/react';
4 | import { AuthTabs } from './auth-tabs';
5 | import Cookies from 'js-cookie';
6 | import { TokensEnum } from '@/types';
7 | import { useQueryClient } from '@tanstack/react-query';
8 |
9 | export const AuthModal: React.FC = () => {
10 | const queryClient = useQueryClient();
11 |
12 | useEffect(() => {
13 | Cookies.remove(TokensEnum.JWT);
14 | queryClient.removeQueries();
15 | }, []);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/shared/modals/auth-modals/auth-tabs.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { LoginForm } from '../../forms';
3 | import { RegisterActions } from './register-actions';
4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui';
5 |
6 | export const AuthTabs: React.FC = () => {
7 | const [activeTab, setActiveTab] = useState('login');
8 |
9 | return (
10 |
11 |
12 |
13 | Вход
14 |
15 |
16 | Регистрация
17 |
18 |
19 |
20 |
21 |
22 |
23 | setActiveTab('login')} />
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/shared/modals/auth-modals/index.ts:
--------------------------------------------------------------------------------
1 | export { AuthModal } from './auth-modal';
2 |
--------------------------------------------------------------------------------
/src/components/shared/modals/auth-modals/register-actions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { RegisterForm, CodeForm } from '../../forms';
3 |
4 | interface Props {
5 | onChangeTab: VoidFunction;
6 | className?: string;
7 | }
8 |
9 | export const RegisterActions: React.FC = ({ onChangeTab }) => {
10 | const [userId, setUserId] = useState('');
11 | const [currentRegisterAction, setCurrentRegisterAction] = useState<'credentials' | 'code'>(
12 | 'credentials',
13 | );
14 |
15 | const onSuccessSendEmail = (userId: string) => {
16 | setCurrentRegisterAction('code');
17 | setUserId(userId);
18 | };
19 |
20 | return (
21 | <>
22 | {currentRegisterAction === 'credentials' ? (
23 | setCurrentRegisterAction('code')}
26 | />
27 | ) : (
28 |
29 | )}
30 | >
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/shared/modals/index.ts:
--------------------------------------------------------------------------------
1 | export { AuthModal } from './auth-modals';
2 | export { SharedModal } from './shared-modal';
3 |
--------------------------------------------------------------------------------
/src/components/shared/modals/shared-modal/index.ts:
--------------------------------------------------------------------------------
1 | export { SharedModal } from './shared-modal';
2 |
--------------------------------------------------------------------------------
/src/components/shared/modals/shared-modal/shared-modal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import { cn } from '@/lib/utils';
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogHeader,
9 | DialogTitle,
10 | } from '@/components/ui';
11 | import { useQuery } from '@tanstack/react-query';
12 | import { Api } from '@/services/api-client';
13 | import { ChatListShareMode } from '../../chat-list';
14 | import { SearchChats } from '../../search-chats';
15 | import { useDebounce } from '@/components/ui/shadcn-expendsions';
16 |
17 | interface Props {
18 | isOpen: boolean;
19 | onClose: VoidFunction;
20 | className?: string;
21 | }
22 |
23 | export const SharedModal: React.FC = ({ isOpen, onClose, className }) => {
24 | const [searchValue, setSearchValue] = useState('');
25 | const searchQuery = useDebounce(searchValue, 500);
26 | const { data: chats, isFetching } = useQuery({
27 | ...Api.chat.getUserChatsQueryOptions(searchQuery),
28 | });
29 |
30 | return (
31 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/index.ts:
--------------------------------------------------------------------------------
1 | export { PostTopActions as TopActions } from './post-top-actions';
2 | export { PostBottomActions as BottomActions } from './post-bottom-actions';
3 | export { PostBody, PostImages, PostGallery } from './post-body';
4 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/index.ts:
--------------------------------------------------------------------------------
1 | export { PostBody } from './post-body';
2 | export { PostImages } from './post-images';
3 | export { PostGallery } from './post-gallery';
4 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-body.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { PostContent } from './post-content';
4 | import { PostImages } from './post-images';
5 | import { PostTags } from './post-tags';
6 | import { Image, Tag } from '../../../../../types/dto';
7 | import { PostGallery } from './post-gallery';
8 |
9 | interface Props {
10 | content: string;
11 | images: Image[] | null;
12 | tags: Tag[];
13 | className?: string;
14 | }
15 |
16 | export const PostBody: React.FC = ({ content, images, tags, className }) => {
17 | const [isOpenGallery, setIsOpenGallery] = useState(false);
18 | const [currentIndex, setCurrentIndex] = useState(0);
19 | const handleClickImage = (i: number) => {
20 | setCurrentIndex(i);
21 | setIsOpenGallery(true);
22 | };
23 |
24 | const onClickPrev = () => {
25 | if (images?.length && currentIndex - 1 >= 0) {
26 | setCurrentIndex(currentIndex - 1);
27 | }
28 | };
29 |
30 | const onClickNext = () => {
31 | if (images?.length && currentIndex + 1 < images.length) {
32 | setCurrentIndex(currentIndex + 1);
33 | }
34 | };
35 |
36 | return (
37 | <>
38 |
43 | setIsOpenGallery(false)}
49 | images={images}
50 | />
51 | >
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-content.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | content: string;
6 | className?: string;
7 | }
8 |
9 | export const PostContent: React.FC = ({ content, className }) => {
10 | return (
11 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-gallery/index.ts:
--------------------------------------------------------------------------------
1 | export { PostGallery } from './post-gallery';
2 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-gallery/post-gallery-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props extends React.ButtonHTMLAttributes {
5 | icon: JSX.Element;
6 | disabled: boolean;
7 | className?: string;
8 | }
9 |
10 | export const PostGalleryButton: React.FC = ({ icon, disabled, className, ...props }) => {
11 | return (
12 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-gallery/post-gallery-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { getAbsoluteUrl } from '@/lib/utils';
4 | import { ChevronLeft, ChevronRight } from 'lucide-react';
5 | import { PostGalleryButton } from './post-gallery-button';
6 | import { Image } from '../../../../../../types/dto';
7 |
8 | interface Props extends React.HTMLAttributes {
9 | currentIndex: number;
10 | images: Image[] | null;
11 | onClickNext: VoidFunction;
12 | onClickPrev: VoidFunction;
13 | className?: string;
14 | }
15 |
16 | export const PostGalleryItem: React.FC = ({
17 | currentIndex,
18 | images,
19 | onClickNext,
20 | onClickPrev,
21 | className,
22 | ...props
23 | }) => {
24 | if (!images) {
25 | return null;
26 | }
27 |
28 | return (
29 |
30 |
}
32 | disabled={currentIndex === 0}
33 | onClick={onClickPrev}
34 | className="absolute left-0 top-1/2 transform -translate-y-1/2"
35 | />
36 |
37 |
})
43 |
44 |
}
46 | disabled={currentIndex >= images?.length - 1}
47 | onClick={onClickNext}
48 | className="absolute right-0 top-1/2 transform -translate-y-1/2"
49 | />
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-gallery/post-gallery.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image } from '../../../../../../types/dto';
3 | import { PostGalleryItem } from './post-gallery-item';
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogHeader,
9 | DialogTitle,
10 | } from '@/components/ui';
11 |
12 | interface Props {
13 | currentIndex: number;
14 | images: Image[] | null;
15 | onClickPrev: VoidFunction;
16 | onClickNext: VoidFunction;
17 | isOpen: boolean;
18 | onClose: VoidFunction;
19 | }
20 |
21 | export const PostGallery: React.FC = ({
22 | currentIndex,
23 | images,
24 | onClickNext,
25 | onClickPrev,
26 | isOpen,
27 | onClose,
28 | }) => {
29 | return (
30 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-images.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { getAbsoluteUrl } from '@/lib/utils';
4 | import { Image } from '../../../../../types/dto';
5 |
6 | interface Props {
7 | items: Image[] | null;
8 | onClick: (id: number) => void;
9 | className?: string;
10 | }
11 |
12 | export const PostImages: React.FC = ({ items, onClick, className }) => {
13 | if (items && items.length === 0) {
14 | return null;
15 | }
16 |
17 | return (
18 |
23 | {items?.map((image, i) => (
24 |
![]()
onClick(i)}
27 | className="w-full h-full aspect-[1/1] object-cover rounded-md"
28 | src={getAbsoluteUrl(image.url)}
29 | alt={'Post image'}
30 | />
31 | ))}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-body/post-tags.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Tag } from '../../../../../types/dto';
4 |
5 | interface Props {
6 | tags: Tag[];
7 | className?: string;
8 | }
9 |
10 | export const PostTags: React.FC = ({ tags, className }) => {
11 | if (!tags || tags.length === 0) {
12 | return null;
13 | }
14 |
15 | return (
16 |
17 | {tags.map((tag) => (
18 | -
19 | {`#${tag.value}`}
20 |
21 | ))}
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-bottom-actions/index.ts:
--------------------------------------------------------------------------------
1 | export { PostBottomActions } from './post-bottom-actions';
2 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-bottom-actions/post-bottom-actions-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 |
5 | interface Props {
6 | icon: React.ReactNode;
7 | count?: number;
8 | onClick?: VoidFunction;
9 | loading?: boolean;
10 | className?: string;
11 | }
12 |
13 | export const PostBottomActionsItem: React.FC = ({
14 | icon,
15 | count,
16 | onClick,
17 | loading,
18 | className,
19 | }) => {
20 | return (
21 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-bottom-actions/shared-body.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { PostBottomActionsItem } from './post-bottom-actions-item';
3 | import { Undo2 } from 'lucide-react';
4 | import { SharedModal } from '@/components/shared/modals';
5 |
6 | interface Props {
7 | shared: number;
8 | onClick?: VoidFunction;
9 | }
10 |
11 | export const SharedBody: React.FC = ({ shared, onClick }) => {
12 | const [isOpen, setIsOpen] = useState(false);
13 | return (
14 | <>
15 | {
18 | setIsOpen(true);
19 | onClick?.();
20 | }}
21 | icon={}
22 | />
23 | setIsOpen(false)} />
24 | >
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/components/post-top-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
3 | import { Ellipsis } from 'lucide-react';
4 | import { useDeletePost } from '@/lib/hooks';
5 |
6 | interface Props {
7 | postId: string;
8 | userId: string;
9 | isOwner: boolean;
10 | }
11 |
12 | export const PostTopActions: React.FC = ({ postId, userId, isOwner }) => {
13 | const { deletePost } = useDeletePost(userId, postId);
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Пожаловаться
22 | {isOwner ? (
23 | deletePost()}>
28 | Удалить
29 |
30 | ) : null}
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/shared/post-item/index.ts:
--------------------------------------------------------------------------------
1 | export { PostItem } from './post-item';
2 | export { PostImages, PostGallery } from './components';
3 |
--------------------------------------------------------------------------------
/src/components/shared/post/comment-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import { WriteComment } from '../write-comment';
4 | import { CommentsList, PageType } from '../comments/comments-list';
5 | import { Comment } from '@/types';
6 |
7 | interface Props {
8 | userId: string;
9 | postId: string;
10 | comments: Comment[] | null;
11 | pageType: PageType;
12 | }
13 |
14 | export const CommentSection: React.FC = ({ userId, postId, comments, pageType }) => {
15 | const [editableComment, setEditableComment] = useState(null);
16 |
17 | return (
18 | <>
19 |
25 | setEditableComment(null)}
30 | className="mb-3 sticky bottom-2"
31 | />
32 | >
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/shared/post/index.ts:
--------------------------------------------------------------------------------
1 | export { PostWrapper } from './post-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/post/post-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { PostItem } from '../post-item';
5 | import { Post } from '@/types';
6 | import { useQuery } from '@tanstack/react-query';
7 | import { Api } from '@/services/api-client';
8 | import { CommentSection } from './comment-section';
9 | import { useGetMe } from '@/lib/hooks';
10 |
11 | interface Props {
12 | initialData: Post;
13 | className?: string;
14 | }
15 |
16 | export const PostWrapper: React.FC = ({ initialData, className }) => {
17 | const { data: userData } = useGetMe();
18 | const { data: postItem, isError } = useQuery({
19 | ...Api.posts.getPostByIdQueryOptions(initialData.id),
20 | initialData,
21 | });
22 |
23 | if (isError) {
24 | return Error
;
25 | }
26 |
27 | return (
28 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/shared/posts-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { PostItem } from './post-item';
4 | import { Post } from '../../types/dto';
5 | import { useGetMe } from '@/lib/hooks';
6 |
7 | interface Props {
8 | items: Post[];
9 | className?: string;
10 | }
11 |
12 | export const PostsList: React.FC = ({ items, className }) => {
13 | const { data: userData } = useGetMe();
14 |
15 | if (!items || items.length === 0) {
16 | return null;
17 | }
18 |
19 | return (
20 |
21 | {items.map((item) => (
22 |
40 | ))}
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/shared/profile-card.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn, getAbsoluteUrl } from '@/lib/utils';
4 | import { useGetMe } from '@/lib/hooks';
5 | import { Avatar } from '../ui';
6 | import Link from 'next/link';
7 | import { RoutesEnum } from '../../types';
8 |
9 | interface Props {
10 | className?: string;
11 | }
12 | export const ProfileCard: React.FC = ({ className }) => {
13 | const { data: me } = useGetMe();
14 |
15 | console.log('PROFILE_CARD:', me);
16 |
17 | if (!me) {
18 | return null;
19 | }
20 | return (
21 |
22 |
23 |
24 |
{me.user.name}
25 |
28 | @{me.user.alias}
29 |
30 |
{me.user.about}
31 |
32 |
35 | - {me.user.followerCount}
36 | - Подписчики
37 |
38 |
41 | - {me.user.followingCount}
42 | - Подписки
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/shared/providers/auth-guard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect } from 'react';
3 | import { useRouter } from 'next/navigation';
4 | import { Spinner } from '@nextui-org/react';
5 | import { useGetMe } from '@/lib/hooks';
6 | import Cookies from 'js-cookie';
7 | import { RoutesEnum, TokensEnum } from '@/types';
8 |
9 | interface Props {
10 | children: React.ReactNode;
11 | }
12 |
13 | export const AuthGuard: React.FC = ({ children }) => {
14 | const router = useRouter();
15 | const { isPending, isError } = useGetMe();
16 |
17 | useEffect(() => {
18 | if (isError) {
19 | Cookies.remove(TokensEnum.JWT);
20 | router.push(RoutesEnum.AUTH);
21 | }
22 | return () => {
23 | console.log('CLEANUP');
24 | };
25 | }, [isError]);
26 |
27 | useEffect(() => {
28 | return () => {
29 | console.log('CLEANUP');
30 | };
31 | }, []);
32 |
33 | console.log(' isPending', isPending);
34 | console.log(' isError', isError);
35 |
36 | if (isPending || isError) {
37 | return (
38 |
42 | );
43 | }
44 |
45 | return children;
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/shared/providers/index.ts:
--------------------------------------------------------------------------------
1 | export { Providers } from './providers';
2 | export { RootProviders } from './root-providers';
3 | export { SocketProvider } from './socket-provider';
4 | export { ThemeProvider } from './theme-provider';
5 | export { AuthGuard } from './auth-guard';
6 |
--------------------------------------------------------------------------------
/src/components/shared/providers/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { NextUIProvider } from '@nextui-org/react';
3 | import NextTopLoader from 'nextjs-toploader';
4 | import React, { ReactNode } from 'react';
5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6 | import { ThemeProvider } from './theme-provider';
7 | import { Toaster } from '../../ui';
8 |
9 | interface Props {
10 | children: ReactNode;
11 | }
12 |
13 | const queryClient = new QueryClient({
14 | defaultOptions: {
15 | queries: {
16 | retry: false,
17 | },
18 | },
19 | });
20 |
21 | export const Providers: React.FC = ({ children }) => {
22 | return (
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/shared/providers/root-providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { ReactNode } from 'react';
3 | import { SocketProvider } from './socket-provider';
4 | import { AuthGuard } from './auth-guard';
5 |
6 | interface Props {
7 | children: ReactNode;
8 | }
9 |
10 | export const RootProviders: React.FC = ({ children }) => {
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/shared/providers/socket-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Cookies from 'js-cookie';
3 | import { useEffect, useRef, ReactNode, useCallback, memo, useMemo } from 'react';
4 | import { usePathname } from 'next/navigation';
5 | import { useNewMarkSlice } from '@/store';
6 | import { SocketEventsEnum, TokensEnum } from '@/types';
7 | import {
8 | useGetMe,
9 | useMessageActions,
10 | useMessageHandlers,
11 | useSocketConnection,
12 | useSocketEvents,
13 | } from '@/lib/hooks';
14 | import { SocketContext } from '@/lib/utils';
15 |
16 | export const SocketProvider = memo(function SocketProvider({ children }: { children: ReactNode }) {
17 | const { data: me } = useGetMe();
18 | const { setNewMark } = useNewMarkSlice();
19 | const token = Cookies.get(TokensEnum.JWT);
20 | const socket = useSocketConnection(token);
21 | const pathName = usePathname();
22 | const pathNameRef = useRef(pathName);
23 | const messageHandlers = useMessageHandlers(pathNameRef, me);
24 | const { sendMessage, updateMessage, deleteMessage } = useMessageActions(socket);
25 |
26 | useEffect(() => {
27 | pathNameRef.current = pathName;
28 | }, [pathName]);
29 |
30 | const socketHandlers = useMemo(
31 | () => ({
32 | ...messageHandlers,
33 | setNewMark,
34 | }),
35 | [messageHandlers, setNewMark],
36 | );
37 |
38 | useSocketEvents(socket, socketHandlers);
39 |
40 | const broadcastNewPost = useCallback(() => {
41 | socket.current?.emit(SocketEventsEnum.POST_NEW, true);
42 | }, []);
43 |
44 | return (
45 |
46 | {children}
47 |
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/src/components/shared/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/shared/search-chats.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Input } from '@nextui-org/react';
4 | import { Loader, Search } from 'lucide-react';
5 |
6 | interface Props {
7 | value: string;
8 | onChange: (value: string) => void;
9 | isLoading?: boolean;
10 | className?: string;
11 | }
12 |
13 | export const SearchChats: React.FC = ({ value, onChange, isLoading, className }) => {
14 | const handleChange = useCallback((e: React.ChangeEvent) => {
15 | onChange(e.target.value);
16 | }, []);
17 |
18 | return (
19 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/shared/select-image.tsx:
--------------------------------------------------------------------------------
1 | import React, { InputHTMLAttributes } from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props extends InputHTMLAttributes {
5 | className?: string;
6 | }
7 |
8 | export const SelectImage: React.FC = ({ className, ...props }) => {
9 | return (
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/shared/sidebar/index.ts:
--------------------------------------------------------------------------------
1 | export { Sidebar } from './sidebar';
2 | export { SidebarDrawer } from './sidebar-drawer';
3 |
--------------------------------------------------------------------------------
/src/components/shared/sidebar/section-title.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | title: string;
6 | className?: string;
7 | }
8 |
9 | export const SectionTitle: React.FC = ({ title, className }) => {
10 | return (
11 |
12 | {title}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/shared/sidebar/sidebar-drawer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Sheet,
4 | SheetTrigger,
5 | SheetContent,
6 | SheetHeader,
7 | SheetTitle,
8 | SheetDescription,
9 | DarkLightBlock,
10 | SheetClose,
11 | } from '../../ui';
12 | import { Sidebar } from './sidebar';
13 | import { Menu, X } from 'lucide-react';
14 |
15 | export const SidebarDrawer: React.FC = () => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Are you absolutely sure?
25 |
26 | This action cannot be undone. This will permanently delete your account and remove
27 | your data from our servers.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/shared/sidebar/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import Link from 'next/link';
4 | import { usePathname } from 'next/navigation';
5 | import type { FC, ReactNode } from 'react';
6 |
7 | interface Props {
8 | icon: ReactNode;
9 | href: string;
10 | mark?: boolean;
11 | children: ReactNode;
12 | }
13 |
14 | export const SidebarItem: FC = ({ icon, href, children, mark }) => {
15 | const pathName = usePathname();
16 | const isPathMatch = new RegExp(`^${href}(?:\\/[^/]+)?$`).test(pathName);
17 | const newMark = pathName === '/news' ? mark : false;
18 |
19 | return (
20 |
28 | {icon} {children}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/shared/sidebar/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { Fragment } from 'react';
3 | import { SidebarItem } from './sidebar-item';
4 | import {} from 'lucide-react';
5 | import { useGetMe } from '@/lib/hooks';
6 | import { DarkLightBlock } from '@/components/ui';
7 | import { useNewMarkSlice } from '@/store';
8 | import { ListNavElement } from '../../ui';
9 | import { cn } from '@/lib/utils';
10 | import { SectionTitle } from './section-title';
11 | import { LogoutButton } from '../logout-button';
12 | import { navCategories } from '@/constants';
13 |
14 | interface Props {
15 | className?: string;
16 | }
17 |
18 | export const Sidebar: React.FC = ({ className }) => {
19 | const { data: me } = useGetMe();
20 | const { newMark } = useNewMarkSlice();
21 |
22 | if (!me) {
23 | return null;
24 | }
25 |
26 | return (
27 |
28 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/shared/user/index.ts:
--------------------------------------------------------------------------------
1 | export { UserWrapper } from './user-wrapper';
2 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/content-tabs/content-tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | // import { Tab, Tabs } from '@nextui-org/react';
4 | import { CommentsTab, PostsTab } from './tabs';
5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui';
6 |
7 | interface Props {
8 | userId: string;
9 | }
10 |
11 | export const ContentTabs: React.FC = ({ userId }) => {
12 | return (
13 |
14 |
15 | Записи
16 | Комментарии
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/content-tabs/index.ts:
--------------------------------------------------------------------------------
1 | export { ContentTabs } from './content-tabs';
2 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/content-tabs/tabs/comments-tab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useInfinityScrollUserComments } from '@/lib/hooks';
3 | import { Spinner } from '@nextui-org/react';
4 | import { CommentsList } from '@/components/shared/comments/comments-list';
5 | import { EmptyState } from '@/components/shared/empty-state';
6 |
7 | interface Props {
8 | userId: string;
9 | }
10 |
11 | export const CommentsTab: React.FC = ({ userId }) => {
12 | const { data: comments, cursor, isFetchingNextPage } = useInfinityScrollUserComments({ userId });
13 |
14 | if (!comments || comments.length === 0) {
15 | return ;
16 | }
17 |
18 | return (
19 | <>
20 |
21 | {cursor}
22 | {isFetchingNextPage && }
23 | >
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/content-tabs/tabs/index.ts:
--------------------------------------------------------------------------------
1 | export { PostsTab } from './posts-tab';
2 | export { CommentsTab } from './comments-tab';
3 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/content-tabs/tabs/posts-tab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useInfinityScrollPostsByUserId } from '@/lib/hooks';
3 | import { PostsList } from '@/components/shared/posts-list';
4 | import { Spinner } from '@nextui-org/react';
5 | import { EmptyState } from '@/components/shared/empty-state';
6 |
7 | interface Props {
8 | userId: string;
9 | }
10 |
11 | export const PostsTab: React.FC = ({ userId }) => {
12 | const {
13 | data: posts,
14 | cursor,
15 | isFetchingNextPage,
16 | } = useInfinityScrollPostsByUserId({
17 | userId,
18 | });
19 |
20 | if (!posts || posts.length === 0) {
21 | return ;
22 | }
23 |
24 | return (
25 | <>
26 |
27 | {cursor}
28 | {isFetchingNextPage && }
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/index.ts:
--------------------------------------------------------------------------------
1 | export { ContentTabs } from './content-tabs';
2 | export { UserInfo } from './user-info';
3 | export { UserActions } from './user-actions';
4 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/user-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { Button } from '@nextui-org/react';
5 | import { MessageCircleMore, Settings } from 'lucide-react';
6 | import Link from 'next/link';
7 | import { RoutesEnum } from '../../../../types';
8 | import { useFollowUser, useUnFollowUser } from '@/lib/hooks';
9 |
10 | interface Props {
11 | userId: string;
12 | isOwner: boolean;
13 | isFollowing: boolean;
14 | onClickChat: (data: { recipientId: string }) => void;
15 | onClickChatLoading: boolean;
16 | className?: string;
17 | }
18 |
19 | export const UserActions: React.FC = ({
20 | userId,
21 | isOwner,
22 | isFollowing,
23 | onClickChat,
24 | onClickChatLoading,
25 | className,
26 | }) => {
27 | const { followUser, isPending: isPendingFollow } = useFollowUser(userId);
28 | const { unFollowUser, isPending: isPendingUnFollow } = useUnFollowUser(userId);
29 |
30 | const onClickFollowUser = () => {
31 | if (!isFollowing) {
32 | followUser();
33 | } else {
34 | unFollowUser();
35 | }
36 | };
37 |
38 | return (
39 |
40 | {isOwner ? (
41 |
42 | Редактировать
43 |
44 | ) : (
45 |
51 | )}
52 | {isOwner ? (
53 |
54 |
55 |
56 | ) : (
57 |
63 | )}
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/components/shared/user/profile/user-info.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import Link from 'next/link';
4 | import { ContentTabs } from './content-tabs';
5 | import { RoutesEnum } from '../../../../types';
6 | import dayjs from 'dayjs';
7 |
8 | interface Props {
9 | userId: string;
10 | name: string;
11 | date: string;
12 | about: string | null;
13 | followerCount: number;
14 | followingCount: number;
15 | className?: string;
16 | }
17 |
18 | export const UserInfo: React.FC = ({
19 | userId,
20 | name,
21 | date,
22 | about,
23 | followerCount,
24 | followingCount,
25 | className,
26 | }) => {
27 | return (
28 |
29 |
{name}
30 |
На сайте с {dayjs(date).format('DD.MM.YYYY')}
31 | {about &&
{about}
}
32 |
33 |
34 | {followerCount} подписчиков
35 |
36 |
37 | {followingCount} подписки
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/shared/user/user-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn, getAbsoluteUrl } from '@/lib/utils';
4 | import { DarkLightBlock, Avatar } from '../../ui';
5 | import { UserActions, UserInfo } from './profile';
6 | import { UserResponse } from '../../../types/response';
7 | import { useQuery } from '@tanstack/react-query';
8 | import { Api } from '@/services/api-client';
9 | import { useCreateChat } from '@/lib/hooks';
10 |
11 | interface Props {
12 | initialData: UserResponse;
13 | className?: string;
14 | }
15 |
16 | export const UserWrapper: React.FC = ({ initialData, className }) => {
17 | const { createChat, isPending: isPendingChat } = useCreateChat();
18 | const { data } = useQuery({
19 | ...Api.users.getUserQueryOptions(initialData.user.id),
20 | initialData,
21 | });
22 |
23 | return (
24 |
25 | Профиль пользователя
26 |
27 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/shared/write-post/index.ts:
--------------------------------------------------------------------------------
1 | export { WritePost } from './write-post';
2 |
--------------------------------------------------------------------------------
/src/components/shared/write-post/selected-images.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch } from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { X } from 'lucide-react';
4 |
5 | interface Props {
6 | selectedFiles: File[] | null;
7 | onDelete: Dispatch>;
8 | className?: string;
9 | }
10 |
11 | export const SelectedImages: React.FC = ({ selectedFiles, onDelete, className }) => {
12 | if (!selectedFiles || selectedFiles.length === 0) {
13 | return null;
14 | }
15 |
16 | return (
17 |
18 | {selectedFiles.map((file, i) => (
19 |
25 |
})
30 |
33 |
34 | ))}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/shared/write-post/write-post-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Camera, Hash } from 'lucide-react';
4 | import { SelectImage } from '../select-image';
5 | import { Button } from '@/components/ui';
6 | import { Button as ButtonUI } from '@nextui-org/react';
7 |
8 | interface Props {
9 | isPendingCreate?: boolean;
10 | tagsIsOpen: boolean;
11 | setTagsIsOpen: React.Dispatch>;
12 | onChangeFile: (event: React.ChangeEvent) => void;
13 | className?: string;
14 | }
15 |
16 | export const WritePostActions: React.FC = ({
17 | isPendingCreate,
18 | tagsIsOpen,
19 | setTagsIsOpen,
20 | onChangeFile,
21 | className,
22 | }) => {
23 | return (
24 |
25 |
26 |
38 |
46 |
47 |
48 | Отправить
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Avatar as NextUIAvatar } from '@nextui-org/react';
3 |
4 | export const Avatar = NextUIAvatar;
5 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/src/components/ui/container.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import React, { ReactNode } from 'react';
3 |
4 | interface Props {
5 | children: ReactNode;
6 | className?: string;
7 | }
8 |
9 | export const Container: React.FC> = ({ className, children }) => {
10 | return {children}
;
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/ui/dark-light-block.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | children: ReactNode;
6 | className?: string;
7 | }
8 |
9 | export const DarkLightBlock: React.FC = ({ children, className }) => {
10 | return (
11 | {children}
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/ui/editable-message.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { X } from 'lucide-react';
4 |
5 | interface Props {
6 | title: string;
7 | text: string;
8 | onClose: VoidFunction;
9 | className?: string;
10 | }
11 |
12 | export const EditableMessage: React.FC = ({ title, text, onClose, className }) => {
13 | return (
14 |
15 |
20 |
{title}
21 |
{text}
22 |
23 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/ui/index.ts:
--------------------------------------------------------------------------------
1 | export { Container } from './container';
2 | export { Avatar } from './avatar';
3 | export { DarkLightBlock } from './dark-light-block';
4 | export { ListNavElement } from './list-nav-element';
5 | export { MotionDiv } from './motion-div';
6 | export { OpacityAnimateBlock } from './opacity-animate-block';
7 | export { EditableMessage } from './editable-message';
8 | export { MultipleSelectorCreatable } from './multiple-selector-creatable';
9 | export { Checkbox } from './checkbox';
10 | export { Button } from './button';
11 | export { Toaster } from './sonner';
12 | export { Logo } from './logo';
13 | export { Line } from './line';
14 | export { Loader } from './loader';
15 | export * from './tabs';
16 | export * from './sheet';
17 | export * from './dialog';
18 | export * from './form';
19 | export * from './shadcn-expendsions';
20 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/line.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 |
4 | interface Props {
5 | className?: string;
6 | }
7 |
8 | export const Line: React.FC = ({ className }) => {
9 | return ;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/ui/list-nav-element.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | interface Props {
4 | children: ReactNode;
5 | }
6 |
7 | export const ListNavElement: React.FC = ({ children }) => {
8 | return (
9 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/ui/loader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { Spinner } from '@nextui-org/react';
4 |
5 | interface Props {
6 | className?: string;
7 | }
8 |
9 | export const Loader: React.FC = ({ className }) => {
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/ui/logo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import Link from 'next/link';
4 | import { RoutesEnum } from '@/types';
5 |
6 | interface Props {
7 | className?: string;
8 | }
9 |
10 | export const Logo: React.FC = ({ className }) => {
11 | return (
12 |
15 |
16 | Chime
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/ui/motion-div.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { motion } from 'framer-motion';
3 |
4 | export const MotionDiv = motion.div;
5 |
--------------------------------------------------------------------------------
/src/components/ui/multiple-selector-creatable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MultipleSelector, { Option } from './shadcn-expendsions';
3 | import { cn } from '@/lib/utils';
4 |
5 | interface Props {
6 | value: Option[];
7 | setValue: (value: Option[]) => void;
8 | className?: string;
9 | }
10 |
11 | export const MultipleSelectorCreatable = ({ value, setValue, className }: Props) => {
12 | return (
13 |
14 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/ui/opacity-animate-block.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MotionDiv } from './motion-div';
3 |
4 | interface Props {
5 | duration?: number;
6 | delay?: number;
7 | children: React.ReactNode;
8 | className?: string;
9 | }
10 |
11 | export const OpacityAnimateBlock: React.FC = ({
12 | duration = 0.5,
13 | delay = 0,
14 | children,
15 | className,
16 | }) => {
17 | return (
18 |
27 | {children}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export * from './navigation';
2 |
--------------------------------------------------------------------------------
/src/constants/navigation/index.ts:
--------------------------------------------------------------------------------
1 | export { navCategories } from './nav-categories';
2 | export { navMenuItems } from './nav-menu-items';
3 |
--------------------------------------------------------------------------------
/src/constants/navigation/nav-categories.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Newspaper,
3 | Flame,
4 | UserRoundCheck,
5 | MessageCircle,
6 | Heart,
7 | Bookmark,
8 | LucideIcon,
9 | } from 'lucide-react';
10 | import { RoutesEnum } from '@/types';
11 |
12 | export interface NavItem {
13 | title: string;
14 | href: RoutesEnum;
15 | icon: LucideIcon;
16 | color?: string;
17 | appendUserId?: boolean;
18 | }
19 |
20 | export interface NavCategory {
21 | title: string;
22 | items: NavItem[];
23 | }
24 |
25 | export const navCategories: NavCategory[] = [
26 | {
27 | title: 'Основное',
28 | items: [
29 | {
30 | title: 'Популярное',
31 | href: RoutesEnum.HOME,
32 | icon: Newspaper,
33 | color: 'text-success-500',
34 | },
35 | {
36 | title: 'Свежее',
37 | href: RoutesEnum.NEW,
38 | icon: Flame,
39 | color: 'text-danger-500',
40 | },
41 | ],
42 | },
43 | {
44 | title: 'Общение',
45 | items: [
46 | {
47 | title: 'Друзья',
48 | href: RoutesEnum.FRIENDS,
49 | icon: UserRoundCheck,
50 | color: 'text-blue-500',
51 | appendUserId: true,
52 | },
53 | {
54 | title: 'Мессенджер',
55 | href: RoutesEnum.MESSAGES,
56 | icon: MessageCircle,
57 | },
58 | ],
59 | },
60 | {
61 | title: 'Любимое',
62 | items: [
63 | {
64 | title: 'Ваши лайки',
65 | href: RoutesEnum.LIKES,
66 | icon: Heart,
67 | color: 'text-red-500',
68 | },
69 | {
70 | title: 'Закладки',
71 | href: RoutesEnum.BOOKMARKS,
72 | icon: Bookmark,
73 | color: 'text-purple-500',
74 | },
75 | ],
76 | },
77 | ];
78 |
--------------------------------------------------------------------------------
/src/constants/navigation/nav-menu-items.ts:
--------------------------------------------------------------------------------
1 | import { navCategories, NavItem } from './nav-categories';
2 |
3 | export const navMenuItems = navCategories.reduce((acc, { items }) => {
4 | acc.push(...items);
5 | return acc;
6 | }, [] as NavItem[]);
7 |
--------------------------------------------------------------------------------
/src/lib/hooks/bookmarks/index.ts:
--------------------------------------------------------------------------------
1 | export { useAddBookmark } from './use-add-bookmark';
2 | export { useRemoveBookmark } from './use-remove-bookmark';
3 |
--------------------------------------------------------------------------------
/src/lib/hooks/bookmarks/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { toggleBookmark } from './toggle-bookmark';
2 |
--------------------------------------------------------------------------------
/src/lib/hooks/bookmarks/lib/toggle-bookmark.ts:
--------------------------------------------------------------------------------
1 | import { InfiniteData } from '@tanstack/react-query';
2 | import { InfinityResponse } from '../../../../types/response';
3 | import { Post } from '../../../../types/dto';
4 |
5 | type TData = InfiniteData, unknown> | undefined;
6 | export const toggleBookmark = (postId: string, data: TData, action: 'add' | 'remove') => {
7 | if (!data) {
8 | return undefined;
9 | }
10 |
11 | return {
12 | ...data,
13 | pages: data.pages.map((page) => ({
14 | ...page,
15 | data: page.data.map((p) =>
16 | p.id === postId
17 | ? {
18 | ...p,
19 | isBookmarked: action === 'add',
20 | }
21 | : p,
22 | ),
23 | })),
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/hooks/chat/index.ts:
--------------------------------------------------------------------------------
1 | export { useInfinityScrollMessages } from './use-infinity-scroll-messages';
2 | export { useCreateChat } from './use-create-chat';
3 | export * from './message';
4 |
--------------------------------------------------------------------------------
/src/lib/hooks/chat/message/index.ts:
--------------------------------------------------------------------------------
1 | export { useMessageHandlers } from './use-message-handlers';
2 | export { useMessageActions } from './use-message-actions';
3 |
--------------------------------------------------------------------------------
/src/lib/hooks/chat/message/use-message-actions.ts:
--------------------------------------------------------------------------------
1 | import { MessageRequest, SocketEventsEnum } from '@/types';
2 | import { MutableRefObject, useCallback } from 'react';
3 | import { Socket } from 'socket.io-client';
4 |
5 | export const useMessageActions = (socket: MutableRefObject) => {
6 | const sendMessage = useCallback((message: MessageRequest) => {
7 | socket.current?.emit(SocketEventsEnum.MESSAGES_POST, message);
8 | }, []);
9 |
10 | const deleteMessage = useCallback((data: { messageId: string }) => {
11 | socket.current?.emit(SocketEventsEnum.MESSAGES_DELETE, data);
12 | }, []);
13 |
14 | const updateMessage = useCallback((data: { messageId: string; messageBody: string }) => {
15 | socket.current?.emit(SocketEventsEnum.MESSAGES_UPDATE, data);
16 | }, []);
17 |
18 | return {
19 | sendMessage,
20 | deleteMessage,
21 | updateMessage,
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/lib/hooks/chat/message/use-message-notifications.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/navigation';
2 | import { toast } from 'sonner';
3 | import { RoutesEnum, ChatUpdate } from '@/types';
4 |
5 | export const useMessageNotifications = (
6 | pathNameRef: { current: string },
7 | me?: { user: { id: string } },
8 | ) => {
9 | const router = useRouter();
10 |
11 | const showNewMessageNotification = (data: ChatUpdate) => {
12 | const isPathMatch = pathNameRef.current === `${RoutesEnum.MESSAGES}/${data.chat.id}`;
13 |
14 | if (me?.user.id !== data.message.UserBase.id && !isPathMatch) {
15 | toast(data.message.UserBase.name, {
16 | description: data.message.content,
17 | action: {
18 | label: 'Читать',
19 | onClick: () => router.push(`${RoutesEnum.MESSAGES}/${data.chat.id}`),
20 | },
21 | });
22 | }
23 | };
24 |
25 | return { showNewMessageNotification };
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/hooks/chat/use-create-chat.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useRouter } from 'next/navigation';
3 | import { RoutesEnum } from '../../../types';
4 | import { hasErrorField } from '@/lib/utils/shared/has-error-field';
5 | import { toast } from 'sonner';
6 | import { useMutation } from '@tanstack/react-query';
7 |
8 | export const useCreateChat = () => {
9 | const router = useRouter();
10 | const createChatMutation = useMutation({
11 | mutationFn: ({ recipientId }: { recipientId: string }) => Api.chat.getChatId(recipientId),
12 | onSuccess: ({ chatId }) => {
13 | router.push(`${RoutesEnum.MESSAGES}/${chatId}`);
14 | },
15 | onError: (error) => {
16 | if (hasErrorField(error)) {
17 | toast.error('Не удалось найти чат', {
18 | description: 'Попробуйте еще раз',
19 | });
20 | }
21 | },
22 | });
23 |
24 | return {
25 | createChat: createChatMutation.mutate,
26 | isPending: createChatMutation.isPending,
27 | isError: createChatMutation.isError,
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/lib/hooks/chat/use-infinity-scroll-messages.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { RefObject, useEffect } from 'react';
4 | import { useInView } from 'react-intersection-observer';
5 |
6 | export const useInfinityScrollMessages = ({
7 | chatId,
8 | chatRef,
9 | }: {
10 | chatId: string;
11 | chatRef: RefObject;
12 | }) => {
13 | const { ref, inView } = useInView();
14 |
15 | const { data, fetchNextPage, isPending, isFetching, isFetchingNextPage } = useInfiniteQuery(
16 | Api.chat.getMessagesByChatIdInfinityQueryOptions(chatId),
17 | );
18 |
19 | console.log('HOOKDATA:', data);
20 | console.log('HOOKPENDING:', isPending);
21 |
22 | useEffect(() => {
23 | if (inView && chatRef.current) {
24 | const scrollTopBefore = chatRef.current.scrollTop;
25 | const scrollHeightBefore = chatRef.current.scrollHeight;
26 |
27 | fetchNextPage().then(() => {
28 | setTimeout(() => {
29 | if (chatRef.current) {
30 | const scrollHeightAfter = chatRef.current.scrollHeight;
31 | const heightDifference = scrollHeightAfter - scrollHeightBefore;
32 | chatRef.current.scrollTop = scrollTopBefore + heightDifference;
33 | }
34 | }, 0);
35 | });
36 | }
37 | }, [inView, fetchNextPage]);
38 |
39 | const cursor = ;
40 |
41 | return {
42 | data,
43 | cursor,
44 | isPending,
45 | isFetching,
46 | isFetchingNextPage,
47 | };
48 | };
49 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/index.ts:
--------------------------------------------------------------------------------
1 | export { useCreateComment } from './use-create-comment';
2 | export { useDeleteComment } from './use-delete-comment';
3 | export { useUpdateComment } from './use-update-comment';
4 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/lib/handle-delete-comment-on-post-page.ts:
--------------------------------------------------------------------------------
1 | import { Post } from '@/types';
2 |
3 | type TData = Post | undefined;
4 |
5 | export const handleDeleteCommentOnPostPage = (commentId: string, data: TData) => {
6 | if (!data) {
7 | return undefined;
8 | }
9 | return {
10 | ...data,
11 | comments: data.comments && data.comments.filter((comment) => comment.id !== commentId),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/lib/handle-delete-comment-on-user-page.ts:
--------------------------------------------------------------------------------
1 | import { InfiniteData } from '@tanstack/react-query';
2 | import { InfinityResponse } from '../../../../types/response';
3 | import { Comment } from '../../../../types/dto';
4 |
5 | type TData = InfiniteData, unknown> | undefined;
6 |
7 | export const handleDeleteCommentOnUserPage = (commentId: string, data: TData): TData => {
8 | if (!data) {
9 | return undefined;
10 | }
11 |
12 | return {
13 | ...data,
14 | pages: data.pages.map((page) => ({
15 | ...page,
16 | data: page.data.filter((c) => c.id !== commentId),
17 | })),
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/lib/handle-post-comment-action.ts:
--------------------------------------------------------------------------------
1 | import { InfiniteData } from '@tanstack/react-query';
2 | import { Post } from '../../../../types/dto';
3 | import { InfinityResponse } from '../../../../types/response';
4 |
5 | type TData = InfiniteData, unknown> | undefined;
6 |
7 | export const handlePostCommentAction = (
8 | postId: string,
9 | data: TData,
10 | action: 'create' | 'delete',
11 | ): TData => {
12 | if (!data) {
13 | return undefined;
14 | }
15 | return {
16 | ...data,
17 | pages: data.pages.map((page) => ({
18 | ...page,
19 | data: page.data.map((post) =>
20 | post.id === postId
21 | ? {
22 | ...post,
23 | commentsCount: action === 'create' ? post.commentsCount + 1 : post.commentsCount - 1,
24 | }
25 | : post,
26 | ),
27 | })),
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/lib/handle-update-comment-on-post-page.ts:
--------------------------------------------------------------------------------
1 | import { Post } from '@/types';
2 |
3 | type TData = Post | undefined;
4 |
5 | export const handleUpdateCommentOnPostPage = (commentId: string, data: TData, content: string) => {
6 | if (!data) {
7 | return undefined;
8 | }
9 |
10 | return {
11 | ...data,
12 | comments:
13 | data.comments &&
14 | data.comments.map((comment) => {
15 | if (comment.id === commentId) {
16 | return {
17 | ...comment,
18 | content: content,
19 | };
20 | }
21 | return comment;
22 | }),
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { handlePostCommentAction } from './handle-post-comment-action';
2 | export { handleDeleteCommentOnUserPage } from './handle-delete-comment-on-user-page';
3 | export { handleDeleteCommentOnPostPage } from './handle-delete-comment-on-post-page';
4 | export { handleUpdateCommentOnPostPage } from './handle-update-comment-on-post-page';
5 |
--------------------------------------------------------------------------------
/src/lib/hooks/comments/use-update-comment.ts:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 | import { handleUpdateCommentOnPostPage } from './lib';
4 |
5 | export const useUpdateComment = (userId: string, postId: string) => {
6 | const queryClient = useQueryClient();
7 |
8 | const updateCommentMutation = useMutation({
9 | mutationFn: (params: { commentId: string; content: string }) =>
10 | Api.comments.updateComment({ id: params.commentId, content: params.content }),
11 |
12 | onMutate({ commentId, content }) {
13 | queryClient.cancelQueries({ queryKey: ['comments'] });
14 |
15 | const previewPostByIdData = queryClient.getQueryData({
16 | ...Api.posts.getPostByIdQueryOptions(postId).queryKey,
17 | });
18 |
19 | queryClient.setQueryData(Api.posts.getPostByIdQueryOptions(postId).queryKey, (old) => {
20 | return handleUpdateCommentOnPostPage(commentId, old, content);
21 | });
22 |
23 | return { previewPostByIdData };
24 | },
25 | onSettled: () => {
26 | queryClient.resetQueries(Api.comments.getUserCommentsInfinityQueryOptions(userId));
27 | },
28 |
29 | onError: (_, __, context) => {
30 | queryClient.setQueryData(
31 | Api.posts.getPostByIdQueryOptions(postId).queryKey,
32 | context?.previewPostByIdData,
33 | );
34 | },
35 | });
36 |
37 | return {
38 | updateComment: updateCommentMutation.mutate,
39 | isPending: updateCommentMutation.isPending,
40 | isError: updateCommentMutation.error,
41 | };
42 | };
43 |
--------------------------------------------------------------------------------
/src/lib/hooks/follows/index.ts:
--------------------------------------------------------------------------------
1 | export { useFollowUser } from './use-follow-user';
2 | export { useUnFollowUser } from './use-un-follow-user';
3 |
--------------------------------------------------------------------------------
/src/lib/hooks/follows/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { toggleUserFollow } from './toggle-user-follow';
2 |
--------------------------------------------------------------------------------
/src/lib/hooks/follows/lib/toggle-user-follow.ts:
--------------------------------------------------------------------------------
1 | import { UserResponse } from '../../../../types/response';
2 |
3 | type TData = UserResponse | undefined;
4 |
5 | export const toggleUserFollow = (data: TData, action: 'follow' | 'unFollow') => {
6 | if (!data) {
7 | return undefined;
8 | }
9 | return {
10 | ...data,
11 | user: {
12 | ...data.user,
13 | isFollowing: action === 'follow',
14 | followerCount:
15 | action === 'follow' ? data.user.followerCount + 1 : data.user.followerCount - 1,
16 | },
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/hooks/follows/use-follow-user.ts:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 | import { toast } from 'sonner';
4 | import { toggleUserFollow } from './lib';
5 |
6 | export const useFollowUser = (followingId: string) => {
7 | const queryClient = useQueryClient();
8 | const followUserMutation = useMutation({
9 | mutationFn: () => Api.follow.followUser({ followingId }),
10 |
11 | onMutate: async () => {
12 | const previousData = queryClient.getQueryData(
13 | Api.users.getUserQueryOptions(followingId).queryKey,
14 | );
15 |
16 | queryClient.setQueryData(Api.users.getUserQueryOptions(followingId).queryKey, (old) => {
17 | return toggleUserFollow(old, 'follow');
18 | });
19 |
20 | return { previousData };
21 | },
22 |
23 | onSettled: () => {
24 | queryClient.invalidateQueries({ queryKey: ['followers'] });
25 | queryClient.invalidateQueries({ queryKey: ['following'] });
26 | queryClient.invalidateQueries({ queryKey: ['friends'] });
27 | queryClient.invalidateQueries(Api.users.getMeQueryOptions());
28 | },
29 |
30 | onError: (error, __, context) => {
31 | queryClient.setQueryData(
32 | Api.users.getUserQueryOptions(followingId).queryKey,
33 | context?.previousData,
34 | );
35 | toast.error('Не удалось подписаться', { description: 'Попробуйте позже' });
36 | },
37 | });
38 |
39 | return {
40 | followUser: followUserMutation.mutate,
41 | isPending: followUserMutation.isPending,
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/hooks/follows/use-un-follow-user.ts:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 | import { toast } from 'sonner';
4 | import { toggleUserFollow } from './lib';
5 |
6 | export const useUnFollowUser = (unFollowingId: string) => {
7 | const queryClient = useQueryClient();
8 | const unFollowUserMutation = useMutation({
9 | mutationFn: () => Api.follow.unFollowUser(unFollowingId),
10 | onMutate: async () => {
11 | const previousData = queryClient.getQueryData(
12 | Api.users.getUserQueryOptions(unFollowingId).queryKey,
13 | );
14 |
15 | queryClient.setQueryData(Api.users.getUserQueryOptions(unFollowingId).queryKey, (old) => {
16 | return toggleUserFollow(old, 'unFollow');
17 | });
18 |
19 | return { previousData };
20 | },
21 |
22 | onError: (error, __, context) => {
23 | queryClient.setQueryData(
24 | Api.users.getUserQueryOptions(unFollowingId).queryKey,
25 | context?.previousData,
26 | );
27 | toast.error('Не удалось отписаться', { description: 'Попробуйте позже' });
28 | },
29 |
30 | onSettled: () => {
31 | queryClient.invalidateQueries({ queryKey: ['followers'] });
32 | queryClient.invalidateQueries({ queryKey: ['following'] });
33 | queryClient.invalidateQueries({ queryKey: ['friends'] });
34 | queryClient.invalidateQueries(Api.users.getMeQueryOptions());
35 | },
36 | });
37 |
38 | return {
39 | unFollowUser: unFollowUserMutation.mutate,
40 | isPending: unFollowUserMutation.isPending,
41 | };
42 | };
43 |
--------------------------------------------------------------------------------
/src/lib/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './follows';
2 | export * from './posts';
3 | export * from './user';
4 | export * from './chat';
5 | export * from './likes';
6 | export * from './comments';
7 | export * from './bookmarks';
8 | export * from './socket';
9 |
--------------------------------------------------------------------------------
/src/lib/hooks/likes/index.ts:
--------------------------------------------------------------------------------
1 | export { useLikePost } from './use-like-post';
2 | export { useUnlikePost } from './use-unlike-post';
3 |
--------------------------------------------------------------------------------
/src/lib/hooks/likes/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { togglePostLike } from './toggle-post-like';
2 |
--------------------------------------------------------------------------------
/src/lib/hooks/likes/lib/toggle-post-like.ts:
--------------------------------------------------------------------------------
1 | import { InfiniteData } from '@tanstack/react-query';
2 | import { Post } from '../../../../types/dto';
3 | import { InfinityResponse } from '../../../../types/response';
4 |
5 | type TData = InfiniteData, unknown> | undefined;
6 |
7 | export const togglePostLike = (postId: string, data: TData, action: 'like' | 'unlike'): TData => {
8 | if (!data) {
9 | return undefined;
10 | }
11 | return {
12 | ...data,
13 | pages: data.pages.map((page) => ({
14 | ...page,
15 | data: page.data.map((post) =>
16 | post.id === postId
17 | ? {
18 | ...post,
19 | isLiked: action === 'like',
20 | likesCount: action === 'like' ? post.likesCount + 1 : post.likesCount - 1,
21 | }
22 | : post,
23 | ),
24 | })),
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/hooks/posts/index.ts:
--------------------------------------------------------------------------------
1 | export { useCreatePost } from './use-create-post';
2 | export { useInfinityScrollPosts } from './use-infinity-scroll-posts';
3 | export { useDeletePost } from './use-delete-post';
4 | export { useInfinityScrollPopularPosts } from './use-infinity-scroll-popular-posts';
5 |
--------------------------------------------------------------------------------
/src/lib/hooks/posts/lib/handle-delete-post.ts:
--------------------------------------------------------------------------------
1 | import { InfiniteData } from '@tanstack/react-query';
2 | import { InfinityResponse } from '../../../../types/response';
3 | import { Post } from '../../../../types/dto';
4 |
5 | type TData = InfiniteData, unknown> | undefined;
6 |
7 | export const handleDeletePost = (postId: string, data: TData) => {
8 | if (!data) {
9 | return undefined;
10 | }
11 |
12 | return {
13 | ...data,
14 | pages: data.pages.map((page) => ({
15 | ...page,
16 | data: page.data.filter((p) => p.id !== postId),
17 | })),
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/hooks/posts/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { handleDeletePost } from './handle-delete-post';
2 |
--------------------------------------------------------------------------------
/src/lib/hooks/posts/use-create-post.ts:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 | import { useSocket } from '../socket/use-socket';
4 |
5 | export const useCreatePost = () => {
6 | const queryClient = useQueryClient();
7 | const { broadcastNewPost } = useSocket();
8 |
9 | const createPostMutation = useMutation({
10 | mutationFn: Api.posts.createPost,
11 | onSettled: () => {
12 | queryClient.resetQueries(Api.posts.getAllPostsInfinityQueryOptions());
13 | queryClient.resetQueries(Api.posts.getAllPopularPostsInfinityQueryOptions());
14 | },
15 |
16 | onSuccess: () => {
17 | broadcastNewPost();
18 | },
19 | });
20 |
21 | return { createPost: createPostMutation.mutate, isPending: createPostMutation.isPending };
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/hooks/posts/use-infinity-scroll-popular-posts.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useInView } from 'react-intersection-observer';
4 | import { Api } from '@/services/api-client';
5 | import { InfinityResponse } from '../../../types/response';
6 | import { Post } from '../../../types/dto';
7 |
8 | export const useInfinityScrollPopularPosts = ({
9 | initialPosts,
10 | }: {
11 | initialPosts: InfinityResponse;
12 | }) => {
13 | const { ref, inView } = useInView();
14 |
15 | const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
16 | ...Api.posts.getAllPopularPostsInfinityQueryOptions(),
17 | initialData: { pages: [initialPosts], pageParams: [1] },
18 | });
19 |
20 | useEffect(() => {
21 | if (inView) {
22 | fetchNextPage();
23 | }
24 | }, [inView]);
25 |
26 | const cursor = ;
27 |
28 | return { data, cursor, isFetchingNextPage };
29 | };
30 |
--------------------------------------------------------------------------------
/src/lib/hooks/posts/use-infinity-scroll-posts.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useInView } from 'react-intersection-observer';
4 | import { Api } from '@/services/api-client';
5 | import { InfinityResponse } from '../../../types/response';
6 | import { Post } from '../../../types/dto';
7 |
8 | export const useInfinityScrollPosts = ({
9 | initialPosts,
10 | }: {
11 | initialPosts: InfinityResponse;
12 | }) => {
13 | const { ref, inView } = useInView();
14 |
15 | const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
16 | ...Api.posts.getAllPostsInfinityQueryOptions(),
17 | initialData: { pages: [initialPosts], pageParams: [1] },
18 | });
19 |
20 | useEffect(() => {
21 | if (inView) {
22 | fetchNextPage();
23 | }
24 | }, [inView]);
25 |
26 | const cursor = ;
27 |
28 | return { data, cursor, isFetchingNextPage };
29 | };
30 |
--------------------------------------------------------------------------------
/src/lib/hooks/socket/index.ts:
--------------------------------------------------------------------------------
1 | export { useSocketEvents } from './use-socket-events';
2 | export { useSocket } from './use-socket';
3 | export { useSocketConnection } from './use-socket-connection';
4 |
--------------------------------------------------------------------------------
/src/lib/hooks/socket/use-socket-connection.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import { io, Socket } from 'socket.io-client';
3 |
4 | export const useSocketConnection = (token: string | undefined) => {
5 | const socket = useRef(null);
6 |
7 | useEffect(() => {
8 | socket.current = io(process.env.NEXT_PUBLIC_SOCKET_API_URL, {
9 | auth: { token },
10 | });
11 |
12 | return () => {
13 | if (socket.current) {
14 | socket.current.disconnect();
15 | console.log('Disconnected from WebSocket');
16 | }
17 | };
18 | }, [token]);
19 |
20 | return socket;
21 | };
22 |
--------------------------------------------------------------------------------
/src/lib/hooks/socket/use-socket-events.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Socket } from 'socket.io-client';
3 | import { ChatUpdate, SocketEventsEnum } from '@/types';
4 |
5 | export const useSocketEvents = (
6 | socketRef: { current: Socket | null },
7 | handlers: {
8 | handleNewMessage: (data: ChatUpdate) => void;
9 | handleDeleteMessage: (data: ChatUpdate) => void;
10 | handleUpdateMessage: (data: ChatUpdate) => void;
11 | setNewMark: (value: boolean) => void;
12 | },
13 | ) => {
14 | useEffect(() => {
15 | const socket = socketRef.current;
16 | if (!socket) return;
17 |
18 | socket.on(SocketEventsEnum.CONNECT, () => {
19 | console.log('Connected to WebSocket');
20 | });
21 |
22 | socket.on(SocketEventsEnum.UNAUTHORIZED, (message) => {
23 | console.log('Authorization error:', message);
24 | alert(message);
25 | });
26 |
27 | socket.on(SocketEventsEnum.POST_NEW, handlers.setNewMark);
28 | socket.on(SocketEventsEnum.MESSAGES_GET, handlers.handleNewMessage);
29 | socket.on(SocketEventsEnum.MESSAGES_DELETE, handlers.handleDeleteMessage);
30 | socket.on(SocketEventsEnum.MESSAGES_UPDATE, handlers.handleUpdateMessage);
31 |
32 | return () => {
33 | socket.off(SocketEventsEnum.CONNECT);
34 | socket.off(SocketEventsEnum.UNAUTHORIZED);
35 | socket.off(SocketEventsEnum.POST_NEW);
36 | socket.off(SocketEventsEnum.MESSAGES_GET);
37 | socket.off(SocketEventsEnum.MESSAGES_DELETE);
38 | socket.off(SocketEventsEnum.MESSAGES_UPDATE);
39 | };
40 | }, [socketRef.current, handlers]);
41 | };
42 |
--------------------------------------------------------------------------------
/src/lib/hooks/socket/use-socket.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { SocketContext } from '../../utils';
3 |
4 | export const useSocket = () => {
5 | const context = useContext(SocketContext);
6 | if (!context) {
7 | throw new Error('cannot find SocketContext');
8 | }
9 | return context;
10 | };
11 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/bookmarks/index.ts:
--------------------------------------------------------------------------------
1 | export { useInfinityScrollUserBookmarks } from './use-infinity-scroll-user-bookmarks';
2 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/bookmarks/use-infinity-scroll-user-bookmarks.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useEffect } from 'react';
4 | import { useInView } from 'react-intersection-observer';
5 | import { InfinityResponse } from '../../../../types/response';
6 | import { Post } from '../../../../types/dto';
7 |
8 | export const useInfinityScrollUserBookmarks = (initialData: InfinityResponse) => {
9 | const { ref, inView } = useInView();
10 |
11 | const { data, isLoading, isPending, isFetching, isError, fetchNextPage, isFetchingNextPage } =
12 | useInfiniteQuery({
13 | ...Api.bookmark.getUserBookmarksInfinityQueryOptions(),
14 | initialData: { pages: [initialData], pageParams: [1] },
15 | });
16 |
17 | useEffect(() => {
18 | if (inView) {
19 | fetchNextPage();
20 | }
21 | }, [inView]);
22 |
23 | const cursor = ;
24 |
25 | return { data, isLoading, isPending, isFetching, isError, cursor, isFetchingNextPage };
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/comments/index.ts:
--------------------------------------------------------------------------------
1 | export { useInfinityScrollUserComments } from './use-infinity-scroll-user-comments';
2 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/comments/use-infinity-scroll-user-comments.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useEffect } from 'react';
4 | import { useInView } from 'react-intersection-observer';
5 |
6 | export const useInfinityScrollUserComments = ({ userId }: { userId: string }) => {
7 | const { ref, inView } = useInView();
8 |
9 | const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
10 | ...Api.comments.getUserCommentsInfinityQueryOptions(userId),
11 | });
12 |
13 | useEffect(() => {
14 | if (inView) {
15 | fetchNextPage();
16 | }
17 | }, [inView]);
18 |
19 | const cursor = ;
20 |
21 | return { data, cursor, isFetchingNextPage };
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/follows/index.ts:
--------------------------------------------------------------------------------
1 | export { useInfinityScrollUserFollowers } from './use-infinity-scroll-user-followers';
2 | export { useInfinityScrollUserFollowing } from './use-infinity-scroll-user-following';
3 | export { useInfinityScrollUserFriends } from './use-infinity-scroll-user-friends';
4 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/follows/use-infinity-scroll-user-followers.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useEffect } from 'react';
4 | import { useInView } from 'react-intersection-observer';
5 | import { Follows } from '../../../../types/dto';
6 | import { InfinityResponse } from '../../../../types/response';
7 |
8 | export const useInfinityScrollUserFollowers = ({
9 | userId,
10 | initialData,
11 | }: {
12 | userId: string;
13 | initialData: InfinityResponse[]>;
14 | }) => {
15 | const { ref, inView } = useInView();
16 |
17 | const { data, isPending, isFetching, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
18 | ...Api.follow.getFollowersInfinityQueryOptions(userId),
19 | initialData: { pages: [initialData], pageParams: [1] },
20 | });
21 |
22 | useEffect(() => {
23 | if (inView) {
24 | fetchNextPage();
25 | }
26 | }, [inView]);
27 |
28 | const cursor = ;
29 |
30 | return { data, isPending, isFetching, cursor, isFetchingNextPage };
31 | };
32 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/follows/use-infinity-scroll-user-following.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useEffect } from 'react';
4 | import { useInView } from 'react-intersection-observer';
5 | import { InfinityResponse } from '../../../../types/response';
6 | import { Follows } from '../../../../types/dto';
7 |
8 | export const useInfinityScrollUserFollowing = ({
9 | userId,
10 | initialData,
11 | }: {
12 | userId: string;
13 | initialData: InfinityResponse[]>;
14 | }) => {
15 | const { ref, inView } = useInView();
16 |
17 | const { data, isPending, isFetching, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
18 | ...Api.follow.getFollowingInfinityQueryOptions(userId),
19 | initialData: { pages: [initialData], pageParams: [1] },
20 | });
21 |
22 | useEffect(() => {
23 | if (inView) {
24 | fetchNextPage();
25 | }
26 | }, [inView]);
27 |
28 | const cursor = ;
29 |
30 | return { data, isPending, isFetching, cursor, isFetchingNextPage };
31 | };
32 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/follows/use-infinity-scroll-user-friends.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { Friend, InfinityResponse } from '@/types';
3 | import { useInfiniteQuery } from '@tanstack/react-query';
4 |
5 | import { useEffect } from 'react';
6 | import { useInView } from 'react-intersection-observer';
7 |
8 | export const useInfinityScrollUserFriends = ({
9 | userId,
10 | initialData,
11 | }: {
12 | userId: string;
13 | initialData: InfinityResponse;
14 | }) => {
15 | const { ref, inView } = useInView();
16 |
17 | const { data, isPending, isFetching, isError, fetchNextPage, isFetchingNextPage } =
18 | useInfiniteQuery({
19 | ...Api.follow.getFriendsInfinityQueryOptions(userId),
20 | initialData: { pages: [initialData], pageParams: [1] },
21 | enabled: !!userId,
22 | });
23 |
24 | useEffect(() => {
25 | if (inView) {
26 | fetchNextPage();
27 | }
28 | }, [inView]);
29 |
30 | const cursor = ;
31 |
32 | return { data, cursor, isPending, isFetching, isError, isFetchingNextPage };
33 | };
34 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/index.ts:
--------------------------------------------------------------------------------
1 | export { useUpdateProfile } from './use-update-profile';
2 | export { useGetMe } from './use-get-me';
3 | export { useGetMeData } from './use-get-me-data';
4 | export { useInfinityScrollPostsByUserId, useInfinityScrollUserLikedPosts } from './posts';
5 | export { useInfinityScrollUserFollowers } from './follows';
6 | export { useInfinityScrollUserFollowing } from './follows';
7 | export { useInfinityScrollUserComments } from './comments';
8 | export { useInfinityScrollUserFriends } from './follows';
9 | export { useInfinityScrollUserBookmarks } from './bookmarks';
10 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/posts/index.ts:
--------------------------------------------------------------------------------
1 | export { useInfinityScrollPostsByUserId } from './use-infinity-scroll-posts-by-userId';
2 | export { useInfinityScrollUserLikedPosts } from './use-infinity-scroll-user-liked-posts';
3 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/posts/use-infinity-scroll-posts-by-userId.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useInView } from 'react-intersection-observer';
4 | import { Api } from '@/services/api-client';
5 |
6 | export const useInfinityScrollPostsByUserId = ({ userId }: { userId: string }) => {
7 | const { ref, inView } = useInView();
8 |
9 | const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
10 | ...Api.posts.getPostsByUserIdInfinityQueryOptions(userId),
11 | });
12 |
13 | useEffect(() => {
14 | if (inView) {
15 | fetchNextPage();
16 | }
17 | }, [inView]);
18 |
19 | const cursor = ;
20 |
21 | return { data, cursor, isFetchingNextPage };
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/posts/use-infinity-scroll-user-liked-posts.tsx:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useInfiniteQuery } from '@tanstack/react-query';
3 | import { useEffect } from 'react';
4 | import { useInView } from 'react-intersection-observer';
5 | import { Post } from '../../../../types/dto';
6 | import { InfinityResponse } from '../../../../types/response';
7 |
8 | export const useInfinityScrollUserLikedPosts = (initialData: InfinityResponse) => {
9 | const { ref, inView } = useInView();
10 |
11 | const { data, isPending, isFetching, isError, fetchNextPage, isFetchingNextPage } =
12 | useInfiniteQuery({
13 | ...Api.posts.getUserLikedPostsInfinityQueryOptions(),
14 | initialData: { pages: [initialData], pageParams: [1] },
15 | });
16 |
17 | useEffect(() => {
18 | if (inView) {
19 | fetchNextPage();
20 | }
21 | }, [inView]);
22 |
23 | const cursor = ;
24 |
25 | return { data, isPending, isFetching, isError, cursor, isFetchingNextPage };
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/use-get-me-data.ts:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useQueryClient } from '@tanstack/react-query';
3 |
4 | export const useGetMeData = () => {
5 | const queryClient = useQueryClient();
6 | const me = queryClient.getQueryData(Api.users.getMeQueryOptions().queryKey);
7 |
8 | return me;
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/use-get-me.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Api } from '@/services/api-client';
3 | import { useQuery } from '@tanstack/react-query';
4 |
5 | export const useGetMe = () => {
6 | return useQuery(Api.users.getMeQueryOptions());
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/hooks/user/use-update-profile.ts:
--------------------------------------------------------------------------------
1 | import { Api } from '@/services/api-client';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | export const useUpdateProfile = (id: string) => {
5 | const queryClient = useQueryClient();
6 | const updateProfileMutation = useMutation({
7 | mutationFn: Api.users.updateUser,
8 | onSettled: () => {
9 | queryClient.invalidateQueries(Api.users.getUserQueryOptions(id));
10 | queryClient.invalidateQueries(Api.users.getMeQueryOptions());
11 | },
12 | });
13 |
14 | return {
15 | updateProfile: updateProfileMutation.mutate,
16 | isPending: updateProfileMutation.isPending,
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/utils/auth/index.ts:
--------------------------------------------------------------------------------
1 | export { saveAuthCookies } from './save-auth-cookies';
2 |
--------------------------------------------------------------------------------
/src/lib/utils/auth/save-auth-cookies.ts:
--------------------------------------------------------------------------------
1 | import { TokensEnum } from '@/types';
2 | import Cookies from 'js-cookie';
3 |
4 | export const saveAuthCookies = async (token: string) => {
5 | Cookies.set(TokensEnum.JWT, token, {
6 | // httpOnly: true,
7 | // sameSite: 'strict',
8 | secure: process.env.NODE_ENV === 'production',
9 | expires: 1,
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/src/lib/utils/chat/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { updateChatsWithNewMessage } from './update-chats-with-new-message';
2 | export { sortChatsByLastMessage } from './sort-chats-by-last-message';
3 | export { updateAndSortChats } from './update-and-sort-chats';
4 | export { updateChatsWithUpdatedMessage } from './update-chats-with-updated-message';
5 |
--------------------------------------------------------------------------------
/src/lib/utils/chat/helpers/sort-chats-by-last-message.ts:
--------------------------------------------------------------------------------
1 | import { UserChat } from '@/types';
2 |
3 | export const sortChatsByLastMessage = (chats: UserChat[]): UserChat[] => {
4 | return chats.sort((a, b) => {
5 | const aDate = a.lastMessage ? new Date(a.lastMessage.createdAt).getTime() : 0;
6 | const bDate = b.lastMessage ? new Date(b.lastMessage.createdAt).getTime() : 0;
7 | return bDate - aDate;
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/utils/chat/helpers/update-and-sort-chats.ts:
--------------------------------------------------------------------------------
1 | import { MessageDto, UserChat } from '@/types';
2 | import { updateChatsWithNewMessage } from './update-chats-with-new-message';
3 | import { sortChatsByLastMessage } from './sort-chats-by-last-message';
4 | import { updateChatsWithUpdatedMessage } from './update-chats-with-updated-message';
5 |
6 | export const updateAndSortChats = (
7 | chats: UserChat[] | undefined,
8 | chat: UserChat,
9 | newMessage: MessageDto | null,
10 | type: 'new' | 'updated' = 'new',
11 | lastMessage: MessageDto | null = null,
12 | ): UserChat[] | undefined => {
13 | if (!chats) return undefined;
14 | const updatedChats =
15 | type === 'updated' && lastMessage
16 | ? updateChatsWithUpdatedMessage(chats, chat, newMessage, lastMessage)
17 | : updateChatsWithNewMessage(chats, chat, newMessage);
18 | return sortChatsByLastMessage(updatedChats);
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/utils/chat/helpers/update-chats-with-new-message.ts:
--------------------------------------------------------------------------------
1 | import { MessageDto, UserChat } from '@/types';
2 |
3 | export const updateChatsWithNewMessage = (
4 | chats: UserChat[],
5 | chat: UserChat,
6 | newMessage: MessageDto | null,
7 | ): UserChat[] => {
8 | const updatedChats = chats.map((ch) => {
9 | if (ch.id === chat.id) {
10 | return { ...ch, lastMessage: newMessage };
11 | }
12 | return ch;
13 | });
14 |
15 | const existingChat = updatedChats.find((chat) => chat.id === chat.id);
16 | if (!existingChat) {
17 | updatedChats.push({ ...chat, lastMessage: newMessage });
18 | }
19 |
20 | return updatedChats;
21 | };
22 |
--------------------------------------------------------------------------------
/src/lib/utils/chat/helpers/update-chats-with-updated-message.ts:
--------------------------------------------------------------------------------
1 | import { MessageDto, UserChat } from '@/types';
2 |
3 | export const updateChatsWithUpdatedMessage = (
4 | chats: UserChat[],
5 | chat: UserChat,
6 | updatedMessage: MessageDto | null,
7 | lastMessage: MessageDto | null,
8 | ): UserChat[] => {
9 | const updatedChats = chats.map((ch) => {
10 | if (ch.id === chat.id) {
11 | return { ...ch, lastMessage: lastMessage };
12 | }
13 | return ch;
14 | });
15 |
16 | const existingChat = updatedChats.find((chat) => chat.id === chat.id);
17 | if (!existingChat) {
18 | updatedChats.push({ ...chat, lastMessage: updatedMessage });
19 | }
20 |
21 | return updatedChats;
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/utils/chat/index.ts:
--------------------------------------------------------------------------------
1 | export * from './helpers';
2 |
--------------------------------------------------------------------------------
/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './shared';
2 | export * from './chat';
3 | export * from './auth';
4 | export * from './chat';
5 | export * from './socket';
6 |
--------------------------------------------------------------------------------
/src/lib/utils/shared/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/lib/utils/shared/get-absolute-url.ts:
--------------------------------------------------------------------------------
1 | export const getAbsoluteUrl = (url: string | null | undefined) => {
2 | if (!url) {
3 | return undefined;
4 | }
5 |
6 | return `${process.env.NEXT_PUBLIC_BASE_API_URL}/uploads/${url}`;
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/utils/shared/get-relative-time.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import relativeTime from 'dayjs/plugin/relativeTime';
3 | import updateLocale from 'dayjs/plugin/updateLocale';
4 |
5 | dayjs.extend(updateLocale);
6 | dayjs.extend(relativeTime);
7 |
8 | dayjs.updateLocale('en', {
9 | relativeTime: {
10 | future: 'in %s',
11 | past: '%s',
12 | s: 'now',
13 | m: 'a minute',
14 | mm: '%dm',
15 | h: 'an hour',
16 | hh: '%dh',
17 | d: 'a day',
18 | dd: '%dd',
19 | M: 'a month',
20 | MM: '%dM',
21 | y: 'a year',
22 | yy: '%dy',
23 | },
24 | });
25 |
26 | export const getRelativeTime = (date?: Date | null) => {
27 | if (!date) {
28 | return undefined;
29 | }
30 |
31 | return dayjs(date).fromNow();
32 | };
33 |
--------------------------------------------------------------------------------
/src/lib/utils/shared/handle-api-error.ts:
--------------------------------------------------------------------------------
1 | import { notFound, redirect } from 'next/navigation';
2 | import { AxiosError } from 'axios';
3 | import { RoutesEnum, TokensEnum } from '../../../types';
4 | import Cookies from 'js-cookie';
5 |
6 | export function handleApiError(error: AxiosError) {
7 | console.log(error);
8 | if (error.response?.status === 401 || error.response?.status === 403) {
9 | Cookies.remove(TokensEnum.JWT);
10 | return redirect(RoutesEnum.AUTH);
11 | }
12 | return notFound();
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/utils/shared/has-error-field.ts:
--------------------------------------------------------------------------------
1 | export function hasErrorField(err: unknown): err is {
2 | error: string;
3 | message: string;
4 | response: { data: { message: string } };
5 | statusCode: number;
6 | } {
7 | return (
8 | typeof err === 'object' &&
9 | err !== null &&
10 | 'response' in err &&
11 | typeof err.response === 'object' &&
12 | err.response !== null &&
13 | 'data' in err.response &&
14 | typeof err.response.data === 'object' &&
15 | err.response.data !== null &&
16 | 'message' in err.response.data &&
17 | typeof err.response.data.message === 'string'
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/utils/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { formatToClientDate } from './format-to-client-date';
2 | export { hasErrorField } from './has-error-field';
3 | export { handleApiError } from './handle-api-error';
4 | export { getAbsoluteUrl } from './get-absolute-url';
5 | export { getRelativeTime } from './get-relative-time';
6 | export { cn } from './utils';
7 |
--------------------------------------------------------------------------------
/src/lib/utils/shared/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/utils/socket/index.ts:
--------------------------------------------------------------------------------
1 | export { SocketContext } from './socket-context';
2 |
--------------------------------------------------------------------------------
/src/lib/utils/socket/socket-context.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { MessageRequest } from '@/types';
4 | import { createContext } from 'react';
5 |
6 | type SocketContextType = {
7 | sendMessage: (message: MessageRequest) => void;
8 | broadcastNewPost: VoidFunction;
9 | deleteMessage: (data: { messageId: string }) => void;
10 | updateMessage: (data: { messageId: string; messageBody: string }) => void;
11 | };
12 |
13 | export const SocketContext = createContext(null);
14 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | export default async function middleware(req: NextRequest) {
4 | const token = req.cookies.get('jwtToken')?.value;
5 | const response = NextResponse.next();
6 |
7 | if (!token) {
8 | return NextResponse.redirect(new URL('/auth', req.url));
9 | }
10 |
11 | response.headers.set('Authorization', `Bearer ${token}`);
12 |
13 | return response;
14 | }
15 | export const config = {
16 | matcher: [
17 | '/((?!auth|_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
18 | ],
19 | };
20 |
--------------------------------------------------------------------------------
/src/services/api-client.ts:
--------------------------------------------------------------------------------
1 | import * as posts from './post-api';
2 | import * as likes from './like-api';
3 | import * as comments from './comment-api';
4 | import * as users from './user-api';
5 | import * as follow from './follow-api';
6 | import * as chat from './chat-api';
7 | import * as bookmark from './bookmark-api';
8 |
9 | export const Api = {
10 | posts,
11 | likes,
12 | comments,
13 | users,
14 | follow,
15 | chat,
16 | bookmark,
17 | };
18 |
--------------------------------------------------------------------------------
/src/services/bookmark-api.ts:
--------------------------------------------------------------------------------
1 | import { infiniteQueryOptions } from '@tanstack/react-query';
2 | import { Post } from '../types/dto';
3 | import { InfinityResponse } from '../types/response';
4 | import { ApiRouter } from './constants';
5 | import { instance } from './instance';
6 | import { AxiosRequestHeaders } from 'axios';
7 |
8 | interface FindAllBookmarksParams {
9 | page?: number;
10 | perPage?: number;
11 | headers?: AxiosRequestHeaders;
12 | }
13 |
14 | export const findAllBookmarks = async ({
15 | page = 1,
16 | perPage = 10,
17 | headers,
18 | }: FindAllBookmarksParams): Promise> => {
19 | return (
20 | await instance.get>(
21 | `${ApiRouter.USER_BOOKMARKS}?page=${page}&perPage=${perPage}`,
22 | { headers },
23 | )
24 | ).data;
25 | };
26 |
27 | export const addBookmark = async (postId: string): Promise => {
28 | return (await instance.post(ApiRouter.USER_BOOKMARKS, { postId })).data;
29 | };
30 |
31 | export const removeBookmark = async (postId: string): Promise => {
32 | return (await instance.delete(`${ApiRouter.USER_BOOKMARKS}/${postId}`)).data;
33 | };
34 |
35 | export const getUserBookmarksInfinityQueryOptions = () => {
36 | const perPage = 10;
37 | return infiniteQueryOptions({
38 | queryKey: ['bookmark', 'list'],
39 | queryFn: (meta) => findAllBookmarks({ page: meta.pageParam, perPage }),
40 | initialPageParam: 1,
41 | select: ({ pages }) => pages.flatMap((page) => page.data),
42 | getNextPageParam(lastPage, allPages) {
43 | return lastPage.data.length === 0 ? undefined : allPages.length + 1;
44 | },
45 | refetchOnWindowFocus: false,
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/src/services/comment-api.ts:
--------------------------------------------------------------------------------
1 | import { ApiRouter } from './constants';
2 | import { Comment } from '../types/dto';
3 | import { instance } from './instance';
4 | import { infiniteQueryOptions } from '@tanstack/react-query';
5 | import { InfinityResponse } from '../types/response';
6 |
7 | export const createComment = async (data: {
8 | content: string;
9 | postId: string;
10 | }): Promise => {
11 | return (await instance.post(ApiRouter.COMMENT, data)).data;
12 | };
13 | export const getUserComments = async ({
14 | userId,
15 | page,
16 | perPage,
17 | }: {
18 | userId: string;
19 | page: number;
20 | perPage: number;
21 | }): Promise> => {
22 | return (
23 | await instance.get>(
24 | `${ApiRouter.USER_COMMENTS}/${userId}?page=${page}&perPage=${perPage}`,
25 | )
26 | ).data;
27 | };
28 |
29 | export const updateComment = async ({
30 | id,
31 | content,
32 | }: {
33 | id: string;
34 | content: string;
35 | }): Promise => {
36 | return (await instance.patch(`${ApiRouter.COMMENT}/${id}`, { content })).data;
37 | };
38 |
39 | export const deleteComment = async (id: string): Promise => {
40 | return (await instance.delete(`${ApiRouter.COMMENT}/${id}`)).data;
41 | };
42 |
43 | export const getUserCommentsInfinityQueryOptions = (userId: string) => {
44 | return infiniteQueryOptions({
45 | queryKey: ['comments', 'list', userId],
46 | queryFn: (meta) => getUserComments({ userId, page: meta.pageParam, perPage: 10 }),
47 | initialPageParam: 1,
48 | select: ({ pages }) => pages.flatMap((page) => page.data),
49 | getNextPageParam(lastPage, allPages) {
50 | return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
51 | },
52 | refetchOnWindowFocus: false,
53 | staleTime: 1 * 60 * 1000,
54 | placeholderData: (previousData) => previousData,
55 | });
56 | };
57 |
--------------------------------------------------------------------------------
/src/services/constants.ts:
--------------------------------------------------------------------------------
1 | export enum ApiRouter {
2 | USER = '/user',
3 | ME = '/user/me',
4 | POST = '/posts',
5 | POST_POPULAR = '/posts/popular',
6 | USER_POSTS = '/posts/user',
7 | COMMENT = '/comment',
8 | USER_COMMENTS = 'comment/user',
9 | LIKE = '/like',
10 | FOLLOW = '/follow',
11 | USER_FOLLOWERS = '/follow/followers',
12 | USER_FOLLOWING = '/follow/following',
13 | FRIENDS = '/follow/friends',
14 | REFRESH = '/refresh',
15 | REGISTER = '/auth/email/sendCode',
16 | LOGIN = '/auth/email/login',
17 | VERIFY = '/auth/email/verify',
18 | CHAT = '/chat',
19 | CHAT_INFO = '/chat/info',
20 | CHAT_GET = 'chat/create',
21 | USER_LIKES = '/posts/user/liked',
22 | USER_BOOKMARKS = '/bookmark',
23 | }
24 |
--------------------------------------------------------------------------------
/src/services/instance.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Cookies from 'js-cookie';
3 | import { TokensEnum } from '../types';
4 |
5 | export const instance = axios.create({
6 | baseURL: process.env.NEXT_PUBLIC_API_URL,
7 | });
8 |
9 | instance.interceptors.request.use(
10 | (config) => {
11 | const token = Cookies.get(TokensEnum.JWT);
12 | if (token) {
13 | config.headers.Authorization = `Bearer ${token}`;
14 | }
15 | return config;
16 | },
17 | (error) => Promise.reject(error),
18 | );
19 |
--------------------------------------------------------------------------------
/src/services/like-api.ts:
--------------------------------------------------------------------------------
1 | import { ApiRouter } from './constants';
2 | import { Like } from '../types/dto';
3 | import { instance } from './instance';
4 |
5 | export const likePost = async (data: { postId: string }): Promise => {
6 | return (await instance.post(ApiRouter.LIKE, data)).data;
7 | };
8 |
9 | export const unlikePost = async (id: string): Promise<{ count: number }> => {
10 | return (await instance.delete<{ count: number }>(`${ApiRouter.LIKE}/${id}`)).data;
11 | };
12 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './slices';
2 |
--------------------------------------------------------------------------------
/src/store/slices/index.ts:
--------------------------------------------------------------------------------
1 | export { useNewMarkSlice } from './new-mark-slice';
2 | export { useSharedPostSlice } from './shared-post-slice';
3 |
--------------------------------------------------------------------------------
/src/store/slices/new-mark-slice.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | interface Store {
4 | newMark: boolean;
5 | setNewMark: (newMark: boolean) => void;
6 | }
7 |
8 | export const useNewMarkSlice = create()((set) => ({
9 | newMark: false,
10 | setNewMark: (newMark: boolean) => set({ newMark }),
11 | }));
12 |
--------------------------------------------------------------------------------
/src/store/slices/shared-post-slice.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | type SharedPostType = {
4 | contentPost: string;
5 | imagePreview: string | null;
6 | postId: string;
7 | };
8 |
9 | interface Store {
10 | sharedPost: SharedPostType | null;
11 | setSharedPost: (post: Store['sharedPost']) => void;
12 | resetSharedPost: VoidFunction;
13 | }
14 |
15 | export const useSharedPostSlice = create()((set) => ({
16 | sharedPost: null,
17 | setSharedPost: (sharedPost: Store['sharedPost']) => set({ sharedPost }),
18 | resetSharedPost: () => set({ sharedPost: null }),
19 | }));
20 |
--------------------------------------------------------------------------------
/src/types/auth.ts:
--------------------------------------------------------------------------------
1 | export type TRegister = {
2 | email: string;
3 | password: string;
4 | name: string;
5 | };
6 |
7 | export type TAuthTokens = {
8 | token: string;
9 | };
10 |
--------------------------------------------------------------------------------
/src/types/chat.ts:
--------------------------------------------------------------------------------
1 | import { MessageDto, PostMessageBody, TextMessageBody } from './dto';
2 | import { User } from './response';
3 |
4 | export type ChatUpdate = {
5 | chat: UserChat;
6 | message: MessageDto;
7 | };
8 |
9 | export type MessageRequest = {
10 | body: TextMessageBody | PostMessageBody;
11 | };
12 |
13 | export type UserChat = {
14 | id: string;
15 | name: string;
16 | avatar: string | null;
17 | imageUrl: string;
18 | lastMessage: MessageDto | null;
19 | createdAt: Date;
20 | };
21 |
22 | export type ChatWithMembers = UserChat & {
23 | members: User[];
24 | };
25 |
--------------------------------------------------------------------------------
/src/types/constants.ts:
--------------------------------------------------------------------------------
1 | export enum TokensEnum {
2 | JWT = 'jwtToken',
3 | }
4 |
5 | export enum RoutesEnum {
6 | HOME = '/',
7 | POST = '/post',
8 | NEW = '/new',
9 | AUTH = '/auth',
10 | USER = '/user',
11 | MESSAGES = '/im',
12 | FOLLOWERS = '/followers',
13 | FOLLOWING = '/following',
14 | BOOKMARKS = '/bookmarks',
15 | LIKES = '/likes',
16 | FRIENDS = '/friends',
17 | PROFILE = '/profile',
18 | SETTINGS = '/profile/settings',
19 | EDIT = '/profile/settings/edit',
20 | THEME = '/profile/settings/theme',
21 | HELP = '/help',
22 | }
23 |
24 | export enum SocketEventsEnum {
25 | CONNECT = 'connect',
26 | UNAUTHORIZED = 'unauthorized',
27 | POST_NEW = 'post:new',
28 | MESSAGES_POST = 'messages:post',
29 | MESSAGES_GET = 'messages:get',
30 | MESSAGES_DELETE = 'messages:delete',
31 | MESSAGES_UPDATE = 'messages:patch',
32 | }
33 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './dto';
3 | export * from './response';
4 | export * from './chat';
5 | export * from './auth';
6 |
--------------------------------------------------------------------------------
/src/types/response.ts:
--------------------------------------------------------------------------------
1 | export type SendEmailResponse = {
2 | message: string;
3 | verified: boolean;
4 | userId: string;
5 | checkPassword: boolean;
6 | };
7 |
8 | export type UserDto = {
9 | user: User;
10 | token: string;
11 | };
12 |
13 | export type UserResponse = {
14 | user: User;
15 | isOwner: boolean;
16 | };
17 |
18 | export type User = {
19 | id: string;
20 | banned: boolean;
21 | role: string;
22 | alias: string;
23 | name: string;
24 | avatar: string | null;
25 | location: string | null;
26 | age: number | null;
27 | status: string | null;
28 | about: string | null;
29 | createdAt: string;
30 | updatedAt: string;
31 | EmailUser: EmailUser;
32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
33 | TelegramUser: any | null;
34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
35 | GoogleUser: any | null;
36 | isFollowing: boolean;
37 | followerCount: number;
38 | followingCount: number;
39 | };
40 |
41 | export type EmailUser = {
42 | id: string;
43 | email: string;
44 | password: string;
45 | userBaseId: number;
46 | };
47 |
48 | export type UserForComments = {
49 | id: string;
50 | banned: boolean;
51 | role: string;
52 | about: string;
53 | name: string;
54 | createdAt: string;
55 | updatedAt: string;
56 | };
57 |
58 | export type InfinityResponse = {
59 | data: T;
60 | totalItems: number;
61 | totalPages: number;
62 | };
63 |
64 | export type CursorInfinityResponse = {
65 | data: T;
66 | nextCursor: string | null;
67 | hasNextPage: boolean;
68 | };
69 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------