├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Avatar, AvatarImage } from "./ui/avatar";
3 | import { AvatarFallback } from "@radix-ui/react-avatar";
4 | import Image from "next/image";
5 | import { blurDataImageUrl } from "@/lib/blur-data-img";
6 |
7 | interface UserAvatarProps {
8 | src?: string;
9 | className?: string;
10 | }
11 |
12 | export const UserAvatar = ({ src, className }: UserAvatarProps) => {
13 | return (
14 |
15 |
16 |
17 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/github_assets/10pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/10pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/10pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/10pic.PNG
--------------------------------------------------------------------------------
/github_assets/11oic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/11oic.PNG
--------------------------------------------------------------------------------
/github_assets/11pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/11pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/12pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/12pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/12pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/12pic.PNG
--------------------------------------------------------------------------------
/github_assets/13mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/13mobile.PNG
--------------------------------------------------------------------------------
/github_assets/13pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/13pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/13pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/13pic.PNG
--------------------------------------------------------------------------------
/github_assets/1mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/1mobile.PNG
--------------------------------------------------------------------------------
/github_assets/1pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/1pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/1pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/1pic.PNG
--------------------------------------------------------------------------------
/github_assets/2mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/2mobile.PNG
--------------------------------------------------------------------------------
/github_assets/2pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/2pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/2pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/2pic.PNG
--------------------------------------------------------------------------------
/github_assets/3mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/3mobile.PNG
--------------------------------------------------------------------------------
/github_assets/3pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/3pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/3pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/3pic.PNG
--------------------------------------------------------------------------------
/github_assets/4mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/4mobile.PNG
--------------------------------------------------------------------------------
/github_assets/4pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/4pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/4pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/4pic.PNG
--------------------------------------------------------------------------------
/github_assets/5mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/5mobile.PNG
--------------------------------------------------------------------------------
/github_assets/5pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/5pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/5pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/5pic.PNG
--------------------------------------------------------------------------------
/github_assets/6mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/6mobile.PNG
--------------------------------------------------------------------------------
/github_assets/6pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/6pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/6pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/6pic.PNG
--------------------------------------------------------------------------------
/github_assets/7mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/7mobile.PNG
--------------------------------------------------------------------------------
/github_assets/7pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/7pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/7pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/7pic.PNG
--------------------------------------------------------------------------------
/github_assets/8mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/8mobile.PNG
--------------------------------------------------------------------------------
/github_assets/8pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/8pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/8pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/8pic.PNG
--------------------------------------------------------------------------------
/github_assets/9mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/9mobile.PNG
--------------------------------------------------------------------------------
/github_assets/9pic-light.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/9pic-light.PNG
--------------------------------------------------------------------------------
/github_assets/9pic.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/9pic.PNG
--------------------------------------------------------------------------------
/github_assets/archive/second-mobile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/archive/second-mobile.PNG
--------------------------------------------------------------------------------
/github_assets/discord-logo-discord-icon-transparent-free-png.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TsotnePharsenadze/Discord-react-nextjs-typescript/077ba3a3f2da14de9810c645725911003292e96f/github_assets/discord-logo-discord-icon-transparent-free-png.webp
--------------------------------------------------------------------------------
/hooks/use-chat-query.ts:
--------------------------------------------------------------------------------
1 | import { useSocket } from "@/components/providers/socket-provider";
2 | import { useInfiniteQuery } from "@tanstack/react-query";
3 | import queryString from "query-string";
4 |
5 | interface ChatQueryProps {
6 | queryKey: string;
7 | apiUrl: string;
8 | paramKey: "channelId" | "conversationId";
9 | paramValue: string;
10 | }
11 |
12 | export const useChatQuery = ({
13 | queryKey,
14 | apiUrl,
15 | paramKey,
16 | paramValue,
17 | }: ChatQueryProps) => {
18 | const { isConnected } = useSocket();
19 |
20 | const fetchMessages = async ({ pageParam = undefined }) => {
21 | const url = queryString.stringifyUrl(
22 | {
23 | url: apiUrl,
24 | query: {
25 | cursor: pageParam,
26 | [paramKey]: paramValue,
27 | },
28 | },
29 | { skipNull: true }
30 | );
31 | const res = await fetch(url);
32 | if (!res.ok) {
33 | throw new Error("Network response was not ok");
34 | }
35 | return res.json();
36 | };
37 |
38 | const {
39 | data,
40 | fetchNextPage,
41 | hasNextPage,
42 | isFetchingNextPage,
43 | status,
44 | } = useInfiniteQuery({
45 | queryKey: [queryKey, paramValue],
46 | queryFn: fetchMessages,
47 | getNextPageParam: (lastPage) => lastPage?.nextCursor,
48 | initialPageParam: undefined,
49 | refetchInterval: 1000,
50 | });
51 |
52 | return {
53 | data,
54 | fetchNextPage,
55 | hasNextPage,
56 | isFetchingNextPage,
57 | status,
58 | };
59 | };
60 |
--------------------------------------------------------------------------------
/hooks/use-chat-scroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | type ChatScrollProps = {
4 | chatRef: React.RefObject;
5 | bottomRef: React.RefObject;
6 | shouldLoadMore: boolean;
7 | loadMore: () => void;
8 | count: number;
9 | };
10 |
11 | export const useChatScroll = ({
12 | chatRef,
13 | bottomRef,
14 | shouldLoadMore,
15 | loadMore,
16 | count,
17 | }: ChatScrollProps) => {
18 | const [hasInit, setHasInit] = useState(false);
19 |
20 | useEffect(() => {
21 | const topDiv = chatRef?.current;
22 |
23 | const handleScroll = () => {
24 | const scrollTop = topDiv?.scrollTop;
25 |
26 | if (scrollTop === 0 && shouldLoadMore) {
27 | loadMore();
28 | }
29 | };
30 |
31 | topDiv?.addEventListener("scroll", handleScroll);
32 |
33 | return () => {
34 | topDiv?.removeEventListener("scroll", handleScroll);
35 | };
36 | }, [shouldLoadMore, loadMore, chatRef]);
37 |
38 | useEffect(() => {
39 | const bottomDiv = bottomRef?.current;
40 | const topDiv = chatRef?.current;
41 |
42 | const shouldAutoScroll = () => {
43 | if (!hasInit && bottomDiv) {
44 | setHasInit(true);
45 | return true;
46 | }
47 | if (!topDiv) return false;
48 |
49 | const distanceFromBottom =
50 | topDiv.scrollHeight - topDiv.scrollTop - topDiv.clientHeight;
51 | return distanceFromBottom <= 100;
52 | };
53 |
54 | if (shouldAutoScroll()) {
55 | setTimeout(() => {
56 | bottomRef.current?.scrollIntoView({
57 | behavior: "smooth",
58 | });
59 | }, 100);
60 | }
61 | }, [bottomRef, chatRef, count, hasInit]);
62 | };
63 |
--------------------------------------------------------------------------------
/hooks/use-chat-socket.ts:
--------------------------------------------------------------------------------
1 | import { useSocket } from "@/components/providers/socket-provider";
2 | import { MessageWithMemberWithProfile } from "@/types";
3 | import { useQueryClient } from "@tanstack/react-query";
4 | import PagesManifestPlugin from "next/dist/build/webpack/plugins/pages-manifest-plugin";
5 | import { useEffect } from "react";
6 |
7 | type UseChatSocketProps = {
8 | addKey: string;
9 | updateKey: string;
10 | queryKey: string;
11 | };
12 |
13 | export const useChatSocket = ({
14 | addKey,
15 | updateKey,
16 | queryKey,
17 | }: UseChatSocketProps) => {
18 | const { socket } = useSocket();
19 | const queryClient = useQueryClient();
20 |
21 | useEffect(() => {
22 | if (!socket) return;
23 |
24 | socket.on(updateKey, (message: MessageWithMemberWithProfile) => {
25 | queryClient.setQueryData([queryKey], (oldData: any) => {
26 | if (!oldData || !oldData.pages || oldData.pages.length == 0) {
27 | return oldData;
28 | }
29 | const newData = oldData.pages.map((page: any) => {
30 | return {
31 | ...page,
32 | items: page.items.map((item: MessageWithMemberWithProfile) => {
33 | if (item.id === message.id) {
34 | return message;
35 | }
36 | return item;
37 | }),
38 | };
39 | });
40 | return {
41 | ...oldData,
42 | pages: newData,
43 | };
44 | });
45 | });
46 |
47 | socket.on(addKey, (message: MessageWithMemberWithProfile) => {
48 | queryClient.setQueryData([queryKey], (oldData: any) => {
49 | if (!oldData || !oldData.pages || oldData.pages.length == 0) {
50 | return {
51 | pages: [
52 | {
53 | items: [message],
54 | },
55 | ],
56 | };
57 | }
58 |
59 | const newData = [...oldData.pages];
60 |
61 | newData[0] = {
62 | ...newData[0],
63 | items: [message, ...newData[0].items],
64 | };
65 |
66 | return {
67 | ...oldData,
68 | pages: newData,
69 | };
70 | });
71 | });
72 |
73 | return () => {
74 | socket.off(addKey);
75 | socket.off(updateKey);
76 | };
77 | }, [queryClient, addKey, queryKey, socket, updateKey]);
78 | };
79 |
--------------------------------------------------------------------------------
/hooks/use-modal-store.ts:
--------------------------------------------------------------------------------
1 | import { Channel, ChannelType, Server } from "@prisma/client";
2 | import {create} from "zustand"
3 |
4 | export type ModalType = "createServer" | "invite" | "editServer" | "createChannel" | "members" | "leaveServer" | "deleteServer" | "deleteChannel" | "editChannel" | "messageFile" | "deleteMessage";
5 |
6 | interface ModalData {
7 | server?: Server;
8 | channel?: Channel;
9 | channelType?: ChannelType;
10 | apiUrl?: string;
11 | query?: Record
12 | }
13 |
14 | interface ModalStore {
15 | type: ModalType | null;
16 | data: ModalData;
17 | isOpen: boolean;
18 | onOpen: (type: ModalType, data?: ModalData) => void;
19 | onClose: () => void;
20 | }
21 |
22 | export const useModal = create((set) => ({
23 | type: null,
24 | isOpen: false,
25 | data: {},
26 | onOpen: (type, data = {}) => set({isOpen: true, type, data}),
27 | onClose: () => set({isOpen: false, type: null, data: {}})
28 | }));
29 |
30 |
--------------------------------------------------------------------------------
/hooks/use-origin.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const useOrigin = () => {
4 | const [isMounted, setIsMounted] = useState(false);
5 |
6 | useEffect(() => {
7 | setIsMounted(true);
8 | }, [])
9 |
10 | const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
11 |
12 | if(!isMounted) return null;
13 |
14 | return origin;
15 | }
--------------------------------------------------------------------------------
/lib/conversation.ts:
--------------------------------------------------------------------------------
1 | import { db } from "./db";
2 |
3 | export const getOrCreateConversation = async (
4 | memberOneId: string,
5 | memberTwoId: string
6 | ) => {
7 | let conversation =
8 | (await findConversaion(memberOneId, memberTwoId)) ||
9 | (await findConversaion(memberTwoId, memberOneId));
10 |
11 | if (!conversation) {
12 | conversation = await createNewConversation(memberOneId, memberTwoId);
13 | }
14 |
15 | return conversation;
16 | };
17 |
18 | export const findConversaion = async (
19 | memberOneId: string,
20 | memberTwoId: string
21 | ) => {
22 | try {
23 | return await db.conversation.findFirst({
24 | where: {
25 | AND: [{ memberOneId }, { memberTwoId }],
26 | },
27 | include: {
28 | memberOne: {
29 | include: {
30 | profile: true,
31 | },
32 | },
33 | memberTwo: {
34 | include: {
35 | profile: true,
36 | },
37 | },
38 | },
39 | });
40 | } catch (err) {
41 | console.log("[Find_conversation_error]", err);
42 | return null;
43 | }
44 | };
45 |
46 | export const createNewConversation = async (
47 | memberOneId: string,
48 | memberTwoId: string
49 | ) => {
50 | try {
51 | return await db.conversation.create({
52 | data: {
53 | memberOneId,
54 | memberTwoId,
55 | },
56 | include: {
57 | memberOne: {
58 | include: {
59 | profile: true,
60 | },
61 | },
62 | memberTwo: {
63 | include: {
64 | profile: true,
65 | },
66 | },
67 | },
68 | });
69 | } catch (err) {
70 | console.log("[Create_conversation_error]", err);
71 | return null;
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/lib/current-profile-pages.ts:
--------------------------------------------------------------------------------
1 | import { getAuth } from "@clerk/nextjs/server";
2 |
3 | import { db } from "@/lib/db";
4 | import { NextApiRequest } from "next";
5 |
6 | export const currentProfilePages = async (req: NextApiRequest) => {
7 | const { userId } = getAuth(req);
8 | if (!userId) return null;
9 |
10 | const profile = await db.profile.findUnique({
11 | where: {
12 | userId,
13 | },
14 | });
15 |
16 | return profile;
17 | };
18 |
--------------------------------------------------------------------------------
/lib/current-profile.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server";
2 |
3 | import { db } from "@/lib/db";
4 |
5 | export const currentProfile = async () => {
6 | const { userId } = auth();
7 | if (!userId) return null;
8 |
9 | const profile = await db.profile.findUnique({
10 | where: {
11 | userId,
12 | },
13 | });
14 |
15 | return profile;
16 | };
17 |
--------------------------------------------------------------------------------
/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined
5 | };
6 |
7 | export const db = globalThis.prisma || new PrismaClient();
8 |
9 | if(process.env.NODE_ENV !== "production") globalThis.prisma = db;
--------------------------------------------------------------------------------
/lib/initial-profile.ts:
--------------------------------------------------------------------------------
1 | import { RedirectToSignIn } from "@clerk/nextjs";
2 | import { auth, currentUser, redirectToSignIn } from "@clerk/nextjs/server"
3 | import { db } from "./db";
4 |
5 | type Profile = {
6 | id: string;
7 | userId: string;
8 | name: string;
9 | imageUrl: string;
10 | email: string;
11 | createdAt: Date;
12 | updatedAt: Date;
13 | };
14 |
15 | export const initialProfile = async (): Promise => {
16 | const user = await currentUser();
17 | if(!user){
18 | return auth().redirectToSignIn();
19 | }
20 |
21 | const profile = await db.profile.findUnique({
22 | where: {userId: user.id}
23 | });
24 |
25 | if(profile) return profile;
26 |
27 | const newProfile = await db.profile.create({
28 | data: {
29 | userId: user.id,
30 | name: `${user.firstName} ${user.lastName}`,
31 | imageUrl: user.imageUrl,
32 | email: user.emailAddresses[0].emailAddress
33 | }
34 | });
35 |
36 | return newProfile;
37 | }
--------------------------------------------------------------------------------
/lib/uploadthing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateUploadButton,
3 | generateUploadDropzone,
4 | } from "@uploadthing/react";
5 |
6 | import type { OurFileRouter } from "@/app/api/uploadthing/core";
7 |
8 | export const UploadButton = generateUploadButton();
9 | export const UploadDropzone = generateUploadDropzone();
10 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | export const ctrlCombinations = (char: string) => {
9 | if(typeof navigator !== "undefined") {
10 | const isMac = /Mac/.test(navigator.platform);
11 | if(isMac) return `⌘ + ${char}`
12 | }
13 | return `Ctrl + ${char}`
14 | }
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware } from "@clerk/nextjs/server";
2 |
3 | export default clerkMiddleware();
4 |
5 | export const config = {
6 | matcher: [
7 | // Skip Next.js internals and all static files, unless found in search params
8 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
9 | // Always run for API routes
10 | '/(api|trpc)(.*)',
11 | ],
12 | };
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: [
5 | "utfs.io",
6 | "img.clerk.com"
7 | ]
8 | }
9 | };
10 |
11 | export default nextConfig;
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discord",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/nextjs": "^5.3.3",
13 | "@clerk/themes": "^2.1.25",
14 | "@emoji-mart/data": "^1.2.1",
15 | "@emoji-mart/react": "^1.1.1",
16 | "@hookform/resolvers": "^3.9.0",
17 | "@livekit/components-react": "^2.4.3",
18 | "@livekit/components-styles": "^1.0.12",
19 | "@prisma/client": "^5.18.0",
20 | "@radix-ui/react-avatar": "^1.1.0",
21 | "@radix-ui/react-dialog": "^1.1.1",
22 | "@radix-ui/react-dropdown-menu": "^2.1.1",
23 | "@radix-ui/react-label": "^2.1.0",
24 | "@radix-ui/react-popover": "^1.1.1",
25 | "@radix-ui/react-scroll-area": "^1.1.0",
26 | "@radix-ui/react-select": "^2.1.1",
27 | "@radix-ui/react-separator": "^1.1.0",
28 | "@radix-ui/react-slot": "^1.1.0",
29 | "@radix-ui/react-tooltip": "^1.1.2",
30 | "@tanstack/react-query": "^5.53.1",
31 | "@uploadthing/react": "^6.7.2",
32 | "axios": "^1.7.5",
33 | "class-variance-authority": "^0.7.0",
34 | "clsx": "^2.1.1",
35 | "cmdk": "^1.0.0",
36 | "date-fns": "^3.6.0",
37 | "emoji-mart": "^5.6.0",
38 | "livekit-server-sdk": "^2.6.1",
39 | "lucide-react": "^0.435.0",
40 | "next": "14.2.6",
41 | "next-themes": "^0.3.0",
42 | "query-string": "^9.1.0",
43 | "react": "^18",
44 | "react-dom": "^18",
45 | "react-dropzone": "^14.2.3",
46 | "react-hook-form": "^7.53.0",
47 | "socket.io": "^4.7.5",
48 | "socket.io-client": "^4.7.5",
49 | "styled-components": "^6.1.12",
50 | "tailwind-merge": "^2.5.2",
51 | "tailwindcss-animate": "^1.0.7",
52 | "uploadthing": "^6.13.2",
53 | "uuid": "^10.0.0",
54 | "zod": "^3.23.8",
55 | "zustand": "^4.5.5"
56 | },
57 | "devDependencies": {
58 | "@types/node": "^20",
59 | "@types/react": "^18",
60 | "@types/react-dom": "^18",
61 | "@types/uuid": "^10.0.0",
62 | "eslint": "^8",
63 | "eslint-config-next": "14.2.6",
64 | "postcss": "^8",
65 | "prisma": "^5.18.0",
66 | "tailwindcss": "^3.4.1",
67 | "typescript": "^5"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/pages/api/socket/direct-messages/[directMessagesId].ts:
--------------------------------------------------------------------------------
1 | import { currentProfilePages } from "@/lib/current-profile-pages";
2 | import { db } from "@/lib/db";
3 | import { NextApiResponseServerIo } from "@/types";
4 | import { MemberRole } from "@prisma/client";
5 | import { NextApiRequest } from "next";
6 |
7 | export default async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponseServerIo
10 | ) {
11 | if (req.method !== "DELETE" && req.method !== "PATCH") {
12 | return res.status(405).json({ error: "Method not allowed" });
13 | }
14 |
15 | try {
16 | const profile = await currentProfilePages(req);
17 | const { directMessagesId, conversationId } = req.query;
18 | const { content } = req.body;
19 |
20 | if (!profile) return res.status(401).json({ error: "Unauthorized access" });
21 |
22 | if (!conversationId)
23 | return res
24 | .status(400)
25 | .json({ error: "req.query.conversationId is missing" });
26 |
27 | const conversation = await db.conversation.findFirst({
28 | where: {
29 | id: conversationId as string,
30 | OR: [
31 | {
32 | memberOne: {
33 | profileId: profile.id,
34 | },
35 | },
36 | {
37 | memberTwo: {
38 | profileId: profile.id,
39 | },
40 | },
41 | ],
42 | },
43 | include: {
44 | memberOne: {
45 | include: {
46 | profile: true,
47 | },
48 | },
49 | memberTwo: {
50 | include: {
51 | profile: true,
52 | },
53 | },
54 | },
55 | });
56 |
57 | if (!conversation)
58 | return res.status(404).json({ error: "Converastion is not found" });
59 |
60 | const member =
61 | conversation.memberOneId === profile.id
62 | ? conversation.memberOne
63 | : conversation.memberTwo;
64 |
65 | if (!member)
66 | return res.status(404).json({ message: "Member is not found" });
67 |
68 | let directMessage = await db.directMessage.findFirst({
69 | where: {
70 | id: directMessagesId as string,
71 | conversationId: conversationId as string,
72 | },
73 | include: {
74 | member: {
75 | include: {
76 | profile: true,
77 | },
78 | },
79 | },
80 | });
81 |
82 | if (!directMessage || directMessage.deleted)
83 | return res.status(404).json({ error: "Message not found" });
84 |
85 | const isOwner = directMessage.member.id === member.id;
86 | const isAdmin = member.role === MemberRole.ADMIN;
87 | const isModerator = member.role === MemberRole.MODERATOR;
88 | const canModify = isOwner || isAdmin || isModerator;
89 |
90 | if (!canModify)
91 | return res.status(401).json({ error: "Unauthorized to modify message" });
92 |
93 | if (req.method === "DELETE") {
94 | directMessage = await db.directMessage.update({
95 | where: {
96 | id: directMessage.id as string,
97 | },
98 | data: {
99 | fileUrl: "",
100 | content: "Message has been deleted",
101 | deleted: true,
102 | },
103 | include: {
104 | member: {
105 | include: {
106 | profile: true,
107 | },
108 | },
109 | },
110 | });
111 | }
112 |
113 | if (req.method === "PATCH") {
114 | if (!isOwner)
115 | return res
116 | .status(401)
117 | .json({ error: "Unauthorized to modify message" });
118 | directMessage = await db.directMessage.update({
119 | where: {
120 | id: directMessage.id as string,
121 | },
122 | data: {
123 | content,
124 | },
125 | include: {
126 | member: {
127 | include: {
128 | profile: true,
129 | },
130 | },
131 | },
132 | });
133 | }
134 |
135 | const updateKey = `chat:${conversationId}:messages:update`;
136 |
137 | res?.socket?.server?.io?.emit(updateKey, directMessage);
138 |
139 | return res.status(200).json(directMessage);
140 | } catch (err) {
141 | console.log("[DIRECT_MESSAGE_PATCH_DELETE_ERROR]", err);
142 | return res.status(501).json({ error: "Internal Error" });
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/pages/api/socket/direct-messages/index.ts:
--------------------------------------------------------------------------------
1 | import { currentProfilePages } from "@/lib/current-profile-pages";
2 | import { db } from "@/lib/db";
3 | import { NextApiResponseServerIo } from "@/types";
4 | import { NextApiRequest } from "next";
5 |
6 | export default async function handler(
7 | req: NextApiRequest,
8 | res: NextApiResponseServerIo
9 | ) {
10 | if (req.method !== "POST")
11 | return res.status(405).json({ error: `Method ${req.method} not allowed!` });
12 |
13 | try {
14 | const profile = await currentProfilePages(req);
15 | if (!profile) return res.status(401).json({ error: "Unauthorized access" });
16 |
17 | const { content, fileUrl } = req.body;
18 | const { conversationId } = req.query;
19 |
20 | if (!content) {
21 | if (!fileUrl)
22 | return res.status(400).json({ error: "req.body.content is missing" });
23 | }
24 |
25 | if (!fileUrl) {
26 | if (!content)
27 | return res.status(400).json({ error: "req.body.fielUrl is missing" });
28 | }
29 |
30 | if (!conversationId)
31 | return res
32 | .status(400)
33 | .json({ error: "req.query.conversationId is missing" });
34 |
35 | const conversation = await db.conversation.findFirst({
36 | where: {
37 | id: conversationId as string,
38 | OR: [
39 | {
40 | memberOne: {
41 | profileId: profile.id,
42 | },
43 | },
44 | {
45 | memberTwo: {
46 | profileId: profile.id,
47 | },
48 | },
49 | ],
50 | },
51 | include: {
52 | memberOne: {
53 | include: {
54 | profile: true,
55 | },
56 | },
57 | memberTwo: {
58 | include: {
59 | profile: true,
60 | },
61 | },
62 | },
63 | });
64 |
65 | if (!conversation)
66 | return res.status(404).json({ error: "Converastion is not found" });
67 |
68 | const member =
69 | conversation.memberOneId === profile.id
70 | ? conversation.memberOne
71 | : conversation.memberTwo;
72 |
73 | if (!member)
74 | return res.status(404).json({ message: "Member is not found" });
75 |
76 | const message = await db.directMessage.create({
77 | data: {
78 | content: content || "",
79 | fileUrl: fileUrl || "",
80 | conversationId: conversationId as string,
81 | memberId: member.id,
82 | },
83 | include: {
84 | member: {
85 | include: {
86 | profile: true,
87 | },
88 | },
89 | },
90 | });
91 |
92 | const channelKey = `chat:${conversationId}:messages`;
93 |
94 | res?.socket?.server?.io?.emit(channelKey);
95 |
96 | return res.status(200).json(message);
97 | } catch (err) {
98 | console.log("[SOCKET_DIRECT_MESSAGES_API]", err);
99 | return res.status(500).json({ message: "Internal Error" });
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/pages/api/socket/io.ts:
--------------------------------------------------------------------------------
1 | import { NextApiResponseServerIo } from "@/types";
2 | import { Server as NetServer } from "http";
3 | import { NextApiRequest } from "next";
4 | import { Server as ServerIO } from "socket.io";
5 |
6 | export const config = {
7 | api: {
8 | bodyParser: false,
9 | },
10 | };
11 |
12 | const ioHandler = (req: NextApiRequest, res: NextApiResponseServerIo) => {
13 | if (!res.socket.server.io) {
14 | const path = "/api/socket/io";
15 | const httpServer: NetServer = res.socket.server as any;
16 | const io = new ServerIO(httpServer, {
17 | path: path,
18 | addTrailingSlash: false,
19 | });
20 | res.socket.server.io = io;
21 | }
22 |
23 | res.end();
24 | };
25 |
26 |
27 | export default ioHandler;
28 |
--------------------------------------------------------------------------------
/pages/api/socket/messages/[messagesId].ts:
--------------------------------------------------------------------------------
1 | import { currentProfilePages } from "@/lib/current-profile-pages";
2 | import { db } from "@/lib/db";
3 | import { NextApiResponseServerIo } from "@/types";
4 | import { MemberRole } from "@prisma/client";
5 | import { NextApiRequest } from "next";
6 |
7 | export default async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponseServerIo
10 | ) {
11 | if (req.method !== "DELETE" && req.method !== "PATCH") {
12 | return res.status(405).json({ error: "Method not allowed" });
13 | }
14 |
15 | try {
16 | const profile = await currentProfilePages(req);
17 | const { serverId, messageId, channelId } = req.query;
18 | const { content } = req.body;
19 |
20 | if (!profile) return res.status(401).json({ error: "Unauthorized access" });
21 | if (!serverId) return res.status(400).json({ error: "Server id missing" });
22 | if (!channelId)
23 | return res.status(400).json({ error: "Channel id missing" });
24 |
25 | const server = await db.server.findFirst({
26 | where: {
27 | id: serverId as string,
28 | members: {
29 | some: {
30 | profileId: profile.id,
31 | },
32 | },
33 | },
34 | include: {
35 | members: true,
36 | },
37 | });
38 |
39 | if (!server) return res.status(404).json({ error: "Server not found" });
40 |
41 | const channel = await db.channel.findFirst({
42 | where: {
43 | id: channelId as string,
44 | serverId: server.id,
45 | },
46 | });
47 |
48 | if (!channel) return res.status(404).json({ error: "Server not found" });
49 |
50 | const member = server.members.find(
51 | (member) => member.profileId === profile.id
52 | );
53 |
54 | if (!member) return res.status(404).json({ error: "Member not found" });
55 |
56 | let message = await db.message.findFirst({
57 | where: {
58 | id: messageId as string,
59 | channelId: channelId as string,
60 | },
61 | include: {
62 | member: {
63 | include: {
64 | profile: true,
65 | },
66 | },
67 | },
68 | });
69 |
70 | if (!message || message.deleted)
71 | return res.status(404).json({ error: "Message not found" });
72 |
73 | const isOwner = message.member.id === member.id;
74 | const isAdmin = member.role === MemberRole.ADMIN;
75 | const isModerator = member.role === MemberRole.MODERATOR;
76 | const canModify = isOwner || isAdmin || isModerator;
77 |
78 | if (!canModify)
79 | return res.status(401).json({ error: "Unauthorized to modify message" });
80 |
81 | if (req.method === "DELETE") {
82 | message = await db.message.update({
83 | where: {
84 | id: message.id as string,
85 | },
86 | data: {
87 | fileUrl: "",
88 | content: "Message has been deleted",
89 | deleted: true,
90 | },
91 | include: {
92 | member: {
93 | include: {
94 | profile: true,
95 | },
96 | },
97 | },
98 | });
99 | }
100 |
101 | if (req.method === "PATCH") {
102 | if (!isOwner)
103 | return res
104 | .status(401)
105 | .json({ error: "Unauthorized to modify message" });
106 | message = await db.message.update({
107 | where: {
108 | id: message.id as string,
109 | },
110 | data: {
111 | content,
112 | },
113 | include: {
114 | member: {
115 | include: {
116 | profile: true,
117 | },
118 | },
119 | },
120 | });
121 | }
122 |
123 | const updateKey = `chat:${channelId}:messages:update`;
124 |
125 | res?.socket?.server?.io?.emit(updateKey, message);
126 |
127 | return res.status(200).json(message);
128 | } catch (err) {
129 | console.log("[MESSAGE_PATCH_DELETE_ERROR]", err);
130 | return res.status(501).json({ error: "Internal Error" });
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/pages/api/socket/messages/index.ts:
--------------------------------------------------------------------------------
1 | import { currentProfilePages } from "@/lib/current-profile-pages";
2 | import { db } from "@/lib/db";
3 | import { NextApiResponseServerIo } from "@/types";
4 | import { NextApiRequest } from "next";
5 |
6 | export default async function handler(
7 | req: NextApiRequest,
8 | res: NextApiResponseServerIo
9 | ) {
10 | if (req.method !== "POST")
11 | return res.status(405).json({ error: `Method ${req.method} not allowed!` });
12 |
13 | try {
14 | const profile = await currentProfilePages(req);
15 | if (!profile) return res.status(401).json({ error: "Unauthorized access" });
16 |
17 | const { content, fileUrl } = req.body;
18 | const { serverId, channelId } = req.query;
19 |
20 | if (!content) {
21 | if (!fileUrl)
22 | return res.status(400).json({ error: "req.body.content is missing" });
23 | }
24 |
25 | if (!fileUrl) {
26 | if (!content)
27 | return res.status(400).json({ error: "req.body.fielUrl is missing" });
28 | }
29 |
30 | if (!serverId)
31 | return res.status(400).json({ error: "req.query.serverId is missing" });
32 | if (!channelId)
33 | return res.status(400).json({ error: "req.query.channelId is missing " });
34 |
35 | const server = await db.server.findFirst({
36 | where: {
37 | id: serverId as string,
38 | members: {
39 | some: {
40 | profileId: profile.id,
41 | },
42 | },
43 | },
44 | include: {
45 | members: true,
46 | },
47 | });
48 |
49 | if (!server) return res.status(404).json({ message: "Server not found" });
50 |
51 | const channel = await db.channel.findFirst({
52 | where: {
53 | id: channelId as string,
54 | serverId: server.id,
55 | },
56 | });
57 |
58 | if (!channel)
59 | return res.status(404).json({ message: "Channel is not found" });
60 |
61 | const member = server.members.find(
62 | (member) => member.profileId === profile.id
63 | );
64 |
65 | if (!member)
66 | return res.status(404).json({ message: "Member is not found" });
67 | const message = await db.message.create({
68 | data: {
69 | content: content || "",
70 | fileUrl: fileUrl || "",
71 | channelId: channelId as string,
72 | memberId: member.id,
73 | },
74 | include: {
75 | member: {
76 | include: {
77 | profile: true,
78 | },
79 | },
80 | },
81 | });
82 |
83 | const channelKey = `chat:${channelId}:messages`;
84 |
85 | res?.socket?.server?.io?.emit(channelKey);
86 |
87 | return res.status(200).json(message);
88 | } catch (err) {
89 | console.log("[SOCKET_MESSAGES_API]", err);
90 | return res.status(500).json({ message: "Internal Error" });
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240827074551_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "MemberRole" AS ENUM ('ADMIN', 'MODERATOR', 'GUEST');
3 |
4 | -- CreateEnum
5 | CREATE TYPE "ChannelType" AS ENUM ('TEXT', 'AUDIO', 'VIDEO');
6 |
7 | -- CreateTable
8 | CREATE TABLE "Profile" (
9 | "id" TEXT NOT NULL,
10 | "userId" TEXT NOT NULL,
11 | "name" TEXT NOT NULL,
12 | "imageUrl" TEXT NOT NULL,
13 | "email" TEXT NOT NULL,
14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 | "updatedAt" TIMESTAMP(3) NOT NULL,
16 |
17 | CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
18 | );
19 |
20 | -- CreateTable
21 | CREATE TABLE "Server" (
22 | "id" TEXT NOT NULL,
23 | "name" TEXT NOT NULL,
24 | "imageUrl" TEXT NOT NULL,
25 | "inviteCode" TEXT NOT NULL,
26 | "profileId" TEXT NOT NULL,
27 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
28 | "updatedAt" TIMESTAMP(3) NOT NULL,
29 |
30 | CONSTRAINT "Server_pkey" PRIMARY KEY ("id")
31 | );
32 |
33 | -- CreateTable
34 | CREATE TABLE "Member" (
35 | "id" TEXT NOT NULL,
36 | "role" "MemberRole" NOT NULL DEFAULT 'GUEST',
37 | "profileId" TEXT NOT NULL,
38 | "serverId" TEXT NOT NULL,
39 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
40 | "updatedAt" TIMESTAMP(3) NOT NULL,
41 |
42 | CONSTRAINT "Member_pkey" PRIMARY KEY ("id")
43 | );
44 |
45 | -- CreateTable
46 | CREATE TABLE "Channel" (
47 | "id" TEXT NOT NULL,
48 | "name" TEXT NOT NULL,
49 | "type" "ChannelType" NOT NULL DEFAULT 'TEXT',
50 | "profileId" TEXT NOT NULL,
51 | "serverId" TEXT NOT NULL,
52 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
53 | "updatedAt" TIMESTAMP(3) NOT NULL,
54 |
55 | CONSTRAINT "Channel_pkey" PRIMARY KEY ("id")
56 | );
57 |
58 | -- CreateIndex
59 | CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId");
60 |
61 | -- CreateIndex
62 | CREATE UNIQUE INDEX "Server_inviteCode_key" ON "Server"("inviteCode");
63 |
64 | -- CreateIndex
65 | CREATE INDEX "Server_profileId_idx" ON "Server"("profileId");
66 |
67 | -- CreateIndex
68 | CREATE INDEX "Member_profileId_idx" ON "Member"("profileId");
69 |
70 | -- CreateIndex
71 | CREATE INDEX "Member_serverId_idx" ON "Member"("serverId");
72 |
73 | -- CreateIndex
74 | CREATE INDEX "Channel_profileId_idx" ON "Channel"("profileId");
75 |
76 | -- CreateIndex
77 | CREATE INDEX "Channel_serverId_idx" ON "Channel"("serverId");
78 |
79 | -- AddForeignKey
80 | ALTER TABLE "Server" ADD CONSTRAINT "Server_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "Profile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
81 |
82 | -- AddForeignKey
83 | ALTER TABLE "Member" ADD CONSTRAINT "Member_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "Profile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
84 |
85 | -- AddForeignKey
86 | ALTER TABLE "Member" ADD CONSTRAINT "Member_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
87 |
88 | -- AddForeignKey
89 | ALTER TABLE "Channel" ADD CONSTRAINT "Channel_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "Profile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
90 |
91 | -- AddForeignKey
92 | ALTER TABLE "Channel" ADD CONSTRAINT "Channel_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
93 |
--------------------------------------------------------------------------------
/prisma/migrations/20240829070717_added_messages_logic_and_stuff/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Message" (
3 | "id" TEXT NOT NULL,
4 | "content" TEXT NOT NULL,
5 | "fileUrl" TEXT NOT NULL,
6 | "memberId" TEXT NOT NULL,
7 | "channelId" TEXT NOT NULL,
8 | "deleted" BOOLEAN NOT NULL DEFAULT false,
9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10 | "updatedAt" TIMESTAMP(3) NOT NULL,
11 |
12 | CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
13 | );
14 |
15 | -- CreateTable
16 | CREATE TABLE "Conversation" (
17 | "id" TEXT NOT NULL,
18 | "memberOneId" TEXT NOT NULL,
19 | "memberTwoId" TEXT NOT NULL,
20 |
21 | CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id")
22 | );
23 |
24 | -- CreateTable
25 | CREATE TABLE "DirectMessage" (
26 | "id" TEXT NOT NULL,
27 | "content" TEXT NOT NULL,
28 | "fileUrl" TEXT,
29 | "memberId" TEXT NOT NULL,
30 | "conversationId" TEXT NOT NULL,
31 | "deleted" BOOLEAN NOT NULL DEFAULT false,
32 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
33 | "updatedAt" TIMESTAMP(3) NOT NULL,
34 |
35 | CONSTRAINT "DirectMessage_pkey" PRIMARY KEY ("id")
36 | );
37 |
38 | -- CreateIndex
39 | CREATE INDEX "Message_channelId_idx" ON "Message"("channelId");
40 |
41 | -- CreateIndex
42 | CREATE INDEX "Message_memberId_idx" ON "Message"("memberId");
43 |
44 | -- CreateIndex
45 | CREATE INDEX "Conversation_memberOneId_idx" ON "Conversation"("memberOneId");
46 |
47 | -- CreateIndex
48 | CREATE INDEX "Conversation_memberTwoId_idx" ON "Conversation"("memberTwoId");
49 |
50 | -- CreateIndex
51 | CREATE UNIQUE INDEX "Conversation_memberOneId_memberTwoId_key" ON "Conversation"("memberOneId", "memberTwoId");
52 |
53 | -- CreateIndex
54 | CREATE INDEX "DirectMessage_memberId_idx" ON "DirectMessage"("memberId");
55 |
56 | -- CreateIndex
57 | CREATE INDEX "DirectMessage_conversationId_idx" ON "DirectMessage"("conversationId");
58 |
59 | -- AddForeignKey
60 | ALTER TABLE "Message" ADD CONSTRAINT "Message_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE CASCADE ON UPDATE CASCADE;
61 |
62 | -- AddForeignKey
63 | ALTER TABLE "Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64 |
65 | -- AddForeignKey
66 | ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_memberOneId_fkey" FOREIGN KEY ("memberOneId") REFERENCES "Member"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67 |
68 | -- AddForeignKey
69 | ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_memberTwoId_fkey" FOREIGN KEY ("memberTwoId") REFERENCES "Member"("id") ON DELETE CASCADE ON UPDATE CASCADE;
70 |
71 | -- AddForeignKey
72 | ALTER TABLE "DirectMessage" ADD CONSTRAINT "DirectMessage_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE CASCADE ON UPDATE CASCADE;
73 |
74 | -- AddForeignKey
75 | ALTER TABLE "DirectMessage" ADD CONSTRAINT "DirectMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
76 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model Profile {
11 | id String @id @default(cuid())
12 | userId String @unique
13 | name String
14 | imageUrl String @db.Text
15 | email String @db.Text
16 |
17 | servers Server[]
18 | members Member[]
19 | channels Channel[]
20 |
21 | createdAt DateTime @default(now())
22 | updatedAt DateTime @updatedAt
23 | }
24 |
25 | model Server {
26 | id String @id @default(cuid())
27 | name String
28 | imageUrl String @db.Text
29 | inviteCode String @unique
30 |
31 | profileId String
32 | profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
33 |
34 | members Member[]
35 | channels Channel[]
36 |
37 | createdAt DateTime @default(now())
38 | updatedAt DateTime @updatedAt
39 |
40 | @@index([profileId])
41 | }
42 |
43 | enum MemberRole {
44 | ADMIN
45 | MODERATOR
46 | GUEST
47 | }
48 |
49 | model Member {
50 | id String @id @default(cuid())
51 | role MemberRole @default(GUEST)
52 |
53 | profileId String
54 | profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
55 |
56 | serverId String
57 | server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
58 |
59 | conversationsInitiated Conversation[] @relation("MemberOne")
60 | conversationsReceived Conversation[] @relation("MemberTwo")
61 |
62 | directMessages DirectMessage[]
63 |
64 | messages Message[]
65 |
66 | createdAt DateTime @default(now())
67 | updatedAt DateTime @updatedAt
68 |
69 | @@index([profileId])
70 | @@index([serverId])
71 | }
72 |
73 | enum ChannelType {
74 | TEXT
75 | AUDIO
76 | VIDEO
77 | }
78 |
79 | model Channel {
80 | id String @id @default(cuid())
81 | name String
82 | type ChannelType @default(TEXT)
83 |
84 | profileId String
85 | profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
86 |
87 | serverId String
88 | server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
89 |
90 | messages Message[]
91 |
92 | createdAt DateTime @default(now())
93 | updatedAt DateTime @updatedAt
94 |
95 | @@index([profileId])
96 | @@index([serverId])
97 | }
98 |
99 | model Message {
100 | id String @id @default(cuid())
101 | content String @db.Text
102 |
103 | fileUrl String @db.Text
104 |
105 | memberId String
106 | member Member @relation(fields: [memberId], references: [id], onDelete: Cascade)
107 |
108 | channelId String
109 | channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
110 |
111 | deleted Boolean @default(false)
112 |
113 | createdAt DateTime @default(now())
114 | updatedAt DateTime @updatedAt
115 |
116 | @@index([channelId])
117 | @@index([memberId])
118 | }
119 |
120 | model Conversation {
121 | id String @id @default(cuid())
122 |
123 | memberOneId String
124 | memberOne Member @relation("MemberOne", fields: [memberOneId], references: [id], onDelete: Cascade)
125 |
126 | memberTwoId String
127 | memberTwo Member @relation("MemberTwo", fields: [memberTwoId], references: [id], onDelete: Cascade)
128 |
129 | directMessages DirectMessage[]
130 |
131 | @@unique([memberOneId, memberTwoId])
132 | @@index([memberOneId])
133 | @@index([memberTwoId])
134 | }
135 |
136 | model DirectMessage {
137 | id String @id @default(cuid())
138 | content String @db.Text
139 | fileUrl String? @db.Text
140 |
141 | memberId String
142 | member Member @relation(fields: [memberId], references: [id], onDelete: Cascade)
143 |
144 | conversationId String
145 | conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
146 |
147 | deleted Boolean @default(false)
148 |
149 | createdAt DateTime @default(now())
150 | updatedAt DateTime @updatedAt
151 |
152 | @@index([memberId])
153 | @@index([conversationId])
154 | }
155 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { Server as NetServer, Socket } from "net";
2 | import { NextApiResponse } from "next";
3 | import { Server as SocketIOServer } from "socket.io";
4 | import { Server, Member, Profile, Message } from "@prisma/client";
5 |
6 | export type ServerWithMembersWithProfiles = Server & {
7 | members: (Member & { profile: Profile })[];
8 | };
9 |
10 | export type NextApiResponseServerIo = NextApiResponse & {
11 | socket: Socket & {
12 | server: NetServer & {
13 | io: SocketIOServer;
14 | };
15 | };
16 | };
17 |
18 | export type MessageWithMemberWithProfile = Message & {
19 | member: Member & {
20 | profile: Profile;
21 | };
22 | };
23 |
--------------------------------------------------------------------------------