├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── env.example ├── next.config.js ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── images │ ├── avatar-placeholder.png │ ├── guides │ │ ├── cloud1.jpeg │ │ ├── cloud2.webp │ │ ├── cloud3.webp │ │ ├── git1.webp │ │ ├── git2.webp │ │ ├── git3.webp │ │ ├── git4.webp │ │ ├── google1.webp │ │ ├── google2.webp │ │ ├── google3.webp │ │ ├── google4.webp │ │ ├── google5.webp │ │ ├── google6.webp │ │ ├── messenger-chat.gif │ │ ├── messenger.gif │ │ ├── mongo2.webp │ │ ├── mongo3.webp │ │ ├── mongo4.jpeg │ │ ├── mongo5.webp │ │ ├── pusher1.webp │ │ └── pusher2.webp │ └── logo.png ├── next.svg └── vercel.svg ├── src ├── app │ ├── (site) │ │ ├── components │ │ │ ├── AuthForm.tsx │ │ │ └── AuthSocialButton.tsx │ │ └── page.tsx │ ├── actions │ │ ├── getConversationById.ts │ │ ├── getConversations.ts │ │ ├── getCurrentUser.ts │ │ ├── getMessages.ts │ │ ├── getSession.ts │ │ └── getUsers.ts │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── conversations │ │ │ ├── [conversationId] │ │ │ │ ├── route.ts │ │ │ │ └── seen │ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── messages │ │ │ └── route.ts │ │ ├── register │ │ │ └── route.ts │ │ └── settings │ │ │ └── route.ts │ ├── components │ │ ├── ActiveStatus.tsx │ │ ├── Avatar.tsx │ │ ├── AvatarGroup.tsx │ │ ├── Button.tsx │ │ ├── EmptyState.tsx │ │ ├── inputs │ │ │ ├── Input.tsx │ │ │ └── Select.tsx │ │ ├── modals │ │ │ ├── GroupChatModal.tsx │ │ │ ├── LoadingModal.tsx │ │ │ └── Modal.tsx │ │ ├── sidebar │ │ │ ├── DesktopItem.tsx │ │ │ ├── DesktopSidebar.tsx │ │ │ ├── MobileFooter.tsx │ │ │ ├── MobileItem.tsx │ │ │ ├── MobileLink.tsx │ │ │ ├── ProfileItem.tsx │ │ │ ├── SettingsModal.tsx │ │ │ └── Sidebar.tsx │ │ └── theme │ │ │ ├── DarkModeSwitch.tsx │ │ │ ├── Providers.tsx │ │ │ └── ThemeToggle.tsx │ ├── context │ │ ├── AuthContext.tsx │ │ └── ToasterContext.tsx │ ├── conversations │ │ ├── [conversationId] │ │ │ ├── components │ │ │ │ ├── Body.tsx │ │ │ │ ├── ChatDrawer.tsx │ │ │ │ ├── ConfirmModal.tsx │ │ │ │ ├── Form.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── ImageModal.tsx │ │ │ │ ├── MessageBox.tsx │ │ │ │ └── MessageInput.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── ConversationBox.tsx │ │ │ └── ConversationList.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── hooks │ │ ├── useActiveChannel.ts │ │ ├── useActiveList.ts │ │ ├── useConversation.ts │ │ ├── useOtherUser.ts │ │ └── useRoutes.ts │ ├── layout.tsx │ ├── libs │ │ ├── prismadb.ts │ │ └── pusher.ts │ ├── types │ │ └── index.ts │ └── users │ │ ├── components │ │ ├── SearchInput.tsx │ │ ├── UserBox.tsx │ │ └── UserList.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx ├── middleware.ts └── pages │ └── api │ └── pusher │ └── auth.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "importOrder": ["^[react*]", "", "^[./]", "^[../]"], 3 | "importOrderSeparation": true, 4 | "importOrderSortSpecifiers": true, 5 | "importOrderCaseInsensitive": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Miftaul Mannan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJs-Messenger-Clone 2 | 3 | 4 | 5 | A fully responsive real-time chat app made with NextJs 13 (app router), MongoDB, Tailwind CSS, Pusher, Next-Auth and Cloudinary. 6 | 7 |

8 | 9 |

10 | 11 | - **[NextJs](https://nextjs.org/)** (13.4.x) 12 | - **[React](https://facebook.github.io/react/)** (18.x) 13 | - **[MongoDB](https://www.mongodb.com/atlas/database)** (6.x) 14 | - **[Tailwind CSS](https://tailwindcss.com/)** (3.x) 15 | - **[Pusher](https://pusher.com/)** (5.x) 16 | - **[Next-Auth](https://next-auth.js.org/)** (4.x) 17 | - **[Typescript](https://www.typescriptlang.org/)** (5.x) 18 | - Production build script 19 | 20 | ## Live Demo 21 | 22 | See a [live demo](https://nextjs-messenger-clone-tasin5541.vercel.app/) on Vercel 23 | 24 | ## Features 25 | 26 |

27 | 28 |

29 | 30 | - Real-time chat update with Pusher 31 | - Group chat 32 | - Delete chat history 33 | - Image hosting with Cloudinary 34 | - Dynamic Theme support (Light and Dark mode) 35 | - Support for both Desktop and Mobile screens 36 | 37 | ## Installation 38 | 39 | ### Setup MongoDB 40 | 41 | 1. Navigate to https://www.mongodb.com/atlas/database and register 42 | 43 | 2. Find and Select `Build a Database`
44 | Select the Free tier and press on `Create` at the bottom
45 | 46 | 47 | 3. Create a user (note down the password)
48 | Scroll down to `Add entries to your IP Access List`
49 | Enter `0.0.0.0/0` and press `Add Entry`
50 | 51 | 52 | 4. Press `Go to Databases`
53 | 54 | 55 | 5. Click on `Connect` and select `MongoDB for VSCode`
56 | Copy the connection string and save it in a notepad
57 | Replace `` with your password set on step 3
58 | Add `test` at the end of the connection string (ex. `connectionstring/test`)
59 | 60 | 61 | ### Setup GitHub Authentication 62 | 63 | 1. Navigate to https://github.com/
64 | Click on your profile dropdown on the top right
65 | Click `Settings`
66 | 67 | 68 | 2. Click `Developer settings`
69 | Click `OAuth Apps`
70 | Click `New OAuth App`
71 | 72 | 73 | 3. Give a name to your app
74 | Type `http://localhost:3020/` in the `Homepage URL` and `Authorization callback URL` fields.
75 | Click "Register application"
76 | 77 | 78 | 4. Copy the `Client Id` and note it down
79 | Click on `Generate a new client secret`, copy and note it down
80 | 81 | 82 | ### Setup Google Authentication 83 | 84 | 1. Navigate to https://console.cloud.google.com and create a new project
85 | 86 | 87 | 2. Navigate to the newly created project and search for `API & Services`
88 | 89 | 90 | 3. Go to `OAuth consent screen`
91 | Click the `External` field
92 | Click `CREATE`
93 | 94 | 95 | 4. Click the `App name` field and give it a name
96 | On User Mail field, select your email
97 | Scroll down to `Developer contact information` and type your email
98 | Click `SAVE AND CONTINUE` until you're on the `Summary` step
99 | 100 | 101 | 5. Go to `Credentials`
102 | Click `CREATE CREDENTIALS`
103 | Select `OAuth client ID`
104 | 105 | 106 | 6. Select `Web application` as Application Type
107 | Scroll down to `Authorized redirect URIs` and add `http://localhost:3020/api/auth/callback/google`
108 | Click `CREATE`
109 | Copy the `CLient ID` and `Client Secret` and note it down
110 | 111 | 112 | ### Setup Cloudinary 113 | 114 | 1. Navigate to https://console.cloudinary.com and login
115 | Go to `Dashboard` and note down the `Cloud name`
116 | 117 | 118 | 2. Go to settings
119 | Then go to `Upload`
120 | 121 | 122 | 3. Click `Add upload preset`
123 | Change `Signing Mode` to `Unsigned`
124 | Click `Save` 125 | Copy the newly added preset name and note it down
126 | 127 | 128 | ### Setup Pusher 129 | 130 | 1. Navigate to https://dashboard.pusher.com/channels
131 | Click `Create app` (or `Get Started`)
132 | Give the app a name
133 | Select `React` for Frontend and `Node.js` for Backend
134 | Create the app
135 | 136 | 137 | 2. Go to `App Keys`
138 | Note down values
139 | 140 | 141 | ### Setup Project 142 | 143 | 1. Clone/download repo 144 | 2. Create a file called .env.local in the root directory of your project, type the following in cmd/powershell 145 | ``` 146 | cp env.example .env.local 147 | ``` 148 | 3. Inside the `.env.local` file, add the MongoDB, Pusher, Cloudinary, GitHub and Google keys from the previous steps 149 | 4. `yarn install` to install the dependencies (run `npm install yarn` if you don't have yarn installed) 150 | 5. `yarn prisma db push` to create the DB collections 151 | 6. `prisma generate` to create the prisma client 152 | 153 | ## Usage 154 | 155 | **Development** 156 | 157 | `yarn dev` 158 | 159 | - Build app continuously (HMR enabled) 160 | 161 | **Production** 162 | 163 | `yarn build` 164 | `yarn start` 165 | 166 | - Build app once (HMR disabled) to `/.next/` 167 | 168 | --- 169 | 170 | **All commands** 171 | 172 | | Command | Description | 173 | | ------------ | ------------------------------------------ | 174 | | `yarn dev` | Build app continuously (HMR enabled) | 175 | | `yarn build` | Build app once (HMR disabled) to `/.next/` | 176 | | `yarn start` | Run production build | 177 | 178 | ## See also 179 | 180 | - Some Design ideas were taken from [Facebook Messenger](https://www.facebook.com/messenger/) 181 | - Some Implementation Ideas for this project are taken from [Josh](https://www.youtube.com/@joshtriedcoding) and [Antonio](https://www.youtube.com/@codewithantonio) 182 | - Project was bootstrapped with [create-next-app](https://nextjs.org/docs/api-reference/create-next-app) 183 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | // README 2 | // This is only a .env example 3 | // Do not change this file! 4 | // Use cp or mv (like this: cp env.example .env), then edit .env with your Keys. 5 | // IMPORTANT: Don't forget to add to update your .gitignore with .env (to avoid public share your key!) 6 | 7 | DATABASE_URL= 8 | NEXTAUTH_SECRET=type-anything-or-generate-a-guid-online-and-use-that 9 | NEXTAUTH_URL=http://localhost:3020/ 10 | 11 | NEXT_PUBLIC_PUSHER_APP_KEY= 12 | NEXT_PUBLIC_PUSHER_CLUSTER= 13 | PUSHER_APP_ID= 14 | PUSHER_SECRET= 15 | 16 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= 17 | NEXT_PUBLIC_CLOUDINARY_PRESET_NAME= 18 | 19 | GITHUB_ID= 20 | GITHUB_SECRET= 21 | 22 | GOOGLE_CLIENT_ID= 23 | GOOGLE_CLIENT_SECRET= -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["res.cloudinary.com", "avatars.githubusercontent.com", "lh3.googleusercontent.com"], 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-messenger-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3020", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^1.7.14", 14 | "@next-auth/prisma-adapter": "^1.0.6", 15 | "@prisma/client": "^4.14.1", 16 | "@tailwindcss/forms": "^0.5.3", 17 | "@types/node": "20.2.0", 18 | "@types/react": "18.2.6", 19 | "@types/react-dom": "18.2.4", 20 | "autoprefixer": "10.4.14", 21 | "axios": "^1.4.0", 22 | "bcrypt": "^5.1.0", 23 | "clsx": "^1.2.1", 24 | "date-fns": "^2.30.0", 25 | "eslint": "8.40.0", 26 | "eslint-config-next": "13.4.2", 27 | "lodash": "^4.17.21", 28 | "next": "13.4.2", 29 | "next-auth": "^4.22.1", 30 | "next-cloudinary": "^4.12.0", 31 | "next-themes": "^0.2.1", 32 | "postcss": "8.4.23", 33 | "pusher": "^5.1.3", 34 | "pusher-js": "^8.0.2", 35 | "react": "18.2.0", 36 | "react-dom": "18.2.0", 37 | "react-hook-form": "^7.43.9", 38 | "react-hot-toast": "^2.4.1", 39 | "react-icons": "^4.8.0", 40 | "react-select": "^5.7.3", 41 | "react-spinners": "^0.13.8", 42 | "react-spring": "^9.7.1", 43 | "tailwindcss": "3.3.2", 44 | "typescript": "5.0.4", 45 | "zustand": "^4.3.8" 46 | }, 47 | "devDependencies": { 48 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 49 | "@types/bcrypt": "^5.0.0", 50 | "@types/lodash": "^4.14.195", 51 | "prettier": "^2.8.8", 52 | "prisma": "^4.14.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | name String? 16 | email String? @unique 17 | emailVerified DateTime? 18 | image String? 19 | hashedPassword String? 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @updatedAt 22 | 23 | conversationIds String[] @db.ObjectId 24 | conversations Conversation[] @relation(fields: [conversationIds], references: [id]) 25 | 26 | seenMessageIds String[] @db.ObjectId 27 | seenMessages Message[] @relation("Seen", fields: [seenMessageIds], references: [id]) 28 | 29 | accounts Account[] 30 | messages Message[] 31 | } 32 | 33 | model Account { 34 | id String @id @default(auto()) @map("_id") @db.ObjectId 35 | userId String @db.ObjectId 36 | type String 37 | provider String 38 | providerAccountId String 39 | refresh_token String? @db.String 40 | access_token String? @db.String 41 | expires_at Int? 42 | token_type String? 43 | scope String? 44 | id_token String? @db.String 45 | session_state String? 46 | 47 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 48 | 49 | @@unique([provider, providerAccountId]) 50 | } 51 | 52 | model Conversation { 53 | id String @id @default(auto()) @map("_id") @db.ObjectId 54 | createdAt DateTime @default(now()) 55 | lastMessageAt DateTime @default(now()) 56 | name String? 57 | isGroup Boolean? 58 | 59 | messagesIds String[] @db.ObjectId 60 | messages Message[] 61 | 62 | userIds String[] @db.ObjectId 63 | users User[] @relation(fields: [userIds], references: [id]) 64 | } 65 | 66 | model Message { 67 | id String @id @default(auto()) @map("_id") @db.ObjectId 68 | body String? 69 | image String? 70 | createdAt DateTime @default(now()) 71 | 72 | seenIds String[] @db.ObjectId 73 | seen User[] @relation("Seen", fields: [seenIds], references: [id]) 74 | 75 | conversationId String @db.ObjectId 76 | conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) 77 | 78 | senderId String @db.ObjectId 79 | sender User @relation(fields: [senderId], references: [id], onDelete: Cascade) 80 | } -------------------------------------------------------------------------------- /public/images/avatar-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/avatar-placeholder.png -------------------------------------------------------------------------------- /public/images/guides/cloud1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/cloud1.jpeg -------------------------------------------------------------------------------- /public/images/guides/cloud2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/cloud2.webp -------------------------------------------------------------------------------- /public/images/guides/cloud3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/cloud3.webp -------------------------------------------------------------------------------- /public/images/guides/git1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/git1.webp -------------------------------------------------------------------------------- /public/images/guides/git2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/git2.webp -------------------------------------------------------------------------------- /public/images/guides/git3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/git3.webp -------------------------------------------------------------------------------- /public/images/guides/git4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/git4.webp -------------------------------------------------------------------------------- /public/images/guides/google1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/google1.webp -------------------------------------------------------------------------------- /public/images/guides/google2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/google2.webp -------------------------------------------------------------------------------- /public/images/guides/google3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/google3.webp -------------------------------------------------------------------------------- /public/images/guides/google4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/google4.webp -------------------------------------------------------------------------------- /public/images/guides/google5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/google5.webp -------------------------------------------------------------------------------- /public/images/guides/google6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/google6.webp -------------------------------------------------------------------------------- /public/images/guides/messenger-chat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/messenger-chat.gif -------------------------------------------------------------------------------- /public/images/guides/messenger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/messenger.gif -------------------------------------------------------------------------------- /public/images/guides/mongo2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/mongo2.webp -------------------------------------------------------------------------------- /public/images/guides/mongo3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/mongo3.webp -------------------------------------------------------------------------------- /public/images/guides/mongo4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/mongo4.jpeg -------------------------------------------------------------------------------- /public/images/guides/mongo5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/mongo5.webp -------------------------------------------------------------------------------- /public/images/guides/pusher1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/pusher1.webp -------------------------------------------------------------------------------- /public/images/guides/pusher2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/guides/pusher2.webp -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/public/images/logo.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(site)/components/AuthForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; 6 | import toast from "react-hot-toast"; 7 | import { BsGithub, BsGoogle } from "react-icons/bs"; 8 | 9 | import { signIn, useSession } from "next-auth/react"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | import Button from "../../components/Button"; 13 | import Input from "../../components/inputs/Input"; 14 | import LoadingModal from "../../components/modals/LoadingModal"; 15 | import AuthSocialButton from "./AuthSocialButton"; 16 | 17 | type Variant = "LOGIN" | "REGISTER"; 18 | 19 | const AuthForm = () => { 20 | const session = useSession(); 21 | const router = useRouter(); 22 | const [variant, setVariant] = useState("LOGIN"); 23 | const [isLoading, setIsLoading] = useState(false); 24 | 25 | const { 26 | register, 27 | handleSubmit, 28 | formState: { errors }, 29 | } = useForm({ 30 | defaultValues: { 31 | name: "", 32 | email: "", 33 | password: "", 34 | }, 35 | }); 36 | 37 | useEffect(() => { 38 | if (session?.status === "authenticated") { 39 | router.push("/conversations"); 40 | } 41 | }, [session?.status, router]); 42 | 43 | const toggleVariant = useCallback(() => { 44 | if (variant === "LOGIN") { 45 | setVariant("REGISTER"); 46 | } else { 47 | setVariant("LOGIN"); 48 | } 49 | }, [variant]); 50 | 51 | const onSubmit: SubmitHandler = (data) => { 52 | setIsLoading(true); 53 | 54 | if (variant === "REGISTER") { 55 | axios 56 | .post("/api/register", data) 57 | .then(() => signIn("credentials", data)) 58 | .catch(() => toast.error("Something went wrong!")) 59 | .finally(() => setIsLoading(false)); 60 | } 61 | 62 | if (variant === "LOGIN") { 63 | signIn("credentials", { 64 | ...data, 65 | redirect: false, 66 | }) 67 | .then((callback) => { 68 | if (callback?.error) { 69 | toast.error("Invalid credentials!"); 70 | return; 71 | } 72 | 73 | if (callback?.ok) { 74 | toast.success("logged in"); 75 | router.push("/conversations"); 76 | } 77 | }) 78 | .finally(() => setIsLoading(false)); 79 | } 80 | }; 81 | 82 | const socialAction = (action: string) => { 83 | setIsLoading(true); 84 | 85 | signIn(action, { redirect: false }) 86 | .then((callback) => { 87 | if (callback?.error) { 88 | toast.error("Invalid credentials!"); 89 | return; 90 | } 91 | 92 | if (callback?.ok) { 93 | toast.success("logged in"); 94 | } 95 | }) 96 | .finally(() => setIsLoading(false)); 97 | }; 98 | 99 | return ( 100 | <> 101 | {session?.status === "loading" && } 102 |
103 |
104 |
105 | {variant === "REGISTER" && ( 106 | 114 | )} 115 | 124 | 133 |
134 | 137 |
138 |
139 |
140 |
141 |
149 |
150 |
151 |
152 | 153 | Or continue with 154 | 155 |
156 |
157 | 158 |
159 | socialAction("github")} /> 160 | socialAction("google")} /> 161 |
162 |
163 |
175 |
{variant === "LOGIN" ? "New to Messenger?" : "Already have an account?"}
176 |
177 | {variant === "LOGIN" ? "Create an account" : "Login"} 178 |
179 |
180 |
181 |
182 | 183 | ); 184 | }; 185 | 186 | export default AuthForm; 187 | -------------------------------------------------------------------------------- /src/app/(site)/components/AuthSocialButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconType } from "react-icons"; 2 | 3 | interface AuthSocialButtonProps { 4 | icon: IconType; 5 | onClick: () => void; 6 | } 7 | 8 | const AuthSocialButton: React.FC = ({ icon: Icon, onClick }) => { 9 | return ( 10 | 35 | ); 36 | }; 37 | 38 | export default AuthSocialButton; 39 | -------------------------------------------------------------------------------- /src/app/(site)/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import AuthForm from "./components/AuthForm"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 | Logo 16 |

