├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (invite) │ └── (routes) │ │ └── invite │ │ └── [inviteCode] │ │ └── page.tsx ├── (main) │ ├── (routes) │ │ └── servers │ │ │ └── [serverId] │ │ │ ├── channels │ │ │ └── [channelId] │ │ │ │ └── page.tsx │ │ │ ├── conversations │ │ │ └── [memberId] │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ └── layout.tsx ├── (setup) │ └── page.tsx ├── api │ ├── channels │ │ ├── [channelId] │ │ │ └── route.ts │ │ └── route.ts │ ├── direct-messages │ │ └── route.ts │ ├── livekit │ │ └── route.ts │ ├── members │ │ └── [memberId] │ │ │ └── route.ts │ ├── messages │ │ └── route.ts │ ├── servers │ │ ├── [serverId] │ │ │ ├── invite-code │ │ │ │ └── route.ts │ │ │ ├── leave │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── route.ts │ └── uploadthing │ │ ├── core.ts │ │ └── route.ts ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── action-tooltip.tsx ├── chat-video-button.tsx ├── chat │ ├── chat-header.tsx │ ├── chat-input.tsx │ ├── chat-item.tsx │ ├── chat-messages.tsx │ └── chat-welcome.tsx ├── emoji-picker.tsx ├── file-upload.tsx ├── media-room.tsx ├── mobile-toggle.tsx ├── modals │ ├── create-channel-modal.tsx │ ├── create-server-modal.tsx │ ├── delete-channel-modal.tsx │ ├── delete-message-modal.tsx │ ├── delete-server-modal.tsx │ ├── edit-channel-modal.tsx │ ├── edit-server-modal.tsx │ ├── initial-modal.tsx │ ├── invite-modal.tsx │ ├── leave-server-modal.tsx │ ├── members-modal.tsx │ └── message-file-modal.tsx ├── mode-toggle.tsx ├── navigation │ ├── navigation-action.tsx │ ├── navigation-item.tsx │ └── navigation-sidebar.tsx ├── providers │ ├── modal-provider.tsx │ ├── query-provider.tsx │ ├── socket-provider.tsx │ └── theme-provider.tsx ├── server │ ├── server-channel.tsx │ ├── server-header.tsx │ ├── server-member.tsx │ ├── server-search.tsx │ ├── server-section.tsx │ └── server-sidebar.tsx ├── socket-indicator.tsx ├── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ └── tooltip.tsx └── user-avatar.tsx ├── github_assets ├── 10pic-light.PNG ├── 10pic.PNG ├── 11oic.PNG ├── 11pic-light.PNG ├── 12pic-light.PNG ├── 12pic.PNG ├── 13mobile.PNG ├── 13pic-light.PNG ├── 13pic.PNG ├── 1mobile.PNG ├── 1pic-light.PNG ├── 1pic.PNG ├── 2mobile.PNG ├── 2pic-light.PNG ├── 2pic.PNG ├── 3mobile.PNG ├── 3pic-light.PNG ├── 3pic.PNG ├── 4mobile.PNG ├── 4pic-light.PNG ├── 4pic.PNG ├── 5mobile.PNG ├── 5pic-light.PNG ├── 5pic.PNG ├── 6mobile.PNG ├── 6pic-light.PNG ├── 6pic.PNG ├── 7mobile.PNG ├── 7pic-light.PNG ├── 7pic.PNG ├── 8mobile.PNG ├── 8pic-light.PNG ├── 8pic.PNG ├── 9mobile.PNG ├── 9pic-light.PNG ├── 9pic.PNG ├── archive │ └── second-mobile.PNG └── discord-logo-discord-icon-transparent-free-png.webp ├── hooks ├── use-chat-query.ts ├── use-chat-scroll.ts ├── use-chat-socket.ts ├── use-modal-store.ts └── use-origin.ts ├── lib ├── blur-data-img.ts ├── conversation.ts ├── current-profile-pages.ts ├── current-profile.ts ├── db.ts ├── initial-profile.ts ├── uploadthing.ts └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pages └── api │ └── socket │ ├── direct-messages │ ├── [directMessagesId].ts │ └── index.ts │ ├── io.ts │ └── messages │ ├── [messagesId].ts │ └── index.ts ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240827074551_ │ │ └── migration.sql │ ├── 20240829070717_added_messages_logic_and_stuff │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── types.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs' 2 | 3 | export default function Page() { 4 | return 5 | } -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs' 2 | 3 | export default function Page() { 4 | return 5 | } -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ children }: { children: React.ReactNode }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default AuthLayout; 6 | -------------------------------------------------------------------------------- /app/(invite)/(routes)/invite/[inviteCode]/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { auth } from "@clerk/nextjs/server"; 4 | import { redirect } from "next/navigation"; 5 | 6 | interface InviteCodePageProps { 7 | params: { 8 | inviteCode: string; 9 | }; 10 | } 11 | 12 | const InviteCodePage = async ({ params }: InviteCodePageProps) => { 13 | const profile = await currentProfile(); 14 | 15 | if (!profile) { 16 | return auth().redirectToSignIn(); 17 | } 18 | 19 | if (!params.inviteCode) { 20 | return redirect("/"); 21 | } 22 | 23 | const existingServer = await db.server.findFirst({ 24 | where: { 25 | inviteCode: params.inviteCode, 26 | members: { 27 | some: { 28 | profileId: profile.id, 29 | }, 30 | }, 31 | }, 32 | }); 33 | 34 | if (existingServer) { 35 | return redirect(`/servers/${existingServer.id}`); 36 | } 37 | 38 | const server = await db.server.update({ 39 | where: { 40 | inviteCode: params.inviteCode, 41 | }, 42 | data: { 43 | members: { 44 | create: [ 45 | { 46 | profileId: profile.id 47 | } 48 | ] 49 | } 50 | } 51 | }) 52 | 53 | if(server) { 54 | return redirect(`/servers/${server.id}`); 55 | } 56 | return null 57 | }; 58 | 59 | export default InviteCodePage; 60 | -------------------------------------------------------------------------------- /app/(main)/(routes)/servers/[serverId]/channels/[channelId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChatHeader } from "@/components/chat/chat-header"; 2 | import { ChatInput } from "@/components/chat/chat-input"; 3 | import { ChatMessages } from "@/components/chat/chat-messages"; 4 | import { MediaRoom } from "@/components/media-room"; 5 | import { currentProfile } from "@/lib/current-profile"; 6 | import { db } from "@/lib/db"; 7 | import { auth } from "@clerk/nextjs/server"; 8 | import { ChannelType } from "@prisma/client"; 9 | import { redirect } from "next/navigation"; 10 | 11 | interface ChannelIdPageProps { 12 | params: { 13 | serverId: string; 14 | channelId: string; 15 | }; 16 | } 17 | 18 | const ChannelIdPage = async ({ params }: ChannelIdPageProps) => { 19 | const profile = await currentProfile(); 20 | if (!profile) return auth().redirectToSignIn(); 21 | 22 | const channel = await db.channel.findUnique({ 23 | where: { 24 | id: params.channelId, 25 | }, 26 | }); 27 | 28 | const member = await db.member.findFirst({ 29 | where: { 30 | serverId: params?.serverId, 31 | profileId: profile.id, 32 | }, 33 | }); 34 | 35 | if (!channel || !member) return redirect("/"); 36 | 37 | return ( 38 |
39 | 44 | {channel.type === ChannelType.TEXT && ( 45 | <> 46 | 60 | 69 | 70 | )} 71 | 72 | {channel.type === ChannelType.AUDIO && ( 73 | 74 | )} 75 | 76 | {channel.type === ChannelType.VIDEO && ( 77 | 78 | )} 79 |
80 | ); 81 | }; 82 | 83 | export default ChannelIdPage; 84 | -------------------------------------------------------------------------------- /app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChatHeader } from "@/components/chat/chat-header"; 2 | import { ChatInput } from "@/components/chat/chat-input"; 3 | import { ChatMessages } from "@/components/chat/chat-messages"; 4 | import { MediaRoom } from "@/components/media-room"; 5 | import { getOrCreateConversation } from "@/lib/conversation"; 6 | import { currentProfile } from "@/lib/current-profile"; 7 | import { db } from "@/lib/db"; 8 | import { auth } from "@clerk/nextjs/server"; 9 | import { redirect } from "next/navigation"; 10 | 11 | interface MemberIdPageProps { 12 | params: { 13 | memberId: string; 14 | serverId: string; 15 | }; 16 | searchParams: { 17 | video?: boolean; 18 | }; 19 | } 20 | 21 | const MemberIdPage = async ({ params, searchParams }: MemberIdPageProps) => { 22 | const profile = await currentProfile(); 23 | if (!profile) { 24 | return auth().redirectToSignIn(); 25 | } 26 | 27 | const currentMember = await db.member.findFirst({ 28 | where: { 29 | serverId: params.serverId, 30 | profileId: profile.id, 31 | }, 32 | include: { 33 | profile: true, 34 | }, 35 | }); 36 | 37 | if (!currentMember) { 38 | return redirect("/"); 39 | } 40 | 41 | const conversation = await getOrCreateConversation( 42 | currentMember.id, 43 | params.memberId 44 | ); 45 | 46 | if (!conversation) { 47 | return redirect(`/servers/${params.serverId}`); 48 | } 49 | const { memberOne, memberTwo } = await conversation; 50 | 51 | const otherMember = 52 | memberOne.profileId === profile.id ? memberTwo : memberOne; 53 | 54 | return ( 55 |
56 | 62 | {searchParams.video && ( 63 | 64 | )} 65 | {!searchParams.video && ( 66 | <> 67 | 80 | 88 | 89 | )} 90 |
91 | ); 92 | }; 93 | 94 | export default MemberIdPage; 95 | -------------------------------------------------------------------------------- /app/(main)/(routes)/servers/[serverId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ServerSidebar } from "@/components/server/server-sidebar"; 2 | import { currentProfile } from "@/lib/current-profile"; 3 | import { db } from "@/lib/db"; 4 | import { auth } from "@clerk/nextjs/server"; 5 | import { redirect } from "next/navigation"; 6 | 7 | const ServerIdLayout = async ({ 8 | children, 9 | params, 10 | }: { 11 | children: React.ReactNode; 12 | params: { serverId: string }; 13 | }) => { 14 | const profile = await currentProfile(); 15 | if (!profile) return auth().redirectToSignIn(); 16 | 17 | const server = await db.server.findFirst({ 18 | where: { 19 | id: params.serverId, 20 | members: { 21 | some: { 22 | profileId: profile.id, 23 | }, 24 | }, 25 | }, 26 | }); 27 | 28 | if (!server) { 29 | return redirect("/"); 30 | } 31 | 32 | return ( 33 |
34 |
35 | 36 |
37 |
{children}
38 |
39 | ); 40 | }; 41 | 42 | export default ServerIdLayout; 43 | -------------------------------------------------------------------------------- /app/(main)/(routes)/servers/[serverId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { auth } from "@clerk/nextjs/server"; 4 | import { redirect } from "next/navigation"; 5 | 6 | interface ServerIdPageProps { 7 | params: { 8 | serverId: string; 9 | }; 10 | } 11 | 12 | const ServerIdPage = async ({ params }: ServerIdPageProps) => { 13 | const profile = await currentProfile(); 14 | if (!profile) return auth().redirectToSignIn(); 15 | const server = await db.server.findUnique({ 16 | where: { 17 | id: params.serverId, 18 | members: { 19 | some: { 20 | profileId: profile.id, 21 | }, 22 | }, 23 | }, 24 | include: { 25 | channels: { 26 | where: { 27 | name: "general", 28 | }, 29 | orderBy: { 30 | createdAt: "asc", 31 | }, 32 | }, 33 | }, 34 | }); 35 | const initialChannel = server?.channels[0]; 36 | if (initialChannel?.name !== "general") return null; 37 | 38 | return redirect(`/servers/${server?.id}/channels/${initialChannel?.id}`); 39 | }; 40 | 41 | export default ServerIdPage; 42 | -------------------------------------------------------------------------------- /app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationSidebar } from "@/components/navigation/navigation-sidebar"; 2 | 3 | const MainLayout = async ({ 4 | children 5 | }: {children: React.ReactNode}) => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | {children} 13 |
14 |
15 | ) 16 | } 17 | 18 | export default MainLayout; -------------------------------------------------------------------------------- /app/(setup)/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { initialProfile } from "@/lib/initial-profile"; 3 | import { InitialModal } from "@/components/modals/initial-modal"; 4 | import { redirect } from "next/navigation"; 5 | 6 | type Profile = { 7 | id: string; 8 | userId: string; 9 | name: string; 10 | imageUrl: string; 11 | email: string; 12 | createdAt: Date; 13 | updatedAt: Date; 14 | }; 15 | 16 | const SetupPage = async () => { 17 | const profile = await initialProfile() as Profile; 18 | 19 | const server = await db.server.findFirst({ 20 | where:{ 21 | members: { 22 | some: { 23 | profileId: profile.id 24 | } 25 | } 26 | } 27 | }); 28 | 29 | if(server){ 30 | redirect(`/servers/${server.id}`); 31 | } 32 | 33 | return 34 | } 35 | 36 | export default SetupPage; -------------------------------------------------------------------------------- /app/api/channels/[channelId]/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { MemberRole } from "@prisma/client"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function DELETE( 7 | req: Request, 8 | { 9 | params, 10 | }: { 11 | params: { 12 | channelId: string; 13 | }; 14 | } 15 | ) { 16 | try { 17 | const profile = await currentProfile(); 18 | const { channelId } = params; 19 | const { searchParams } = new URL(req.url); 20 | const serverId = searchParams.get("serverId"); 21 | 22 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 23 | if (!channelId) 24 | return new NextResponse("Missing channelId", { status: 400 }); 25 | if (!serverId) return new NextResponse("Missing serverId", { status: 400 }); 26 | 27 | const server = await db.server.update({ 28 | where: { 29 | id: serverId, 30 | members: { 31 | some: { 32 | profileId: profile.id, 33 | role: { 34 | in: [MemberRole.ADMIN, MemberRole.MODERATOR], 35 | }, 36 | }, 37 | }, 38 | }, 39 | data: { 40 | channels: { 41 | delete: { 42 | id: channelId, 43 | name: { 44 | not: "general", 45 | }, 46 | }, 47 | }, 48 | }, 49 | }); 50 | 51 | return NextResponse.json(server); 52 | } catch (err) { 53 | console.log("[DELETE_CHANNEL_ERROR]", err); 54 | return new NextResponse("Internal Error", { status: 500 }); 55 | } 56 | } 57 | 58 | export async function PATCH( 59 | req: Request, 60 | { 61 | params, 62 | }: { 63 | params: { 64 | channelId: string; 65 | }; 66 | } 67 | ) { 68 | try { 69 | const { searchParams } = new URL(req.url); 70 | const profile = await currentProfile(); 71 | const serverId = searchParams.get("serverId"); 72 | const { name: channelName, type: channelType } = await req.json(); 73 | 74 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 75 | if (!serverId) 76 | return new NextResponse("Missing server id", { status: 400 }); 77 | if (!channelName) 78 | return new NextResponse("Missing channel name", { status: 400 }); 79 | if (channelName === "general" || channelName === "General") 80 | return new NextResponse("Channel name cannot be 'general' or 'General'", { 81 | status: 400, 82 | }); 83 | if (!channelType) 84 | return new NextResponse("Missing channel type", { status: 400 }); 85 | 86 | const server = await db.server.update({ 87 | where: { 88 | id: serverId, 89 | members: { 90 | some: { 91 | profileId: profile.id, 92 | role: { 93 | in: [MemberRole.ADMIN, MemberRole.MODERATOR], 94 | }, 95 | }, 96 | }, 97 | }, 98 | data: { 99 | channels: { 100 | update: { 101 | where: { 102 | id: params.channelId, 103 | NOT: { 104 | name: "general", 105 | }, 106 | }, 107 | data: { 108 | name: channelName, 109 | type: channelType, 110 | }, 111 | }, 112 | }, 113 | }, 114 | }); 115 | 116 | return NextResponse.json(server); 117 | } catch (error) { 118 | console.log("[CHANNEL_EDIT_ERROR]", error); 119 | return new NextResponse("Internal Error", { status: 500 }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/api/channels/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { MemberRole } from "@prisma/client"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function POST(req: Request) { 7 | try { 8 | const { searchParams } = new URL(req.url); 9 | const profile = await currentProfile(); 10 | const serverId = searchParams.get("serverId"); 11 | const { name: channelName, type: channelType } = await req.json(); 12 | 13 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 14 | if (!serverId) 15 | return new NextResponse("Missing server id", { status: 400 }); 16 | if (!channelName) 17 | return new NextResponse("Missing channel name", { status: 400 }); 18 | if (channelName === "general" || channelName === "General") 19 | return new NextResponse("Channel name cannot be 'general' or 'General'", { 20 | status: 400, 21 | }); 22 | if (!channelType) 23 | return new NextResponse("Missing channel type", { status: 400 }); 24 | 25 | const server = await db.server.update({ 26 | where: { 27 | id: serverId, 28 | members: { 29 | some: { 30 | profileId: profile.id, 31 | role: { 32 | in: [MemberRole.ADMIN, MemberRole.MODERATOR], 33 | }, 34 | }, 35 | }, 36 | }, 37 | data: { 38 | channels: { 39 | create: { 40 | profileId: profile.id, 41 | name: channelName, 42 | type: channelType, 43 | }, 44 | }, 45 | }, 46 | }); 47 | 48 | return NextResponse.json(server); 49 | } catch (error) { 50 | console.log("[CHANNEL_API_ERROR]", error); 51 | return new NextResponse("Internal Error", { status: 500 }); 52 | } 53 | } -------------------------------------------------------------------------------- /app/api/direct-messages/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { DirectMessage } from "@prisma/client"; 4 | import { NextResponse } from "next/server"; 5 | 6 | const BATCH_AMOUNT = 15; 7 | 8 | export const GET = async (req: Request) => { 9 | try { 10 | const profile = await currentProfile(); 11 | const { searchParams } = new URL(req.url); 12 | 13 | const cursor = searchParams.get("cursor"); 14 | const conversationId = searchParams.get("conversationId"); 15 | 16 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 17 | 18 | if (!conversationId) 19 | return new NextResponse("Missing Conversation ID", { status: 400 }); 20 | 21 | let messages: DirectMessage[] = []; 22 | 23 | if (cursor) { 24 | messages = await db.directMessage.findMany({ 25 | take: BATCH_AMOUNT, 26 | skip: 1, 27 | cursor: { 28 | id: cursor, 29 | }, 30 | where: { 31 | conversationId, 32 | }, 33 | include: { 34 | member: { 35 | include: { 36 | profile: true, 37 | }, 38 | }, 39 | }, 40 | orderBy: { 41 | createdAt: "desc", 42 | }, 43 | }); 44 | } else { 45 | messages = await db.directMessage.findMany({ 46 | take: BATCH_AMOUNT, 47 | where: { 48 | conversationId, 49 | }, 50 | include: { 51 | member: { 52 | include: { 53 | profile: true, 54 | }, 55 | }, 56 | }, 57 | orderBy: { 58 | createdAt: "desc", 59 | }, 60 | }); 61 | } 62 | 63 | let nextCursor = null; 64 | 65 | if (messages.length === BATCH_AMOUNT) { 66 | nextCursor = messages[BATCH_AMOUNT - 1].id; 67 | } 68 | 69 | return NextResponse.json({ 70 | items: messages, 71 | nextCursor, 72 | }); 73 | } catch (err) { 74 | console.log("[DIRECT_MESSAGES_ERROR]", err); 75 | return new NextResponse("Internal Error", { status: 500 }); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /app/api/livekit/route.ts: -------------------------------------------------------------------------------- 1 | import { AccessToken } from "livekit-server-sdk"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET(req: NextRequest) { 5 | const room = req.nextUrl.searchParams.get("room"); 6 | const username = req.nextUrl.searchParams.get("username"); 7 | if (!room) { 8 | return NextResponse.json( 9 | { error: 'Missing "room" query parameter' }, 10 | { status: 400 } 11 | ); 12 | } else if (!username) { 13 | return NextResponse.json( 14 | { error: 'Missing "username" query parameter' }, 15 | { status: 400 } 16 | ); 17 | } 18 | 19 | const apiKey = process.env.LIVEKIT_API_KEY; 20 | const apiSecret = process.env.LIVEKIT_API_SECRET; 21 | const wsUrl = process.env.NEXT_PUBLIC_LIVEKIT_URL; 22 | 23 | if (!apiKey || !apiSecret || !wsUrl) { 24 | return NextResponse.json( 25 | { error: "Server misconfigured" }, 26 | { status: 500 } 27 | ); 28 | } 29 | 30 | const at = new AccessToken(apiKey, apiSecret, { identity: username }); 31 | 32 | at.addGrant({ room, roomJoin: true, canPublish: true, canSubscribe: true }); 33 | 34 | return NextResponse.json({ token: await at.toJwt() }); 35 | } -------------------------------------------------------------------------------- /app/api/members/[memberId]/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { 8 | params, 9 | }: { 10 | params: { 11 | memberId: string; 12 | }; 13 | } 14 | ) { 15 | try { 16 | const profile = await currentProfile(); 17 | const { searchParams } = new URL(req.url); 18 | const serverId = searchParams.get("serverId"); 19 | const memberId = params.memberId; 20 | const role = await req.json(); 21 | 22 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 23 | 24 | if (!serverId) 25 | return new NextResponse("Missing Server ID", { status: 400 }); 26 | 27 | if (!memberId) 28 | return new NextResponse("Missing Memeber ID", { status: 400 }); 29 | 30 | const server = await db.server.update({ 31 | where: { 32 | id: serverId, 33 | profileId: profile.id, 34 | }, 35 | data: { 36 | members: { 37 | update: { 38 | where: { 39 | id: memberId, 40 | profileId: { 41 | not: profile.id, 42 | }, 43 | }, 44 | data: { 45 | role: role.role, 46 | }, 47 | }, 48 | }, 49 | }, 50 | include: { 51 | members: { 52 | include: { 53 | profile: true, 54 | }, 55 | orderBy: { 56 | role: "asc", 57 | }, 58 | }, 59 | }, 60 | }); 61 | return NextResponse.json(server); 62 | } catch (err) { 63 | console.log("[ROLE_PATCH_ERROR]", err); 64 | return new NextResponse("Internal Error", { status: 500 }); 65 | } 66 | } 67 | 68 | export async function DELETE( 69 | req: Request, 70 | { 71 | params, 72 | }: { 73 | params: { 74 | memberId: string; 75 | }; 76 | } 77 | ) { 78 | try { 79 | const profile = await currentProfile(); 80 | const { searchParams } = new URL(req.url); 81 | const serverId = searchParams.get("serverId"); 82 | const memberId = params.memberId; 83 | 84 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 85 | 86 | if (!serverId) 87 | return new NextResponse("Missing Server ID", { status: 400 }); 88 | 89 | if (!memberId) 90 | return new NextResponse("Missing Memeber ID", { status: 400 }); 91 | 92 | const server = await db.server.update({ 93 | where: { 94 | id: serverId, 95 | profileId: profile.id, 96 | }, 97 | data: { 98 | members: { 99 | delete: { 100 | id: memberId, 101 | profileId: { 102 | not: profile.id, 103 | }, 104 | }, 105 | }, 106 | }, 107 | include: { 108 | members: { 109 | include: { 110 | profile: true, 111 | }, 112 | orderBy: { 113 | role: "asc", 114 | }, 115 | }, 116 | }, 117 | }); 118 | 119 | return NextResponse.json(server); 120 | } catch (err) { 121 | console.log("[DELETE_MEMBER_ERROR", err); 122 | return new NextResponse("Internal Error", { status: 500 }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/api/messages/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { Message } from "@prisma/client"; 4 | import { NextResponse } from "next/server"; 5 | 6 | const BATCH_AMOUNT = 15; 7 | 8 | export const GET = async (req: Request) => { 9 | try { 10 | const profile = await currentProfile(); 11 | const { searchParams } = new URL(req.url); 12 | 13 | const cursor = searchParams.get("cursor"); 14 | const channelId = searchParams.get("channelId"); 15 | 16 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 17 | 18 | if (!channelId) 19 | return new NextResponse("Missing Channel ID", { status: 400 }); 20 | 21 | let messages: Message[] = []; 22 | 23 | if (cursor) { 24 | messages = await db.message.findMany({ 25 | take: BATCH_AMOUNT, 26 | skip: 1, 27 | cursor: { 28 | id: cursor, 29 | }, 30 | where: { 31 | channelId, 32 | }, 33 | include: { 34 | member: { 35 | include: { 36 | profile: true, 37 | }, 38 | }, 39 | }, 40 | orderBy: { 41 | createdAt: "desc", 42 | }, 43 | }); 44 | } else { 45 | messages = await db.message.findMany({ 46 | take: BATCH_AMOUNT, 47 | where: { 48 | channelId, 49 | }, 50 | include: { 51 | member: { 52 | include: { 53 | profile: true, 54 | }, 55 | }, 56 | }, 57 | orderBy: { 58 | createdAt: "desc", 59 | }, 60 | }); 61 | } 62 | 63 | let nextCursor = null; 64 | 65 | if (messages.length === BATCH_AMOUNT) { 66 | nextCursor = messages[BATCH_AMOUNT - 1].id; 67 | } 68 | 69 | return NextResponse.json({ 70 | items: messages, 71 | nextCursor, 72 | }); 73 | } catch (err) { 74 | console.log("[MESSAGES_ERROR]", err); 75 | return new NextResponse("Internal Error", { status: 500 }); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /app/api/servers/[serverId]/invite-code/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { NextResponse } from "next/server"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | export async function PATCH( 7 | req: Request, 8 | { params }: { params: { serverId: string } } 9 | ) { 10 | try { 11 | const profile = await currentProfile(); 12 | if(!profile) return new NextResponse("Unauthorized", {status: 401}); 13 | if(!params.serverId) return new NextResponse("ServerId is missing", {status: 400}); 14 | 15 | const server = await db.server.update({ 16 | where: { 17 | id: params.serverId, 18 | profileId: profile.id 19 | }, 20 | data: { 21 | inviteCode: uuidv4() 22 | } 23 | }); 24 | 25 | return NextResponse.json(server); 26 | } catch (err) { 27 | console.log("[SERVER_ID]", err); 28 | return new NextResponse("Internal Error", { status: 500 }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/api/servers/[serverId]/leave/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { 8 | params, 9 | }: { 10 | params: { serverId: string }; 11 | } 12 | ) { 13 | try { 14 | const profile = await currentProfile(); 15 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 16 | if (!params.serverId) 17 | return new NextResponse("ServerId is missing", { status: 400 }); 18 | 19 | const server = await db.server.update({ 20 | where: { 21 | id: params.serverId, 22 | profileId: { 23 | not: profile.id, 24 | }, 25 | members: { 26 | some: { 27 | profileId: profile.id, 28 | }, 29 | }, 30 | }, 31 | data: { 32 | members: { 33 | deleteMany: { 34 | profileId: profile.id, 35 | }, 36 | }, 37 | }, 38 | }); 39 | 40 | return NextResponse.json(server); 41 | } catch (err) { 42 | console.log("[LEAVE_SERVER_API]", err); 43 | return new NextResponse("Internal Error", { status: 500 }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/api/servers/[serverId]/route.ts: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function DELETE( 6 | req: Request, 7 | { params }: { params: { serverId: string } } 8 | ) { 9 | try { 10 | const profile = await currentProfile(); 11 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 12 | if (!params.serverId) 13 | return new NextResponse("Missing Server Id", { status: 400 }); 14 | 15 | const server = await db.server.delete({ 16 | where: { 17 | id: params.serverId, 18 | profileId: profile.id, 19 | } 20 | }); 21 | 22 | return NextResponse.json(server); 23 | } catch (err) { 24 | console.log("[SERVER_DELETE]", err); 25 | return new NextResponse("Internal Error", { status: 500 }); 26 | } 27 | } 28 | 29 | export async function PATCH( 30 | req: Request, 31 | { params }: { params: { serverId: string } } 32 | ) { 33 | try { 34 | const profile = await currentProfile(); 35 | const { name, imageUrl } = await req.json(); 36 | if (!profile) return new NextResponse("Unauthorized", { status: 401 }); 37 | if (!params.serverId) 38 | return new NextResponse("Missing Server Id", { status: 400 }); 39 | 40 | const server = await db.server.update({ 41 | where: { 42 | id: params.serverId, 43 | profileId: profile.id, 44 | }, 45 | data: { 46 | name, 47 | imageUrl, 48 | }, 49 | }); 50 | 51 | return NextResponse.json(server); 52 | } catch (err) { 53 | console.log("[SERVER_ID_PATCH]", err); 54 | return new NextResponse("Internal Error", { status: 500 }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/api/servers/route.ts: -------------------------------------------------------------------------------- 1 | import {v4 as uuidv4} from 'uuid'; 2 | import { NextResponse } from "next/server"; 3 | 4 | import { currentProfile } from "@/lib/current-profile"; 5 | import { db } from "@/lib/db"; 6 | import { MemberRole } from '@prisma/client'; 7 | 8 | export async function POST(req: Request){ 9 | try { 10 | const {name, imageUrl} = await req.json(); 11 | const profile = await currentProfile(); 12 | 13 | if(!profile){ 14 | return new NextResponse("Unauthorized", {status: 401}); 15 | } 16 | 17 | const server = await db.server.create({ 18 | data: { 19 | profileId: profile.id, 20 | name, 21 | imageUrl, 22 | inviteCode: uuidv4(), 23 | channels: { 24 | create: [ 25 | { 26 | name: "general", profileId: profile.id 27 | } 28 | ] 29 | }, 30 | members: { 31 | create: [ 32 | { 33 | profileId: profile.id, role: MemberRole.ADMIN 34 | } 35 | ] 36 | } 37 | } 38 | }) 39 | 40 | return NextResponse.json(server) 41 | } catch (error){ 42 | console.log('[SERVERS_POST]', error); 43 | return new NextResponse("Internal Error", {status: 500}) 44 | } 45 | } -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server"; 2 | 3 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 4 | 5 | const f = createUploadthing(); 6 | 7 | const handleAuth = () => { 8 | const userId = auth(); 9 | if (!userId) throw new Error("Unauthorized"); 10 | return { userId }; 11 | }; 12 | 13 | export const ourFileRouter = { 14 | serverImage: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) 15 | .middleware(() => handleAuth()) 16 | .onUploadComplete(() => {}), 17 | messageFile: f(["image", "pdf"]) 18 | .middleware(() => handleAuth()) 19 | .onUploadComplete(() => {}), 20 | } satisfies FileRouter; 21 | 22 | export type OurFileRouter = typeof ourFileRouter; -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createRouteHandler({ 7 | router: ourFileRouter, 8 | 9 | // Apply an (optional) custom config: 10 | // config: { ... }, 11 | }); -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 20 14.3% 4.1%; 15 | --card: 0 0% 100%; 16 | --card-foreground: 20 14.3% 4.1%; 17 | --popover: 0 0% 100%; 18 | --popover-foreground: 20 14.3% 4.1%; 19 | --primary: 24 9.8% 10%; 20 | --primary-foreground: 60 9.1% 97.8%; 21 | --secondary: 60 4.8% 95.9%; 22 | --secondary-foreground: 24 9.8% 10%; 23 | --muted: 60 4.8% 95.9%; 24 | --muted-foreground: 25 5.3% 44.7%; 25 | --accent: 60 4.8% 95.9%; 26 | --accent-foreground: 24 9.8% 10%; 27 | --destructive: 0 84.2% 60.2%; 28 | --destructive-foreground: 60 9.1% 97.8%; 29 | --border: 20 5.9% 90%; 30 | --input: 20 5.9% 90%; 31 | --ring: 20 14.3% 4.1%; 32 | --radius: 0.5rem; 33 | --chart-1: 12 76% 61%; 34 | --chart-2: 173 58% 39%; 35 | --chart-3: 197 37% 24%; 36 | --chart-4: 43 74% 66%; 37 | --chart-5: 27 87% 67%; 38 | } 39 | 40 | .dark { 41 | --background: 20 14.3% 4.1%; 42 | --foreground: 60 9.1% 97.8%; 43 | --card: 20 14.3% 4.1%; 44 | --card-foreground: 60 9.1% 97.8%; 45 | --popover: 20 14.3% 4.1%; 46 | --popover-foreground: 60 9.1% 97.8%; 47 | --primary: 60 9.1% 97.8%; 48 | --primary-foreground: 24 9.8% 10%; 49 | --secondary: 12 6.5% 15.1%; 50 | --secondary-foreground: 60 9.1% 97.8%; 51 | --muted: 12 6.5% 15.1%; 52 | --muted-foreground: 24 5.4% 63.9%; 53 | --accent: 12 6.5% 15.1%; 54 | --accent-foreground: 60 9.1% 97.8%; 55 | --destructive: 0 62.8% 30.6%; 56 | --destructive-foreground: 60 9.1% 97.8%; 57 | --border: 12 6.5% 15.1%; 58 | --input: 12 6.5% 15.1%; 59 | --ring: 24 5.7% 82.9%; 60 | --chart-1: 220 70% 50%; 61 | --chart-2: 160 60% 45%; 62 | --chart-3: 30 80% 55%; 63 | --chart-4: 280 65% 60%; 64 | --chart-5: 340 75% 55%; 65 | } 66 | } 67 | 68 | @layer base { 69 | * { 70 | @apply border-border; 71 | } 72 | body { 73 | @apply bg-background text-foreground; 74 | } 75 | } 76 | 77 | @import "~@uploadthing/react/styles.css"; 78 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Open_Sans } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { ThemeProvider } from "@/components/providers/theme-provider"; 6 | import { cn } from "@/lib/utils"; 7 | import { ModalProvider } from "@/components/providers/modal-provider"; 8 | import { SocketProvider } from "@/components/providers/socket-provider"; 9 | import { QueryProvider } from "@/components/providers/query-provider"; 10 | 11 | const font = Open_Sans({ subsets: ["latin"] }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Geriord", 15 | description: "Discord? no, Geriord? yes!", 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 26 | 27 | 33 | 34 | 35 | {children} 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/action-tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipProvider, 7 | TooltipTrigger 8 | } from '@/components/ui/tooltip'; 9 | 10 | interface ActionTooltipProps { 11 | label: string; 12 | children: React.ReactNode; 13 | side?: 'left' | "right" | "top" | "bottom"; 14 | align?: "start" | "center" | "end"; 15 | } 16 | 17 | export const ActionTooltip = ({ 18 | label, children, side, align 19 | }: ActionTooltipProps) => { 20 | return ( 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 |

28 | {label.toLowerCase()} 29 |

30 |
31 |
32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /components/chat-video-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useSearchParams } from "next/navigation"; 4 | import { ActionTooltip } from "./action-tooltip"; 5 | import { useRouter } from "next/navigation"; 6 | import { Video, VideoOff } from "lucide-react"; 7 | import queryString from "query-string"; 8 | 9 | export const ChatVideoButton = () => { 10 | const pathname = usePathname(); 11 | const router = useRouter(); 12 | const searchParams = useSearchParams(); 13 | const isVideo = searchParams?.get("video"); 14 | const Icon = isVideo ? VideoOff : Video; 15 | const tooltipLabel = isVideo ? "End video call" : "Start video call"; 16 | const onClick = () => { 17 | const url = queryString.stringifyUrl( 18 | { 19 | url: pathname || "", 20 | query: { 21 | video: isVideo ? undefined : true, 22 | }, 23 | }, 24 | { 25 | skipNull: true, 26 | } 27 | ); 28 | router.push(url); 29 | }; 30 | return ( 31 | 32 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /components/chat/chat-header.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Hash, Menu } from "lucide-react"; 2 | import { MobileToggle } from "../mobile-toggle"; 3 | import { UserAvatar } from "../user-avatar"; 4 | import { SocketIndicator } from "../socket-indicator"; 5 | import { ChatVideoButton } from "../chat-video-button"; 6 | 7 | interface ChatHeaderProps { 8 | serverId: string; 9 | name: string; 10 | type: "channel" | "conversation"; 11 | imageUrl?: string; 12 | } 13 | 14 | export const ChatHeader = ({ 15 | serverId, 16 | name, 17 | type, 18 | imageUrl, 19 | }: ChatHeaderProps) => { 20 | return ( 21 |
22 | 23 | {type === "channel" && ( 24 | 25 | )} 26 | {type === "conversation" && ( 27 | 28 | )} 29 |

{name}

30 |
31 | {type === "conversation" && } 32 | {/* */} 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /components/chat/chat-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Form, FormField, FormItem, FormControl } from "@/components/ui/form"; 4 | 5 | import { Input } from "../ui/input"; 6 | 7 | import { zodResolver } from "@hookform/resolvers/zod"; 8 | import { useForm } from "react-hook-form"; 9 | import * as z from "zod"; 10 | import { Plus } from "lucide-react"; 11 | import queryString from "query-string"; 12 | import axios from "axios"; 13 | import { useModal } from "@/hooks/use-modal-store"; 14 | import { EmojiPicker } from "../emoji-picker"; 15 | import { useRouter } from "next/navigation"; 16 | 17 | interface ChatInputProps { 18 | apiUrl: string; 19 | query: Record; 20 | name: string; 21 | type: "conversation" | "channel"; 22 | } 23 | 24 | const formSchema = z.object({ 25 | content: z.string().min(1), 26 | }); 27 | 28 | export const ChatInput = ({ apiUrl, query, name, type }: ChatInputProps) => { 29 | const { onOpen } = useModal(); 30 | const router = useRouter(); 31 | 32 | const form = useForm>({ 33 | resolver: zodResolver(formSchema), 34 | defaultValues: { 35 | content: "", 36 | }, 37 | }); 38 | 39 | const isSubmitting = form.formState.isSubmitting; 40 | 41 | const onSubmit = async (values: z.infer) => { 42 | try { 43 | const url = queryString.stringifyUrl({ 44 | url: apiUrl, 45 | query, 46 | }); 47 | 48 | await axios.post(url, values); 49 | form.reset(); 50 | router.refresh(); 51 | } catch (error) { 52 | console.log("chat-input, onSubmit error:", error); 53 | } 54 | }; 55 | 56 | return ( 57 |
58 | 59 | ( 63 | 64 | 65 |
66 | 73 | 82 |
83 | 85 | field.onChange(`${field.value} ${emoji}`) 86 | } 87 | /> 88 |
89 |
90 |
91 |
92 | )} 93 | /> 94 | 95 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /components/chat/chat-messages.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Member } from "@prisma/client"; 4 | import { ChatWelcome } from "./chat-welcome"; 5 | import { useChatQuery } from "@/hooks/use-chat-query"; 6 | import { Loader2, ServerCrash } from "lucide-react"; 7 | import { Fragment } from "react"; 8 | import { format } from "date-fns"; 9 | import { MessageWithMemberWithProfile } from "@/types"; 10 | import { ChatItem } from "./chat-item"; 11 | import { useChatSocket } from "@/hooks/use-chat-socket"; 12 | import { useRef, ElementRef } from "react"; 13 | import { useChatScroll } from "@/hooks/use-chat-scroll"; 14 | 15 | interface ChatMessagesProps { 16 | name: string; 17 | member: Member; 18 | chatId: string; 19 | apiUrl: string; 20 | socketUrl: string; 21 | socketQuery: Record; 22 | paramKey: "channelId" | "conversationId"; 23 | paramValue: string; 24 | type: "channel" | "conversation"; 25 | } 26 | 27 | const DATE_FORMAT = "d MMM yyyy HH:mm"; 28 | 29 | export const ChatMessages = ({ 30 | name, 31 | member, 32 | chatId, 33 | apiUrl, 34 | socketUrl, 35 | socketQuery, 36 | paramKey, 37 | paramValue, 38 | type, 39 | }: ChatMessagesProps) => { 40 | const queryKey = `chat:${chatId}`; 41 | const addKey = `chat:${chatId}:messages`; 42 | const updateKey = `chat:${chatId}:messages:update`; 43 | 44 | const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = 45 | useChatQuery({ 46 | queryKey, 47 | apiUrl, 48 | paramKey, 49 | paramValue, 50 | }); 51 | 52 | const chatRef = useRef>(null); 53 | const bottomRef = useRef>(null); 54 | 55 | useChatSocket({ queryKey, addKey, updateKey }); 56 | useChatScroll({ 57 | chatRef, 58 | bottomRef, 59 | loadMore: fetchNextPage, 60 | shouldLoadMore: !isFetchingNextPage && !!hasNextPage, 61 | count: data?.pages?.[0]?.items?.length ?? 0, 62 | }); 63 | 64 | if (status === "pending") { 65 | return ( 66 |
67 | 68 |

69 | Loading messages... 70 |

71 |
72 | ); 73 | } 74 | if (status === "error") { 75 | return ( 76 |
77 | 78 |

79 | Something went wrong! 80 |

81 |
82 | ); 83 | } 84 | return ( 85 |
86 | {!hasNextPage &&
} 87 | {!hasNextPage && } 88 | {hasNextPage && ( 89 |
90 | {isFetchingNextPage ? ( 91 | 92 | ) : ( 93 | 99 | )} 100 |
101 | )} 102 |
103 | {data?.pages?.map((group, i) => { 104 | console.log(group); 105 | return ( 106 | 107 | {group.items.map((message: MessageWithMemberWithProfile) => ( 108 | 121 | ))} 122 | 123 | ); 124 | })} 125 |
126 |
127 |
128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /components/chat/chat-welcome.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Hash } from "lucide-react"; 4 | 5 | interface ChatWelcomeProps { 6 | name: string; 7 | type: "channel" | "conversation"; 8 | } 9 | 10 | export const ChatWelcome = ({ name, type }: ChatWelcomeProps) => { 11 | return ( 12 |
13 | {type === "channel" && ( 14 |
15 | 16 |
17 | )} 18 |

19 | {type === "channel" ? "Welcome to #" : ""} 20 | {name} 21 |

22 |

23 | {type === "channel" 24 | ? `This is the start of the #${name} channel` 25 | : `This is the start of your conversation with ${name}`} 26 |

27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /components/emoji-picker.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover"; 3 | import Picker from "@emoji-mart/react"; 4 | import { Smile } from "lucide-react"; 5 | import data from "@emoji-mart/data"; 6 | 7 | interface EmojiPickerProps { 8 | onChange: (values: string) => void; 9 | } 10 | 11 | export const EmojiPicker = ({ onChange }: EmojiPickerProps) => { 12 | const { resolvedTheme } = useTheme(); 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 24 | onChange(emoji.native)} 28 | /> 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /components/file-upload.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { blurDataImageUrl } from "@/lib/blur-data-img"; 4 | import { UploadDropzone } from "@/lib/uploadthing"; 5 | import { FileIcon, X } from "lucide-react"; 6 | import Image from "next/image"; 7 | 8 | interface FileUploadProps { 9 | onChange: (url?: string) => void; 10 | value: string; 11 | endpoint: "messageFile" | "serverImage"; 12 | } 13 | 14 | export const FileUpload = ({ onChange, value, endpoint }: FileUploadProps) => { 15 | const fileType = value?.split(".").pop(); 16 | if (value && fileType !== "pdf") { 17 | return ( 18 |
19 | upload 27 | 34 |
35 | ); 36 | } 37 | if (value && fileType === "pdf") { 38 | return ( 39 |
40 | 41 | 47 | {value} 48 | 49 | 56 |
57 | ); 58 | } 59 | return ( 60 | { 63 | // alert(JSON.stringify(res)); 64 | onChange(res?.[0].url); 65 | }} 66 | onUploadError={(error: Error) => { 67 | console.log(error); 68 | }} 69 | /> 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /components/media-room.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ControlBar, 5 | GridLayout, 6 | LiveKitRoom, 7 | ParticipantTile, 8 | RoomAudioRenderer, 9 | useTracks, 10 | } from "@livekit/components-react"; 11 | import "@livekit/components-styles"; 12 | import { useUser } from "@clerk/nextjs"; 13 | import { Loader2 } from "lucide-react"; 14 | import { useEffect, useState } from "react"; 15 | 16 | interface MediaRoomProps { 17 | chatId: string; 18 | video: boolean; 19 | audio: boolean; 20 | } 21 | 22 | export const MediaRoom = ({ chatId, video, audio }: MediaRoomProps) => { 23 | const { user } = useUser(); 24 | const [token, setToken] = useState(""); 25 | 26 | useEffect(() => { 27 | if (!user?.firstName || !user?.lastName) return; 28 | 29 | const name = `${user.firstName} ${user.lastName}`; 30 | 31 | (async () => { 32 | try { 33 | const res = await fetch(`/api/livekit?room=${chatId}&username=${name}`); 34 | const data = await res.json(); 35 | console.log(data); 36 | setToken(data.token); 37 | } catch (err) { 38 | console.log(err); 39 | } 40 | })(); 41 | }, [user?.firstName, user?.lastName, chatId]); 42 | 43 | if (token === "") { 44 | return ( 45 |
46 | 47 |

Loading...

48 |
49 | ); 50 | } 51 | 52 | return ( 53 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /components/mobile-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "lucide-react"; 2 | 3 | import { 4 | Sheet, 5 | SheetContent, 6 | SheetHeader, 7 | SheetTitle, 8 | SheetDescription, 9 | SheetTrigger, 10 | } from "@/components/ui/sheet"; 11 | import { Button } from "./ui/button"; 12 | import { NavigationSidebar } from "./navigation/navigation-sidebar"; 13 | import { ServerSidebar } from "./server/server-sidebar"; 14 | 15 | export const MobileToggle = ({ serverId }: { serverId: string }) => { 16 | return ( 17 | 18 | 19 | 22 | 23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/modals/create-channel-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogFooter, 12 | DialogHeader, 13 | DialogTitle, 14 | } from "@/components/ui/dialog"; 15 | 16 | import { 17 | Select, 18 | SelectContent, 19 | SelectItem, 20 | SelectTrigger, 21 | SelectValue, 22 | } from "@/components/ui/select"; 23 | 24 | import { 25 | Form, 26 | FormControl, 27 | FormField, 28 | FormItem, 29 | FormLabel, 30 | FormMessage, 31 | } from "@/components/ui/form"; 32 | 33 | import { Input } from "@/components/ui/input"; 34 | 35 | import { Button } from "@/components/ui/button"; 36 | import { useParams, useRouter } from "next/navigation"; 37 | import { useModal } from "../../hooks/use-modal-store"; 38 | import { ChannelType } from "@prisma/client"; 39 | import queryString from "query-string"; 40 | import { useEffect } from "react"; 41 | 42 | const formSchema = z.object({ 43 | name: z 44 | .string() 45 | .min(1, { 46 | message: "Channel name is required", 47 | }) 48 | .refine((name) => name !== "general" && name !== "General", { 49 | message: "Channel name cannot be 'general' or 'General", 50 | }), 51 | type: z.nativeEnum(ChannelType), 52 | }); 53 | 54 | export const CreateChannelModal = () => { 55 | const { isOpen, onClose, type, data } = useModal(); 56 | const router = useRouter(); 57 | const params = useParams(); 58 | 59 | const isModalOpen = isOpen && type === "createChannel"; 60 | 61 | const {channelType} = data; 62 | 63 | const form = useForm({ 64 | resolver: zodResolver(formSchema), 65 | defaultValues: { 66 | name: "", 67 | type: channelType || ChannelType.TEXT, 68 | }, 69 | }); 70 | 71 | useEffect(() => { 72 | if(channelType) { 73 | form.setValue("type", channelType); 74 | } else { 75 | form.setValue("type", ChannelType.TEXT); 76 | } 77 | }, [channelType, form]) 78 | 79 | const isLoading = form.formState.isSubmitting; 80 | 81 | const onSubmit = async (values: z.infer) => { 82 | try { 83 | const url = queryString.stringifyUrl({ 84 | url: "/api/channels", 85 | query: { 86 | serverId: params?.serverId, 87 | }, 88 | }); 89 | 90 | await axios.post(url, values); 91 | form.reset(); 92 | router.refresh(); 93 | onClose(); 94 | } catch (error) { 95 | console.log(error); 96 | } 97 | }; 98 | 99 | const handleClose = () => { 100 | form.reset(); 101 | onClose(); 102 | }; 103 | 104 | return ( 105 | 106 | 107 | 108 | 109 | Create Channel 110 | 111 | 112 |
113 | 114 |
115 | { 119 | return ( 120 | 121 | 122 | Channel Name 123 | 124 | 125 | 131 | 132 | 133 | 134 | ); 135 | }} 136 | /> 137 | { 141 | return ( 142 | 143 | Channel Name 144 | 166 | 167 | 168 | ); 169 | }} 170 | /> 171 |
172 | 173 | 176 | 177 |
178 | 179 |
180 |
181 | ); 182 | }; 183 | -------------------------------------------------------------------------------- /components/modals/create-server-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogFooter, 13 | DialogHeader, 14 | DialogTitle, 15 | } from "@/components/ui/dialog"; 16 | 17 | import { 18 | Form, 19 | FormControl, 20 | FormField, 21 | FormItem, 22 | FormLabel, 23 | FormMessage, 24 | } from "@/components/ui/form"; 25 | 26 | import { Input } from "@/components/ui/input"; 27 | 28 | import { Button } from "@/components/ui/button"; 29 | import { FileUpload } from "@/components/file-upload"; 30 | import { useRouter } from "next/navigation"; 31 | import { useModal } from "../../hooks/use-modal-store"; 32 | 33 | const formSchema = z.object({ 34 | name: z.string().min(1, { 35 | message: "Server name is required", 36 | }), 37 | imageUrl: z.string().min(1, { 38 | message: "Server image is required", 39 | }), 40 | }); 41 | 42 | export const CreateServerModal = () => { 43 | const { isOpen, onClose, type } = useModal(); 44 | const router = useRouter(); 45 | 46 | const isModalOpen = isOpen && type === "createServer"; 47 | 48 | const form = useForm({ 49 | resolver: zodResolver(formSchema), 50 | defaultValues: { 51 | name: "", 52 | imageUrl: "", 53 | }, 54 | }); 55 | 56 | const isLoading = form.formState.isSubmitting; 57 | 58 | const onSubmit = async (values: z.infer) => { 59 | try { 60 | await axios.post("/api/servers", values); 61 | form.reset(); 62 | router.refresh(); 63 | onClose(); 64 | } catch (error) { 65 | console.log(error); 66 | } 67 | }; 68 | 69 | const handleClose = () => { 70 | form.reset(); 71 | onClose(); 72 | } 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | Customize your server 80 | 81 | 82 | Give your server a personality with an name and image (which you can 83 | always change it later) 84 | 85 | 86 |
87 | 88 |
89 |
90 | { 94 | return ( 95 | 96 | Server image 97 | 98 | 103 | 104 | 105 | 106 | ); 107 | }} 108 | /> 109 |
110 | { 114 | return ( 115 | 116 | 117 | Server name 118 | 119 | 120 | 126 | 127 | 128 | 129 | ); 130 | }} 131 | /> 132 |
133 | 134 | 137 | 138 |
139 | 140 |
141 |
142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /components/modals/delete-channel-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog"; 9 | 10 | import { useModal } from "../../hooks/use-modal-store"; 11 | import { Button } from "@/components/ui/button"; 12 | import { useState } from "react"; 13 | import { DialogDescription } from "@radix-ui/react-dialog"; 14 | import axios from "axios"; 15 | import { useParams, useRouter } from "next/navigation"; 16 | import { revalidatePath } from "next/cache"; 17 | import queryString from "query-string"; 18 | 19 | export const DeleteChannelModal = () => { 20 | const { isOpen, onClose, type, data } = useModal(); 21 | const router = useRouter(); 22 | const params = useParams(); 23 | 24 | const isModalOpen = isOpen && type === "deleteChannel"; 25 | const { server } = data; 26 | const { channel } = data; 27 | 28 | const [isLoading, setIsLoading] = useState(false); 29 | 30 | const onClickPrimary = async () => { 31 | try { 32 | setIsLoading(true); 33 | const url = queryString.stringifyUrl({ 34 | url: `/api/channels/${channel?.id}`, 35 | query: { 36 | serverId: params?.serverId, 37 | }, 38 | }); 39 | await axios.delete(url); 40 | onClose(); 41 | router.refresh(); 42 | revalidatePath("/servers/${params?.serverId}") 43 | router.push(`/servers/${params?.serverId}`); 44 | } catch (error) { 45 | console.log(error); 46 | } finally { 47 | setIsLoading(false); 48 | } 49 | }; 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | Delete Channel 57 | 58 | 59 | Are you sure you want to permanently delete{" "} 60 | 61 | {channel?.name} 62 | {" "} 63 | channel? 64 | 65 | 66 | 67 |
68 | 71 | 78 |
79 |
80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /components/modals/delete-message-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog"; 9 | 10 | import { useModal } from "../../hooks/use-modal-store"; 11 | import { Button } from "@/components/ui/button"; 12 | import { useState } from "react"; 13 | import { DialogDescription } from "@radix-ui/react-dialog"; 14 | import axios from "axios"; 15 | import { useParams, useRouter } from "next/navigation"; 16 | import { revalidatePath } from "next/cache"; 17 | import queryString from "query-string"; 18 | 19 | export const DeleteMessageModal = () => { 20 | const { isOpen, onClose, type, data } = useModal(); 21 | const router = useRouter(); 22 | const params = useParams(); 23 | 24 | const isModalOpen = isOpen && type === "deleteMessage"; 25 | const { apiUrl, query } = data; 26 | 27 | const [isLoading, setIsLoading] = useState(false); 28 | 29 | const onClickPrimary = async () => { 30 | try { 31 | setIsLoading(true); 32 | const url = queryString.stringifyUrl({ 33 | url: apiUrl || "", 34 | query, 35 | }); 36 | await axios.delete(url); 37 | onClose(); 38 | } catch (error) { 39 | console.log(error); 40 | } finally { 41 | setIsLoading(false); 42 | } 43 | }; 44 | 45 | return ( 46 | 47 | 48 | 49 | 50 | Delete Message 51 | 52 | 53 | Are you sure you want to permanently delete the message? 54 | 55 | 56 | 57 |
58 | 61 | 68 |
69 |
70 |
71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /components/modals/delete-server-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog"; 9 | 10 | import { useModal } from "../../hooks/use-modal-store"; 11 | import { Button } from "@/components/ui/button"; 12 | import { useState } from "react"; 13 | import { DialogDescription } from "@radix-ui/react-dialog"; 14 | import axios from "axios"; 15 | import { useRouter } from "next/navigation"; 16 | import { revalidatePath } from "next/cache"; 17 | 18 | export const DeleteServerModal = () => { 19 | const { isOpen, onClose, type, data } = useModal(); 20 | const router = useRouter(); 21 | 22 | const isModalOpen = isOpen && type === "deleteServer"; 23 | const { server } = data; 24 | 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | const onClickPrimary = async () => { 28 | try { 29 | setIsLoading(true); 30 | await axios.delete(`/api/servers/${server?.id}`); 31 | onClose(); 32 | router.refresh(); 33 | revalidatePath("/servers"); 34 | router.push("/"); 35 | } catch (error) { 36 | console.log(error); 37 | } finally { 38 | setIsLoading(false); 39 | } 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | Delete Server 48 | 49 | 50 | Are you sure you want to permanently delete{" "} 51 | 52 | {server?.name} 53 | {" "} 54 | server? 55 | 56 | 57 | 58 |
59 | 62 | 65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /components/modals/edit-channel-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogFooter, 12 | DialogHeader, 13 | DialogTitle, 14 | } from "@/components/ui/dialog"; 15 | 16 | import { 17 | Select, 18 | SelectContent, 19 | SelectItem, 20 | SelectTrigger, 21 | SelectValue, 22 | } from "@/components/ui/select"; 23 | 24 | import { 25 | Form, 26 | FormControl, 27 | FormField, 28 | FormItem, 29 | FormLabel, 30 | FormMessage, 31 | } from "@/components/ui/form"; 32 | 33 | import { Input } from "@/components/ui/input"; 34 | 35 | import { Button } from "@/components/ui/button"; 36 | import { useParams, useRouter } from "next/navigation"; 37 | import { useModal } from "../../hooks/use-modal-store"; 38 | import { ChannelType } from "@prisma/client"; 39 | import queryString from "query-string"; 40 | import { useEffect } from "react"; 41 | 42 | const formSchema = z.object({ 43 | name: z 44 | .string() 45 | .min(1, { 46 | message: "Channel name is required", 47 | }) 48 | .refine((name) => name !== "general" && name !== "General", { 49 | message: "Channel name cannot be 'general' or 'General", 50 | }), 51 | type: z.nativeEnum(ChannelType), 52 | }); 53 | 54 | export const EditChannelModal = () => { 55 | const { isOpen, onClose, type, data } = useModal(); 56 | const router = useRouter(); 57 | const params = useParams(); 58 | 59 | const isModalOpen = isOpen && type === "editChannel"; 60 | 61 | const {channel, server} = data; 62 | 63 | const form = useForm({ 64 | resolver: zodResolver(formSchema), 65 | defaultValues: { 66 | name: "", 67 | type: channel?.type || ChannelType.TEXT, 68 | }, 69 | }); 70 | 71 | useEffect(() => { 72 | if(channel){ 73 | form.setValue("name", channel.name); 74 | form.setValue("type", channel.type); 75 | } 76 | }, [form, channel]); 77 | 78 | const isLoading = form.formState.isSubmitting; 79 | 80 | const onSubmit = async (values: z.infer) => { 81 | try { 82 | const url = queryString.stringifyUrl({ 83 | url: `/api/channels/${channel?.id}`, 84 | query: { 85 | serverId: server?.id, 86 | }, 87 | }); 88 | 89 | await axios.patch(url, values); 90 | form.reset(); 91 | router.refresh(); 92 | onClose(); 93 | } catch (error) { 94 | console.log(error); 95 | } 96 | }; 97 | 98 | const handleClose = () => { 99 | form.reset(); 100 | onClose(); 101 | }; 102 | 103 | return ( 104 | 105 | 106 | 107 | 108 | Edit Channel 109 | 110 | 111 |
112 | 113 |
114 | { 118 | return ( 119 | 120 | 121 | Channel Name 122 | 123 | 124 | 130 | 131 | 132 | 133 | ); 134 | }} 135 | /> 136 | { 140 | return ( 141 | 142 | Channel Name 143 | 165 | 166 | 167 | ); 168 | }} 169 | /> 170 |
171 | 172 | 175 | 176 |
177 | 178 |
179 |
180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /components/modals/edit-server-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogFooter, 13 | DialogHeader, 14 | DialogTitle, 15 | } from "@/components/ui/dialog"; 16 | 17 | import { 18 | Form, 19 | FormControl, 20 | FormField, 21 | FormItem, 22 | FormLabel, 23 | FormMessage, 24 | } from "@/components/ui/form"; 25 | 26 | import { Input } from "@/components/ui/input"; 27 | 28 | import { Button } from "@/components/ui/button"; 29 | import { FileUpload } from "@/components/file-upload"; 30 | import { useRouter } from "next/navigation"; 31 | import { useModal } from "../../hooks/use-modal-store"; 32 | import { useEffect } from "react"; 33 | 34 | const formSchema = z.object({ 35 | name: z.string().min(1, { 36 | message: "Server name is required", 37 | }), 38 | imageUrl: z.string().min(1, { 39 | message: "Server image is required", 40 | }), 41 | }); 42 | 43 | export const EditServerModal = () => { 44 | const { isOpen, onClose, type, data } = useModal(); 45 | const router = useRouter(); 46 | 47 | const isModalOpen = isOpen && type === "editServer"; 48 | const { server } = data; 49 | 50 | const form = useForm({ 51 | resolver: zodResolver(formSchema), 52 | defaultValues: { 53 | name: "", 54 | imageUrl: "", 55 | }, 56 | }); 57 | 58 | useEffect(() => { 59 | if (server) { 60 | form.setValue("name", server.name); 61 | form.setValue("imageUrl", server.imageUrl); 62 | } 63 | }, [server, form]); 64 | 65 | const isLoading = form.formState.isSubmitting; 66 | 67 | const onSubmit = async (values: z.infer) => { 68 | try { 69 | const res = await axios.patch(`/api/servers/${server?.id}`, values); 70 | form.reset(); 71 | router.refresh(); 72 | onClose(); 73 | } catch (error) { 74 | console.log(error); 75 | } 76 | }; 77 | 78 | const handleClose = () => { 79 | form.reset(); 80 | onClose(); 81 | }; 82 | 83 | return ( 84 | 85 | 86 | 87 | 88 | Customize your server 89 | 90 | 91 | Give your server a personality with an name and image (which you can 92 | always change it later) 93 | 94 | 95 |
96 | 97 |
98 |
99 | { 103 | return ( 104 | 105 | Server image 106 | 107 | 112 | 113 | 114 | 115 | ); 116 | }} 117 | /> 118 |
119 | { 123 | return ( 124 | 125 | 126 | Server name 127 | 128 | 129 | 135 | 136 | 137 | 138 | ); 139 | }} 140 | /> 141 |
142 | 143 | 146 | 147 |
148 | 149 |
150 |
151 | ); 152 | }; 153 | -------------------------------------------------------------------------------- /components/modals/initial-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogFooter, 13 | DialogHeader, 14 | DialogTitle, 15 | } from "@/components/ui/dialog"; 16 | 17 | import { 18 | Form, 19 | FormControl, 20 | FormField, 21 | FormItem, 22 | FormLabel, 23 | FormMessage, 24 | } from "@/components/ui/form"; 25 | 26 | import { Input } from "@/components/ui/input"; 27 | 28 | import { Button } from "@/components/ui/button"; 29 | import { useEffect, useState } from "react"; 30 | import { FileUpload } from "../file-upload"; 31 | import { useRouter } from "next/navigation"; 32 | 33 | const formSchema = z.object({ 34 | name: z.string().min(1, { 35 | message: "Server name is required", 36 | }), 37 | imageUrl: z.string().min(1, { 38 | message: "Server image is required", 39 | }), 40 | }); 41 | 42 | export const InitialModal = () => { 43 | const [isMounted, setIsMounted] = useState(false); 44 | 45 | const router = useRouter(); 46 | 47 | useEffect(() => { 48 | setIsMounted(true); 49 | }, []); 50 | 51 | const form = useForm({ 52 | resolver: zodResolver(formSchema), 53 | defaultValues: { 54 | name: "", 55 | imageUrl: "", 56 | }, 57 | }); 58 | 59 | const isLoading = form.formState.isSubmitting; 60 | 61 | const onSubmit = async (values: z.infer) => { 62 | try { 63 | await axios.post("/api/servers", values); 64 | form.reset(); 65 | router.refresh(); 66 | window.location.reload(); 67 | } catch (error){ 68 | console.log(error); 69 | } 70 | }; 71 | 72 | if (!isMounted) return null; 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | Customize your server 80 | 81 | 82 | Give your server a personality with an name and image (which you can 83 | always change it later) 84 | 85 | 86 |
87 | 88 |
89 |
90 | { 94 | return ( 95 | 96 | Server image 97 | 98 | 103 | 104 | 105 | 106 | ); 107 | }} 108 | /> 109 |
110 | { 114 | return ( 115 | 116 | 117 | Server name 118 | 119 | 120 | 126 | 127 | 128 | 129 | ); 130 | }} 131 | /> 132 |
133 | 134 | 137 | 138 |
139 | 140 |
141 |
142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /components/modals/invite-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog"; 9 | 10 | import { useModal } from "../../hooks/use-modal-store"; 11 | import { Label } from "@/components/ui/label"; 12 | import { Input } from "@/components/ui/input"; 13 | import { Button } from "@/components/ui/button"; 14 | import { Check, Copy, RefreshCw } from "lucide-react"; 15 | import { useOrigin } from "@/hooks/use-origin"; 16 | import { useState } from "react"; 17 | import axios from "axios"; 18 | 19 | export const InviteModal = () => { 20 | const { isOpen, onOpen, onClose, type, data } = useModal(); 21 | const origin = useOrigin(); 22 | 23 | const isModalOpen = isOpen && type === "invite"; 24 | const { server } = data; 25 | 26 | const [copied, setCopied] = useState(false); 27 | const [isLoading, setIsLoading] = useState(false); 28 | 29 | const inviteUrl = `${origin}/invite/${server?.inviteCode}`; 30 | 31 | const onCopy = () => { 32 | navigator.clipboard.writeText(inviteUrl); 33 | setCopied(true); 34 | 35 | setTimeout(() => { 36 | setCopied(false); 37 | }, 1000); 38 | }; 39 | 40 | const onNew = async () => { 41 | try { 42 | setIsLoading(true); 43 | const response = await axios.patch( 44 | `/api/servers/${server?.id}/invite-code` 45 | ); 46 | 47 | onOpen("invite", { server: response.data }); 48 | } catch (err) { 49 | console.log(err); 50 | } finally { 51 | setIsLoading(false); 52 | } 53 | }; 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | Invite friends 61 | 62 | 63 |
64 | 67 |
68 | 73 | 80 |
81 | 91 |
92 |
93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /components/modals/leave-server-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from "@/components/ui/dialog"; 9 | 10 | import { useModal } from "../../hooks/use-modal-store"; 11 | import { Button } from "@/components/ui/button"; 12 | import { useState } from "react"; 13 | import { DialogDescription } from "@radix-ui/react-dialog"; 14 | import axios from "axios"; 15 | import { useRouter } from "next/navigation"; 16 | import { revalidatePath } from "next/cache"; 17 | 18 | export const LeaveServerModal = () => { 19 | const { isOpen, onClose, type, data } = useModal(); 20 | const router = useRouter(); 21 | 22 | const isModalOpen = isOpen && type === "leaveServer"; 23 | const { server } = data; 24 | 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | const onClickPrimary = async () => { 28 | try { 29 | setIsLoading(true); 30 | await axios.patch(`/api/servers/${server?.id}/leave`); 31 | onClose(); 32 | router.refresh(); 33 | revalidatePath("/servers"); 34 | router.push("/"); 35 | } catch (error) { 36 | console.log(error); 37 | } finally { 38 | setIsLoading(false); 39 | } 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | Leave Server 48 | 49 | 50 | Are you sure you want to leave{" "} 51 | 52 | {server?.name} 53 | {" "} 54 | server? 55 | 56 | 57 | 58 |
59 | 62 | 65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /components/modals/message-file-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import axios from "axios"; 7 | 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogFooter, 12 | DialogHeader, 13 | DialogTitle, 14 | } from "@/components/ui/dialog"; 15 | 16 | import { 17 | Form, 18 | FormControl, 19 | FormField, 20 | FormItem, 21 | FormMessage, 22 | } from "@/components/ui/form"; 23 | 24 | import { Button } from "@/components/ui/button"; 25 | import { FileUpload } from "@/components/file-upload"; 26 | import { useRouter } from "next/navigation"; 27 | import { useModal } from "../../hooks/use-modal-store"; 28 | import queryString from "query-string"; 29 | 30 | const formSchema = z.object({ 31 | fileUrl: z.string().min(1, { 32 | message: "Attachement is required", 33 | }), 34 | }); 35 | 36 | export const MessageFileModal = () => { 37 | const { isOpen, onClose, type, data } = useModal(); 38 | const router = useRouter(); 39 | const { apiUrl, query } = data; 40 | 41 | const isModalOpen = isOpen && type === "messageFile"; 42 | 43 | const form = useForm({ 44 | resolver: zodResolver(formSchema), 45 | defaultValues: { 46 | fileUrl: "", 47 | }, 48 | }); 49 | 50 | const isLoading = form.formState.isSubmitting; 51 | 52 | const onSubmit = async (values: z.infer) => { 53 | try { 54 | const url = queryString.stringifyUrl({ 55 | url: apiUrl || "", 56 | query, 57 | }); 58 | 59 | await axios.post(url, { 60 | ...values, 61 | fileUrl: values.fileUrl, 62 | }); 63 | form.reset(); 64 | router.refresh(); 65 | onClose(); 66 | } catch (error) { 67 | console.log(error); 68 | } 69 | }; 70 | 71 | const handleClose = () => { 72 | form.reset(); 73 | onClose(); 74 | }; 75 | 76 | return ( 77 | 78 | 79 | 80 | 81 | Add an attachement 82 | 83 | 84 |
85 | 86 |
87 |
88 | { 92 | return ( 93 | 94 | 95 | 100 | 101 | 102 | 103 | ); 104 | }} 105 | /> 106 |
107 |
108 | 109 | 112 | 113 |
114 | 115 |
116 |
117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Moon, Sun } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/navigation/navigation-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Plus } from "lucide-react"; 4 | import { ActionTooltip } from "../action-tooltip"; 5 | import { useModal } from "@/hooks/use-modal-store"; 6 | 7 | const NavigationAction = () => { 8 | const { onOpen } = useModal(); 9 | return ( 10 |
11 | 12 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default NavigationAction; 28 | -------------------------------------------------------------------------------- /components/navigation/navigation-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useParams, useRouter } from "next/navigation"; 4 | import Image from "next/image"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { ActionTooltip } from "../action-tooltip"; 8 | import { blurDataImageUrl } from "@/lib/blur-data-img"; 9 | 10 | interface NavigationItemProps { 11 | id: string; 12 | name: string; 13 | imageUrl: string; 14 | } 15 | 16 | export const NavigationItem = ({ id, name, imageUrl }: NavigationItemProps) => { 17 | const params = useParams(); 18 | const router = useRouter(); 19 | const onClick = () => { 20 | router.push(`/servers/${id}`); 21 | } 22 | return ( 23 | 24 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /components/navigation/navigation-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { currentProfile } from "@/lib/current-profile"; 2 | import { db } from "@/lib/db"; 3 | import { redirect } from "next/navigation"; 4 | import NavigationAction from "./navigation-action"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import { ScrollArea } from "../ui/scroll-area"; 7 | import { NavigationItem } from "./navigation-item"; 8 | import { ModeToggle } from "../mode-toggle"; 9 | import { UserButton } from "@clerk/nextjs"; 10 | 11 | export const NavigationSidebar = async () => { 12 | const profile = await currentProfile(); 13 | 14 | if (!profile) return redirect("/"); 15 | 16 | const servers = await db.server.findMany({ 17 | where: { 18 | members: { 19 | some: { 20 | profileId: profile.id, 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | {servers.map((server) => ( 32 |
33 | 38 |
39 | ))} 40 |
41 |
42 | 43 | 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /components/providers/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CreateServerModal } from "@/components/modals/create-server-modal"; 4 | import { InviteModal } from "@/components/modals/invite-modal"; 5 | import { useEffect, useState } from "react"; 6 | import { EditServerModal } from "../modals/edit-server-modal"; 7 | import { MembersModal } from "../modals/members-modal"; 8 | import { CreateChannelModal } from "../modals/create-channel-modal"; 9 | import { LeaveServerModal } from "../modals/leave-server-modal"; 10 | import { DeleteServerModal } from "../modals/delete-server-modal"; 11 | import { DeleteChannelModal } from "../modals/delete-channel-modal"; 12 | import { EditChannelModal } from "../modals/edit-channel-modal"; 13 | import { MessageFileModal } from "../modals/message-file-modal"; 14 | import { DeleteMessageModal } from "../modals/delete-message-modal"; 15 | 16 | export const ModalProvider = () => { 17 | const [isMounted, setIsMounted] = useState(false); 18 | 19 | useEffect(() => { 20 | setIsMounted(true); 21 | }, []) 22 | 23 | if(!isMounted) { 24 | return null; 25 | } 26 | 27 | return ( 28 | <> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } -------------------------------------------------------------------------------- /components/providers/query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { useState } from "react"; 5 | 6 | export const QueryProvider = ({ children }: { children: React.ReactNode }) => { 7 | const [queryClient] = useState(() => new QueryClient()); 8 | return ( 9 | {children} 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /components/providers/socket-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useContext, useEffect, useState } from "react"; 4 | import { io as ClientIO } from "socket.io-client"; 5 | 6 | type SocketContextType = { 7 | socket: any | null; 8 | isConnected: boolean; 9 | }; 10 | 11 | const SocketContext = createContext({ 12 | socket: null, 13 | isConnected: false, 14 | }); 15 | 16 | export const SocketProvider = ({ children }: { children: React.ReactNode }) => { 17 | const [socket, setSocket] = useState(false); 18 | const [isConnected, setIsConnected] = useState(false); 19 | 20 | useEffect(() => { 21 | const socketInstance = new (ClientIO as any)( 22 | process.env.NEXT_PUBLIC_SITE_URL!, 23 | { 24 | path: "/api/socket/io", 25 | addTrailingSlash: false, 26 | } 27 | ); 28 | 29 | socketInstance.on("connect", () => { 30 | setIsConnected(true); 31 | }); 32 | 33 | socketInstance.on("disconnect", () => { 34 | setIsConnected(false); 35 | }); 36 | 37 | setSocket(socketInstance); 38 | 39 | return () => { 40 | socketInstance.disconnect(); 41 | }; 42 | }, []); 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | }; 50 | 51 | export const useSocket = () => { 52 | const context = useContext(SocketContext); 53 | if (context === undefined) 54 | throw new Error( 55 | "useSocket context is used outside the SocketContext Provider" 56 | ); 57 | return context; 58 | }; 59 | -------------------------------------------------------------------------------- /components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /components/server/server-channel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Channel, ChannelType, MemberRole, Server } from "@prisma/client"; 5 | import { Edit, Hash, icons, Lock, Mic, Trash, Video } from "lucide-react"; 6 | import { useParams, useRouter } from "next/navigation"; 7 | import { ActionTooltip } from "../action-tooltip"; 8 | import { ModalType, useModal } from "@/hooks/use-modal-store"; 9 | 10 | interface ServerChannelProps { 11 | channel: Channel; 12 | server: Server; 13 | role?: MemberRole; 14 | } 15 | 16 | const iconStore = { 17 | [ChannelType.AUDIO]: Mic, 18 | [ChannelType.VIDEO]: Video, 19 | [ChannelType.TEXT]: Hash, 20 | }; 21 | 22 | export const ServerChannel = ({ 23 | channel, 24 | server, 25 | role, 26 | }: ServerChannelProps) => { 27 | const { onOpen } = useModal(); 28 | const params = useParams(); 29 | const router = useRouter(); 30 | 31 | const Icon = iconStore[channel?.type]; 32 | const onClick = () => { 33 | router.push(`/servers/${server.id}/channels/${channel.id}`); 34 | }; 35 | const onAction = (e: React.MouseEvent, action: ModalType) => { 36 | e.stopPropagation(); 37 | onOpen(action, {server, channel}); 38 | } 39 | return ( 40 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /components/server/server-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ServerWithMembersWithProfiles } from "@/types"; 4 | import { MemberRole } from "@prisma/client"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuTrigger, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuSeparator, 11 | } from "../ui/dropdown-menu"; 12 | import { 13 | ChevronDown, 14 | ChevronUp, 15 | LogOut, 16 | PlusCircle, 17 | Settings, 18 | Trash, 19 | User, 20 | UserPlus, 21 | } from "lucide-react"; 22 | import { useState } from "react"; 23 | import { useModal } from "@/hooks/use-modal-store"; 24 | 25 | interface ServerHeaderProps { 26 | server: ServerWithMembersWithProfiles; 27 | role?: MemberRole; 28 | type?: string; 29 | } 30 | 31 | export const ServerHeader = ({ server, role, type }: ServerHeaderProps) => { 32 | const { onOpen } = useModal(); 33 | const [state, setState] = useState(false); 34 | const isAdmin = role === MemberRole.ADMIN; 35 | const isModerator = isAdmin || role === MemberRole.MODERATOR; 36 | const onClickHandler = () => { 37 | setState((state) => !state); 38 | }; 39 | 40 | return ( 41 | 42 | 43 | 58 | 59 | 60 | {isModerator && ( 61 | onOpen("invite", { server })} 63 | className="text-indigo-500 focus:text-indigo-500 px-3 py-2 text-sm cursor-pointer" 64 | > 65 | Invite People 66 | 67 | 68 | )} 69 | {isAdmin && ( 70 | onOpen("editServer", { server })} 72 | className="px-3 py-2 text-sm cursor-pointer" 73 | > 74 | Server Settings 75 | 76 | 77 | )} 78 | {isAdmin && ( 79 | onOpen("members", { server })} 81 | className="px-3 py-2 text-sm cursor-pointer" 82 | > 83 | Manage Members 84 | 85 | 86 | )} 87 | {isModerator && ( 88 | onOpen("createChannel", { server })} 90 | className="px-3 py-2 text-sm cursor-pointer" 91 | > 92 | Create Channel 93 | 94 | 95 | )} 96 | {isModerator && } 97 | {isAdmin && ( 98 | onOpen("deleteServer", { server })} 100 | className="dark:bg-rose-500/60 bg-rose-500 focus:bg-rose-500 focus:text-white focus:ring-1 text-white px-3 py-2 text-sm cursor-pointer" 101 | > 102 | Delete Server 103 | 104 | 105 | )} 106 | {!isAdmin && ( 107 | onOpen("leaveServer", { server })} 109 | className="dark:bg-rose-500/60 bg-rose-500 focus:bg-rose-500 focus:text-white focus:ring-1 text-white px-3 py-2 text-sm cursor-pointer" 110 | > 111 | Leave Server 112 | 113 | 114 | )} 115 | 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /components/server/server-member.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Member, MemberRole, Profile } from "@prisma/client"; 5 | import { Server } from "@prisma/client"; 6 | import { ShieldAlert, ShieldCheck } from "lucide-react"; 7 | import { useParams } from "next/navigation"; 8 | import { useRouter } from "next/navigation"; 9 | import { UserAvatar } from "../user-avatar"; 10 | 11 | interface ServerMemberProps { 12 | member: Member & { profile: Profile }; 13 | server: Server; 14 | } 15 | 16 | const roleIconStore = { 17 | [MemberRole.ADMIN]: , 18 | [MemberRole.GUEST]: null, 19 | [MemberRole.MODERATOR]: , 20 | }; 21 | 22 | export const ServerMember = ({ member, server }: ServerMemberProps) => { 23 | const params = useParams(); 24 | const router = useRouter(); 25 | 26 | const icon = roleIconStore[member?.role]; 27 | 28 | const onClick = () => { 29 | router.push(`/servers/${server.id}/conversations/${member.id}`); 30 | } 31 | 32 | return ( 33 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /components/server/server-search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Search } from "lucide-react"; 4 | import React, { useEffect, useState } from "react"; 5 | import { ctrlCombinations } from "@/lib/utils"; 6 | import { 7 | CommandDialog, 8 | CommandInput, 9 | CommandEmpty, 10 | CommandList, 11 | CommandGroup, 12 | CommandItem, 13 | } from "../ui/command"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | 16 | interface ServerSearchProps { 17 | data: { 18 | label: string; 19 | type: "channel" | "member"; 20 | data: 21 | | { 22 | icon: React.ReactNode; 23 | name: string; 24 | id: string; 25 | }[] 26 | | undefined; 27 | }[]; 28 | } 29 | 30 | export const ServerSearch = ({ data }: ServerSearchProps) => { 31 | const [open, setOpen] = useState(false); 32 | const router = useRouter(); 33 | const params = useParams(); 34 | 35 | useEffect(() => { 36 | const down = (e: KeyboardEvent) => { 37 | if(e.key === 'k' && (e.metaKey || e.ctrlKey)){ 38 | e.preventDefault(); 39 | setOpen((open) => !open); 40 | } 41 | } 42 | 43 | document.addEventListener("keydown", down); 44 | 45 | return () => document.removeEventListener("keydown", down); 46 | }, []); 47 | 48 | const onClick = ({id, type}: {id: string, type: "channel" | "member"}) => { 49 | setOpen(false); 50 | 51 | if(type === "member"){ 52 | router.push(`/servers/${params?.serverId}/conversations/${id}`); 53 | } 54 | 55 | if(type === "channel"){ 56 | router.push(`/servers/${params?.serverId}/channels/${id}`); 57 | } 58 | } 59 | 60 | return ( 61 | <> 62 | 74 | 75 | 76 | 77 | No Results found 78 | {data.map(({ label, type, data }) => { 79 | if (!data?.length) return null; 80 | 81 | return ( 82 | 83 | {data.map(({ name, icon, id }) => { 84 | return ( 85 | 86 | {icon} 87 | {name} 88 | 89 | ); 90 | })} 91 | 92 | ); 93 | })} 94 | 95 | 96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /components/server/server-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ServerWithMembersWithProfiles } from "@/types"; 4 | import { ChannelType, MemberRole } from "@prisma/client"; 5 | import { ActionTooltip } from "../action-tooltip"; 6 | import { Plus, Settings } from "lucide-react"; 7 | import { useModal } from "@/hooks/use-modal-store"; 8 | 9 | interface ServerSectionInterface { 10 | label: string; 11 | role?: MemberRole; 12 | sectionType: "channel" | "member"; 13 | channelType?: ChannelType; 14 | server?: ServerWithMembersWithProfiles; 15 | } 16 | 17 | export const ServerSection = ({ 18 | label, 19 | role, 20 | sectionType, 21 | channelType, 22 | server, 23 | }: ServerSectionInterface): React.JSX.Element | null => { 24 | const { onOpen } = useModal(); 25 | return ( 26 |
27 |

28 | {label} 29 |

30 | {role !== MemberRole.GUEST && sectionType === "channel" && ( 31 | 32 | 38 | 39 | )} 40 | {role === MemberRole.ADMIN && sectionType === "member" && ( 41 | 42 | 48 | 49 | )} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /components/socket-indicator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { useSocket } from "./providers/socket-provider"; 5 | import { Badge } from "./ui/badge"; 6 | 7 | export const SocketIndicator = () => { 8 | const { isConnected } = useSocket(); 9 | 10 | if (!isConnected) { 11 | return ( 12 | 13 | 🛰 Waiting for connection... 14 | 15 | ); 16 | } 17 | 18 | return ( 19 | 20 | Connection is established 👍 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | primary: "bg-indigo-500 text-white hover:bg-indigo-500/90", 22 | modeToggle: "dark:bg-[#313338] border border-input bg-background hover:bg-accent hover:text-accent-foreground" 23 | }, 24 | size: { 25 | default: "h-10 px-4 py-2", 26 | sm: "h-9 rounded-md px-3", 27 | lg: "h-11 rounded-md px-8", 28 | icon: "h-10 w-10", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button" 47 | return ( 48 | 53 | ) 54 | } 55 | ) 56 | Button.displayName = "Button" 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )) 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )) 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )) 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )) 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )) 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ) 142 | } 143 | CommandShortcut.displayName = "CommandShortcut" 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | } 156 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |