├── .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 |
33 | 34 | 35 | 36 |
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 |
23 | 29 |
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 |
28 | 29 |
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 |
11 |

Мессенджер

12 |
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 | 32 | 33 | 34 | Поделиться: 35 | Send to friends 36 | 37 | 38 | 44 | 45 | 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 |
39 | 40 | 41 | 42 |
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 |
12 |

{content}

13 |
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 | 31 | 32 | 33 | Image Gallery 34 | Navigate through the images in the gallery. 35 | 36 |
37 | 43 |
44 |
45 |
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 |
29 | 46 | 52 |
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 |
20 | onChange('')} 24 | variant="bordered" 25 | autoComplete="off" 26 | isClearable 27 | startContent={} 28 | placeholder="Поиск..." 29 | endContent={isLoading && } 30 | /> 31 | 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 |
28 |
29 | 35 | 42 |
43 | 51 |
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 | {file?.name} 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 | Chime 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 | --------------------------------------------------------------------------------