17 | Sign in to your account 18 |

19 |
20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/actions/getConversationById.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../libs/prismadb"; 2 | import getCurrentUser from "./getCurrentUser"; 3 | 4 | const getConversationById = async (conversationId: string) => { 5 | try { 6 | const currentUser = await getCurrentUser(); 7 | 8 | if (!currentUser?.email) { 9 | return null; 10 | } 11 | 12 | const conversation = await prisma.conversation.findUnique({ 13 | where: { 14 | id: conversationId, 15 | }, 16 | include: { 17 | users: true, 18 | }, 19 | }); 20 | 21 | return conversation; 22 | } catch (error: any) { 23 | console.log(error, "SERVER_ERROR"); 24 | return null; 25 | } 26 | }; 27 | 28 | export default getConversationById; 29 | -------------------------------------------------------------------------------- /src/app/actions/getConversations.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../libs/prismadb"; 2 | import getCurrentUser from "./getCurrentUser"; 3 | 4 | const getConversations = async () => { 5 | const currentUser = await getCurrentUser(); 6 | 7 | if (!currentUser?.id) { 8 | return []; 9 | } 10 | 11 | try { 12 | const conversations = await prisma.conversation.findMany({ 13 | orderBy: { 14 | lastMessageAt: "desc", 15 | }, 16 | where: { 17 | userIds: { 18 | has: currentUser.id, 19 | }, 20 | }, 21 | include: { 22 | users: true, 23 | messages: { 24 | include: { 25 | sender: true, 26 | seen: true, 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | return conversations; 33 | } catch (error: any) { 34 | return []; 35 | } 36 | }; 37 | 38 | export default getConversations; 39 | -------------------------------------------------------------------------------- /src/app/actions/getCurrentUser.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../libs/prismadb"; 2 | import getSession from "./getSession"; 3 | 4 | const getCurrentUser = async () => { 5 | try { 6 | const session = await getSession(); 7 | 8 | if (!session?.user?.email) { 9 | return null; 10 | } 11 | 12 | const currentUser = await prisma.user.findUnique({ 13 | where: { 14 | email: session.user.email as string, 15 | }, 16 | }); 17 | 18 | if (!currentUser) { 19 | return null; 20 | } 21 | 22 | return currentUser; 23 | } catch (error: any) { 24 | return null; 25 | } 26 | }; 27 | 28 | export default getCurrentUser; 29 | -------------------------------------------------------------------------------- /src/app/actions/getMessages.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../libs/prismadb"; 2 | 3 | const getMessages = async (conversationId: string) => { 4 | try { 5 | const messages = await prisma.message.findMany({ 6 | where: { 7 | conversationId: conversationId, 8 | }, 9 | include: { 10 | sender: true, 11 | seen: true, 12 | }, 13 | orderBy: { 14 | createdAt: "asc", 15 | }, 16 | }); 17 | 18 | return messages; 19 | } catch (error: any) { 20 | return []; 21 | } 22 | }; 23 | 24 | export default getMessages; 25 | -------------------------------------------------------------------------------- /src/app/actions/getSession.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth"; 2 | 3 | import { authOptions } from "../api/auth/[...nextauth]/route"; 4 | 5 | export default async function getSession() { 6 | return await getServerSession(authOptions); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/actions/getUsers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/libs/prismadb"; 2 | 3 | import getSession from "./getSession"; 4 | 5 | const getUsers = async () => { 6 | const session = await getSession(); 7 | 8 | if (!session?.user?.email) { 9 | return []; 10 | } 11 | 12 | try { 13 | const users = await prisma.user.findMany({ 14 | orderBy: { 15 | createdAt: "desc", 16 | }, 17 | where: { 18 | NOT: { 19 | email: session.user.email, 20 | }, 21 | }, 22 | }); 23 | 24 | return users; 25 | } catch (error: any) { 26 | return []; 27 | } 28 | }; 29 | 30 | export default getUsers; 31 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 2 | import bcrypt from "bcrypt"; 3 | import NextAuth, { AuthOptions } from "next-auth"; 4 | import CredentialsProvider from "next-auth/providers/credentials"; 5 | import GithubProvider from "next-auth/providers/github"; 6 | import GoogleProvider from "next-auth/providers/google"; 7 | 8 | import prisma from "../../../libs/prismadb"; 9 | 10 | export const authOptions: AuthOptions = { 11 | adapter: PrismaAdapter(prisma), 12 | providers: [ 13 | GithubProvider({ 14 | clientId: process.env.GITHUB_ID as string, 15 | clientSecret: process.env.GITHUB_SECRET as string, 16 | }), 17 | GoogleProvider({ 18 | clientId: process.env.GOOGLE_CLIENT_ID as string, 19 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 20 | }), 21 | CredentialsProvider({ 22 | name: "credentials", 23 | credentials: { 24 | email: { label: "email", type: "text" }, 25 | password: { label: "password", type: "password" }, 26 | }, 27 | async authorize(credentials) { 28 | if (!credentials?.email || !credentials?.password) { 29 | throw new Error("Invalid credentials"); 30 | } 31 | 32 | const user = await prisma.user.findUnique({ 33 | where: { 34 | email: credentials.email, 35 | }, 36 | }); 37 | 38 | if (!user || !user?.hashedPassword) { 39 | throw new Error("Invalid credentials"); 40 | } 41 | 42 | const isCorrectPassword = await bcrypt.compare(credentials.password, user.hashedPassword); 43 | 44 | if (!isCorrectPassword) { 45 | throw new Error("Invalid credentials"); 46 | } 47 | 48 | return user; 49 | }, 50 | }), 51 | ], 52 | debug: process.env.NODE_ENV === "development", 53 | session: { 54 | strategy: "jwt", 55 | }, 56 | secret: process.env.NEXTAUTH_SECRET, 57 | }; 58 | 59 | const handler = NextAuth(authOptions); 60 | 61 | export { handler as GET, handler as POST }; 62 | -------------------------------------------------------------------------------- /src/app/api/conversations/[conversationId]/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/libs/prismadb"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import getCurrentUser from "../../../actions/getCurrentUser"; 5 | import { pusherEvents, pusherServer } from "../../../libs/pusher"; 6 | 7 | interface IParams { 8 | conversationId?: string; 9 | } 10 | 11 | export async function DELETE(request: Request, { params }: { params: IParams }) { 12 | try { 13 | const { conversationId } = params; 14 | const currentUser = await getCurrentUser(); 15 | 16 | if (!currentUser?.id) { 17 | return NextResponse.json(null); 18 | } 19 | 20 | const existingConversation = await prisma.conversation.findUnique({ 21 | where: { 22 | id: conversationId, 23 | }, 24 | include: { 25 | users: true, 26 | }, 27 | }); 28 | 29 | if (!existingConversation) { 30 | return new NextResponse("Invalid ID", { status: 400 }); 31 | } 32 | 33 | const deletedConversation = await prisma.conversation.deleteMany({ 34 | where: { 35 | id: conversationId, 36 | userIds: { 37 | hasSome: [currentUser.id], 38 | }, 39 | }, 40 | }); 41 | 42 | existingConversation.users.forEach((user) => { 43 | if (user.email) { 44 | pusherServer.trigger(user.email, pusherEvents.DELETE_CONVERSATION, existingConversation); 45 | } 46 | }); 47 | 48 | return NextResponse.json(deletedConversation); 49 | } catch (error) { 50 | return NextResponse.json(null); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/conversations/[conversationId]/seen/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import getCurrentUser from "../../../../actions/getCurrentUser"; 4 | import prisma from "../../../../libs/prismadb"; 5 | import { pusherEvents, pusherServer } from "../../../../libs/pusher"; 6 | 7 | interface IParams { 8 | conversationId?: string; 9 | } 10 | 11 | export async function POST(request: Request, { params }: { params: IParams }) { 12 | try { 13 | const currentUser = await getCurrentUser(); 14 | const { conversationId } = params; 15 | 16 | if (!currentUser?.id || !currentUser?.email) { 17 | return new NextResponse("Unauthorized", { status: 401 }); 18 | } 19 | 20 | // Find existing conversation 21 | const conversation = await prisma.conversation.findUnique({ 22 | where: { 23 | id: conversationId, 24 | }, 25 | include: { 26 | messages: { 27 | include: { 28 | seen: true, 29 | }, 30 | }, 31 | users: true, 32 | }, 33 | }); 34 | 35 | if (!conversation) { 36 | return new NextResponse("Invalid ID", { status: 400 }); 37 | } 38 | 39 | // Find last message 40 | const lastMessage = conversation.messages[conversation.messages.length - 1]; 41 | 42 | if (!lastMessage) { 43 | return NextResponse.json(conversation); 44 | } 45 | 46 | // Update seen of last message 47 | const updatedMessage = await prisma.message.update({ 48 | where: { 49 | id: lastMessage.id, 50 | }, 51 | include: { 52 | sender: true, 53 | seen: true, 54 | }, 55 | data: { 56 | seen: { 57 | connect: { 58 | id: currentUser.id, 59 | }, 60 | }, 61 | }, 62 | }); 63 | 64 | // Update all connections with new seen 65 | await pusherServer.trigger(currentUser.email, pusherEvents.UPDATE_CONVERSATION, { 66 | id: conversationId, 67 | messages: [updatedMessage], 68 | }); 69 | 70 | // If user has already seen the message, no need to go further 71 | if (lastMessage.seenIds.indexOf(currentUser.id) !== -1) { 72 | return NextResponse.json(conversation); 73 | } 74 | 75 | // Update last message seen 76 | await pusherServer.trigger(conversationId!, pusherEvents.UPDATE_MESSAGE, updatedMessage); 77 | 78 | return NextResponse.json(updatedMessage); 79 | } catch (error) { 80 | console.log(error, "ERROR_MESSAGES_SEEN"); 81 | return new NextResponse("Error", { status: 500 }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/api/conversations/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import getCurrentUser from "../../actions/getCurrentUser"; 4 | import prisma from "../../libs/prismadb"; 5 | import { pusherEvents, pusherServer } from "../../libs/pusher"; 6 | 7 | export async function POST(request: Request) { 8 | try { 9 | const currentUser = await getCurrentUser(); 10 | const body = await request.json(); 11 | const { userId, isGroup, members, name } = body; 12 | 13 | if (!currentUser?.id || !currentUser?.email) { 14 | return new NextResponse("Unauthorized", { status: 400 }); 15 | } 16 | 17 | if (isGroup && (!members || members.length < 2 || !name)) { 18 | return new NextResponse("Invalid data", { status: 400 }); 19 | } 20 | 21 | if (isGroup) { 22 | const newConversation = await prisma.conversation.create({ 23 | data: { 24 | name, 25 | isGroup, 26 | users: { 27 | connect: [ 28 | ...members.map((member: { value: string }) => ({ 29 | id: member.value, 30 | })), 31 | { 32 | id: currentUser.id, 33 | }, 34 | ], 35 | }, 36 | }, 37 | include: { 38 | users: true, 39 | }, 40 | }); 41 | 42 | // Update all connections with new conversation 43 | newConversation.users.forEach((user) => { 44 | if (user.email) { 45 | pusherServer.trigger(user.email, pusherEvents.NEW_CONVERSATION, newConversation); 46 | } 47 | }); 48 | 49 | return NextResponse.json(newConversation); 50 | } 51 | 52 | const existingConversations = await prisma.conversation.findMany({ 53 | where: { 54 | OR: [ 55 | { 56 | userIds: { 57 | equals: [currentUser.id, userId], 58 | }, 59 | }, 60 | { 61 | userIds: { 62 | equals: [userId, currentUser.id], 63 | }, 64 | }, 65 | ], 66 | }, 67 | }); 68 | 69 | const singleConversation = existingConversations[0]; 70 | 71 | if (singleConversation) { 72 | return NextResponse.json(singleConversation); 73 | } 74 | 75 | const newConversation = await prisma.conversation.create({ 76 | data: { 77 | users: { 78 | connect: [ 79 | { 80 | id: currentUser.id, 81 | }, 82 | { 83 | id: userId, 84 | }, 85 | ], 86 | }, 87 | }, 88 | include: { 89 | users: true, 90 | }, 91 | }); 92 | 93 | // Update all connections with new conversation 94 | newConversation.users.map((user) => { 95 | if (user.email) { 96 | pusherServer.trigger(user.email, pusherEvents.NEW_CONVERSATION, newConversation); 97 | } 98 | }); 99 | 100 | return NextResponse.json(newConversation); 101 | } catch (error) { 102 | return new NextResponse("Internal Error", { status: 500 }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/api/messages/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import getCurrentUser from "../../actions/getCurrentUser"; 4 | import prisma from "../../libs/prismadb"; 5 | import { pusherEvents, pusherServer } from "../../libs/pusher"; 6 | 7 | export async function POST(request: Request) { 8 | try { 9 | const currentUser = await getCurrentUser(); 10 | const body = await request.json(); 11 | const { message, image, conversationId } = body; 12 | 13 | if (!currentUser?.id || !currentUser?.email) { 14 | return new NextResponse("Unauthorized", { status: 401 }); 15 | } 16 | 17 | const newMessage = await prisma.message.create({ 18 | include: { 19 | seen: true, 20 | sender: true, 21 | }, 22 | data: { 23 | body: message, 24 | image: image, 25 | conversation: { 26 | connect: { id: conversationId }, 27 | }, 28 | sender: { 29 | connect: { id: currentUser.id }, 30 | }, 31 | seen: { 32 | connect: { 33 | id: currentUser.id, 34 | }, 35 | }, 36 | }, 37 | }); 38 | 39 | const updatedConversation = await prisma.conversation.update({ 40 | where: { 41 | id: conversationId, 42 | }, 43 | data: { 44 | lastMessageAt: new Date(), 45 | messages: { 46 | connect: { 47 | id: newMessage.id, 48 | }, 49 | }, 50 | }, 51 | include: { 52 | users: true, 53 | messages: { 54 | include: { 55 | seen: true, 56 | }, 57 | }, 58 | }, 59 | }); 60 | 61 | await pusherServer.trigger(conversationId, pusherEvents.NEW_MESSAGE, newMessage); 62 | 63 | const lastMessage = updatedConversation.messages[updatedConversation.messages.length - 1]; 64 | 65 | updatedConversation.users.map((user) => { 66 | pusherServer.trigger(user.email!, pusherEvents.UPDATE_CONVERSATION, { 67 | id: conversationId, 68 | messages: [lastMessage], 69 | }); 70 | }); 71 | 72 | return NextResponse.json(newMessage); 73 | } catch (error) { 74 | console.log(error, "ERROR_MESSAGES"); 75 | return new NextResponse("Error", { status: 500 }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/register/route.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import prisma from "../../libs/prismadb"; 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const body = await request.json(); 9 | const { email, name, password } = body; 10 | 11 | if (!email || !name || !password) { 12 | return new NextResponse("Missing Info", { status: 400 }); 13 | } 14 | 15 | const hashedPassword = await bcrypt.hash(password, 12); 16 | 17 | const user = await prisma.user.create({ 18 | data: { 19 | email, 20 | name, 21 | hashedPassword, 22 | }, 23 | }); 24 | 25 | return NextResponse.json(user); 26 | } catch (error: any) { 27 | console.log(error, "REGITRATION ERROR"); 28 | return new NextResponse("Internal Error", { status: 500 }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/api/settings/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/libs/prismadb"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import getCurrentUser from "../../actions/getCurrentUser"; 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const currentUser = await getCurrentUser(); 9 | const body = await request.json(); 10 | const { name, image } = body; 11 | 12 | if (!currentUser?.id) { 13 | return new NextResponse("Unauthorized", { status: 401 }); 14 | } 15 | 16 | const updatedUser = await prisma.user.update({ 17 | where: { 18 | id: currentUser.id, 19 | }, 20 | data: { 21 | image: image, 22 | name: name, 23 | }, 24 | }); 25 | 26 | return NextResponse.json(updatedUser); 27 | } catch (error) { 28 | console.log(error, "ERROR_MESSAGES"); 29 | return new NextResponse("Error", { status: 500 }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/ActiveStatus.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useActiveChannel from "../hooks/useActiveChannel"; 4 | 5 | const ActiveStatus = () => { 6 | useActiveChannel(); 7 | 8 | return null; 9 | }; 10 | 11 | export default ActiveStatus; 12 | -------------------------------------------------------------------------------- /src/app/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { User } from "@prisma/client"; 4 | import Image from "next/image"; 5 | 6 | import useActiveList from "../hooks/useActiveList"; 7 | 8 | interface AvatarProps { 9 | user?: User; 10 | } 11 | 12 | const Avatar: React.FC = ({ user }) => { 13 | const { members } = useActiveList(); 14 | const isActive = members.indexOf(user?.email!) !== -1; 15 | 16 | return ( 17 |
18 |
30 | Avatar 36 |
37 | {isActive && ( 38 | 55 | )} 56 |
57 | ); 58 | }; 59 | 60 | export default Avatar; 61 | -------------------------------------------------------------------------------- /src/app/components/AvatarGroup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { User } from "@prisma/client"; 4 | import Image from "next/image"; 5 | 6 | interface AvatarGroupProps { 7 | users?: User[]; 8 | } 9 | 10 | const AvatarGroup: React.FC = ({ users = [] }) => { 11 | const slicedUsers = users.slice(0, 3); 12 | 13 | const positionMap = { 14 | 0: "top-0 left-[8px] md:left-[12px]", 15 | 1: "bottom-0", 16 | 2: "bottom-0 right-0", 17 | }; 18 | 19 | return ( 20 |
21 | {slicedUsers.map((user, index) => ( 22 |
34 | Avatar 40 |
41 | ))} 42 |
43 | ); 44 | }; 45 | 46 | export default AvatarGroup; 47 | -------------------------------------------------------------------------------- /src/app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | interface ButtonProps { 4 | type?: "button" | "submit" | "reset" | undefined; 5 | fullWidth?: boolean; 6 | children?: React.ReactNode; 7 | onClick?: () => void; 8 | secondary?: boolean; 9 | danger?: boolean; 10 | disabled?: boolean; 11 | } 12 | 13 | const Button: React.FC = ({ 14 | type = "button", 15 | fullWidth, 16 | children, 17 | onClick, 18 | secondary, 19 | danger, 20 | disabled, 21 | }) => { 22 | return ( 23 | 49 | ); 50 | }; 51 | 52 | export default Button; 53 | -------------------------------------------------------------------------------- /src/app/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | const EmptyState = () => { 2 | return ( 3 |
18 |
19 |

20 | Select a chat or start a new conversation 21 |

22 |
23 |
24 | ); 25 | }; 26 | 27 | export default EmptyState; 28 | -------------------------------------------------------------------------------- /src/app/components/inputs/Input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | import { FieldErrors, FieldValues, UseFormRegister } from "react-hook-form"; 5 | 6 | interface InputProps { 7 | label: string; 8 | id: string; 9 | type?: string; 10 | required?: boolean; 11 | register: UseFormRegister; 12 | errors: FieldErrors; 13 | disabled?: boolean; 14 | } 15 | 16 | const Input: React.FC = ({ 17 | label, 18 | id, 19 | register, 20 | required, 21 | errors, 22 | type = "text", 23 | disabled, 24 | }) => { 25 | return ( 26 |
27 | 33 |
34 | 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Input; 72 | -------------------------------------------------------------------------------- /src/app/components/inputs/Select.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | import ReactSelect from "react-select"; 5 | 6 | interface SelectProps { 7 | label: string; 8 | value?: Record; 9 | onChange: (value: Record) => void; 10 | options: Record[]; 11 | disabled?: boolean; 12 | } 13 | 14 | const Select: React.FC = ({ label, value, onChange, options, disabled }) => { 15 | return ( 16 |
17 | 29 |
30 | ({ ...base, zIndex: 9999 }), 39 | option: (base, { isFocused, isSelected }) => ({ 40 | ...base, 41 | backgroundColor: isFocused 42 | ? "rgb(107 114 128)" 43 | : isSelected 44 | ? "rgb(156 163 175)" 45 | : "#3a3b3c", 46 | }), 47 | }} 48 | classNames={{ 49 | control: (state) => 50 | clsx( 51 | ` 52 | text-sm 53 | dark:bg-lightgray 54 | dark:border-gray-500`, 55 | state.isFocused && "border-gray-400" 56 | ), 57 | menu: () => "dark:bg-lightgray", 58 | }} 59 | /> 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default Select; 66 | -------------------------------------------------------------------------------- /src/app/components/modals/GroupChatModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import React, { useState } from "react"; 5 | import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; 6 | import { toast } from "react-hot-toast"; 7 | 8 | import { User } from "@prisma/client"; 9 | import { useRouter } from "next/navigation"; 10 | 11 | import Button from "../Button"; 12 | import Input from "../inputs/Input"; 13 | import Select from "../inputs/Select"; 14 | import Modal from "./Modal"; 15 | 16 | interface GroupChatModalProps { 17 | isOpen?: boolean; 18 | onClose: () => void; 19 | users: User[]; 20 | } 21 | 22 | const GroupChatModal: React.FC = ({ isOpen, onClose, users = [] }) => { 23 | const router = useRouter(); 24 | const [isLoading, setIsLoading] = useState(false); 25 | 26 | const { 27 | register, 28 | handleSubmit, 29 | setValue, 30 | watch, 31 | formState: { errors }, 32 | } = useForm({ 33 | defaultValues: { 34 | name: "", 35 | members: [], 36 | }, 37 | }); 38 | 39 | const members = watch("members"); 40 | 41 | const onSubmit: SubmitHandler = (data) => { 42 | setIsLoading(true); 43 | 44 | axios 45 | .post("/api/conversations", { 46 | ...data, 47 | isGroup: true, 48 | }) 49 | .then(() => { 50 | router.refresh(); 51 | onClose(); 52 | }) 53 | .catch(() => toast.error("Something went wrong!")) 54 | .finally(() => setIsLoading(false)); 55 | }; 56 | 57 | return ( 58 | 59 |
60 |
61 |
62 |

71 | Create a group chat 72 |

73 |

74 | Create a chat with more than 2 people. 75 |

76 |
77 | 85 | 90 |
91 | 104 |
105 | Avatar 112 | 117 | 120 | 121 |
122 |
123 |
124 |
125 |
126 | 127 |
136 | 139 | 142 |
143 |
144 |
145 | ); 146 | }; 147 | 148 | export default SettingsModal; 149 | -------------------------------------------------------------------------------- /src/app/components/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import getCurrentUser from "../../actions/getCurrentUser"; 2 | import DesktopSidebar from "./DesktopSidebar"; 3 | import MobileFooter from "./MobileFooter"; 4 | 5 | async function Sidebar({ children }: { children: React.ReactNode }) { 6 | const currentUser = await getCurrentUser(); 7 | 8 | return ( 9 |
10 | 11 | 12 |
{children}
13 |
14 | ); 15 | } 16 | 17 | export default Sidebar; 18 | -------------------------------------------------------------------------------- /src/app/components/theme/DarkModeSwitch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CSSProperties, HTMLAttributes, useEffect, useMemo, useState } from "react"; 4 | import { animated, useSpring } from "react-spring"; 5 | 6 | const defaultProperties = { 7 | dark: { 8 | circle: { 9 | r: 9, 10 | }, 11 | mask: { 12 | cx: "50%", 13 | cy: "23%", 14 | }, 15 | svg: { 16 | transform: "rotate(40deg)", 17 | }, 18 | lines: { 19 | opacity: 0, 20 | }, 21 | }, 22 | light: { 23 | circle: { 24 | r: 5, 25 | }, 26 | mask: { 27 | cx: "100%", 28 | cy: "0%", 29 | }, 30 | svg: { 31 | transform: "rotate(90deg)", 32 | }, 33 | lines: { 34 | opacity: 1, 35 | }, 36 | }, 37 | springConfig: { mass: 4, tension: 250, friction: 35 }, 38 | }; 39 | 40 | let REACT_TOGGLE_DARK_MODE_GLOBAL_ID = 0; 41 | 42 | type SVGProps = Omit, "onChange">; 43 | export interface Props extends SVGProps { 44 | onChange: () => void; 45 | checked: boolean; 46 | style?: CSSProperties; 47 | size?: number | string; 48 | moonColor?: string; 49 | sunColor?: string; 50 | } 51 | 52 | export const DarkModeSwitch: React.FC = ({ 53 | onChange, 54 | children, 55 | checked = false, 56 | size = 24, 57 | moonColor = "rgb(229 231 235)", 58 | sunColor = "#242526", 59 | style, 60 | ...rest 61 | }) => { 62 | const [id, setId] = useState(0); 63 | 64 | useEffect(() => { 65 | REACT_TOGGLE_DARK_MODE_GLOBAL_ID += 1; 66 | setId(REACT_TOGGLE_DARK_MODE_GLOBAL_ID); 67 | }, [setId]); 68 | 69 | const { circle, svg, lines, mask } = defaultProperties[checked ? "dark" : "light"]; 70 | 71 | const svgContainerProps = useSpring({ 72 | ...svg, 73 | config: defaultProperties.springConfig, 74 | }); 75 | const centerCircleProps = useSpring({ 76 | ...circle, 77 | config: defaultProperties.springConfig, 78 | }); 79 | const maskedCircleProps = useSpring({ 80 | ...mask, 81 | config: defaultProperties.springConfig, 82 | }); 83 | const linesProps = useSpring({ 84 | ...lines, 85 | config: defaultProperties.springConfig, 86 | }); 87 | 88 | const uniqueMaskId = `circle-mask-${id}`; 89 | 90 | return ( 91 |
106 | 123 | 124 | 125 | 131 | 132 | 133 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
153 | ); 154 | }; 155 | -------------------------------------------------------------------------------- /src/app/components/theme/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider } from "next-themes"; 4 | 5 | export function Providers({ children }: { children: React.ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/components/theme/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { useTheme } from "next-themes"; 4 | 5 | import { DarkModeSwitch } from "./DarkModeSwitch"; 6 | 7 | const ThemeToggle = () => { 8 | const [mounted, setMounted] = useState(false); 9 | const { resolvedTheme, setTheme } = useTheme(); 10 | 11 | const toggleTheme = () => { 12 | resolvedTheme == "dark" ? setTheme("light") : setTheme("dark"); 13 | }; 14 | 15 | // useEffect only runs on the client, so now we can safely show the UI 16 | useEffect(() => { 17 | setMounted(true); 18 | }, []); 19 | 20 | if (!mounted) { 21 | return null; 22 | } 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | }; 30 | 31 | export default ThemeToggle; 32 | -------------------------------------------------------------------------------- /src/app/context/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SessionProvider } from "next-auth/react"; 4 | 5 | export interface AuthContextProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function AuthContext({ children }: AuthContextProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/context/ToasterContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "react-hot-toast"; 4 | 5 | const ToasterContext = () => { 6 | return ( 7 | 12 | ); 13 | }; 14 | 15 | export default ToasterContext; 16 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/Body.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useEffect, useRef, useState } from "react"; 5 | 6 | import useConversation from "@/app/hooks/useConversation"; 7 | import { find } from "lodash"; 8 | 9 | import { pusherClient, pusherEvents } from "../../../libs/pusher"; 10 | import { FullMessageType } from "../../../types"; 11 | import MessageBox from "./MessageBox"; 12 | 13 | interface BodyProps { 14 | initialMessages: FullMessageType[]; 15 | } 16 | 17 | const Body: React.FC = ({ initialMessages = [] }) => { 18 | const bottomRef = useRef(null); 19 | const [messages, setMessages] = useState(initialMessages); 20 | 21 | const { conversationId } = useConversation(); 22 | 23 | useEffect(() => { 24 | axios.post(`/api/conversations/${conversationId}/seen`); 25 | }, [conversationId]); 26 | 27 | useEffect(() => { 28 | bottomRef?.current?.scrollIntoView({ behavior: "smooth" }); 29 | }, [messages]); 30 | 31 | useEffect(() => { 32 | pusherClient.subscribe(conversationId); 33 | bottomRef?.current?.scrollIntoView(); 34 | 35 | const messageHandler = (message: FullMessageType) => { 36 | axios.post(`/api/conversations/${conversationId}/seen`); 37 | 38 | setMessages((current) => { 39 | if (find(current, { id: message.id })) { 40 | return current; 41 | } 42 | 43 | return [...current, message]; 44 | }); 45 | }; 46 | 47 | const updateMessageHandler = (newMessage: FullMessageType) => { 48 | setMessages((current) => 49 | current.map((currentMessage) => { 50 | // update the message only if it matches the new message id 51 | if (currentMessage.id === newMessage.id) { 52 | return newMessage; 53 | } 54 | 55 | return currentMessage; 56 | }) 57 | ); 58 | }; 59 | 60 | pusherClient.bind(pusherEvents.NEW_MESSAGE, messageHandler); 61 | pusherClient.bind(pusherEvents.UPDATE_MESSAGE, updateMessageHandler); 62 | 63 | return () => { 64 | pusherClient.unsubscribe(conversationId); 65 | pusherClient.unbind(pusherEvents.NEW_MESSAGE, messageHandler); 66 | pusherClient.unbind(pusherEvents.UPDATE_MESSAGE, updateMessageHandler); 67 | }; 68 | }, [conversationId]); 69 | 70 | return ( 71 |
72 | {messages.map((message, i) => ( 73 | 74 | ))} 75 |
76 |
77 | ); 78 | }; 79 | 80 | export default Body; 81 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/ChatDrawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Fragment, useMemo, useState } from "react"; 4 | import { IoClose, IoTrash } from "react-icons/io5"; 5 | 6 | import { Dialog, Transition } from "@headlessui/react"; 7 | import { Conversation, User } from "@prisma/client"; 8 | import { format } from "date-fns"; 9 | 10 | import Avatar from "../../../components/Avatar"; 11 | import AvatarGroup from "../../../components/AvatarGroup"; 12 | import useActiveList from "../../../hooks/useActiveList"; 13 | import useOtherUser from "../../../hooks/useOtherUser"; 14 | import ConfirmModal from "./ConfirmModal"; 15 | 16 | interface ProfileDrawerProps { 17 | isOpen: boolean; 18 | onClose: () => void; 19 | data: Conversation & { 20 | users: User[]; 21 | }; 22 | } 23 | 24 | const ChatDrawer: React.FC = ({ isOpen, onClose, data }) => { 25 | const [confirmOpen, setConfirmOpen] = useState(false); 26 | const otherUser = useOtherUser(data); 27 | 28 | const joinedDate = useMemo(() => { 29 | return format(new Date(otherUser.createdAt), "PP"); 30 | }, [otherUser.createdAt]); 31 | 32 | const title = useMemo(() => { 33 | return data.name || otherUser.name; 34 | }, [data.name, otherUser.name]); 35 | 36 | const { members } = useActiveList(); 37 | const isActive = members.indexOf(otherUser?.email!) !== -1; 38 | const statusText = useMemo(() => { 39 | if (data.isGroup) { 40 | return `${data.users.length} members`; 41 | } 42 | 43 | return isActive ? "Active" : "Offline"; 44 | }, [data.isGroup, data.users.length, isActive]); 45 | 46 | return ( 47 | <> 48 | setConfirmOpen(false)} /> 49 | 50 | 51 | 60 |
61 | 62 | 63 |
64 |
65 |
66 | 75 | 76 |
77 |
78 |
79 |
80 | 88 |
89 |
90 |
91 |
92 |
93 |
94 | {data.isGroup ? ( 95 | 96 | ) : ( 97 | 98 | )} 99 |
100 |
{title}
101 |
{statusText}
102 |
103 |
setConfirmOpen(true)} 105 | className="flex flex-col gap-3 items-center cursor-pointer hover:opacity-75" 106 | > 107 |
108 | 109 |
110 |
111 | Delete 112 |
113 |
114 |
115 |
116 |
117 | {data.isGroup && ( 118 |
119 |
129 | Emails 130 |
131 |
140 | {data.users.map((user) => user.email).join(", ")} 141 |
142 |
143 | )} 144 | {!data.isGroup && ( 145 |
146 |
156 | Email 157 |
158 |
167 | {otherUser.email} 168 |
169 |
170 | )} 171 | {!data.isGroup && ( 172 | <> 173 |
174 |
175 |
185 | Joined 186 |
187 |
196 | 197 |
198 |
199 | 200 | )} 201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 | 214 | ); 215 | }; 216 | 217 | export default ChatDrawer; 218 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/ConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import React, { useCallback, useState } from "react"; 5 | import { toast } from "react-hot-toast"; 6 | import { FiAlertTriangle } from "react-icons/fi"; 7 | 8 | import { Dialog } from "@headlessui/react"; 9 | import { useRouter } from "next/navigation"; 10 | 11 | import Button from "../../../components/Button"; 12 | import Modal from "../../../components/modals/Modal"; 13 | import useConversation from "../../../hooks/useConversation"; 14 | 15 | interface ConfirmModalProps { 16 | isOpen?: boolean; 17 | onClose: () => void; 18 | } 19 | 20 | const ConfirmModal: React.FC = ({ isOpen, onClose }) => { 21 | const router = useRouter(); 22 | const { conversationId } = useConversation(); 23 | const [isLoading, setIsLoading] = useState(false); 24 | 25 | const onDelete = useCallback(() => { 26 | setIsLoading(true); 27 | 28 | axios 29 | .delete(`/api/conversations/${conversationId}`) 30 | .then(() => { 31 | onClose(); 32 | router.push("/conversations"); 33 | router.refresh(); 34 | }) 35 | .catch(() => toast.error("Something went wrong!")) 36 | .finally(() => setIsLoading(false)); 37 | }, [router, conversationId, onClose]); 38 | 39 | return ( 40 | 41 |
42 |
58 |
60 |
69 | 73 | Delete conversation 74 | 75 |
76 |

77 | Are you sure you want to delete this conversation? This action cannot be undone. 78 |

79 |
80 |
81 |
82 |
83 | 86 | 89 |
90 |
91 | ); 92 | }; 93 | 94 | export default ConfirmModal; 95 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/Form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; 5 | import { HiPaperAirplane, HiPhoto } from "react-icons/hi2"; 6 | 7 | import { CldUploadButton } from "next-cloudinary"; 8 | 9 | import useConversation from "../../../hooks/useConversation"; 10 | import MessageInput from "./MessageInput"; 11 | 12 | const Form = () => { 13 | const { conversationId } = useConversation(); 14 | 15 | const { 16 | register, 17 | handleSubmit, 18 | setValue, 19 | formState: { errors }, 20 | } = useForm({ 21 | defaultValues: { 22 | message: "", 23 | }, 24 | }); 25 | 26 | const onSubmit: SubmitHandler = (data) => { 27 | setValue("message", "", { shouldValidate: true }); 28 | axios.post("/api/messages", { 29 | ...data, 30 | conversationId, 31 | }); 32 | }; 33 | 34 | const handleUpload = (result: any) => { 35 | axios.post("/api/messages", { 36 | image: result.info.secure_url, 37 | conversationId: conversationId, 38 | }); 39 | }; 40 | 41 | return ( 42 |
57 | 62 | 63 | 64 |
65 | 72 | 85 | 86 |
87 | ); 88 | }; 89 | 90 | export default Form; 91 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo, useState } from "react"; 4 | import { HiChevronLeft } from "react-icons/hi"; 5 | import { HiEllipsisHorizontal } from "react-icons/hi2"; 6 | 7 | import useOtherUser from "@/app/hooks/useOtherUser"; 8 | import { Conversation, User } from "@prisma/client"; 9 | import Link from "next/link"; 10 | 11 | import Avatar from "../../../components/Avatar"; 12 | import AvatarGroup from "../../../components/AvatarGroup"; 13 | import useActiveList from "../../../hooks/useActiveList"; 14 | import ChatDrawer from "./ChatDrawer"; 15 | 16 | interface HeaderProps { 17 | conversation: Conversation & { 18 | users: User[]; 19 | }; 20 | } 21 | 22 | const Header: React.FC = ({ conversation }) => { 23 | const otherUser = useOtherUser(conversation); 24 | const [drawerOpen, setDrawerOpen] = useState(false); 25 | 26 | const { members } = useActiveList(); 27 | const isActive = members.indexOf(otherUser?.email!) !== -1; 28 | const statusText = useMemo(() => { 29 | if (conversation.isGroup) { 30 | return `${conversation.users.length} members`; 31 | } 32 | 33 | return isActive ? "Active" : "Offline"; 34 | }, [conversation.isGroup, conversation.users.length, isActive]); 35 | 36 | return ( 37 | <> 38 | setDrawerOpen(false)} /> 39 |
56 |
57 | 68 | 69 | 70 | {conversation.isGroup ? ( 71 | 72 | ) : ( 73 | 74 | )} 75 | 76 |
77 |
{conversation.name || otherUser.name}
78 |
79 | {statusText} 80 |
81 |
82 |
83 | setDrawerOpen(true)} 86 | className=" 87 | text-sky-500 88 | cursor-pointer 89 | hover:text-sky-600 90 | transition 91 | " 92 | /> 93 |
94 | 95 | ); 96 | }; 97 | 98 | export default Header; 99 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/ImageModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | 5 | import Modal from "../../../components/modals/Modal"; 6 | 7 | interface ImageModalProps { 8 | isOpen?: boolean; 9 | onClose: () => void; 10 | src?: string | null; 11 | } 12 | 13 | const ImageModal: React.FC = ({ isOpen, onClose, src }) => { 14 | if (!src) { 15 | return null; 16 | } 17 | 18 | return ( 19 | 20 |
21 | Image 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default ImageModal; 28 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/MessageBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | import { useState } from "react"; 5 | 6 | import { format } from "date-fns"; 7 | import { useSession } from "next-auth/react"; 8 | import Image from "next/image"; 9 | 10 | import Avatar from "../../../components/Avatar"; 11 | import { FullMessageType } from "../../../types"; 12 | import ImageModal from "./ImageModal"; 13 | 14 | interface MessageBoxProps { 15 | data: FullMessageType; 16 | isLast?: boolean; 17 | } 18 | 19 | const MessageBox: React.FC = ({ data, isLast }) => { 20 | const session = useSession(); 21 | const [imageModalOpen, setImageModalOpen] = useState(false); 22 | 23 | const isOwn = session.data?.user?.email === data?.sender?.email; 24 | const seenList = (data.seen || []) 25 | .filter((user) => user.email !== data?.sender?.email) 26 | .map((user) => user.name) 27 | .join(", "); 28 | 29 | const container = clsx("flex gap-3 p-4", isOwn && "justify-end"); 30 | const avatar = clsx(isOwn && "order-2"); 31 | const body = clsx("flex flex-col gap-2", isOwn && "items-end"); 32 | const message = clsx( 33 | "text-sm w-fit overflow-hidden", 34 | isOwn ? "bg-sky-500 text-white" : "bg-gray-100 dark:bg-lightgray", 35 | data.image ? "rounded-md p-0" : "rounded-full py-2 px-3" 36 | ); 37 | 38 | return ( 39 |
40 | {!isOwn && ( 41 |
42 | 43 |
44 | )} 45 |
46 |
47 | {!isOwn &&
{data.sender.name}
} 48 |
{format(new Date(data.createdAt), "p")}
49 |
50 |
51 | setImageModalOpen(false)} 55 | /> 56 | {data.image ? ( 57 | Image setImageModalOpen(true)} 62 | src={data.image} 63 | className=" 64 | object-cover 65 | cursor-pointer 66 | hover:scale-110 67 | transition 68 | " 69 | /> 70 | ) : ( 71 |
{data.body}
72 | )} 73 |
74 | {isLast && isOwn && seenList.length > 0 && ( 75 |
82 | {`Seen by ${seenList}`} 83 |
84 | )} 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default MessageBox; 91 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/components/MessageInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FieldErrors, FieldValues, UseFormRegister } from "react-hook-form"; 4 | 5 | interface MessageInputProps { 6 | placeholder?: string; 7 | id: string; 8 | type?: string; 9 | required?: boolean; 10 | register: UseFormRegister; 11 | errors: FieldErrors; 12 | } 13 | 14 | const MessageInput: React.FC = ({ 15 | placeholder, 16 | id, 17 | type, 18 | required, 19 | register, 20 | }) => { 21 | return ( 22 |
23 | 42 |
43 | ); 44 | }; 45 | 46 | export default MessageInput; 47 | -------------------------------------------------------------------------------- /src/app/conversations/[conversationId]/page.tsx: -------------------------------------------------------------------------------- 1 | import getConversationById from "../../actions/getConversationById"; 2 | import getMessages from "../../actions/getMessages"; 3 | import EmptyState from "../../components/EmptyState"; 4 | import Body from "./components/Body"; 5 | import Form from "./components/Form"; 6 | import Header from "./components/Header"; 7 | 8 | interface IParams { 9 | conversationId: string; 10 | } 11 | 12 | const ConversationId = async ({ params }: { params: IParams }) => { 13 | const conversation = await getConversationById(params.conversationId); 14 | const messages = await getMessages(params.conversationId); 15 | 16 | if (!conversation) { 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | return ( 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default ConversationId; 38 | -------------------------------------------------------------------------------- /src/app/conversations/components/ConversationBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | import { useCallback, useMemo } from "react"; 5 | 6 | import { format } from "date-fns"; 7 | import { useSession } from "next-auth/react"; 8 | import { useRouter } from "next/navigation"; 9 | 10 | import Avatar from "../../components/Avatar"; 11 | import AvatarGroup from "../../components/AvatarGroup"; 12 | import useOtherUser from "../../hooks/useOtherUser"; 13 | import { FullConversationType } from "../../types"; 14 | 15 | interface ConversationBoxProps { 16 | data: FullConversationType; 17 | selected?: boolean; 18 | } 19 | 20 | const ConversationBox: React.FC = ({ data, selected }) => { 21 | const otherUser = useOtherUser(data); 22 | const session = useSession(); 23 | const router = useRouter(); 24 | 25 | const handleClick = useCallback(() => { 26 | router.push(`/conversations/${data.id}`); 27 | }, [data, router]); 28 | 29 | const lastMessage = useMemo(() => { 30 | const messages = data.messages || []; 31 | 32 | return messages[messages.length - 1]; 33 | }, [data.messages]); 34 | 35 | const userEmail = useMemo(() => session.data?.user?.email, [session.data?.user?.email]); 36 | 37 | const hasSeen = useMemo(() => { 38 | if (!lastMessage) { 39 | return false; 40 | } 41 | 42 | const seenArray = lastMessage.seen || []; 43 | 44 | if (!userEmail) { 45 | return false; 46 | } 47 | 48 | return seenArray.filter((user) => user.email === userEmail).length !== 0; 49 | }, [userEmail, lastMessage]); 50 | 51 | const lastMessageText = useMemo(() => { 52 | if (lastMessage?.image) { 53 | return "Sent an image"; 54 | } 55 | 56 | if (lastMessage?.body) { 57 | return lastMessage?.body; 58 | } 59 | 60 | return "Started a conversation"; 61 | }, [lastMessage]); 62 | 63 | return ( 64 |
83 | {data.isGroup ? : } 84 | 85 |
86 |
87 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default ConversationBox; 124 | -------------------------------------------------------------------------------- /src/app/conversations/components/ConversationList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | import { useEffect, useMemo, useState } from "react"; 5 | import { MdOutlineGroupAdd } from "react-icons/md"; 6 | 7 | import { User } from "@prisma/client"; 8 | import { find } from "lodash"; 9 | import { useSession } from "next-auth/react"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | import GroupChatModal from "../../components/modals/GroupChatModal"; 13 | import useConversation from "../../hooks/useConversation"; 14 | import { pusherClient, pusherEvents } from "../../libs/pusher"; 15 | import { FullConversationType } from "../../types"; 16 | import ConversationBox from "./ConversationBox"; 17 | 18 | interface ConversationListProps { 19 | initialItems: FullConversationType[]; 20 | users: User[]; 21 | } 22 | 23 | const ConversationList: React.FC = ({ initialItems, users }) => { 24 | const [items, setItems] = useState(initialItems); 25 | const [isModalOpen, setIsModalOpen] = useState(false); 26 | 27 | const router = useRouter(); 28 | const session = useSession(); 29 | 30 | const { conversationId, isOpen } = useConversation(); 31 | 32 | const pusherKey = useMemo(() => { 33 | return session.data?.user?.email; 34 | }, [session.data?.user?.email]); 35 | 36 | useEffect(() => { 37 | if (!pusherKey) { 38 | return; 39 | } 40 | 41 | pusherClient.subscribe(pusherKey); 42 | 43 | const updateHandler = (conversation: FullConversationType) => { 44 | setItems((current) => 45 | current.map((currentConversation) => { 46 | if (currentConversation.id === conversation.id) { 47 | return { 48 | ...currentConversation, 49 | messages: conversation.messages, 50 | }; 51 | } 52 | 53 | return currentConversation; 54 | }) 55 | ); 56 | }; 57 | 58 | const newHandler = (conversation: FullConversationType) => { 59 | setItems((current) => { 60 | // skip if the conversation already exists 61 | if (find(current, { id: conversation.id })) { 62 | return current; 63 | } 64 | 65 | return [conversation, ...current]; 66 | }); 67 | }; 68 | 69 | const removeHandler = (conversation: FullConversationType) => { 70 | setItems((current) => { 71 | return [...current.filter((convo) => convo.id !== conversation.id)]; 72 | }); 73 | 74 | if (conversationId == conversation.id) { 75 | router.push("/conversations"); 76 | } 77 | }; 78 | 79 | pusherClient.bind(pusherEvents.UPDATE_CONVERSATION, updateHandler); 80 | pusherClient.bind(pusherEvents.NEW_CONVERSATION, newHandler); 81 | pusherClient.bind(pusherEvents.DELETE_CONVERSATION, removeHandler); 82 | }, [conversationId, pusherKey, router]); 83 | 84 | return ( 85 | <> 86 | setIsModalOpen(false)} /> 87 | 130 | 131 | ); 132 | }; 133 | 134 | export default ConversationList; 135 | -------------------------------------------------------------------------------- /src/app/conversations/layout.tsx: -------------------------------------------------------------------------------- 1 | import getConversations from "../actions/getConversations"; 2 | import getUsers from "../actions/getUsers"; 3 | import Sidebar from "../components/sidebar/Sidebar"; 4 | import ConversationList from "./components/ConversationList"; 5 | 6 | export default async function ConversationsLayout({ children }: { children: React.ReactNode }) { 7 | const conversations = await getConversations(); 8 | const users = await getUsers(); 9 | 10 | return ( 11 | // @ts-expect-error Server Component 12 | 13 |
14 | 15 | {children} 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/conversations/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingModal from "../components/modals/LoadingModal"; 2 | 3 | const Loading = () => { 4 | return ; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/app/conversations/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import clsx from "clsx"; 4 | 5 | import EmptyState from "../components/EmptyState"; 6 | import useConversation from "../hooks/useConversation"; 7 | 8 | const Home = () => { 9 | const { isOpen } = useConversation(); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | export default Home; 19 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tasin5541/NextJs-Messenger-Clone/8664b1056f7545942c71d35ac83972e5f4606e4a/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | body { 13 | @apply dark:bg-dusk; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/hooks/useActiveChannel.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { useSession } from "next-auth/react"; 4 | import { Channel, Members } from "pusher-js"; 5 | 6 | import { pusherClient } from "../libs/pusher"; 7 | import useActiveList from "./useActiveList"; 8 | 9 | const useActiveChannel = () => { 10 | const session = useSession(); 11 | 12 | const { set, add, remove } = useActiveList(); 13 | const [activeChannel, setActiveChannel] = useState(null); 14 | 15 | useEffect(() => { 16 | let channel = activeChannel; 17 | 18 | // Only subscribe to channel after user logs in so the user's active status gets updated real time 19 | if (session?.status !== "authenticated") { 20 | return; 21 | } 22 | 23 | if (!channel) { 24 | channel = pusherClient.subscribe("presence-messenger"); 25 | setActiveChannel(channel); 26 | } 27 | 28 | channel.bind("pusher:subscription_succeeded", (members: Members) => { 29 | const initialMembers: string[] = []; 30 | 31 | members.each((member: Record) => initialMembers.push(member.id)); 32 | set(initialMembers); 33 | }); 34 | 35 | channel.bind("pusher:member_added", (member: Record) => { 36 | add(member.id); 37 | }); 38 | 39 | channel.bind("pusher:member_removed", (member: Record) => { 40 | remove(member.id); 41 | }); 42 | 43 | return () => { 44 | if (activeChannel) { 45 | pusherClient.unsubscribe("presence-messenger"); 46 | setActiveChannel(null); 47 | } 48 | }; 49 | }, [activeChannel, set, add, remove, session?.status]); 50 | }; 51 | 52 | export default useActiveChannel; 53 | -------------------------------------------------------------------------------- /src/app/hooks/useActiveList.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface ActiveListStore { 4 | members: string[]; 5 | add: (id: string) => void; 6 | remove: (id: string) => void; 7 | set: (ids: string[]) => void; 8 | } 9 | 10 | const useActiveList = create((set) => ({ 11 | members: [], 12 | add: (id) => set((state) => ({ members: [...state.members, id] })), 13 | remove: (id) => 14 | set((state) => ({ members: state.members.filter((memberId) => memberId !== id) })), 15 | set: (ids) => set({ members: ids }), 16 | })); 17 | 18 | export default useActiveList; 19 | -------------------------------------------------------------------------------- /src/app/hooks/useConversation.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import { useParams } from "next/navigation"; 4 | 5 | const useConversation = () => { 6 | const params = useParams(); 7 | 8 | const conversationId = useMemo(() => { 9 | if (!params?.conversationId) { 10 | return ""; 11 | } 12 | 13 | return params.conversationId as string; 14 | }, [params?.conversationId]); 15 | 16 | const isOpen = useMemo(() => !!conversationId, [conversationId]); 17 | 18 | return useMemo( 19 | () => ({ 20 | isOpen, 21 | conversationId, 22 | }), 23 | [isOpen, conversationId] 24 | ); 25 | }; 26 | 27 | export default useConversation; 28 | -------------------------------------------------------------------------------- /src/app/hooks/useOtherUser.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import { User } from "@prisma/client"; 4 | import { useSession } from "next-auth/react"; 5 | 6 | import { FullConversationType } from "../types"; 7 | 8 | const useOtherUser = (conversation: FullConversationType | { users: User[] }) => { 9 | const session = useSession(); 10 | 11 | const otherUser = useMemo(() => { 12 | const currentUserEmail = session.data?.user?.email; 13 | 14 | const otherUser = conversation.users.filter((user) => user.email !== currentUserEmail); 15 | 16 | return otherUser[0]; 17 | }, [session.data?.user?.email, conversation.users]); 18 | 19 | return otherUser; 20 | }; 21 | 22 | export default useOtherUser; 23 | -------------------------------------------------------------------------------- /src/app/hooks/useRoutes.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { HiChat } from "react-icons/hi"; 3 | import { HiArrowLeftOnRectangle, HiUsers } from "react-icons/hi2"; 4 | 5 | import { signOut } from "next-auth/react"; 6 | import { usePathname } from "next/navigation"; 7 | 8 | import useConversation from "./useConversation"; 9 | 10 | const useRoutes = () => { 11 | const pathname = usePathname(); 12 | const { conversationId } = useConversation(); 13 | 14 | const routes = useMemo( 15 | () => [ 16 | { 17 | label: "Chat", 18 | href: "/conversations", 19 | icon: HiChat, 20 | active: pathname === "/conversations" || !!conversationId, 21 | }, 22 | { 23 | label: "Users", 24 | href: "/users", 25 | icon: HiUsers, 26 | active: pathname === "/users", 27 | }, 28 | { 29 | label: "Logout", 30 | onClick: () => signOut(), 31 | href: "#", 32 | icon: HiArrowLeftOnRectangle, 33 | }, 34 | ], 35 | [pathname, conversationId] 36 | ); 37 | 38 | return routes; 39 | }; 40 | 41 | export default useRoutes; 42 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import ActiveStatus from "./components/ActiveStatus"; 2 | import { Providers } from "./components/theme/Providers"; 3 | import AuthContext from "./context/AuthContext"; 4 | import ToasterContext from "./context/ToasterContext"; 5 | import "./globals.css"; 6 | 7 | export const metadata = { 8 | title: "NextJs Messenger Clone", 9 | description: "NextJs Messenger Clone", 10 | }; 11 | 12 | export default function RootLayout({ children }: { children: React.ReactNode }) { 13 | return ( 14 | 15 | 16 | 17 | 18 | <> 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/libs/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | const client = globalThis.prisma || new PrismaClient(); 8 | if (process.env.NODE_ENV !== "production") globalThis.prisma = client; 9 | 10 | export default client; 11 | -------------------------------------------------------------------------------- /src/app/libs/pusher.ts: -------------------------------------------------------------------------------- 1 | import PusherServer from "pusher"; 2 | import PusherClient from "pusher-js"; 3 | 4 | export const pusherEvents = { 5 | NEW_MESSAGE: "messages:new", 6 | UPDATE_MESSAGE: "message:update", 7 | NEW_CONVERSATION: "conversation:new", 8 | UPDATE_CONVERSATION: "conversation:update", 9 | DELETE_CONVERSATION: "conversation:remove", 10 | }; 11 | 12 | export const pusherServer = new PusherServer({ 13 | appId: process.env.PUSHER_APP_ID!, 14 | key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY!, 15 | secret: process.env.PUSHER_SECRET!, 16 | cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!, 17 | useTLS: true, 18 | }); 19 | 20 | export const pusherClient = new PusherClient(process.env.NEXT_PUBLIC_PUSHER_APP_KEY!, { 21 | channelAuthorization: { 22 | endpoint: "/api/pusher/auth", 23 | transport: "ajax", 24 | }, 25 | cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!, 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Conversation, Message, User } from "@prisma/client"; 2 | 3 | export type FullMessageType = Message & { 4 | sender: User; 5 | seen: User[]; 6 | }; 7 | 8 | export type FullConversationType = Conversation & { 9 | users: User[]; 10 | messages: FullMessageType[]; 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/users/components/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChangeEvent, Dispatch, SetStateAction, useEffect, useRef } from "react"; 4 | 5 | import { debounce } from "lodash"; 6 | 7 | interface SearchInputProps { 8 | placeholder?: string; 9 | id: string; 10 | setSearchBy: Dispatch>; 11 | } 12 | 13 | const SearchInput: React.FC = ({ placeholder, id, setSearchBy }) => { 14 | const debouncedSearch = useRef( 15 | debounce(async (criteria) => { 16 | setSearchBy(criteria); 17 | }, 300) 18 | ).current; 19 | 20 | useEffect(() => { 21 | return () => { 22 | debouncedSearch.cancel(); 23 | }; 24 | }, [debouncedSearch]); 25 | 26 | const handleSearch = (e: ChangeEvent) => { 27 | debouncedSearch(e.target.value); 28 | }; 29 | 30 | return ( 31 | 50 | ); 51 | }; 52 | 53 | export default SearchInput; 54 | -------------------------------------------------------------------------------- /src/app/users/components/UserBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useCallback, useState } from "react"; 5 | 6 | import { User } from "@prisma/client"; 7 | import { useRouter } from "next/navigation"; 8 | 9 | import Avatar from "../../components/Avatar"; 10 | import LoadingModal from "../../components/modals/LoadingModal"; 11 | 12 | interface UserBoxProps { 13 | data: User; 14 | } 15 | 16 | const UserBox: React.FC = ({ data }) => { 17 | const router = useRouter(); 18 | const [isLoading, setIsLoading] = useState(false); 19 | 20 | const handleClick = useCallback(() => { 21 | setIsLoading(true); 22 | 23 | axios 24 | .post("/api/conversations", { userId: data.id }) 25 | .then((data) => { 26 | router.push(`/conversations/${data.data.id}`); 27 | }) 28 | .finally(() => setIsLoading(false)); 29 | }, [data, router]); 30 | 31 | return ( 32 | <> 33 | {isLoading && } 34 |
51 | 52 |
53 |
54 |
55 |

{data.name}

56 |
57 |
58 |
59 |
60 | 61 | ); 62 | }; 63 | 64 | export default UserBox; 65 | -------------------------------------------------------------------------------- /src/app/users/components/UserList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | import { User } from "@prisma/client"; 6 | 7 | import SearchInput from "./SearchInput"; 8 | import UserBox from "./UserBox"; 9 | 10 | interface UserListProps { 11 | items: User[]; 12 | } 13 | 14 | const UserList: React.FC = ({ items }) => { 15 | const [searchBy, setSearchBy] = useState(""); 16 | 17 | const filterBySearch = (user: User) => { 18 | if (searchBy) { 19 | const lowerCaseSearch = searchBy.toLocaleLowerCase(); 20 | const email = user.email || ""; 21 | const name = user.name || ""; 22 | return email.includes(lowerCaseSearch) || name.includes(lowerCaseSearch); 23 | } 24 | return true; 25 | }; 26 | 27 | return ( 28 | 68 | ); 69 | }; 70 | 71 | export default UserList; 72 | -------------------------------------------------------------------------------- /src/app/users/layout.tsx: -------------------------------------------------------------------------------- 1 | import getUsers from "../actions/getUsers"; 2 | import Sidebar from "../components/sidebar/Sidebar"; 3 | import UserList from "./components/UserList"; 4 | 5 | export default async function UsersLayout({ children }: { children: React.ReactNode }) { 6 | const users = await getUsers(); 7 | 8 | return ( 9 | // @ts-expect-error Server Component 10 | 11 |
12 | 13 | {children} 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/users/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingModal from "../components/modals/LoadingModal"; 2 | 3 | const Loading = () => { 4 | return ; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/app/users/page.tsx: -------------------------------------------------------------------------------- 1 | import EmptyState from "../components/EmptyState"; 2 | 3 | const Users = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Users; 12 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { withAuth } from "next-auth/middleware"; 2 | 3 | export default withAuth({ 4 | pages: { 5 | signIn: "/", 6 | }, 7 | }); 8 | 9 | export const config = { 10 | matcher: ["/users/:path*", "/conversations/:path*"], 11 | }; 12 | -------------------------------------------------------------------------------- /src/pages/api/pusher/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getServerSession } from "next-auth"; 3 | 4 | import { authOptions } from "../../../app/api/auth/[...nextauth]/route"; 5 | import { pusherServer } from "../../../app/libs/pusher"; 6 | 7 | export default async function handler(request: NextApiRequest, response: NextApiResponse) { 8 | const session = await getServerSession(request, response, authOptions); 9 | 10 | if (!session?.user?.email) { 11 | return response.status(401); 12 | } 13 | 14 | const socketId = request.body.socket_id; 15 | const channel = request.body.channel_name; 16 | const data = { 17 | user_id: session.user.email, 18 | }; 19 | 20 | const authResponse = pusherServer.authorizeChannel(socketId, channel, data); 21 | return response.send(authResponse); 22 | } 23 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | dusk: "#242526", 12 | lightgray: "#3a3b3c", 13 | }, 14 | }, 15 | }, 16 | plugins: [ 17 | require("@tailwindcss/forms")({ 18 | strategy: "class", 19 | }), 20 | ], 21 | darkMode: "class", 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------