From d38fe92b552a2a436cf55fdea40208e86913a4df Mon Sep 17 00:00:00 2001 From: Rohan mittal Date: Wed, 26 Jun 2024 08:20:17 +0530 Subject: [PATCH] Added Initial implementation --- .eslintrc.json | 3 + .gitignore | 37 + README.md | 36 + .../(routes)/sign-in/[[...sign-in]]/page.tsx | 4 + .../(routes)/sign-up/[[...sign-up]]/page.tsx | 4 + app/(auth)/layout.tsx | 9 + .../(routes)/invite/[inviteCode]/page.tsx | 47 + .../[serverId]/channels/[channelId]/page.tsx | 87 + .../conversations/[memberId]/page.tsx | 99 + .../(routes)/servers/[serverId]/layout.tsx | 35 + .../(routes)/servers/[serverId]/page.tsx | 50 + app/(main)/layout.tsx | 16 + app/(setup)/page.tsx | 21 + app/api/channel/[channelId]/route.ts | 122 + app/api/channel/route.ts | 45 + app/api/direct-messages/route.ts | 81 + app/api/livekit/route.ts | 35 + app/api/member/[memberId]/route.ts | 103 + app/api/messages/route.ts | 81 + app/api/servers/[serverId]/leave/route.ts | 66 + app/api/servers/[serverId]/route.ts | 54 + app/api/servers/route.ts | 38 + app/api/uploadthing/core.ts | 22 + app/api/uploadthing/route.ts | 11 + app/favicon.ico | Bin 0 -> 114781 bytes app/globals.css | 82 + app/layout.tsx | 42 + components.json | 17 + components/Action-tooltip.tsx | 32 + components/avatar.tsx | 15 + components/chat/chat-header.tsx | 35 + components/chat/chat-input.tsx | 97 + components/chat/chat-item.tsx | 259 + components/chat/chat-messages.tsx | 132 + components/chat/chat-welcome.tsx | 27 + components/emojiPicker.tsx | 39 + components/fileUpload.tsx | 72 + components/media-room.tsx | 59 + components/mobile-toggle.tsx | 27 + components/modals/category-modal.tsx | 121 + components/modals/channel-modal.tsx | 157 + components/modals/create-server-modal.tsx | 141 + components/modals/delete-channel.tsx | 67 + components/modals/delete-message-modal.tsx | 78 + components/modals/delete-server.tsx | 69 + components/modals/download-modal.tsx | 79 + components/modals/downloadModalItem.tsx | 43 + components/modals/edit-channel.tsx | 156 + components/modals/edit-server-modal.tsx | 146 + components/modals/event-modal.tsx | 55 + components/modals/initial-modal.tsx | 138 + components/modals/invite-modal.tsx | 115 + components/modals/leave-server.tsx | 69 + components/modals/manage-members-modal.tsx | 183 + components/modals/message-file-modal.tsx | 115 + components/mode-toggle.tsx | 44 + components/navigations/Navigation-action.tsx | 25 + components/navigations/Navigation-item.tsx | 45 + components/navigations/NavigationDownload.tsx | 25 + components/navigations/NavigationSidebar.tsx | 59 + components/provider/modal-provider.tsx | 44 + components/provider/query-provider.tsx | 12 + components/provider/socket-provider.tsx | 53 + components/provider/theme-provider.tsx | 9 + components/server/serverChannel.tsx | 80 + components/server/serverEventHeader.tsx | 20 + components/server/serverHeader.tsx | 109 + components/server/serverMember.tsx | 57 + components/server/serverSearch.tsx | 105 + components/server/serverSection.tsx | 52 + components/server/serverSidebar.tsx | 208 + components/socket-indicator.tsx | 22 + components/ui/avatar.tsx | 50 + components/ui/badge.tsx | 36 + components/ui/button.tsx | 56 + components/ui/checkbox.tsx | 30 + components/ui/command.tsx | 155 + components/ui/dialog.tsx | 122 + components/ui/dropdown-menu.tsx | 200 + components/ui/form.tsx | 176 + components/ui/input.tsx | 25 + components/ui/label.tsx | 26 + components/ui/popover.tsx | 31 + components/ui/scroll-area.tsx | 48 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/tooltip.tsx | 30 + hooks/use-chat-query.tsx | 51 + hooks/use-chat-scroll.tsx | 64 + hooks/use-chat-socket.tsx | 87 + hooks/use-modal-store.ts | 40 + hooks/use-origin.ts | 15 + lib/conversation.ts | 68 + lib/current-profile-pages.ts | 20 + lib/currentProfile.ts | 14 + lib/db.ts | 9 + lib/initial-profile.ts | 25 + lib/uploadThing.ts | 6 + lib/utils.ts | 6 + middleware.ts | 12 + next.config.mjs | 15 + package-lock.json | 8294 +++++++++++++++++ package.json | 75 + .../direct-messages/[directMessageId].ts | 150 + pages/api/socket/direct-messages/index.ts | 100 + pages/api/socket/io.ts | 28 + pages/api/socket/messages/[messageId].ts | 148 + pages/api/socket/messages/index.ts | 98 + postcss.config.mjs | 8 + .../20240613105847_init/migration.sql | 89 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 159 + tailwind.config.ts | 80 + tsconfig.json | 26 + types.ts | 19 + 116 files changed, 15767 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx create mode 100644 app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx create mode 100644 app/(auth)/layout.tsx create mode 100644 app/(invite)/(routes)/invite/[inviteCode]/page.tsx create mode 100644 app/(main)/(routes)/servers/[serverId]/channels/[channelId]/page.tsx create mode 100644 app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx create mode 100644 app/(main)/(routes)/servers/[serverId]/layout.tsx create mode 100644 app/(main)/(routes)/servers/[serverId]/page.tsx create mode 100644 app/(main)/layout.tsx create mode 100644 app/(setup)/page.tsx create mode 100644 app/api/channel/[channelId]/route.ts create mode 100644 app/api/channel/route.ts create mode 100644 app/api/direct-messages/route.ts create mode 100644 app/api/livekit/route.ts create mode 100644 app/api/member/[memberId]/route.ts create mode 100644 app/api/messages/route.ts create mode 100644 app/api/servers/[serverId]/leave/route.ts create mode 100644 app/api/servers/[serverId]/route.ts create mode 100644 app/api/servers/route.ts create mode 100644 app/api/uploadthing/core.ts create mode 100644 app/api/uploadthing/route.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 components.json create mode 100644 components/Action-tooltip.tsx create mode 100644 components/avatar.tsx create mode 100644 components/chat/chat-header.tsx create mode 100644 components/chat/chat-input.tsx create mode 100644 components/chat/chat-item.tsx create mode 100644 components/chat/chat-messages.tsx create mode 100644 components/chat/chat-welcome.tsx create mode 100644 components/emojiPicker.tsx create mode 100644 components/fileUpload.tsx create mode 100644 components/media-room.tsx create mode 100644 components/mobile-toggle.tsx create mode 100644 components/modals/category-modal.tsx create mode 100644 components/modals/channel-modal.tsx create mode 100644 components/modals/create-server-modal.tsx create mode 100644 components/modals/delete-channel.tsx create mode 100644 components/modals/delete-message-modal.tsx create mode 100644 components/modals/delete-server.tsx create mode 100644 components/modals/download-modal.tsx create mode 100644 components/modals/downloadModalItem.tsx create mode 100644 components/modals/edit-channel.tsx create mode 100644 components/modals/edit-server-modal.tsx create mode 100644 components/modals/event-modal.tsx create mode 100644 components/modals/initial-modal.tsx create mode 100644 components/modals/invite-modal.tsx create mode 100644 components/modals/leave-server.tsx create mode 100644 components/modals/manage-members-modal.tsx create mode 100644 components/modals/message-file-modal.tsx create mode 100644 components/mode-toggle.tsx create mode 100644 components/navigations/Navigation-action.tsx create mode 100644 components/navigations/Navigation-item.tsx create mode 100644 components/navigations/NavigationDownload.tsx create mode 100644 components/navigations/NavigationSidebar.tsx create mode 100644 components/provider/modal-provider.tsx create mode 100644 components/provider/query-provider.tsx create mode 100644 components/provider/socket-provider.tsx create mode 100644 components/provider/theme-provider.tsx create mode 100644 components/server/serverChannel.tsx create mode 100644 components/server/serverEventHeader.tsx create mode 100644 components/server/serverHeader.tsx create mode 100644 components/server/serverMember.tsx create mode 100644 components/server/serverSearch.tsx create mode 100644 components/server/serverSection.tsx create mode 100644 components/server/serverSidebar.tsx create mode 100644 components/socket-indicator.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-chat-query.tsx create mode 100644 hooks/use-chat-scroll.tsx create mode 100644 hooks/use-chat-socket.tsx create mode 100644 hooks/use-modal-store.ts create mode 100644 hooks/use-origin.ts create mode 100644 lib/conversation.ts create mode 100644 lib/current-profile-pages.ts create mode 100644 lib/currentProfile.ts create mode 100644 lib/db.ts create mode 100644 lib/initial-profile.ts create mode 100644 lib/uploadThing.ts create mode 100644 lib/utils.ts create mode 100644 middleware.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/api/socket/direct-messages/[directMessageId].ts create mode 100644 pages/api/socket/direct-messages/index.ts create mode 100644 pages/api/socket/io.ts create mode 100644 pages/api/socket/messages/[messageId].ts create mode 100644 pages/api/socket/messages/index.ts create mode 100644 postcss.config.mjs create mode 100644 prisma/migrations/20240613105847_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types.ts diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00bba9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..c403366 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 0000000..0e8f69c --- /dev/null +++ b/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,4 @@ +import { SignIn } from '@clerk/nextjs'; +export default function Page() { + return ; +} diff --git a/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx b/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx new file mode 100644 index 0000000..5590276 --- /dev/null +++ b/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx @@ -0,0 +1,4 @@ +import { SignUp } from '@clerk/nextjs'; +export default function Page() { + return ; +} diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..6d1a296 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,9 @@ +export default function authLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
{children}
+ ); +} diff --git a/app/(invite)/(routes)/invite/[inviteCode]/page.tsx b/app/(invite)/(routes)/invite/[inviteCode]/page.tsx new file mode 100644 index 0000000..19edf44 --- /dev/null +++ b/app/(invite)/(routes)/invite/[inviteCode]/page.tsx @@ -0,0 +1,47 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { redirect } from 'next/navigation'; + +export default async function InviteCodePage({ + params, +}: { + params: { inviteCode: string }; +}) { + const profile = await currentProfile(); + if (!profile) { + return redirect(`${process.env.NEXT_PUBLIC_APP_URL}/sign-in`); + } + + if (!params.inviteCode) return redirect('/'); + + const existingUser = await db.server.findFirst({ + where: { + inviteCode: params.inviteCode, + members: { + some: { + profileId: profile.id, + }, + }, + }, + }); + + if (existingUser) return redirect(`/servers/${existingUser.id}`); + + const server = await db.server.update({ + where: { + inviteCode: params.inviteCode, + }, + data: { + members: { + create: [ + { + profileId: profile.id, + }, + ], + }, + }, + }); + + if (server) return redirect(`/servers/${server.id}`); + return null; +} diff --git a/app/(main)/(routes)/servers/[serverId]/channels/[channelId]/page.tsx b/app/(main)/(routes)/servers/[serverId]/channels/[channelId]/page.tsx new file mode 100644 index 0000000..b0717a0 --- /dev/null +++ b/app/(main)/(routes)/servers/[serverId]/channels/[channelId]/page.tsx @@ -0,0 +1,87 @@ +import { redirectToSignIn } from '@clerk/nextjs'; +import { redirect } from 'next/navigation'; +import { ChannelType } from '@prisma/client'; + +import { db } from '@/lib/db'; +import { currentProfile } from '@/lib/currentProfile'; +import ChatHeader from '@/components/chat/chat-header'; +import { ChatInput } from '@/components/chat/chat-input'; +import { ChatMessages } from '@/components/chat/chat-messages'; +import { MediaRoom } from '@/components/media-room'; + +interface ChannelIdPageProps { + params: { + serverId: string; + channelId: string; + }; +} + +const ChannelIdPage = async ({ params }: ChannelIdPageProps) => { + const profile = await currentProfile(); + + if (!profile) { + return redirectToSignIn(); + } + + const channel = await db.channel.findUnique({ + where: { + id: params.channelId, + }, + }); + + const member = await db.member.findFirst({ + where: { + serverId: params.serverId, + profileId: profile.id, + }, + }); + + if (!channel || !member) { + redirect('/'); + } + + return ( +
+ + {channel.type === ChannelType.TEXT && ( + <> + + + + )} + {channel.type === ChannelType.AUDIO && ( + + )} + {channel.type === ChannelType.VIDEO && ( + + )} +
+ ); +}; + +export default ChannelIdPage; diff --git a/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx b/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx new file mode 100644 index 0000000..5ee0cfe --- /dev/null +++ b/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx @@ -0,0 +1,99 @@ +import { redirectToSignIn } from '@clerk/nextjs'; +import { redirect } from 'next/navigation'; + +import { db } from '@/lib/db'; +import { getOrCreateConversation } from '@/lib/conversation'; +import { ChatMessages } from '@/components/chat/chat-messages'; +import { ChatInput } from '@/components/chat/chat-input'; +import { currentProfile } from '@/lib/currentProfile'; +import ChatHeader from '@/components/chat/chat-header'; +import { MediaRoom } from '@/components/media-room'; + +interface MemberIdPageProps { + params: { + memberId: string; + serverId: string; + }; + searchParams: { + video?: boolean; + }; +} + +const MemberIdPage = async ({ params, searchParams }: MemberIdPageProps) => { + const profile = await currentProfile(); + + if (!profile) { + return redirectToSignIn(); + } + + const currentMember = await db.member.findFirst({ + where: { + serverId: params.serverId, + profileId: profile.id, + }, + include: { + profile: true, + }, + }); + + if (!currentMember) { + return redirect('/'); + } + + const conversation = await getOrCreateConversation( + currentMember.id, + params.memberId + ); + + if (!conversation) { + return redirect(`/servers/${params.serverId}`); + } + + const { memberOne, memberTwo } = conversation; + + const otherMember = + memberOne.profileId === profile.id ? memberTwo : memberOne; + + return ( +
+ + + {searchParams.video && ( + + )} + + {!searchParams.video && ( + <> + + + + )} +
+ ); +}; + +export default MemberIdPage; diff --git a/app/(main)/(routes)/servers/[serverId]/layout.tsx b/app/(main)/(routes)/servers/[serverId]/layout.tsx new file mode 100644 index 0000000..37836a1 --- /dev/null +++ b/app/(main)/(routes)/servers/[serverId]/layout.tsx @@ -0,0 +1,35 @@ +import ServerSidebar from '@/components/server/serverSidebar'; +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { redirectToSignIn } from '@clerk/nextjs'; +import { redirect } from 'next/navigation'; + +export default async function MainLayout({ + children, + params, +}: { + children: React.ReactNode; + params: { serverId: string }; +}) { + const profile = await currentProfile(); + if (!profile) return redirectToSignIn(); + const server = await db.server.findUnique({ + where: { + id: params.serverId, + members: { + some: { + profileId: profile.id, + }, + }, + }, + }); + if (!server) redirect('/'); + return ( +
+
+ +
+
{children}
+
+ ); +} diff --git a/app/(main)/(routes)/servers/[serverId]/page.tsx b/app/(main)/(routes)/servers/[serverId]/page.tsx new file mode 100644 index 0000000..5c7cdea --- /dev/null +++ b/app/(main)/(routes)/servers/[serverId]/page.tsx @@ -0,0 +1,50 @@ +import { redirectToSignIn } from '@clerk/nextjs'; +import { redirect } from 'next/navigation'; + +import { db } from '@/lib/db'; +import { currentProfile } from '@/lib/currentProfile'; + +interface ServerIdPageProps { + params: { + serverId: string; + }; +} + +const ServerIdPage = async ({ params }: ServerIdPageProps) => { + const profile = await currentProfile(); + + if (!profile) { + return redirectToSignIn(); + } + + const server = await db.server.findUnique({ + where: { + id: params.serverId, + members: { + some: { + profileId: profile.id, + }, + }, + }, + include: { + channels: { + where: { + name: 'general', + }, + orderBy: { + createdAt: 'asc', + }, + }, + }, + }); + + const initialChannel = server?.channels[0]; + + if (initialChannel?.name !== 'general') { + return null; + } + + return redirect(`/servers/${params.serverId}/channels/${initialChannel?.id}`); +}; + +export default ServerIdPage; diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx new file mode 100644 index 0000000..1695131 --- /dev/null +++ b/app/(main)/layout.tsx @@ -0,0 +1,16 @@ +import NavigationSidebar from '@/components/navigations/NavigationSidebar'; + +export default function MainLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ +
+
{children}
+
+ ); +} diff --git a/app/(setup)/page.tsx b/app/(setup)/page.tsx new file mode 100644 index 0000000..936ce80 --- /dev/null +++ b/app/(setup)/page.tsx @@ -0,0 +1,21 @@ +import InitialModal from '@/components/modals/initial-modal'; +import { db } from '@/lib/db'; +import { initialProfile } from '@/lib/initial-profile'; +import { redirect } from 'next/navigation'; + +export default async function Home() { + const profile = await initialProfile(); + const server = await db.server.findFirst({ + where: { + members: { + some: { + profileId: profile.id, + }, + }, + }, + }); + if (server) { + return redirect(`/servers/${server.id}`); + } + return ; +} diff --git a/app/api/channel/[channelId]/route.ts b/app/api/channel/[channelId]/route.ts new file mode 100644 index 0000000..52119eb --- /dev/null +++ b/app/api/channel/[channelId]/route.ts @@ -0,0 +1,122 @@ +import { NextResponse } from 'next/server'; +import { MemberRole } from '@prisma/client'; + +import { db } from '@/lib/db'; +import { currentProfile } from '@/lib/currentProfile'; + +export async function DELETE( + req: Request, + { params }: { params: { channelId: string } } +) { + try { + const profile = await currentProfile(); + const { searchParams } = new URL(req.url); + + const serverId = searchParams.get('serverId'); + + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + if (!serverId) { + return new NextResponse('Server ID missing', { status: 400 }); + } + + if (!params.channelId) { + return new NextResponse('Channel ID missing', { status: 400 }); + } + + const server = await db.server.update({ + where: { + id: serverId, + members: { + some: { + profileId: profile.id, + role: { + in: [MemberRole.ADMIN, MemberRole.MODERATOR], + }, + }, + }, + }, + data: { + channels: { + delete: { + id: params.channelId, + name: { + not: 'general', + }, + }, + }, + }, + }); + + return NextResponse.json(server); + } catch (error) { + console.log('[CHANNEL_ID_DELETE]', error); + return new NextResponse('Internal Error', { status: 500 }); + } +} + +export async function PUT( + req: Request, + { params }: { params: { channelId: string } } +) { + try { + const profile = await currentProfile(); + const { name, type } = await req.json(); + const { searchParams } = new URL(req.url); + + const serverId = searchParams.get('serverId'); + + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + if (!serverId) { + return new NextResponse('Server ID missing', { status: 400 }); + } + + if (!params.channelId) { + return new NextResponse('Channel ID missing', { status: 400 }); + } + + if (name === 'general') { + return new NextResponse("Name cannot be 'general'", { status: 400 }); + } + + const server = await db.server.update({ + where: { + id: serverId, + members: { + some: { + profileId: profile.id, + role: { + in: [MemberRole.ADMIN, MemberRole.MODERATOR], + }, + }, + }, + }, + data: { + channels: { + update: { + where: { + id: params.channelId, + NOT: { + name: 'general', + }, + }, + data: { + name, + type, + }, + }, + }, + }, + }); + + return NextResponse.json(server); + } catch (error) { + console.log('[CHANNEL_ID_PATCH]', error); + return new NextResponse('Internal Error', { status: 500 }); + } +} diff --git a/app/api/channel/route.ts b/app/api/channel/route.ts new file mode 100644 index 0000000..a67b265 --- /dev/null +++ b/app/api/channel/route.ts @@ -0,0 +1,45 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { MemberRole } from '@prisma/client'; +import { NextResponse } from 'next/server'; +export async function POST(req: Request) { + try { + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + const { name, type } = await req.json(); + const { searchParams } = new URL(req.url); + const serverId = searchParams.get('serverId'); + if (!serverId) { + return new NextResponse('server Id missing', { status: 400 }); + } + + const server = await db.server.update({ + where: { + id: serverId, + members: { + some: { + profileId: profile.id, + role: { + in: [MemberRole.ADMIN, MemberRole.MODERATOR], + }, + }, + }, + }, + data: { + channels: { + create: { + profileId: profile.id, + name, + type, + }, + }, + }, + }); + return NextResponse.json(server); + } catch (err: any) { + console.log('[API-MEMBER_PATCH]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} diff --git a/app/api/direct-messages/route.ts b/app/api/direct-messages/route.ts new file mode 100644 index 0000000..68d7b2d --- /dev/null +++ b/app/api/direct-messages/route.ts @@ -0,0 +1,81 @@ +import { NextResponse } from 'next/server'; +import { DirectMessage } from '@prisma/client'; + +import { db } from '@/lib/db'; +import { currentProfile } from '@/lib/currentProfile'; + +const MESSAGES_BATCH = 10; + +export async function GET(req: Request) { + try { + const profile = await currentProfile(); + const { searchParams } = new URL(req.url); + + const cursor = searchParams.get('cursor'); + const conversationId = searchParams.get('conversationId'); + + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + if (!conversationId) { + return new NextResponse('Conversation ID missing', { status: 400 }); + } + + let messages: DirectMessage[] = []; + + if (cursor) { + messages = await db.directMessage.findMany({ + take: MESSAGES_BATCH, + skip: 1, + cursor: { + id: cursor, + }, + where: { + conversationId, + }, + include: { + member: { + include: { + profile: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + } else { + messages = await db.directMessage.findMany({ + take: MESSAGES_BATCH, + where: { + conversationId, + }, + include: { + member: { + include: { + profile: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + } + + let nextCursor = null; + + if (messages.length === MESSAGES_BATCH) { + nextCursor = messages[MESSAGES_BATCH - 1].id; + } + + return NextResponse.json({ + items: messages, + nextCursor, + }); + } catch (error) { + console.log('[DIRECT_MESSAGES_GET]', error); + return new NextResponse('Internal Error', { status: 500 }); + } +} diff --git a/app/api/livekit/route.ts b/app/api/livekit/route.ts new file mode 100644 index 0000000..f38c237 --- /dev/null +++ b/app/api/livekit/route.ts @@ -0,0 +1,35 @@ +import { AccessToken } from 'livekit-server-sdk'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + const room = req.nextUrl.searchParams.get('room'); + const username = req.nextUrl.searchParams.get('username'); + if (!room) { + return NextResponse.json( + { error: 'Missing "room" query parameter' }, + { status: 400 } + ); + } else if (!username) { + return NextResponse.json( + { error: 'Missing "username" query parameter' }, + { status: 400 } + ); + } + + const apiKey = process.env.LIVEKIT_API_KEY; + const apiSecret = process.env.LIVEKIT_API_SECRET; + const wsUrl = process.env.NEXT_PUBLIC_LIVEKIT_URL; + + if (!apiKey || !apiSecret || !wsUrl) { + return NextResponse.json( + { error: 'Server misconfigured' }, + { status: 500 } + ); + } + + const at = new AccessToken(apiKey, apiSecret, { identity: username }); + + at.addGrant({ room, roomJoin: true, canPublish: true, canSubscribe: true }); + + return NextResponse.json({ token: await at.toJwt() }); +} diff --git a/app/api/member/[memberId]/route.ts b/app/api/member/[memberId]/route.ts new file mode 100644 index 0000000..dd4aad7 --- /dev/null +++ b/app/api/member/[memberId]/route.ts @@ -0,0 +1,103 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { NextResponse } from 'next/server'; +export async function PATCH( + req: Request, + { params }: { params: { memberId: string } } +) { + try { + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + const { role, serverId } = await req.json(); + if (!serverId) { + return new NextResponse('Server Id is Required', { status: 400 }); + } + const server = await db.server.update({ + where: { + id: serverId, + profileId: profile.id, + }, + data: { + members: { + update: { + where: { + id: params.memberId, + profileId: { + not: profile.id, + }, + }, + data: { + role, + }, + }, + }, + }, + include: { + members: { + include: { + profile: true, + }, + orderBy: { + role: 'asc', + }, + }, + }, + }); + return NextResponse.json(server); + } catch (err: any) { + console.log('[API-MEMBER_PATCH]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} + +export async function DELETE( + req: Request, + { params }: { params: { memberId: string } } +) { + try { + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + const { searchParams } = new URL(req.url); + const serverId = searchParams.get('serverId'); + if (!serverId) { + return new NextResponse('server Id missing', { status: 400 }); + } + if (!params.memberId) { + return new NextResponse('Member Id missing', { status: 400 }); + } + const server = await db.server.update({ + where: { + id: serverId, + profileId: profile.id, + }, + data: { + members: { + deleteMany: { + id: params.memberId, + profileId: { + not: profile.id, + }, + }, + }, + }, + include: { + members: { + include: { + profile: true, + }, + orderBy: { + role: 'asc', + }, + }, + }, + }); + return NextResponse.json(server); + } catch (err) { + console.log('[API-MEMBER_DELETE]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts new file mode 100644 index 0000000..73e4ae0 --- /dev/null +++ b/app/api/messages/route.ts @@ -0,0 +1,81 @@ +import { NextResponse } from 'next/server'; +import { Message } from '@prisma/client'; + +import { db } from '@/lib/db'; +import { currentProfile } from '@/lib/currentProfile'; + +const MESSAGES_BATCH = 10; + +export async function GET(req: Request) { + try { + const profile = await currentProfile(); + const { searchParams } = new URL(req.url); + + const cursor = searchParams.get('cursor'); + const channelId = searchParams.get('channelId'); + + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + if (!channelId) { + return new NextResponse('Channel ID missing', { status: 400 }); + } + + let messages: Message[] = []; + + if (cursor) { + messages = await db.message.findMany({ + take: MESSAGES_BATCH, + skip: 1, + cursor: { + id: cursor, + }, + where: { + channelId, + }, + include: { + member: { + include: { + profile: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + } else { + messages = await db.message.findMany({ + take: MESSAGES_BATCH, + where: { + channelId, + }, + include: { + member: { + include: { + profile: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + } + + let nextCursor = null; + + if (messages.length === MESSAGES_BATCH) { + nextCursor = messages[MESSAGES_BATCH - 1].id; + } + + return NextResponse.json({ + items: messages, + nextCursor, + }); + } catch (error) { + console.log('[MESSAGES_GET]', error); + return new NextResponse('Internal Error', { status: 500 }); + } +} diff --git a/app/api/servers/[serverId]/leave/route.ts b/app/api/servers/[serverId]/leave/route.ts new file mode 100644 index 0000000..12a411a --- /dev/null +++ b/app/api/servers/[serverId]/leave/route.ts @@ -0,0 +1,66 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { NextResponse } from 'next/server'; +export async function POST( + req: Request, + { params }: { params: { serverId: string } } +) { + try { + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + if (!params.serverId) { + return new NextResponse('Server Id missing', { status: 400 }); + } + const server = await db.server.update({ + where: { + id: params.serverId, + profileId: { + not: profile.id, + }, + members: { + some: { + profileId: profile.id, + }, + }, + }, + data: { + members: { + deleteMany: { + profileId: profile.id, + }, + }, + }, + }); + return NextResponse.json(server); + } catch (err: any) { + console.log('[API-SERVERS_POST]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} + +export async function DELETE( + req: Request, + { params }: { params: { serverId: string } } +) { + try { + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + if (!params.serverId) { + return new NextResponse('Server Id missing', { status: 400 }); + } + const server = await db.server.delete({ + where: { + id: params.serverId, + profileId: profile.id, + }, + }); + return NextResponse.json(server); + } catch (err: any) { + console.log('[SERVER_ID_DELETE]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} diff --git a/app/api/servers/[serverId]/route.ts b/app/api/servers/[serverId]/route.ts new file mode 100644 index 0000000..9d36e27 --- /dev/null +++ b/app/api/servers/[serverId]/route.ts @@ -0,0 +1,54 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { NextResponse } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +export async function PATCH( + req: Request, + { params }: { params: { serverId: string } } +) { + try { + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + const server = await db.server.update({ + where: { + id: params.serverId, + profileId: profile.id, + }, + data: { + inviteCode: uuidv4(), + }, + }); + return NextResponse.json(server); + } catch (err: any) { + console.log('[API-SERVERS_POST]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} + +export async function PUT( + req: Request, + { params }: { params: { serverId: string } } +) { + try { + const { name, imageUrl } = await req.json(); + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + const server = await db.server.update({ + where: { + id: params.serverId, + }, + data: { + name, + imageUrl, + }, + }); + return NextResponse.json(server); + } catch (err: any) { + console.log('[API-SERVERS_POST]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} diff --git a/app/api/servers/route.ts b/app/api/servers/route.ts new file mode 100644 index 0000000..c679fc6 --- /dev/null +++ b/app/api/servers/route.ts @@ -0,0 +1,38 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { MemberRole } from '@prisma/client'; +import { NextResponse } from 'next/server'; +import { v4 as uuidv4 } from 'uuid'; +export async function POST(req: Request) { + try { + const { name, imageUrl } = await req.json(); + const profile = await currentProfile(); + if (!profile) { + return new NextResponse('Unauthorized', { status: 401 }); + } + const server = await db.server.create({ + data: { + profileId: profile.id, + name, + imageUrl, + inviteCode: uuidv4(), + channels: { + create: [ + { + name: 'general', + profileId: profile.id, + }, + ], + }, + members: { + create: [{ profileId: profile.id, role: MemberRole.ADMIN }], + }, + }, + }); + console.log(server); + return NextResponse.json(server); + } catch (err: any) { + console.log('[API-SERVERS_POST]', err); + return new NextResponse('Internal error', { status: 500 }); + } +} diff --git a/app/api/uploadthing/core.ts b/app/api/uploadthing/core.ts new file mode 100644 index 0000000..58b361b --- /dev/null +++ b/app/api/uploadthing/core.ts @@ -0,0 +1,22 @@ +import { createUploadthing, type FileRouter } from 'uploadthing/next'; +import { auth } from '@clerk/nextjs'; +import { UploadThingError } from 'uploadthing/server'; +const handleAuth = () => { + const { userId } = auth(); + if (!userId) throw new Error('Unauthorized'); + return { userId }; +}; + +const f = createUploadthing(); + +// FileRouter for your app, can contain multiple FileRoutes +export const ourFileRouter = { + serverImage: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } }) + .middleware(handleAuth) + .onUploadComplete(() => {}), + messageFile: f(['image', 'pdf']) + .middleware(handleAuth) + .onUploadComplete(() => {}), +} satisfies FileRouter; + +export type OurFileRouter = typeof ourFileRouter; diff --git a/app/api/uploadthing/route.ts b/app/api/uploadthing/route.ts new file mode 100644 index 0000000..371e838 --- /dev/null +++ b/app/api/uploadthing/route.ts @@ -0,0 +1,11 @@ +import { createRouteHandler } from 'uploadthing/next'; + +import { ourFileRouter } from './core'; + +// Export routes for Next App Router +export const { GET, POST } = createRouteHandler({ + router: ourFileRouter, + + // Apply an (optional) custom config: + // config: { ... }, +}); diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..916e1fce29ad92350238ac2c15aa0a0bd8de1567 GIT binary patch literal 114781 zcmYIQ2{_c-`=4(6wYA)$gxr*}3|c7L+)^Z4F_yBEtV3Cb3`QxG&?QTR8zzmcF{3dt z%2r7-vdtK@*kZ^wmN7H`^Zic0=Rc3<>E7l&?|ILA-t)en_j8W!T{Jfm`*ZJ~2n0gR z`21N*1Y!&L&n<{8o4_A0i@Mao9~(SRo1I1=Dw9RlTsMOM-|v3j(hPwJKZHO;-$5W& z!Cys>BM>1#_NNO1p`D39NM3(Zdr233u{qe-?A+#25%C>6j})D%<$=H2`NxMyzPehcwO`+oXAe{Ai?mHreB z?sf82JbU2&{`~yy3x}^?EWe&)D!cvS)!k_m{~0~nv+nV~m#_V?VY#Q~1vS-tseZ=) z1Fp_?!;bLh#e81wm>_~$tiv)c3h!Mi3&_e|4@@Y#J2BAM794dltE59x4f8Q1FbtV` zVE(y|j~&-N5YtMa=RLd-%0n~6+;A8C0y2H5myj&8Z{M?9Iw&_^*L%FyKahc)Twd0! z+Wp~&d878qtV2fB--+o_N27A6tti~JGG1NWcfLN6vy?D2$tRl1O63VAmOW*6=pA=L z>1OatmS>F$wNGh9@o&y@#lFvJOv*I@mP*;Q#nc9 z2bKDhZqX5yTt~BA%dHy1o%CP2FNX@6p;@g_BbSD51dibap?SM#(^IsO2WU?L1}BgX z*vrgrnvoW;t57j@_}yCZLo=(hleydSbdXMRJMrR1s#CmUPxk4T%zA21_eyN)y6Nl- zZxI3!;hg-O+B??y;~kn&*~rNIi>2O+4G(^SrZNe1)~To|MJvqGM*=m#Pi`A?bJ2{f zA4;d2|9Y7sIl^$>r>D7^=D-MOPDbhJ3rfOxYhk>UaqHQ6XGtbLzJsQ$FL*P)b9Pl^ zb~5rnRG#tc`a@Y||6gBbOSx6#2NpLHAmD$u$ z*IyB=b)f(obC)hh^Qh)McV$&|+G-2e+MrYsG8xQY>r844Vket~1b(P6_fI#$GaVSo zMU0%sKFsQfyKARrDRFG;fFIVDfmbCl~?ot$x<&RsZ*C(qg6aTZF?*>#=v1zP9IlLT@2`lZ6#+B(Gl z1a;$Dmfp!7B&rHIWJYG2kB(Z#wCn8RY(rS&%<*ab-`#x9?5ygh1a@pp=Z`yY+{?5B zuTKQ$&|H7-7q=ly*D;ju)jE4~faNxmwoDLA^oa*yB**Ac2Q#K8h}e#VWKY`^n#$^n zaj_MuErS-hGW;v9pud|_rQAu|8>xKdio5N=B)7(Mr7+QdMIovG{l2^;{T+3vaLOLt(;+UCwFy>gMPaIh;W4Ta*rDOPG*QN7JxqB&)Jg&()5b+VzUYJCoyBIpp}r z7iSs4mvHCO`#$-X3Mxm|7Y|*jFXc`lCa<99HLpyd@qa2kgp`!sC;Y*_E^IMg&Gud>*dv`j;Nm zx0IauY_;t0@9Ts{mSy+PcWh2bJwOi&j3{G&L36W5=&VC&*fBJf+S)2%PH=u8$0d#T z&#u@uC)1S4&JOiQhi0|IGk!lf6h9ZG2LqAy<|qhd6hYS zAf+uY+Xk}nC@0$Ca04Veub`v3V?w4PP27bLLL_%IO@^K;PjHgbI}m=RS_-8fFNZ(B zrpG6uSLHwi+(h!kVwvelQld}x3#UW87#zv47~3sZ71a3FD=@H(Elv#1wq(arf-^?w zx!Tk~Jl14xJfNV)P%f$dRRlM%W;nyfsQ2tp5?^)IWg&f>)TTJCtIRJG*!bZ!!_jLj zTEJ_G0qmG^$dQ&J$?XNQ{*dYlVolFeT|Jr*X0zm_tT0N&0&u zT~}PbA~%~84Dw$TC}&t~TRy+=X}p~hTpqs^Ka?yhNkDUPW6jvE>XG8c*9CvTdK=bV z|8$VIweb>_ITh_Vh)dveUXRaMddoTMH!P)%XSdnT&OG3pL?}(*j^hIQwuf|T<*OOx zCF-a9EVYE;SH6ci-;7Y=70h`BW(1L^Pcd|$(h5EiaxtZ;iy}~5^6#LTM~J~iWAveG zu3t2p$t`11YT-8*1|4dZSy|5X(@KN5+VXQVecbVf^xP~T!8lrR$>}-!!SaBaCEGly zLneE-VjQO4PkTm27FHg8!dpGFtS!OBsE9FiI0MUds6A%Ij?zveKlH_Yn@d{@xX(YK zU-QGIpwK#4@m9Z#k^WzGnyZZP?TC5-KY$&~`fU6JzPwhLJxaAlVZ`;xx#ewBm; zyl%ZdFceV{5~w$%A=G9k|12oxQmBs-Tb5Gox^O3qXG9@qsLG8CycXKMGS=Tt(KY-p zQgPLEp`%mUGkNEE5_e{{ex+}GK=7b{rS5DlSxZ&qcMhgJ3;3FIn<1V_bTIk|F#HnN`SL|-;e!I6iR&ikgvJ4Gb7ws4mXz6GJSqp&Yry^9KGIg^;1AA@sdB82YsZE#V~u?XR6P{`Rv zOYaCdyO=xl4u;=2uG5Eao~t6tn5GtXv!?3SUUb@9Vbng71c_e0#VmvGL7~#=?2bWyGd1E4+I9*pPI86y=T4F4cdOPkpQI{xmQl!TUnJ)gwnEtf$Q(dye zb2h8<#W(KTDl2c=-g31I1gZdRy>;+q~*& zyNK6aWz5}@+uWdFEEvr7;?8Y#mCIqN?hg+sW*z&+eb<9t&D-a2y?Zet4#J}dL|?L| zvt|w|K=;&PbA`vy`J9OF{|z@D}JG`;)b1+G^yTP@H# zSvsAsAM-MJz(svAQ3_Tx%#iZSDUnNJ0B5Df6--~FRO5_L8752lXsSi?Sm>#DYR1|R z6*o66<-PcZ>x{GV?l2?CjoiT3ghh5atV*jqIAkQ&(iKu417S-9BCfhhvM6XQu}m8v zy=n($jwL4B(p#i+rn$A;gv1?~TZiBLpMtS6@kPGH2%WD~AE$S)7htDl{n^M#4h5@- z`kBI()^Z+A62@c0Ub1&ruzv-Hdx>J|pK}4{QijVTt=U&b-dG@yjA=N3Z%9$_PCKD< znLXc$I*Z@I3omAEBnB@f&V*`p6ukczoD=}rp^r*+MxtWP2NDjAWNTSs)IN}S zkqez8o~k<66^yU=vXkm%Blm}YpEF)Z+o7jY`ySMfNw&Ojyz-!nJqqP86_9Dje&SF& z)K03C2YI^rd7hq0|1EiAx_sD~eU6`=D}tU z??@%qb|Tr`%Dc!Y`5j45FT8awjmMiimLl^jLtz4yx2@usPp4Mk1Rq&nHe2=|7rkVW7X)~;@!UDt6&e6eFyBi>Ri&ow>4$oPRR&2>{i&a4w!9LYfnFoyN z`V%^o>YA3Nz9OTsrqvX&A4p@}V&O*8l$ifIzcS2rY1sAL$YQ{#uy?wwY& z^0v<(`ber%Uu_*15nnb*3I5F76?tftI=(_AD>j1~(z>Z&dJFC-Jn;G!X}`%{2@`y{ z68kl}wrGUG*AB-o5WM2ntEMQF_Hk5Dx5wr?arwPxX*oW)9~ITo5{5ZcVXQS=(Rvje zZYN8k{$NBM$>6l0IZnnWsRRLMBxb94@%fbMaEmRk{$346vL-Dt$=Q(|au15)nCszl z9c}Hat##N$+HxYpF!aj1>x=(Rco`*USH7{0p3(jP^$9IUb43C%_=gvkILm$PwS&g= zTRi{4}M{4!x?z+@cuo(2SxO}lnYc_CDvUExyTS-x70%; zIgb1jr(nFQ&%R&3txhBS3KH`d53OXP2pu?1H9Xh_CGDWi$~0p4P{^v3>LLQ&Nv7P> zb}q6J!vj+%|9TZ2U?kYV-Ch15@LY`r+JpjrRuf7RumWF@ybrW>+9qN!by zyA_RYUWLaY;vV&<@hP0vQO5KWA05REPIl*H;6(jM^lfmQL(--{^zkuM9czY4l>s}s zCp$GnzF%vVfQCksytG3Hy(X3&R&k?ry+di70S6}PoO7r>-Z-V&FpJ&gu$||=&`PPk zF*QGcdCF<+liUCebm^9MdNn_eX2j&T1+j3x0*SM>+W+h~PU%^5|HFrCxWFH`u2fri zK{1U|a;%;|kBf&!Axiefn2_btT6jDwjD^LJ-moCe>)Qn|SMZjhJ^v-w12|oeX3R8A z;?y26w{!Nv+0Sl!%l8QfCHAlJ!n-$g4)prl7qipaY8Hp8o}2?i3k6_r`i<8)egkHXGJ17dHlaYqgQ67R78jn$eaav1pCvm9Zdg zPI)J!YOFp{3iE1;;F{PVaWHW;HI{@wTg(ay#LOhr|HZD}q6S=Zk+Do6kXT!8>E*_e+nn>E`=UY@rkEM`N&h;d`ef`GLbD_XDX`!Cc$knsO z>{Ap@_V-2O&VMUWHBtWlK1Py|t-}}9K{Jriq9fi$fZF>p2d;>fx7z(=nyZE*2)ht~ zVn_P)wKx--yuVC8^wGI#!m4-0z7;yi_oK@f%sT>0p_dbl=C-zQ)B@SpY*(G#I~~B- z_T$u;u2xW8;*sVgF42{3q35gX=$Q;xT$f$a4~piA{wT0t@~x^nkiU8rcX`RIuLaVj!f$MLIz(m zk*g{~VySSBB%=cCnd(|&mxLvo+dlQ7#5Lzh)&;83@ec&vIVD!_z6D?>z5bQXKU3~e z$ZpQ-KL2}BZg#bkOcA)6^%)^)5!y8bPHPeU(??SGxJ5zoIa~X}bTBH)2e-gSGXR~5An5?Rd$IL!>54FRF4`lR+wX#1E$$4!g-sOdkwhmv?epRsl>-)6|YK%JC3HA=4 zU%1QYV7kLIEw7tuQ@QBTQ~)6B4xdHVbCdUFuri);=Yb%M+C9GNPZ@YQf1>vMcP=j}H*fOCX(C5a-(ZuhxDTK*ys~s zQZBjFcnN<&iM@_OcD_L3nJuV_w19C}4A}1pktfJ%iM+uCVC24Zc55b_l%h|#Uc~PE zx9!I#A~xd(!|AU`x0Q6_?syPW)#*&xI<;`D5_=8JwSQ7~(lJ0-c5HHt(sC3FZ_q$v zbb29oJR4UY|8koOaGk*mR%b>*O(7%qUmrn(h-?+_`cBvt75Ny(aYrqwkkEGoZo2~` zE)X*mUq&8R22sJ*+k@o}rmnldE$U0M#AvCXgHwX?TNho&3=_NqlC4SPB)=!s=`p@S zc~4;?P8%u=gjpP`O(m$Oi;rpf@KcfUYzl??^ zavUo&CPnT~!`=)QEH5t{|0!_{2T#Rq|MiWq(+VT;iNw3IusY7%?F-gl7#VIGU4FJa zo;TeJNQQu?WOuW_;VJYh0kVv`xN2WWH|#VC!pJ3KuRKXpFizq4nsmnH1NI2{u7Gb? zMZE#XwSzBc48DSP*cQoN3c}1JmD$z*{obtv3 zW-|XktYiS{6!~B{cTSaG9ysg_ZRB>vKc-Ack$ToHkmX%2`%ONmc3uzcT&jI zXAEQ_d&qF8GF}Oiuf?|N^NQ+mM+0=!()gpEYUXiLWO&?enkdt$JJcDag7C?{Xo4-ZEJDxv5ZLv@tl zBpcbXO8|sZ%G`Adk_mLaN}B&kJ3qy&SCD=e6f>efnDrmo{s&!8qnDFD?%OFVk`Bpp zPE!li3GfPDx;F2mXYk_JLvT1P z%LZZ;pE2}~lRF;p57vJ8Lnz&+5N@QzCW78d8aUQ07+UqdId~_M6pIQO7s@c$Y=K2> zHw{v5P5(50pfka8A~u=9NLuoOq!HrAfcn`P=xuR

1TpdITqKJPY~%MFrrcEGGuD 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.=Mf|-JLlesWssUW|0h8tr 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 literal 0 HcmV?d00001 diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..cb8a82e --- /dev/null +++ b/app/globals.css @@ -0,0 +1,82 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +:root { + height: 100%; +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + + --radius: 0.5rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 24 5.7% 82.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..25c1632 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,42 @@ +import type { Metadata } from 'next'; +import { Open_Sans } from 'next/font/google'; +import { ClerkProvider } from '@clerk/nextjs'; +import './globals.css'; +import { ThemeProvider } from '@/components/provider/theme-provider'; +import { cn } from '@/lib/utils'; +import { ModalProvider } from '@/components/provider/modal-provider'; +import { SocketProvider } from '@/components/provider/socket-provider'; +import { QueryProvider } from '@/components/provider/query-provider'; + +const font = Open_Sans({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'Discord - Group Chat That’s All Fun & Games', + description: 'Generated by create next app', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + + {children} + + + + + + ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..a25c94c --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/Action-tooltip.tsx b/components/Action-tooltip.tsx new file mode 100644 index 0000000..0b48800 --- /dev/null +++ b/components/Action-tooltip.tsx @@ -0,0 +1,32 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import React from 'react'; + +export default function ActionTooltip({ + label, + children, + side, + align, +}: { + label: string; + children: React.ReactNode; + side?: 'top' | 'right' | 'bottom' | 'left'; + align?: 'start' | 'center' | 'end'; +}) { + return ( + + + {children} + +

+ {label.toLowerCase()} +

+ + + + ); +} diff --git a/components/avatar.tsx b/components/avatar.tsx new file mode 100644 index 0000000..195d9f1 --- /dev/null +++ b/components/avatar.tsx @@ -0,0 +1,15 @@ +import { Avatar, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; + +interface UserAvatarProps { + src?: string; + className?: string; +} + +export const UserAvatar = ({ src, className }: UserAvatarProps) => { + return ( + + + + ); +}; diff --git a/components/chat/chat-header.tsx b/components/chat/chat-header.tsx new file mode 100644 index 0000000..ccb78e9 --- /dev/null +++ b/components/chat/chat-header.tsx @@ -0,0 +1,35 @@ +import { Hash } from 'lucide-react'; +import { UserAvatar } from '../avatar'; +import MobileToggle from '../mobile-toggle'; +import { SocketIndicator } from '../socket-indicator'; + +interface ChatHeaderProps { + name: string; + serverId: string; + type: string; + imageUrl?: string; +} + +export default function ChatHeader({ + name, + serverId, + type, + imageUrl, +}: ChatHeaderProps) { + return ( +
+ + {type === 'channel' && ( + + )} + {type === 'conversation' && ( + + )} +

{name}

+
+ {/* {type === 'conversation' && } */} + +
+
+ ); +} diff --git a/components/chat/chat-input.tsx b/components/chat/chat-input.tsx new file mode 100644 index 0000000..d79961f --- /dev/null +++ b/components/chat/chat-input.tsx @@ -0,0 +1,97 @@ +'use client'; + +import * as z from 'zod'; +import axios from 'axios'; +import qs from 'query-string'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Plus } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +import { Form, FormControl, FormField, FormItem } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useModal } from '@/hooks/use-modal-store'; +import { EmojiPicker } from '../emojiPicker'; +// import { EmojiPicker } from '@/components/emoji-picker'; + +interface ChatInputProps { + apiUrl: string; + query: Record; + name: string; + type: 'conversation' | 'channel'; +} + +const formSchema = z.object({ + content: z.string().min(1), +}); + +export const ChatInput = ({ apiUrl, query, name, type }: ChatInputProps) => { + const { onOpen } = useModal(); + const router = useRouter(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + content: '', + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmit = async (values: z.infer) => { + try { + const url = qs.stringifyUrl({ + url: apiUrl, + query, + }); + + await axios.post(url, values); + + form.reset(); + router.refresh(); + } catch (error) { + console.log(error); + } + }; + + return ( +
+ + ( + + +
+ + +
+ + field.onChange(`${field.value} ${emoji}`) + } + /> +
+
+
+
+ )} + /> + + + ); +}; diff --git a/components/chat/chat-item.tsx b/components/chat/chat-item.tsx new file mode 100644 index 0000000..d623579 --- /dev/null +++ b/components/chat/chat-item.tsx @@ -0,0 +1,259 @@ +'use client'; + +import * as z from 'zod'; +import axios from 'axios'; +import qs from 'query-string'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Member, MemberRole, Profile } from '@prisma/client'; +import { Edit, FileIcon, ShieldAlert, ShieldCheck, Trash } from 'lucide-react'; +import Image from 'next/image'; +import { useEffect, useState } from 'react'; +import { useRouter, useParams } from 'next/navigation'; + +import { cn } from '@/lib/utils'; +import { Form, FormControl, FormField, FormItem } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { useModal } from '@/hooks/use-modal-store'; +import { UserAvatar } from '../avatar'; +import ActionTooltip from '../Action-tooltip'; + +interface ChatItemProps { + id: string; + content: string; + member: Member & { + profile: Profile; + }; + timestamp: string; + fileUrl: string | null; + deleted: boolean; + currentMember: Member; + isUpdated: boolean; + socketUrl: string; + socketQuery: Record; +} + +const roleIconMap = { + GUEST: null, + MODERATOR: , + ADMIN: , +}; + +const formSchema = z.object({ + content: z.string().min(1), +}); + +export const ChatItem = ({ + id, + content, + member, + timestamp, + fileUrl, + deleted, + currentMember, + isUpdated, + socketUrl, + socketQuery, +}: ChatItemProps) => { + const [isEditing, setIsEditing] = useState(false); + const { onOpen } = useModal(); + const params = useParams(); + const router = useRouter(); + + const onMemberClick = () => { + if (member.id === currentMember.id) { + return; + } + + router.push(`/servers/${params?.serverId}/conversations/${member.id}`); + }; + + useEffect(() => { + const handleKeyDown = (event: any) => { + if (event.key === 'Escape' || event.keyCode === 27) { + setIsEditing(false); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => window.removeEventListener('keyDown', handleKeyDown); + }, []); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + content: content, + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmit = async (values: z.infer) => { + try { + const url = qs.stringifyUrl({ + url: `${socketUrl}/${id}`, + query: socketQuery, + }); + + await axios.patch(url, values); + + form.reset(); + setIsEditing(false); + } catch (error) { + console.log(error); + } + }; + + useEffect(() => { + form.reset({ + content: content, + }); + }, [content, form]); + + const fileType = fileUrl?.split('.').pop(); + + const isAdmin = currentMember.role === MemberRole.ADMIN; + const isModerator = currentMember.role === MemberRole.MODERATOR; + const isOwner = currentMember.id === member.id; + const canDeleteMessage = !deleted && (isAdmin || isModerator || isOwner); + const canEditMessage = !deleted && isOwner && !fileUrl; + const isPDF = fileType === 'pdf' && fileUrl; + const isImage = !isPDF && fileUrl; + + return ( +
+
+
+ +
+
+
+
+

+ {member.profile.name} +

+ + {roleIconMap[member.role]} + +
+ + {timestamp} + +
+ {isImage && ( +
+ {content} + + )} + {isPDF && ( + + )} + {!fileUrl && !isEditing && ( +

+ {content} + {isUpdated && !deleted && ( + + (edited) + + )} +

+ )} + {!fileUrl && isEditing && ( +
+ + ( + + +
+ +
+
+
+ )} + /> + + + + Press escape to cancel, enter to save + + + )} +
+
+ {canDeleteMessage && ( +
+ {canEditMessage && ( + + setIsEditing(true)} + className="cursor-pointer ml-auto w-4 h-4 text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition" + /> + + )} + + + onOpen('deleteMessage', { + apiUrl: `${socketUrl}/${id}`, + query: socketQuery, + }) + } + className="cursor-pointer ml-auto w-4 h-4 text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition" + /> + +
+ )} +
+ ); +}; diff --git a/components/chat/chat-messages.tsx b/components/chat/chat-messages.tsx new file mode 100644 index 0000000..ae8f7c4 --- /dev/null +++ b/components/chat/chat-messages.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { Fragment, useRef, ElementRef } from 'react'; +import { format } from 'date-fns'; +import { Member, Message, Profile } from '@prisma/client'; +import { Loader2, ServerCrash } from 'lucide-react'; + +import { ChatWelcome } from './chat-welcome'; +import { useChatQuery } from '@/hooks/use-chat-query'; +import { useChatSocket } from '@/hooks/use-chat-socket'; +import { useChatScroll } from '@/hooks/use-chat-scroll'; +import { ChatItem } from './chat-item'; + +const DATE_FORMAT = 'd MMM yyyy, HH:mm'; + +type MessageWithMemberWithProfile = Message & { + member: Member & { + profile: Profile; + }; +}; + +interface ChatMessagesProps { + name: string; + member: Member; + chatId: string; + apiUrl: string; + socketUrl: string; + socketQuery: Record; + paramKey: 'channelId' | 'conversationId'; + paramValue: string; + type: 'channel' | 'conversation'; +} + +export const ChatMessages = ({ + name, + member, + chatId, + apiUrl, + socketUrl, + socketQuery, + paramKey, + paramValue, + type, +}: ChatMessagesProps) => { + const queryKey = `chat:${chatId}`; + const addKey = `chat:${chatId}:messages`; + const updateKey = `chat:${chatId}:messages:update`; + + const chatRef = useRef>(null); + const bottomRef = useRef>(null); + + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = + useChatQuery({ + queryKey, + apiUrl, + paramKey, + paramValue, + }); + useChatSocket({ queryKey, addKey, updateKey }); + useChatScroll({ + chatRef, + bottomRef, + loadMore: fetchNextPage, + shouldLoadMore: !isFetchingNextPage && !!hasNextPage, + count: data?.pages?.[0]?.items?.length ?? 0, + }); + + if (status === 'pending') { + return ( +
+ +

+ Loading messages... +

+
+ ); + } + + if (status === 'error') { + return ( +
+ +

+ Something went wrong! +

+
+ ); + } + + return ( +
+ {!hasNextPage &&
} + {!hasNextPage && } + {hasNextPage && ( +
+ {isFetchingNextPage ? ( + + ) : ( + + )} +
+ )} +
+ {data?.pages?.map((group, i) => ( + + {group.items.map((message: MessageWithMemberWithProfile) => ( + + ))} + + ))} +
+
+
+ ); +}; diff --git a/components/chat/chat-welcome.tsx b/components/chat/chat-welcome.tsx new file mode 100644 index 0000000..4a18f1e --- /dev/null +++ b/components/chat/chat-welcome.tsx @@ -0,0 +1,27 @@ +import { Hash } from 'lucide-react'; + +interface ChatWelcomeProps { + name: string; + type: 'channel' | 'conversation'; +} + +export const ChatWelcome = ({ name, type }: ChatWelcomeProps) => { + return ( +
+ {type === 'channel' && ( +
+ +
+ )} +

+ {type === 'channel' ? 'Welcome to #' : ''} + {name} +

+

+ {type === 'channel' + ? `This is the start of the #${name} channel.` + : `This is the start of your conversation with ${name}`} +

+
+ ); +}; diff --git a/components/emojiPicker.tsx b/components/emojiPicker.tsx new file mode 100644 index 0000000..291e66b --- /dev/null +++ b/components/emojiPicker.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { Smile } from 'lucide-react'; +import Picker from '@emoji-mart/react'; +import data from '@emoji-mart/data'; +import { useTheme } from 'next-themes'; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; + +interface EmojiPickerProps { + onChange: (value: string) => void; +} + +export const EmojiPicker = ({ onChange }: EmojiPickerProps) => { + const { resolvedTheme } = useTheme(); + + return ( + + + + + + onChange(emoji.native)} + /> + + + ); +}; diff --git a/components/fileUpload.tsx b/components/fileUpload.tsx new file mode 100644 index 0000000..88282ee --- /dev/null +++ b/components/fileUpload.tsx @@ -0,0 +1,72 @@ +import { UploadButton } from '@/lib/uploadThing'; +import Image from 'next/image'; +import '@uploadthing/react/styles.css'; +import { Button } from './ui/button'; +import { FileIcon, X } from 'lucide-react'; + +export default function FileUpload({ + endpoint, + onChange, + value, +}: { + endpoint: 'serverImage' | 'messageFile'; + value: string; + onChange: (url: string) => void; +}) { + const filetype = value?.split('.').pop(); + if (value && filetype != 'pdf') { + return ( +
+ upload + +
+ ); + } + if (value && filetype === 'pdf') { + return ( +
+ + + {value} + + {' '} +
+ ); + } + return ( + <> + { + onChange(res[0].url); + }} + onUploadError={(error: Error) => { + console.log(error); + }} + /> + + ); +} diff --git a/components/media-room.tsx b/components/media-room.tsx new file mode 100644 index 0000000..8ad2e49 --- /dev/null +++ b/components/media-room.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { LiveKitRoom, VideoConference } from '@livekit/components-react'; +import '@livekit/components-styles'; +import { useUser } from '@clerk/nextjs'; +import { Loader2 } from 'lucide-react'; + +interface MediaRoomProps { + chatId: string; + video: boolean; + audio: boolean; +} + +export const MediaRoom = ({ chatId, video, audio }: MediaRoomProps) => { + const { user } = useUser(); + const [token, setToken] = useState(''); + + useEffect(() => { + if (!user?.firstName || !user?.lastName) return; + + const name = `${user.firstName} ${user.lastName}`; + + (async () => { + try { + const resp = await fetch( + `/api/livekit?room=${chatId}&username=${name}` + ); + const data = await resp.json(); + setToken(data.token); + } catch (e) { + console.log(e); + } + })(); + }, [user?.firstName, user?.lastName, chatId]); + + if (token === '') { + return ( +
+ +

Loading...

+
+ ); + } + + return ( + + + + ); +}; diff --git a/components/mobile-toggle.tsx b/components/mobile-toggle.tsx new file mode 100644 index 0000000..c43da63 --- /dev/null +++ b/components/mobile-toggle.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; +import { Button } from './ui/button'; +import { Menu } from 'lucide-react'; +import ServerSidebar from './server/serverSidebar'; +import NavigationSidebar from './navigations/NavigationSidebar'; + +interface MobileToggleProps { + serverId: string; +} +export default function MobileToggle({ serverId }: MobileToggleProps) { + return ( + + + + + +
+ +
+ +
+
+ ); +} diff --git a/components/modals/category-modal.tsx b/components/modals/category-modal.tsx new file mode 100644 index 0000000..f9cb49b --- /dev/null +++ b/components/modals/category-modal.tsx @@ -0,0 +1,121 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import axios from 'axios'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; + +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; +import { ChannelType } from '@prisma/client'; + +const formSchema = z.object({ + name: z + .string() + .min(1, { + message: 'category name is required', + }) + .refine((name) => name.toLocaleLowerCase() !== 'text', { + message: 'Channel name cannot be "text"', + }) + .refine((name) => name.toLocaleLowerCase() !== 'audio', { + message: 'Channel name cannot be "audio"', + }) + .refine((name) => name.toLocaleLowerCase() !== 'video', { + message: 'Channel name cannot be "video"', + }), +}); + +export default function CategoryModal() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'category'; + const handleClose = () => { + form.reset(); + onClose(); + }; + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + }, + }); + + const isloading = form.formState.isSubmitting; + async function onSubmit(values: z.infer) { + try { + // const res = await axios.post( + // `/api/category?serverId=${data.server?.id}`, + // values + // ); + form.reset(); + router.refresh(); + onClose(); + } catch (error) { + console.log(error); + } + } + return ( + + + + + Create a Category + + +
+ + ( + + CATEGORY NAME + + + + + + )} + /> + + + + + + +
+
+ ); +} diff --git a/components/modals/channel-modal.tsx b/components/modals/channel-modal.tsx new file mode 100644 index 0000000..1cd7f60 --- /dev/null +++ b/components/modals/channel-modal.tsx @@ -0,0 +1,157 @@ +'use client'; +import React, { use, useEffect, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import axios from 'axios'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { UploadButton } from '@/lib/uploadThing'; +import FileUpload from '../fileUpload'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; +import { ChannelType } from '@prisma/client'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +const formSchema = z.object({ + name: z + .string() + .min(1, { + message: 'Channel name is required', + }) + .refine((name) => name !== 'general', { + message: 'Channel name cannot be "general"', + }), + type: z.nativeEnum(ChannelType), +}); + +export default function ChannelModal() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'channel'; + const handleClose = () => { + form.reset(); + onClose(); + }; + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + type: ChannelType.TEXT, + }, + }); + + useEffect(() => { + if (data.channelType) { + form.setValue('type', data.channelType); + } else { + form.setValue('type', ChannelType.TEXT); + } + }, [data.channelType, form]); + + const isloading = form.formState.isSubmitting; + async function onSubmit(values: z.infer) { + try { + const res = await axios.post( + `/api/channel?serverId=${data.server?.id}`, + values + ); + form.reset(); + router.refresh(); + onClose(); + } catch (error) { + console.log(error); + } + } + return ( + + + + + Create Channel + + +
+ + ( + + CHANNEL NAME + + + + + + )} + /> + ( + + Channel Type + + + )} + /> + + + + + +
+
+ ); +} diff --git a/components/modals/create-server-modal.tsx b/components/modals/create-server-modal.tsx new file mode 100644 index 0000000..5743b75 --- /dev/null +++ b/components/modals/create-server-modal.tsx @@ -0,0 +1,141 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import axios from 'axios'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { UploadButton } from '@/lib/uploadThing'; +import FileUpload from '../fileUpload'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; + +const formSchema = z.object({ + name: z.string().min(1, { + message: 'Server name is Required', + }), + imageUrl: z.string().min(1, { + message: 'Server image is Required', + }), +}); + +export default function CreateServerModal() { + const { isOpen, onClose, type } = useModal(); + const isModalOpen = isOpen && type === 'createServer'; + const handleClose = () => { + form.reset(); + onClose(); + }; + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + imageUrl: '', + }, + }); + + const isloading = form.formState.isSubmitting; + async function onSubmit(values: z.infer) { + try { + const res = await axios.post('/api/servers', values); + form.reset(); + router.refresh(); + onClose(); + } catch (error) { + console.log(error); + } + } + return ( + + + + + Customize Your Server + + + Give your new server a personality with a name and an icon. You can + always change it later + + +
+ +
+ ( + + + + + + )} + /> +
+ ( + + SERVER NAME + + + + + + By Creating a Server, you agree to Discord's{' '} + + Community Guidlines + + + + )} + /> + + + + + +
+
+ ); +} diff --git a/components/modals/delete-channel.tsx b/components/modals/delete-channel.tsx new file mode 100644 index 0000000..5207414 --- /dev/null +++ b/components/modals/delete-channel.tsx @@ -0,0 +1,67 @@ +'use client'; +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import axios from 'axios'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; + +import { Loader2Icon } from 'lucide-react'; + +export default function DeleteChannel() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'deleteChannel'; + const handleClose = () => { + onClose(); + }; + const router = useRouter(); + + const [isloading, setIsLoading] = useState(false); + + async function onLeave() { + try { + setIsLoading(true); + const res = await axios.delete( + `/api/channel/${data.channel?.id}?serverId=${data.server?.id}` + ); + onClose(); + router.refresh(); + setIsLoading(false); + } catch (err) { + console.log(err); + } finally { + router.push('/'); + } + } + return ( + + + + + Delete Channel + + + Are you sure you want to delete{' '} + + {data.channel?.name} + + ? + + + + + + + ); +} diff --git a/components/modals/delete-message-modal.tsx b/components/modals/delete-message-modal.tsx new file mode 100644 index 0000000..a2a7de6 --- /dev/null +++ b/components/modals/delete-message-modal.tsx @@ -0,0 +1,78 @@ +'use client'; + +import qs from 'query-string'; +import axios from 'axios'; +import { useState } from 'react'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { useModal } from '@/hooks/use-modal-store'; +import { Button } from '@/components/ui/button'; + +export const DeleteMessage = () => { + const { isOpen, onClose, type, data } = useModal(); + + const isModalOpen = isOpen && type === 'deleteMessage'; + const { apiUrl, query } = data; + + const [isLoading, setIsLoading] = useState(false); + + const onClick = async () => { + try { + setIsLoading(true); + const url = qs.stringifyUrl({ + url: apiUrl || '', + query, + }); + + await axios.delete(url); + + onClose(); + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + Delete Message + + + Are you sure you want to do this?
+ The message will be permanently deleted. +
+
+ +
+ + +
+
+
+
+ ); +}; diff --git a/components/modals/delete-server.tsx b/components/modals/delete-server.tsx new file mode 100644 index 0000000..30da442 --- /dev/null +++ b/components/modals/delete-server.tsx @@ -0,0 +1,69 @@ +'use client'; +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import axios from 'axios'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; + +import { Loader2Icon } from 'lucide-react'; + +export default function DeleteServer() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'deleteServer'; + const handleClose = () => { + onClose(); + }; + const router = useRouter(); + + const [isloading, setIsLoading] = useState(false); + + async function onLeave() { + try { + setIsLoading(true); + const res = await axios.delete(`/api/servers/${data.server?.id}/leave`); + onClose(); + router.refresh(); + setIsLoading(false); + } catch (err) { + console.log(err); + } finally { + router.push('/'); + } + } + return ( + + + + + Tussi ja rhe ho + + + Are you sure you want to delete{' '} + + {data.server?.name} + + ? + + + + + + + ); +} diff --git a/components/modals/download-modal.tsx b/components/modals/download-modal.tsx new file mode 100644 index 0000000..51c3e28 --- /dev/null +++ b/components/modals/download-modal.tsx @@ -0,0 +1,79 @@ +'use client'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { useModal } from '@/hooks/use-modal-store'; +import DownloadItem from './downloadModalItem'; + +export default function DownloadModal() { + const { isOpen, onClose, type } = useModal(); + const isModalOpen = isOpen && type === 'downloadApps'; + + const handleClose = () => { + onClose(); + }; + + return ( + + +
+ + + +
+

or on the go

+
+ + +
+
+
+ ); +} diff --git a/components/modals/downloadModalItem.tsx b/components/modals/downloadModalItem.tsx new file mode 100644 index 0000000..9c59aca --- /dev/null +++ b/components/modals/downloadModalItem.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; +import React from 'react'; +import { Button } from '../ui/button'; +import { redirect } from 'next/navigation'; + +export default function DownloadItem({ + name, + src, + buttons, +}: { + name: string; + src: string; + buttons: { + name: string; + url: string; + }[]; +}) { + return ( +
+ {name} +

{name}

+
+ {buttons.map((button, id) => { + return ( + + ); + })} +
+
+ ); +} diff --git a/components/modals/edit-channel.tsx b/components/modals/edit-channel.tsx new file mode 100644 index 0000000..49f62aa --- /dev/null +++ b/components/modals/edit-channel.tsx @@ -0,0 +1,156 @@ +'use client'; +import React, { use, useEffect, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import axios from 'axios'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { UploadButton } from '@/lib/uploadThing'; +import FileUpload from '../fileUpload'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; +import { ChannelType } from '@prisma/client'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +const formSchema = z.object({ + name: z + .string() + .min(1, { + message: 'Channel name is required', + }) + .refine((name) => name !== 'general', { + message: 'Channel name cannot be "general"', + }), + type: z.nativeEnum(ChannelType), +}); + +export default function ChannelEdit() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'editChannel'; + const handleClose = () => { + form.reset(); + onClose(); + }; + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: data.channel?.name, + type: data.channel?.type, + }, + }); + + useEffect(() => { + if (data.channel?.type) { + form.setValue('type', data.channel?.type); + } + if (data.channel?.name) { + form.setValue('name', data.channel?.name); + } + }, [form, data.channel]); + + const isloading = form.formState.isSubmitting; + async function onSubmit(values: z.infer) { + try { + const res = await axios.put( + `/api/channel/${data.channel?.id}?serverId=${data.server?.id}`, + values + ); + form.reset(); + router.refresh(); + onClose(); + } catch (error) { + console.log(error); + } + } + return ( + + + + Edit Channel + +
+ + ( + + CHANNEL NAME + + + + + + )} + /> + ( + + Channel Type + + + )} + /> + + + + + +
+
+ ); +} diff --git a/components/modals/edit-server-modal.tsx b/components/modals/edit-server-modal.tsx new file mode 100644 index 0000000..b0e08fb --- /dev/null +++ b/components/modals/edit-server-modal.tsx @@ -0,0 +1,146 @@ +'use client'; +import React, { useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import axios from 'axios'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; + +import FileUpload from '../fileUpload'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; + +const formSchema = z.object({ + name: z.string().min(1, { + message: 'Server name is Required', + }), + imageUrl: z.string().min(1, { + message: 'Server image is Required', + }), +}); + +export default function EditServerModal() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'editServer'; + const handleClose = () => { + form.reset(); + onClose(); + }; + + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + imageUrl: '', + }, + }); + useEffect(() => { + if (data.server) { + form.setValue('name', data.server.name); + form.setValue('imageUrl', data.server.imageUrl); + } + }, [data.server, form]); + + const isloading = form.formState.isSubmitting; + async function onSubmit(values: z.infer) { + try { + const res = await axios.put(`/api/servers/${data.server?.id}`, values); + form.reset(); + router.refresh(); + onClose(); + } catch (error) { + console.log(error); + } + } + return ( + + + + + Edit Your Server + + + Give your new server a personality with a name and an icon. You can + always change it later + + +
+ +
+ ( + + + + + + )} + /> +
+ ( + + SERVER NAME + + + + + + By Creating a Server, you agree to Discord's{' '} + + Community Guidlines + + + + )} + /> + + + + + +
+
+ ); +} diff --git a/components/modals/event-modal.tsx b/components/modals/event-modal.tsx new file mode 100644 index 0000000..98b9dc6 --- /dev/null +++ b/components/modals/event-modal.tsx @@ -0,0 +1,55 @@ +'use client'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +import { useModal } from '@/hooks/use-modal-store'; + +import { CalendarRange } from 'lucide-react'; + +export default function EventModal() { + const { isOpen, onClose, type } = useModal(); + const isModalOpen = isOpen && type === 'events'; + + const handleClose = () => { + onClose(); + }; + + return ( + + + + +
+ + events +
+ +
+
+
+
+ +
+

+ There are no upcoming events. +

+

+ Schedule an event for any planned activity in your server +

+

+ You can give other people permission to create events in +

+ + Server Settings < Roles. + +
+
+
+ ); +} diff --git a/components/modals/initial-modal.tsx b/components/modals/initial-modal.tsx new file mode 100644 index 0000000..77482b6 --- /dev/null +++ b/components/modals/initial-modal.tsx @@ -0,0 +1,138 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import axios from 'axios'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import FileUpload from '../fileUpload'; +import { useRouter } from 'next/navigation'; + +const formSchema = z.object({ + name: z.string().min(1, { + message: 'Server name is Required', + }), + imageUrl: z.string().min(1, { + message: 'Server image is Required', + }), +}); + +export default function InitialModal() { + const [isMounted, setIsMounted] = useState(false); + useEffect(() => { + setIsMounted(true); + }, []); + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + imageUrl: '', + }, + }); + + const isloading = form.formState.isSubmitting; + async function onSubmit(values: z.infer) { + try { + const res = await axios.post('/api/servers', values); + form.reset(); + router.refresh(); + window.location.reload(); + } catch (error) { + console.log(error); + } + } + if (!isMounted) { + return null; + } + return ( + + + + + Customize Your Server + + + Give your new server a personality with a name and an icon. You can + always change it later + + +
+ +
+ ( + + + + + + )} + /> +
+ ( + + SERVER NAME + + + + + + By Creating a Server, you agree to Discord's{' '} + + Community Guidlines + + + + )} + /> + + + + + +
+
+ ); +} diff --git a/components/modals/invite-modal.tsx b/components/modals/invite-modal.tsx new file mode 100644 index 0000000..87d8f15 --- /dev/null +++ b/components/modals/invite-modal.tsx @@ -0,0 +1,115 @@ +'use client'; +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import axios from 'axios'; +import { Button } from '@/components/ui/button'; +import { useModal } from '@/hooks/use-modal-store'; +import { Checkbox } from '@/components/ui/checkbox'; + +import { Input } from '../ui/input'; +import { RefreshCwIcon, Settings } from 'lucide-react'; +import { useOrigin } from '@/hooks/use-origin'; +import { cn } from '@/lib/utils'; + +export default function InviteModal() { + const { onOpen, isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'invite'; + + const handleClose = () => { + onClose(); + }; + + const handleCopy = () => { + navigator.clipboard.writeText(inviteUrl); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 1000); + }; + const origin = useOrigin(); + const [isChecked, setIsChecked] = useState(false); + const [isCopy, setIsCopied] = useState(false); + + const inviteUrl = `${origin}/invite/${data.server?.inviteCode}`; + const [isLoading, setisLoading] = useState(false); + const handleNew = async () => { + setisLoading(true); + try { + const res = await axios.patch(`/api/servers/${data.server?.id}`); + onOpen('invite', { server: res.data }); + } catch (err) { + console.log(err); + } finally { + setisLoading(false); + } + }; + + return ( + + + + + invite friends to {data.server?.name} + + + # General + + +
+ +
+ + +
+

+ Your invite link + {isChecked ? ' will never expire' : ' expires in 7 days'}. +

+
+
+
+ setIsChecked(!isChecked)} + disabled={isLoading} + /> + +
+ +
+
+
+ ); +} diff --git a/components/modals/leave-server.tsx b/components/modals/leave-server.tsx new file mode 100644 index 0000000..c0878ba --- /dev/null +++ b/components/modals/leave-server.tsx @@ -0,0 +1,69 @@ +'use client'; +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import axios from 'axios'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; + +import { Loader2Icon } from 'lucide-react'; + +export default function LeaveServer() { + const { isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'leaveServer'; + const handleClose = () => { + onClose(); + }; + const router = useRouter(); + + const [isloading, setIsLoading] = useState(false); + + async function onLeave() { + try { + setIsLoading(true); + const res = await axios.post(`/api/servers/${data.server?.id}/leave`); + onClose(); + router.refresh(); + setIsLoading(false); + } catch (err) { + console.log(err); + } finally { + router.push('/'); + } + } + return ( + + + + + Tussi ja rhe ho + + + Are you sure you want to leave + + {data.server?.name} + + ? + + + + + + + ); +} diff --git a/components/modals/manage-members-modal.tsx b/components/modals/manage-members-modal.tsx new file mode 100644 index 0000000..9c00ab6 --- /dev/null +++ b/components/modals/manage-members-modal.tsx @@ -0,0 +1,183 @@ +'use client'; +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import axios from 'axios'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +import { Button } from '@/components/ui/button'; + +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +import { ScrollArea } from '@radix-ui/react-scroll-area'; +import { + Check, + GavelIcon, + Loader2, + MoreVertical, + Shield, + ShieldAlert, + ShieldCheck, + ShieldCheckIcon, + ShieldQuestionIcon, +} from 'lucide-react'; + +export default function ManageMemberModal() { + const { onOpen, isOpen, onClose, type, data } = useModal(); + const isModalOpen = isOpen && type === 'members'; + const handleClose = () => { + onClose(); + }; + const router = useRouter(); + + const roleIconMap = { + GUEST: null, + MODERATOR: , + ADMIN: , + }; + const [loadingId, setloadingId] = useState(''); + const onRoleChange = async (role: string, memberId: string) => { + try { + setloadingId(memberId); + const response = await axios.patch(`/api/member/${memberId}`, { + role: role, + serverId: data.server?.id, + }); + onOpen('members', { server: response.data }); + } catch (err) { + console.log(err); + } + setloadingId(''); + router.refresh(); + }; + const onKick = async (memberId: string) => { + try { + setloadingId(memberId); + const response = await axios.delete( + `/api/member/${memberId}?serverId=${data.server?.id}` + ); + onOpen('members', { server: response.data }); + } catch (err) { + console.log(err); + } + setloadingId(''); + router.refresh(); + }; + if (data.server?.members == undefined) return null; + return ( + + + + + Manage Members + + + {data.server?.members?.length <= 1 + ? `${data.server?.members?.length} Member` + : `${data.server?.members?.length} Members`} + + + + {data.server?.members?.map((member, id) => { + return ( +
+
+ + + CN + +
+
+

{member.profile.name}

+

{roleIconMap[member.role]}

+
+

+ {member.profile.email} +

+
+
+ {data.server?.profileId != member.profileId && + loadingId != member.id && ( + + + + + + + Role + + + { + onRoleChange(val, member.id); + }} + > + +
+ + Guest +
+ {member.role === 'GUEST' && ( + + )} +
+ +
+ + Moderator +
+ {member.role === 'MODERATOR' && ( + + )} +
+
+ + + onKick(member.id)} + > +
+ + Kick +
+
+
+
+
+ )} + {loadingId == member.id && ( + + )} +
+ ); + })} +
+
+
+ ); +} diff --git a/components/modals/message-file-modal.tsx b/components/modals/message-file-modal.tsx new file mode 100644 index 0000000..18196c4 --- /dev/null +++ b/components/modals/message-file-modal.tsx @@ -0,0 +1,115 @@ +'use client'; + +import axios from 'axios'; +import qs from 'query-string'; +import * as z from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Form, FormControl, FormField, FormItem } from '@/components/ui/form'; +import { Button } from '@/components/ui/button'; +import { useRouter } from 'next/navigation'; +import { useModal } from '@/hooks/use-modal-store'; +import FileUpload from '../fileUpload'; + +const formSchema = z.object({ + fileUrl: z.string().min(1, { + message: 'Attachment is required.', + }), +}); + +export const MessageFileModal = () => { + const { isOpen, onClose, type, data } = useModal(); + const router = useRouter(); + + const isModalOpen = isOpen && type === 'messageFile'; + const { apiUrl, query } = data; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + fileUrl: '', + }, + }); + + const handleClose = () => { + form.reset(); + onClose(); + }; + + const isLoading = form.formState.isSubmitting; + + const onSubmit = async (values: z.infer) => { + try { + const url = qs.stringifyUrl({ + url: apiUrl || '', + query, + }); + + await axios.post(url, { + ...values, + content: values.fileUrl, + }); + + form.reset(); + router.refresh(); + handleClose(); + } catch (error) { + console.log(error); + } + }; + + return ( + + + + + Add an attachment + + + Send a file as a message + + +
+ +
+
+ ( + + + + + + )} + /> +
+
+ + + +
+ +
+
+ ); +}; diff --git a/components/mode-toggle.tsx b/components/mode-toggle.tsx new file mode 100644 index 0000000..5b08611 --- /dev/null +++ b/components/mode-toggle.tsx @@ -0,0 +1,44 @@ +'use client'; + +import * as React from 'react'; +import { MoonIcon, SunIcon } from 'lucide-react'; +import { useTheme } from 'next-themes'; + +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme('light')}> + Light + + setTheme('dark')}> + Dark + + setTheme('system')}> + System + + + + ); +} diff --git a/components/navigations/Navigation-action.tsx b/components/navigations/Navigation-action.tsx new file mode 100644 index 0000000..2470b9f --- /dev/null +++ b/components/navigations/Navigation-action.tsx @@ -0,0 +1,25 @@ +'use client'; +import { Plus } from 'lucide-react'; +import ActionTooltip from '../Action-tooltip'; +import { useModal } from '@/hooks/use-modal-store'; + +export default function NavigationAction() { + const { onOpen } = useModal(); + return ( +
+ + + +
+ ); +} diff --git a/components/navigations/Navigation-item.tsx b/components/navigations/Navigation-item.tsx new file mode 100644 index 0000000..931f403 --- /dev/null +++ b/components/navigations/Navigation-item.tsx @@ -0,0 +1,45 @@ +'use client'; +import ActionTooltip from '../Action-tooltip'; +import Image from 'next/image'; +import { cn } from '@/lib/utils'; +import { useParams, useRouter } from 'next/navigation'; + +function NavigationItem({ + id, + imageUrl, + name, +}: { + id: string; + imageUrl: string; + name: string; +}) { + const router = useRouter(); + const onclick = () => { + router.push(`/servers/${id}`); + }; + const params = useParams(); + return ( + + + + ); +} + +export default NavigationItem; diff --git a/components/navigations/NavigationDownload.tsx b/components/navigations/NavigationDownload.tsx new file mode 100644 index 0000000..92e7247 --- /dev/null +++ b/components/navigations/NavigationDownload.tsx @@ -0,0 +1,25 @@ +'use client'; +import { Download } from 'lucide-react'; +import ActionTooltip from '../Action-tooltip'; +import { useModal } from '@/hooks/use-modal-store'; + +export default function NavigationDownload() { + const { onOpen } = useModal(); + return ( +
+ + + +
+ ); +} diff --git a/components/navigations/NavigationSidebar.tsx b/components/navigations/NavigationSidebar.tsx new file mode 100644 index 0000000..f13b877 --- /dev/null +++ b/components/navigations/NavigationSidebar.tsx @@ -0,0 +1,59 @@ +import { currentProfile } from '@/lib/currentProfile'; +import { db } from '@/lib/db'; +import { redirect } from 'next/navigation'; +import React from 'react'; +import NavigationAction from './Navigation-action'; +import { Separator } from '@/components/ui/separator'; +import { ScrollArea } from '../ui/scroll-area'; +import NavigationItem from './Navigation-item'; +import { ModeToggle } from '../mode-toggle'; +import { UserButton } from '@clerk/nextjs'; +import NavigationDownload from './NavigationDownload'; + +export default async function NavigationSidebar() { + const profile = await currentProfile(); + if (!profile) { + return redirect('/'); + } + const servers = await db.server.findMany({ + where: { + members: { + some: { + profileId: profile.id, + }, + }, + }, + }); + return ( +
+ + + + + {servers.map((server) => { + return ( +
+ +
+ ); + })} +
+ +
+ + +
+
+ ); +} diff --git a/components/provider/modal-provider.tsx b/components/provider/modal-provider.tsx new file mode 100644 index 0000000..6c902c8 --- /dev/null +++ b/components/provider/modal-provider.tsx @@ -0,0 +1,44 @@ +'use client'; +import { useEffect, useState } from 'react'; +import CreateServerModal from '../modals/create-server-modal'; +import InviteModal from '../modals/invite-modal'; +import EventModal from '../modals/event-modal'; +import EditServerModal from '../modals/edit-server-modal'; +import DownloadModal from '../modals/download-modal'; +import ManageMemberModal from '../modals/manage-members-modal'; +import ChannelModal from '../modals/channel-modal'; +import LeaveServer from '../modals/leave-server'; +import DeleteServer from '../modals/delete-server'; +import CategoryModal from '../modals/category-modal'; +import DeleteChannel from '../modals/delete-channel'; +import ChannelEdit from '../modals/edit-channel'; +import { MessageFileModal } from '../modals/message-file-modal'; +import { DeleteMessage } from '../modals/delete-message-modal'; + +export const ModalProvider = () => { + const [isMounted, setIsMounted] = useState(false); + useEffect(() => { + setIsMounted(true); + }, []); + if (!isMounted) { + return null; + } + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/components/provider/query-provider.tsx b/components/provider/query-provider.tsx new file mode 100644 index 0000000..26af9d1 --- /dev/null +++ b/components/provider/query-provider.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useState } from 'react'; + +export const QueryProvider = ({ children }: { children: React.ReactNode }) => { + const [queryClient] = useState(() => new QueryClient()); + + return ( + {children} + ); +}; diff --git a/components/provider/socket-provider.tsx b/components/provider/socket-provider.tsx new file mode 100644 index 0000000..c9656c8 --- /dev/null +++ b/components/provider/socket-provider.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { createContext, useContext, useEffect, useState } from 'react'; +import { io as ClientIO } from 'socket.io-client'; + +type SocketContextType = { + socket: any | null; + isConnected: boolean; +}; + +const SocketContext = createContext({ + socket: null, + isConnected: false, +}); + +export const useSocket = () => { + return useContext(SocketContext); +}; + +export const SocketProvider = ({ children }: { children: React.ReactNode }) => { + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + + useEffect(() => { + const socketInstance = new (ClientIO as any)( + process.env.NEXT_PUBLIC_SITE_URL!, + { + path: '/api/socket/io', + addTrailingSlash: false, + } + ); + + socketInstance.on('connect', () => { + setIsConnected(true); + }); + + socketInstance.on('disconnect', () => { + setIsConnected(false); + }); + + setSocket(socketInstance); + + return () => { + socketInstance.disconnect(); + }; + }, []); + + return ( + + {children} + + ); +}; diff --git a/components/provider/theme-provider.tsx b/components/provider/theme-provider.tsx new file mode 100644 index 0000000..32a8453 --- /dev/null +++ b/components/provider/theme-provider.tsx @@ -0,0 +1,9 @@ +'use client'; + +import * as React from 'react'; +import { ThemeProvider as NextThemesProvider } from 'next-themes'; +import { type ThemeProviderProps } from 'next-themes/dist/types'; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/components/server/serverChannel.tsx b/components/server/serverChannel.tsx new file mode 100644 index 0000000..3ee54a5 --- /dev/null +++ b/components/server/serverChannel.tsx @@ -0,0 +1,80 @@ +'use client'; +import { ModalType, useModal } from '@/hooks/use-modal-store'; +import { Channel, ChannelType, MemberRole, Server } from '@prisma/client'; +import { Edit, Hash, Lock, Mic, Trash, Video } from 'lucide-react'; +import { useParams, useRouter } from 'next/navigation'; +import ActionTooltip from '../Action-tooltip'; +import { cn } from '@/lib/utils'; + +interface ServerChannelProps { + channel: Channel; + server: Server; + role?: MemberRole; +} + +const iconMap = { + [ChannelType.TEXT]: Hash, + [ChannelType.AUDIO]: Mic, + [ChannelType.VIDEO]: Video, +}; + +export const ServerChannel = ({ + channel, + server, + role, +}: ServerChannelProps) => { + const { onOpen } = useModal(); + const params = useParams(); + const router = useRouter(); + + const Icon = iconMap[channel.type]; + + const onClick = () => { + router.push(`/servers/${params?.serverId}/channels/${channel.id}`); + }; + + const onAction = (e: React.MouseEvent, action: ModalType) => { + e.stopPropagation(); + onOpen(action, { channel, server }); + }; + + return ( + + ); +}; diff --git a/components/server/serverEventHeader.tsx b/components/server/serverEventHeader.tsx new file mode 100644 index 0000000..bc7341d --- /dev/null +++ b/components/server/serverEventHeader.tsx @@ -0,0 +1,20 @@ +'use client'; +import { useModal } from '@/hooks/use-modal-store'; +import { CalendarRange } from 'lucide-react'; +import React from 'react'; + +export default function ServerEventHeader() { + const { onOpen } = useModal(); + const handleClick = () => { + onOpen('events'); + }; + return ( + + ); +} diff --git a/components/server/serverHeader.tsx b/components/server/serverHeader.tsx new file mode 100644 index 0000000..30af4b7 --- /dev/null +++ b/components/server/serverHeader.tsx @@ -0,0 +1,109 @@ +'use client'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + ChevronDown, + FolderClosedIcon, + LogOutIcon, + PlusCircle, + Settings, + Trash, + UserPlus, + Users, +} from 'lucide-react'; +import { Channel, Member, MemberRole, Profile, Server } from '@prisma/client'; +import { useModal } from '@/hooks/use-modal-store'; +import { ServerWithMembersWithProfiles } from '@/types'; +export default function ServerHeader({ + server, + role, +}: { + server: ServerWithMembersWithProfiles; + role?: MemberRole; +}) { + const { onOpen } = useModal(); + const isAdmin = role === MemberRole.ADMIN; + const isModerator = isAdmin || role === MemberRole.MODERATOR; + return ( + + + + + + {isModerator && ( + onOpen('invite', { server })} + className="flex flex-row items-center justify-between text-indigo-600 dark:text-indigo-400 px-3 py-2 text-sm cursor-pointer" + > + Invite People + + + )} + {isAdmin && ( + onOpen('editServer', { server })} + className="flex flex-row items-center justify-between px-3 py-2 text-sm cursor-pointer" + > + Server Settings + + + )} + {isAdmin && ( + onOpen('members', { server })} + className="flex flex-row items-center justify-between px-3 py-2 text-sm cursor-pointer" + > + Manage Members + + + )} + {isModerator && ( + onOpen('channel', { server })} + className="flex flex-row items-center justify-between px-3 py-2 text-sm cursor-pointer" + > + Create Channel + + + )} + {isModerator && ( + onOpen('category', { server })} + className="flex flex-row items-center justify-between px-3 py-2 text-sm cursor-pointer" + > + Create Category + + + )} + {isModerator && } + + {isAdmin && ( + onOpen('deleteServer', { server })} + className=" text-rose-500 flex flex-row items-center justify-between px-3 py-2 text-sm cursor-pointer" + > + Delete Server + + + )} + {!isAdmin && ( + onOpen('leaveServer', { server })} + className="text-rose-500 flex flex-row items-center justify-between px-3 py-2 text-sm cursor-pointer" + > + Leave Server + + + )} + + + ); +} diff --git a/components/server/serverMember.tsx b/components/server/serverMember.tsx new file mode 100644 index 0000000..0452dd3 --- /dev/null +++ b/components/server/serverMember.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { Member, MemberRole, Profile, Server } from '@prisma/client'; +import { ShieldAlert, ShieldCheck } from 'lucide-react'; +import { useParams, useRouter } from 'next/navigation'; + +import { cn } from '@/lib/utils'; +import { UserAvatar } from '../avatar'; + +interface ServerMemberProps { + member: Member & { profile: Profile }; + server: Server; +} + +const roleIconMap = { + [MemberRole.GUEST]: null, + [MemberRole.MODERATOR]: ( + + ), + [MemberRole.ADMIN]: , +}; + +export const ServerMember = ({ member, server }: ServerMemberProps) => { + const params = useParams(); + const router = useRouter(); + + const icon = roleIconMap[member.role]; + + const onClick = () => { + router.push(`/servers/${params?.serverId}/conversations/${member.id}`); + }; + + return ( + + ); +}; diff --git a/components/server/serverSearch.tsx b/components/server/serverSearch.tsx new file mode 100644 index 0000000..e62963c --- /dev/null +++ b/components/server/serverSearch.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { Search } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; + +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +interface ServerSearchProps { + data: { + label: string; + type: 'channel' | 'member'; + data: + | { + icon: React.ReactNode; + name: string; + id: string; + }[] + | undefined; + }[]; +} + +export default function ServerSearch({ data }: ServerSearchProps) { + const [open, setOpen] = useState(false); + const router = useRouter(); + const params = useParams(); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); + }, []); + + const onClick = ({ + id, + type, + }: { + id: string; + type: 'channel' | 'member'; + }) => { + setOpen(false); + + if (type === 'member') { + return router.push(`/servers/${params?.serverId}/conversations/${id}`); + } + + if (type === 'channel') { + return router.push(`/servers/${params?.serverId}/channels/${id}`); + } + }; + + return ( + <> + + + + + No Results found + {data.map(({ label, type, data }) => { + if (!data?.length) return null; + + return ( + + {data?.map(({ id, icon, name }) => { + return ( + onClick({ id, type })} + > + {icon} + {name} + + ); + })} + + ); + })} + + + + ); +} diff --git a/components/server/serverSection.tsx b/components/server/serverSection.tsx new file mode 100644 index 0000000..420a947 --- /dev/null +++ b/components/server/serverSection.tsx @@ -0,0 +1,52 @@ +'use client'; +import { useModal } from '@/hooks/use-modal-store'; +import { ServerWithMembersWithProfiles } from '@/types'; +import { ChannelType, MemberRole } from '@prisma/client'; +import ActionTooltip from '../Action-tooltip'; +import { Plus, Settings } from 'lucide-react'; + +interface ServerSectionProps { + label: string; + role?: MemberRole; + sectionType: 'channels' | 'members'; + channelType?: ChannelType; + server?: ServerWithMembersWithProfiles; +} + +export const ServerSection = ({ + label, + role, + sectionType, + channelType, + server, +}: ServerSectionProps) => { + const { onOpen, data } = useModal(); + + return ( +
+

+ {label} +

+ {role !== MemberRole.GUEST && sectionType === 'channels' && ( + + + + )} + {role === MemberRole.ADMIN && sectionType === 'members' && ( + + + + )} +
+ ); +}; diff --git a/components/server/serverSidebar.tsx b/components/server/serverSidebar.tsx new file mode 100644 index 0000000..5e5d49b --- /dev/null +++ b/components/server/serverSidebar.tsx @@ -0,0 +1,208 @@ +import React from 'react'; + +import ServerHeader from './serverHeader'; +import { db } from '@/lib/db'; +import { ChannelType, MemberRole } from '@prisma/client'; +import { redirectToSignIn } from '@clerk/nextjs'; +import { currentProfile } from '@/lib/currentProfile'; +import { redirect } from 'next/navigation'; +import ServerEventHeader from './serverEventHeader'; +import ServerSearch from './serverSearch'; +import { Hash, Mic, ShieldAlert, ShieldCheck, Video } from 'lucide-react'; +import { ServerSection } from './serverSection'; +import { ScrollArea } from '../ui/scroll-area'; +import { Separator } from '../ui/separator'; +import { ServerChannel } from './serverChannel'; +import { ServerMember } from './serverMember'; +interface ServerSidebarProps { + serverId: string; +} + +const iconMap = { + [ChannelType.TEXT]: , + [ChannelType.AUDIO]: , + [ChannelType.VIDEO]: