Compare commits
10 Commits
master
...
Kylas-disc
Author | SHA1 | Date | |
---|---|---|---|
297b8a9b2d | |||
4c0473ae02 | |||
0095ba4433 | |||
dc356f833c | |||
239ff1b33b | |||
fa4a4a565c | |||
6e504f2e0e | |||
c756583dc7 | |||
bf667ce93f | |||
29cb3462f7 |
@ -1,62 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { redirectToSignIn } from "@clerk/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
interface InviteCodePageProps {
|
||||
params: {
|
||||
inviteCode: string;
|
||||
};
|
||||
};
|
||||
|
||||
const InviteCodePage = async ({
|
||||
params
|
||||
}: InviteCodePageProps) => {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirectToSignIn();
|
||||
}
|
||||
|
||||
if (!params?.inviteCode) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const existingServer = await db.server.findFirst({
|
||||
where: {
|
||||
inviteCode: params.inviteCode,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingServer) {
|
||||
return redirect(`/servers/${existingServer.id}`);
|
||||
}
|
||||
|
||||
const server= await db.server.update({
|
||||
where: {
|
||||
inviteCode: params.inviteCode,
|
||||
},
|
||||
data: {
|
||||
members: {
|
||||
create: [
|
||||
{
|
||||
profileId: profile.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if(server) {
|
||||
return redirect(`/servers/${server.id}`);
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default InviteCodePage;
|
13
app/(main)/(routes)/page.tsx
Normal file
13
app/(main)/(routes)/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { UserButton } from "@clerk/nextjs";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<UserButton
|
||||
afterSignOutUrl="/"
|
||||
/>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { redirectToSignIn } from "@clerk/nextjs";
|
||||
import { db } from "@/lib/db";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ChatHeader } from "@/components/chat/chat-header";
|
||||
import { ChatInput } from "@/components/chat/chat-input";
|
||||
import { ChatMessages } from "@/components/chat/chat-messages";
|
||||
import { ChannelType } from "@prisma/client";
|
||||
import { MediaRoom } from "@/components/media-room";
|
||||
|
||||
interface ChannelIdPageProps {
|
||||
params: {
|
||||
serverId: string;
|
||||
channelId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ChannelIdPage = async ({
|
||||
params
|
||||
}: ChannelIdPageProps) => {
|
||||
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirectToSignIn();
|
||||
}
|
||||
|
||||
const channel = await db.channel.findUnique({
|
||||
where: {
|
||||
id: params.channelId,
|
||||
},
|
||||
});
|
||||
|
||||
const member = await db.member.findFirst({
|
||||
where: {
|
||||
serverId: params.serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!channel || !member) {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-[#313338] flex flex-col h-full">
|
||||
<ChatHeader
|
||||
name={channel.name}
|
||||
serverId={channel.serverId}
|
||||
type={"channel"}
|
||||
/>
|
||||
{channel.type === ChannelType.TEXT && (
|
||||
<>
|
||||
<ChatMessages
|
||||
member={member}
|
||||
name={channel.name}
|
||||
chatId={channel.id}
|
||||
type="channel"
|
||||
apiUrl="/api/messages"
|
||||
socketUrl="/api/socket/messages"
|
||||
socketQuery={{
|
||||
channelId: channel.id,
|
||||
serverId: channel.serverId,
|
||||
}}
|
||||
paramKey="channelId"
|
||||
paramValue={channel.id}
|
||||
/>
|
||||
<ChatInput name={channel.name}
|
||||
type="channel"
|
||||
apiUrl="/api/socket/messages"
|
||||
query={{
|
||||
channelId: channel.id,
|
||||
serverId: channel.serverId,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{channel.type === ChannelType.AUDIO && (
|
||||
<MediaRoom
|
||||
chatId={channel.id}
|
||||
video={false}
|
||||
audio={true}
|
||||
/>
|
||||
)}
|
||||
{channel.type === ChannelType.VIDEO && (
|
||||
<MediaRoom
|
||||
chatId={channel.id}
|
||||
video={true}
|
||||
audio={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChannelIdPage
|
@ -1,99 +0,0 @@
|
||||
import { ChatHeader } from "@/components/chat/chat-header";
|
||||
import { ChatInput } from "@/components/chat/chat-input";
|
||||
import { ChatMessages } from "@/components/chat/chat-messages";
|
||||
import { MediaRoom } from "@/components/media-room";
|
||||
import { getOrCreateConversation } from "@/lib/conversation";
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
import { redirectToSignIn } from "@clerk/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface MemberIdPageProps{
|
||||
params: {
|
||||
serverId: string;
|
||||
memberId: string;
|
||||
},
|
||||
searchParams: {
|
||||
video?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const MemberIdPage = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: MemberIdPageProps) => {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirectToSignIn();
|
||||
}
|
||||
|
||||
const currentMember = await db.member.findFirst({
|
||||
where: {
|
||||
serverId: params.serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentMember) {
|
||||
return redirect(`/`);
|
||||
}
|
||||
|
||||
const conversation = await getOrCreateConversation(currentMember.id, params.memberId);
|
||||
|
||||
if (!conversation) {
|
||||
return redirect(`/servers/${params.serverId}`);
|
||||
}
|
||||
|
||||
const { memberOne, memberTwo } = conversation;
|
||||
|
||||
const otherMember = memberOne.profileId === profile.id ? memberTwo : memberOne;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-[#313338] flex flex-col h-full">
|
||||
<ChatHeader
|
||||
imageUrl={otherMember.profile.imageUrl}
|
||||
name={otherMember.profile.name}
|
||||
serverId={params.serverId}
|
||||
type={"conversation"}
|
||||
/>
|
||||
{searchParams.video && (
|
||||
<MediaRoom
|
||||
chatId={conversation.id}
|
||||
video={true}
|
||||
audio={true}
|
||||
/>
|
||||
)}
|
||||
{!searchParams.video && (
|
||||
<>
|
||||
<ChatMessages
|
||||
member={currentMember}
|
||||
name={otherMember.profile.name}
|
||||
chatId={conversation.id}
|
||||
type="conversation"
|
||||
apiUrl="/api/direct-messages"
|
||||
paramKey="conversationId"
|
||||
paramValue={conversation.id}
|
||||
socketUrl="/api/socket/direct-messages"
|
||||
socketQuery={{
|
||||
conversationId: conversation.id,
|
||||
}}
|
||||
/>
|
||||
<ChatInput
|
||||
name={otherMember.profile.name}
|
||||
type="conversation"
|
||||
apiUrl="/api/socket/direct-messages"
|
||||
query={{
|
||||
conversationId: conversation.id,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MemberIdPage
|
@ -1,51 +0,0 @@
|
||||
|
||||
import { redirectToSignIn } from "@clerk/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
import { ServerSidebar } from "@/components/server/server-sidebar";
|
||||
|
||||
const ServerIdLayout = async ({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { serverId: string};
|
||||
}) => {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirectToSignIn();
|
||||
}
|
||||
|
||||
const server = await db.server.findUnique({
|
||||
where: {
|
||||
id: params.serverId,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (!server) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div
|
||||
className="hidden md:flex h-full w-60 z-20 flex-col fixed inset-y-0">
|
||||
<ServerSidebar serverId={params.serverId} />
|
||||
</div>
|
||||
<main className="h-full md:pl-60">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ServerIdLayout;
|
@ -1,50 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
import { redirectToSignIn } from "@clerk/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface ServerIdPageProps {
|
||||
params: {
|
||||
serverId: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ServerIdPage = async ({
|
||||
params
|
||||
}: ServerIdPageProps) => {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirectToSignIn();
|
||||
}
|
||||
|
||||
const server = await db.server.findUnique({
|
||||
where: {
|
||||
id: params.serverId,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
channels: {
|
||||
where: {
|
||||
name: "general"
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inistailChannel = server?.channels[0];
|
||||
|
||||
if (inistailChannel?.name !== "general") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return redirect(`/servers/${params?.serverId}/channels/${inistailChannel?.id}`);
|
||||
}
|
||||
export default ServerIdPage;
|
@ -1,20 +0,0 @@
|
||||
import { NavigationSidebar } from "@/components/navigation/navigation-sidebar";
|
||||
|
||||
const MainLayout = async ({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="hidden md:flex h-full w-[72px] z-30 flex-col fixed inset-y-0">
|
||||
<NavigationSidebar/>
|
||||
</div>
|
||||
<main className="md:pl-[72px] h-full">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainLayout;
|
@ -1,7 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { initialProfile } from "@/lib/initial-profile";
|
||||
import { initialProfile } from "@/lib/initial.profile";
|
||||
import { InitialModal } from "@/components/modals/initial-modal";
|
||||
|
||||
const SetupPage =async () => {
|
||||
@ -11,9 +11,9 @@ const SetupPage = async () => {
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
},
|
||||
},
|
||||
profileId: profile.id
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -24,4 +24,4 @@ const SetupPage = async () => {
|
||||
return <InitialModal />;
|
||||
}
|
||||
|
||||
export default SetupPage;
|
||||
export default SetupPage
|
@ -1,123 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
|
||||
export async function DELETE(
|
||||
req: Request,
|
||||
{ params }: { params: { channelId: string } }
|
||||
) {
|
||||
try {
|
||||
const profile = await currentProfile();
|
||||
const {searchParams} = new URL(req.url);
|
||||
|
||||
const serverId = searchParams.get("serverId");
|
||||
|
||||
if (!profile) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!serverId) {
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
if (!params.channelId) {
|
||||
return new NextResponse("Channel ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update({
|
||||
where: {
|
||||
id: serverId,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
role: {
|
||||
in: [MemberRole.ADMIN, MemberRole.MODERATOR]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
data: {
|
||||
channels: {
|
||||
delete: {
|
||||
id: params.channelId,
|
||||
name: {
|
||||
not: "general"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error) {
|
||||
console.log("[CHANNEL_ID_DELETE]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: { channelId: string } }
|
||||
) {
|
||||
try {
|
||||
const profile = await currentProfile();
|
||||
const { name, type } = await req.json();
|
||||
const {searchParams} = new URL(req.url);
|
||||
|
||||
const serverId = searchParams.get("serverId");
|
||||
|
||||
if (!profile) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!serverId) {
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
if (!params.channelId) {
|
||||
return new NextResponse("Channel ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
if (name === "general") {
|
||||
return new NextResponse("Channel name cannot be 'general'", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update({
|
||||
where: {
|
||||
id: serverId,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
role: {
|
||||
in: [MemberRole.ADMIN, MemberRole.MODERATOR]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
data: {
|
||||
channels: {
|
||||
update: {
|
||||
where: {
|
||||
id: params.channelId,
|
||||
name: {
|
||||
not: "general"
|
||||
}
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
type,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error) {
|
||||
console.log("[CHANNEL_ID_PATCH]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function POST(req: Request)
|
||||
{
|
||||
try
|
||||
{
|
||||
const profile = await currentProfile();
|
||||
const { name, type } = await req.json();
|
||||
const {searchParams } = new URL(req.url);
|
||||
|
||||
const serverId = searchParams.get("serverId");
|
||||
|
||||
if (!profile)
|
||||
{
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!serverId)
|
||||
{
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
if (name === "general")
|
||||
{
|
||||
return new NextResponse("Channel name cannot be general", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update({
|
||||
where:
|
||||
{
|
||||
id: serverId,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
role: {
|
||||
in: [MemberRole.ADMIN, MemberRole.MODERATOR]
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
data: {
|
||||
channels: {
|
||||
create: [
|
||||
{
|
||||
profileId: profile.id,
|
||||
name,
|
||||
type,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log("[CHANNEL_POST]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { DirectMessage } from "@prisma/client";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
const MESSAGES_BATCH = 10;
|
||||
|
||||
export async function GET(
|
||||
req: Request
|
||||
) {
|
||||
try {
|
||||
const profile = await currentProfile();
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
const cursor = searchParams.get("cursor");
|
||||
const conversationId = searchParams.get("conversationId");
|
||||
|
||||
if (!profile) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!conversationId) {
|
||||
return new NextResponse("Conversation ID missing", { status: 400 });
|
||||
}
|
||||
|
||||
let messages: DirectMessage[] = [];
|
||||
|
||||
if (cursor) {
|
||||
messages = await db.directMessage.findMany({
|
||||
take: MESSAGES_BATCH,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
id: cursor,
|
||||
},
|
||||
where: {
|
||||
conversationId,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
}
|
||||
})
|
||||
} else {
|
||||
messages = await db.directMessage.findMany({
|
||||
take: MESSAGES_BATCH,
|
||||
where: {
|
||||
conversationId,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let nextCursor = null;
|
||||
|
||||
if (messages.length === MESSAGES_BATCH) {
|
||||
nextCursor = messages[MESSAGES_BATCH - 1].id;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
items: messages,
|
||||
nextCursor
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("[DIRECT_MESSAGES_GET]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { AccessToken } from "livekit-server-sdk";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const room = req.nextUrl.searchParams.get("room");
|
||||
const username = req.nextUrl.searchParams.get("username");
|
||||
if (!room) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing "room" query parameter' },
|
||||
{ status: 400 }
|
||||
);
|
||||
} else if (!username) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing "username" query parameter' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const apiKey = process.env.LIVEKIT_API_KEY;
|
||||
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
||||
const wsUrl = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||
|
||||
if (!apiKey || !apiSecret || !wsUrl) {
|
||||
return NextResponse.json(
|
||||
{ error: "Server misconfigured" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const at = new AccessToken(apiKey, apiSecret, { identity: username });
|
||||
|
||||
at.addGrant({ room, roomJoin: true, canPublish: true, canSubscribe: true });
|
||||
|
||||
return NextResponse.json({ token: at.toJwt() });
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function DELETE(req: Request, { params }: { params: { memberId: string }})
|
||||
{
|
||||
try
|
||||
{
|
||||
const profile = await currentProfile();
|
||||
const { searchParams } = new URL(req.url);
|
||||
const serverId = searchParams.get("serverId");
|
||||
|
||||
if (!profile)
|
||||
{
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!serverId)
|
||||
{
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
if (!params.memberId)
|
||||
{
|
||||
return new NextResponse("Member ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update({
|
||||
where: {
|
||||
id: serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
data: {
|
||||
members: {
|
||||
deleteMany: {
|
||||
id: params.memberId,
|
||||
profileId: {
|
||||
not: profile.id
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
profile: true
|
||||
},
|
||||
orderBy: {
|
||||
role: "asc"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log("[MEMBER_ID_DELETE]", error);
|
||||
return new NextResponse("Internal Error", { status: 500});
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request, { params }: { params: { memberId: string }})
|
||||
{
|
||||
try
|
||||
{
|
||||
const profile = await currentProfile();
|
||||
const { searchParams } = new URL(req.url);
|
||||
const { role } = await req.json();
|
||||
|
||||
const serverId = searchParams.get("serverId");
|
||||
|
||||
if (!profile)
|
||||
{
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!serverId)
|
||||
{
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
if (!params.memberId)
|
||||
{
|
||||
return new NextResponse("Member ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update({
|
||||
where: {
|
||||
id: serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
data: {
|
||||
members: {
|
||||
update: {
|
||||
where: {
|
||||
id: params.memberId,
|
||||
profileId: {
|
||||
not: profile.id
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
profile: true
|
||||
},
|
||||
orderBy: {
|
||||
role: "asc"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log("[MEMBER_ID_PATCH]", error);
|
||||
return new NextResponse("Internal Error", { status: 500});
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { Message } from "@prisma/client";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
const MESSAGES_BATCH = 10;
|
||||
|
||||
export async function GET(
|
||||
req: Request
|
||||
) {
|
||||
try {
|
||||
const profile = await currentProfile();
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
const cursor = searchParams.get("cursor");
|
||||
const channelId = searchParams.get("channelId");
|
||||
|
||||
if (!profile) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
return new NextResponse("Channel ID missing", { status: 400 });
|
||||
}
|
||||
|
||||
let messages: Message[] = [];
|
||||
|
||||
if (cursor) {
|
||||
messages = await db.message.findMany({
|
||||
take: MESSAGES_BATCH,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
id: cursor,
|
||||
},
|
||||
where: {
|
||||
channelId,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
}
|
||||
})
|
||||
} else {
|
||||
messages = await db.message.findMany({
|
||||
take: MESSAGES_BATCH,
|
||||
where: {
|
||||
channelId,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let nextCursor = null;
|
||||
|
||||
if (messages.length === MESSAGES_BATCH) {
|
||||
nextCursor = messages[MESSAGES_BATCH - 1].id;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
items: messages,
|
||||
nextCursor
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("[MESSAGES_GET]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: { serverId: string } }
|
||||
) {
|
||||
try {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!params.serverId) {
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update({
|
||||
where: {
|
||||
id: params.serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
data: {
|
||||
inviteCode: uuidv4(),
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(server)
|
||||
} catch (error) {
|
||||
console.log("[SERVER_ID]", error);
|
||||
return new NextResponse("Internal Error", { status: 500});
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function PATCH(req: Request, {params}: {params: {serverId: string}})
|
||||
{
|
||||
try
|
||||
{
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile)
|
||||
{
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (!params.serverId)
|
||||
{
|
||||
return new NextResponse("Server ID Missing", { status: 400 });
|
||||
}
|
||||
|
||||
const server = await db.server.update
|
||||
({
|
||||
where:
|
||||
{
|
||||
id: params.serverId,
|
||||
profileId:
|
||||
{
|
||||
not: profile.id
|
||||
},
|
||||
members:
|
||||
{
|
||||
some:
|
||||
{
|
||||
profileId: profile.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
data:
|
||||
{
|
||||
members:
|
||||
{
|
||||
deleteMany:
|
||||
{
|
||||
profileId: profile.id
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log("[SERVER_ID_LEAVE]", error);
|
||||
return new NextResponse("Internal Error", { status: 500});
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function DELETE(req: Request, { params }: { params: { serverId: string }})
|
||||
{
|
||||
try
|
||||
{
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile)
|
||||
{
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const server = await db.server.delete
|
||||
({
|
||||
where:
|
||||
{
|
||||
id: params.serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log("[SERVER_ID_DELETE]", error);
|
||||
return new NextResponse("Internal Error", { status: 500});
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request, { params }: { params: { serverId: string }})
|
||||
{
|
||||
try
|
||||
{
|
||||
const profile = await currentProfile();
|
||||
const { name, imageUrl } = await req.json();
|
||||
|
||||
if (!profile)
|
||||
{
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const server = await db.server.update
|
||||
({
|
||||
where:
|
||||
{
|
||||
id: params.serverId,
|
||||
profileId: profile.id,
|
||||
},
|
||||
data:
|
||||
{
|
||||
name,
|
||||
imageUrl,
|
||||
}
|
||||
});
|
||||
return NextResponse.json(server);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log("[SERVER_ID_PATCH]", error);
|
||||
return new NextResponse("Internal Error", { status: 500});
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { NextResponse } from "next/server";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { name, imageUrl } = await req.json();
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const server = await db.server.create({
|
||||
data: {
|
||||
profileId: profile.id,
|
||||
name,
|
||||
imageUrl,
|
||||
inviteCode: uuidv4(),
|
||||
channels: {
|
||||
create: [
|
||||
{ name: "general", profileId: profile.id }
|
||||
],
|
||||
},
|
||||
members: {
|
||||
create: [
|
||||
{ profileId: profile.id, role: MemberRole.ADMIN }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ server });
|
||||
} catch (error) {
|
||||
console.log("[SERVER POST]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { createUploadthing, type FileRouter } from "uploadthing/next";
|
||||
|
||||
const f = createUploadthing();
|
||||
|
||||
const handleAuth = () => {
|
||||
const { userId } = auth();
|
||||
if (!userId) throw new Error("Unauthorized");
|
||||
return { userId: userId };
|
||||
}
|
||||
|
||||
export const ourFileRouter = {
|
||||
serverImage: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
|
||||
.middleware(() => handleAuth())
|
||||
.onUploadComplete(() => {}),
|
||||
messageFile: f(["image", "pdf"])
|
||||
.middleware(() => handleAuth())
|
||||
.onUploadComplete(() => {})
|
||||
} satisfies FileRouter;
|
||||
|
||||
export type OurFileRouter = typeof ourFileRouter;
|
@ -1,8 +0,0 @@
|
||||
import { createNextRouteHandler } from "uploadthing/next";
|
||||
|
||||
import { ourFileRouter } from "./core";
|
||||
|
||||
// Export routes for Next App Router
|
||||
export const { GET, POST } = createNextRouteHandler({
|
||||
router: ourFileRouter,
|
||||
});
|
@ -2,17 +2,13 @@ import './globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { Open_Sans } from 'next/font/google'
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ThemeProvider } from '@/components/providers/theme-provider'
|
||||
import { ModalProvider } from '@/components/providers/modal-provider'
|
||||
import { SocketProvider } from '@/components/providers/socket-provider'
|
||||
import { QueryProvider } from '@/components/providers/query-provider'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const font = Open_Sans({ subsets: ['latin'] })
|
||||
const Font = Open_Sans({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
title: 'Discord-Clone',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
@ -25,21 +21,16 @@ export default function RootLayout({
|
||||
<ClerkProvider>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={cn(
|
||||
font.className,
|
||||
Font.className,
|
||||
"bg-white dark:bg-[#313338]"
|
||||
)}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
attribute='class'
|
||||
defaultTheme='dark'
|
||||
enableSystem={false}
|
||||
storageKey="discord-theme"
|
||||
storageKey='discord-theme'
|
||||
>
|
||||
<SocketProvider>
|
||||
<ModalProvider/>
|
||||
<QueryProvider>
|
||||
{children}
|
||||
</QueryProvider>
|
||||
</SocketProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,37 +0,0 @@
|
||||
"user client"
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface ActionTooltipProps {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "start" | "center" | "end";
|
||||
}
|
||||
|
||||
export const ActionTooltip = ({
|
||||
label,
|
||||
children,
|
||||
side,
|
||||
align,
|
||||
}: ActionTooltipProps) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={50}>
|
||||
<TooltipTrigger asChild>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side} align={align}>
|
||||
<p className="font-semibold text-sm capitalize">
|
||||
{label.toLowerCase()}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { Hash, Menu } from "lucide-react"
|
||||
import { MobileToggle } from "@/components/mobile-toggle";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { SocketIndicator } from "@/components/socket-indicator";
|
||||
import { ChatVideoButton } from "./chat-video-button";
|
||||
|
||||
interface ChatHeaderProps {
|
||||
serverId: string;
|
||||
name: string;
|
||||
type: "channel" | "conversation";
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
export const ChatHeader = ({
|
||||
serverId,
|
||||
name,
|
||||
type,
|
||||
imageUrl
|
||||
}: ChatHeaderProps) => {
|
||||
return (
|
||||
<div className="text-md font-semibold px-3 flex items-center h-12 border-neutral-200 dark:border-neutral-800 border-b-2">
|
||||
<MobileToggle serverId={serverId} />
|
||||
{type === "channel" && (
|
||||
<Hash className="w-5 h-5 text-zinc-500 dark:text-zinc-400 mr-2"/>
|
||||
)}
|
||||
{type === "conversation" && (
|
||||
<UserAvatar
|
||||
src={imageUrl}
|
||||
className="w-8 h-8 md:h-8 md:w-8 mr-2"
|
||||
/>
|
||||
)}
|
||||
<p className="font-semibold text-md text-black dark:text-white">
|
||||
{name}
|
||||
</p>
|
||||
<div className="ml-auto flex items-center">
|
||||
{type === "conversation" && (
|
||||
<ChatVideoButton/>
|
||||
)}
|
||||
<SocketIndicator/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import axios from "axios";
|
||||
import qs from "query-string";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { EmojiPicker } from "@/components/emoji-picker";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface ChatInputProps {
|
||||
apiUrl: string;
|
||||
query: Record<string, any>;
|
||||
name: string;
|
||||
type: "conversation" | "channel";
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ChatInput = ({
|
||||
apiUrl,
|
||||
query,
|
||||
name,
|
||||
type
|
||||
}: ChatInputProps) => {
|
||||
const { onOpen } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
content: "",
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try{
|
||||
const url = qs.stringifyUrl({
|
||||
url: apiUrl,
|
||||
query,
|
||||
});
|
||||
|
||||
await axios.post(url, values);
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="relative p-4 pb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpen("messageFile", { apiUrl, query})}
|
||||
className="absolute top-7 left-8 h-[24px] w-[24px] bg-zinc-500 dark:bg-zinc-400 hover:bg-zinc-600 dark:hover:bg-zinc-300 transition rounded-full p-1 flex items-center justify-center"
|
||||
>
|
||||
<Plus className="text-white dark:text-[#313338]"/>
|
||||
</button>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="px-14 py-6 bg-zinc-200/90 dark:bg-zinc-700/75 border-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-zinc-600 dark:text-zinc-200"
|
||||
placeholder={`Message ${type === "conversation" ? name : "#" + name}`}
|
||||
{...field}
|
||||
/>
|
||||
<div className="absolute top-7 right-8">
|
||||
<EmojiPicker
|
||||
onChange={(emoji: string) => field.onChange(`${field.value} ${emoji}`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as z from "zod";
|
||||
import axios from "axios";
|
||||
import qs from "query-string";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Member, MemberRole, Profile } from "@prisma/client";
|
||||
import { Edit, FileIcon, ShieldAlert, ShieldCheck, Trash } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { ActionTooltip } from "@/components/action-tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
|
||||
interface ChatItemProps {
|
||||
id: string;
|
||||
content: string;
|
||||
member: Member & {
|
||||
profile: Profile;
|
||||
};
|
||||
timestamp: string
|
||||
fileUrl: string | null;
|
||||
deleted: boolean;
|
||||
currentMember: Member;
|
||||
isUpdated: boolean;
|
||||
socketUrl: string;
|
||||
socketQuery: Record<string, string>;
|
||||
}
|
||||
|
||||
const roleIconMap = {
|
||||
"GUEST": null,
|
||||
"MODERATOR": <ShieldCheck className="h-4 w-4 ml-2 text-indigo-500"/>,
|
||||
"ADMIN": <ShieldAlert className="h-4 w-4 ml-2 text-rose-500"/>,
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
})
|
||||
|
||||
export const ChatItem = ({
|
||||
id,
|
||||
content,
|
||||
member,
|
||||
timestamp,
|
||||
fileUrl,
|
||||
deleted,
|
||||
currentMember,
|
||||
isUpdated,
|
||||
socketUrl,
|
||||
socketQuery,
|
||||
}: ChatItemProps) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { onOpen } = useModal();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const onMemberClick = () => {
|
||||
if (member.id === currentMember.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/servers/${params?.serverId}/conversations/${member.id}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" || event.keyCode === 27) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
content: content
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
const url = qs.stringifyUrl({
|
||||
url: `${socketUrl}/${id}`,
|
||||
query: socketQuery,
|
||||
});
|
||||
|
||||
await axios.patch(url, values);
|
||||
|
||||
form.reset();
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
content: content,
|
||||
})
|
||||
}, [content]);
|
||||
|
||||
const filetype = fileUrl?.split(".").pop();
|
||||
|
||||
const isAdmin = currentMember.role === MemberRole.ADMIN;
|
||||
const isModerator = currentMember.role === MemberRole.MODERATOR;
|
||||
const isOwner = currentMember.id === member.id;
|
||||
const canDeleteMessage = !deleted && (isAdmin || isModerator || isOwner);
|
||||
const canEditMessage = !deleted && isOwner && !fileUrl;
|
||||
const isPDF = filetype == "pdf" && fileUrl;
|
||||
const isImage = !isPDF && fileUrl;
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative group flex items-center hover:bg-black/5 p-4 transition w-full">
|
||||
<div className="group flex gap-x-2 items-start w-full">
|
||||
<div onClick={onMemberClick} className="cursor-pointer hover:drop-shadow-md transition">
|
||||
<UserAvatar src={member.profile.imageUrl}/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-center">
|
||||
<p onClick={onMemberClick} className="font-semibold text-sm hover:underline cursor-pointer">
|
||||
{member.profile.name}
|
||||
</p>
|
||||
<ActionTooltip label={member.role}>
|
||||
{roleIconMap[member.role]}
|
||||
</ActionTooltip>
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{timestamp}
|
||||
</span>
|
||||
</div>
|
||||
{isImage && (
|
||||
<a
|
||||
href={fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative aspect-square rounded-md mt-2 overflow-hidden border flex items-center bg-secondary h-48 w-48"
|
||||
>
|
||||
<Image
|
||||
src={fileUrl}
|
||||
alt={content}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
{isPDF && (
|
||||
<div className="relative flex items-center p-2 mt-2 rounded-md bg-background/10">
|
||||
<FileIcon className="h-10 w-10 fill-indigo-200 stroke-indigo-400 "/>
|
||||
<a
|
||||
href={fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-sm text-indigo-500 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
PDF File
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!fileUrl && !isEditing && (
|
||||
<p className={cn(
|
||||
"text-sm text-zinc-600 dark:text-zinc-300",
|
||||
deleted && "italic text-zinc-500 dark:text-zinc-400 text-xs mt-1"
|
||||
)}>
|
||||
{content}
|
||||
{isUpdated && (
|
||||
<span className="text-[10px] ml-2 text-zinc-500 dark:text-zinc-400">
|
||||
(edited)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{!fileUrl && isEditing && (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex items-center w-full gap-x-2 pt-2"
|
||||
onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="p-2 bg-zinc-200/90 dark:bg-zinc-700/75 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-zinc-600 dark:text-zinc-200"
|
||||
placeholder="Edited message"
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button disabled={isLoading} size="sm" variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
<span className="text-[10px] mt-1 text-zinc-400">
|
||||
Press escape to cancel, enter to save
|
||||
|
||||
</span>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{canDeleteMessage && (
|
||||
<div className="hidden group-hover:flex items-center gap-x-2 absolute p-1 -top-2 right-5 bg-white dark:bg-zinc-800 border rounded-sm">
|
||||
{canEditMessage && (
|
||||
<ActionTooltip label="Edit">
|
||||
<Edit
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="cursor-pointer ml-auto w-4 h-4 text-zinc-500 hover:text-zinc-600 dark:hove:text-zinc-300 transition"
|
||||
/>
|
||||
|
||||
</ActionTooltip>
|
||||
)}
|
||||
<ActionTooltip label="Delete">
|
||||
<Trash
|
||||
onClick={() => onOpen("deleteMessage", {
|
||||
apiUrl: `${socketUrl}/${id}`,
|
||||
query: socketQuery,
|
||||
})}
|
||||
className="cursor-pointer ml-auto w-4 h-4 text-zinc-500 hover:text-zinc-600 dark:hove:text-zinc-300 transition"
|
||||
/>
|
||||
|
||||
</ActionTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, useRef, ElementRef, use } from "react";
|
||||
import { format } from "date-fns"
|
||||
import { Member, Message, Profile } from "@prisma/client"
|
||||
|
||||
import { ChatWelcome } from "./chat-welcome";
|
||||
import { useChatQuery } from "@/hooks/use-chat-query";
|
||||
import { Loader2, ServerCrash } from "lucide-react";
|
||||
import { ChatItem } from "./chat-item";
|
||||
import { useChatSocket } from "@/hooks/use-chat-socket";
|
||||
import { useChatScroll } from "@/hooks/use-chat-scroll";
|
||||
|
||||
const DATE_FORMAT = "d MMM yyyy, HH:mm";
|
||||
|
||||
type MessageWithMemberWithProfile = Message & {
|
||||
member: Member & {
|
||||
profile: Profile
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatMessagesProps {
|
||||
name: string;
|
||||
member: Member;
|
||||
chatId: string;
|
||||
apiUrl: string;
|
||||
socketUrl: string;
|
||||
socketQuery: Record<string, string>;
|
||||
paramKey: "channelId" | "conversationId";
|
||||
paramValue: string;
|
||||
type: "channel" | "conversation";
|
||||
}
|
||||
|
||||
export const ChatMessages = ({
|
||||
name,
|
||||
member,
|
||||
chatId,
|
||||
apiUrl,
|
||||
socketUrl,
|
||||
socketQuery,
|
||||
paramKey,
|
||||
paramValue,
|
||||
type,
|
||||
}: ChatMessagesProps) => {
|
||||
const queryKey = `chat:${chatId}`;
|
||||
const addKey = `chat:${chatId}:messages`;
|
||||
const updateKey = `chat:${chatId}:messages:update`;
|
||||
|
||||
const chatRef = useRef<ElementRef<"div">>(null);
|
||||
const bottomRef = useRef<ElementRef<"div">>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
status,
|
||||
} = useChatQuery({
|
||||
queryKey,
|
||||
apiUrl,
|
||||
paramKey,
|
||||
paramValue,
|
||||
});
|
||||
useChatSocket({ queryKey, addKey, updateKey});
|
||||
useChatScroll({
|
||||
chatRef,
|
||||
bottomRef,
|
||||
loadMore: fetchNextPage,
|
||||
shouldLoadMore: !isFetchingNextPage && !!hasNextPage,
|
||||
count: data?.pages?.[0]?.items?.length ?? 0,
|
||||
|
||||
})
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 justify-center items-center">
|
||||
<Loader2 className="h-7 w-7 text-zinc-500 animate-spin my-4"/>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Loading Messages...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === "error") {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 justify-center items-center">
|
||||
<ServerCrash className="h-7 w-7 text-zinc-500 my-4"/>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Something went wrong!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={chatRef} className="flex-1 flex flex-col py-4 overflow-y-auto">
|
||||
{!hasNextPage && <div className="flex-1"/>}
|
||||
{!hasNextPage &&(
|
||||
<ChatWelcome
|
||||
type={type}
|
||||
name={name}
|
||||
/>
|
||||
)}
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center">
|
||||
{isFetchingNextPage ? (
|
||||
<Loader2 className="h-6 w-6 text-zinc-500 animate-spin my-4"/>
|
||||
): (
|
||||
<button
|
||||
onClick={() => fetchNextPage()}
|
||||
className="text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 text-xs dark:hover:text-zinc-300 transition"
|
||||
>
|
||||
Load previous messages
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col-reverse mt-auto">
|
||||
{data?.pages.map((group, i) => (
|
||||
<Fragment key={i}>
|
||||
{group.items.map((message: MessageWithMemberWithProfile) => (
|
||||
<ChatItem
|
||||
key={message.id}
|
||||
id={message.id}
|
||||
currentMember={member}
|
||||
member={message.member}
|
||||
content={message.content}
|
||||
fileUrl={message.fileUrl}
|
||||
deleted={message.deleted}
|
||||
timestamp={format(new Date(message.createdAt), DATE_FORMAT)}
|
||||
isUpdated={message.updatedAt !== message.createdAt}
|
||||
socketUrl={socketUrl}
|
||||
socketQuery={socketQuery}
|
||||
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div ref={bottomRef}/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import qs from "query-string";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Video, VideoOff } from "lucide-react";
|
||||
|
||||
import { ActionTooltip } from "@/components/action-tooltip";
|
||||
|
||||
export const ChatVideoButton = () => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const isVideo = searchParams?.get("video");
|
||||
|
||||
const onClick = () => {
|
||||
const url = qs.stringifyUrl({
|
||||
url: pathname || "",
|
||||
query: {
|
||||
video: isVideo ? undefined : true,
|
||||
}
|
||||
}, { skipNull: true});
|
||||
|
||||
router.push(url);
|
||||
}
|
||||
|
||||
const Icon = isVideo ? VideoOff : Video;
|
||||
const tooltiplabel = isVideo ? "End video call" : "Start video call";
|
||||
|
||||
return (
|
||||
<ActionTooltip side="bottom" label={tooltiplabel}>
|
||||
<button onClick={onClick} className="hover:opacity=75 transition mr-4">
|
||||
<Icon className="h-6 w-6 text-zinc-500 dark:text-zinc-400"/>
|
||||
</button>
|
||||
|
||||
</ActionTooltip>
|
||||
)
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { Hash } from "lucide-react";
|
||||
|
||||
interface ChatWelcomeProps {
|
||||
name: string;
|
||||
type: "channel" | "conversation";
|
||||
}
|
||||
|
||||
export const ChatWelcome = ({
|
||||
name,
|
||||
type,
|
||||
}: ChatWelcomeProps) => {
|
||||
return (
|
||||
<div className="space-y-2 px-4 mb-4">
|
||||
{type === "channel" && (
|
||||
<div className="h-[75px] w-[75px] rounded-full bg-zinc-500 dark:bg-zinc-700 flex items-center justify-center">
|
||||
<Hash className="h-12 w-12 text-white"/>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xl md:text-3l font-bold">
|
||||
{type === "channel" ? "Welcome to #" : ""}{name}
|
||||
</p>
|
||||
<p className="text-zinc-600 dark:text-zinc-400 text-sm">
|
||||
{type === "channel" ? `This is the start of the #${name} channel.` : `This is the start of your conversation with ${name}`}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Smile } from "lucide-react";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import data from "@emoji-mart/data";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import{
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
|
||||
interface EmojiPickerProps {
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
onChange,
|
||||
}: EmojiPickerProps) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Smile className="text-zinc-500 dark:text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition"/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
sideOffset={40}
|
||||
className="bg-transparent border-none shadow-none drop-shadow-none mb-16"
|
||||
>
|
||||
<Picker
|
||||
theme={resolvedTheme}
|
||||
data={data}
|
||||
onEmojiSelect={(emoji: any) => onChange(emoji.native)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FileIcon, X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { UploadDropzone } from "@/lib/uploadthing";
|
||||
|
||||
import "@uploadthing/react/styles.css";
|
||||
|
||||
interface FileUploadProps {
|
||||
onChange: (url?: string) => void;
|
||||
value: string;
|
||||
endpoint: "messageFile" | "serverImage";
|
||||
}
|
||||
|
||||
export const FileUpload = ({
|
||||
onChange,
|
||||
value,
|
||||
endpoint,
|
||||
}: FileUploadProps) => {
|
||||
const fileType = value?.split(".").pop();
|
||||
|
||||
if (value && fileType !== "pdf") {
|
||||
return (
|
||||
<div className="relative h-20 w-20">
|
||||
<Image
|
||||
fill
|
||||
src={value}
|
||||
alt="Upload"
|
||||
className="rounded-full"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onChange("")}
|
||||
className="bg-rose-500 text-white p-1 rounded-full absolute top-0 right-0 shadow-sm"
|
||||
type="button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (value && fileType === "pdf") {
|
||||
return (
|
||||
<div className="relative flex items-center p-2 mt-2 rounded-md bg-background/10">
|
||||
<FileIcon className="h-10 w-10 fill-indigo-200 stroke-indigo-400 "/>
|
||||
<a
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-sm text-indigo-500 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
<button
|
||||
onClick={() => onChange("")}
|
||||
className="bg-rose-500 text-white p-1 rounded-full absolute -top-2 -right-2 shadow-sm"
|
||||
type="button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<UploadDropzone
|
||||
endpoint={endpoint}
|
||||
onClientUploadComplete={(res) => {
|
||||
onChange(res?.[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
console.error(error);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { LiveKitRoom, VideoConference} from "@livekit/components-react"
|
||||
import "@livekit/components-styles"
|
||||
import { Channel } from "@prisma/client";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface MediaRoomProps {
|
||||
chatId: string;
|
||||
video: boolean;
|
||||
audio: boolean;
|
||||
};
|
||||
|
||||
export const MediaRoom = ({
|
||||
chatId,
|
||||
video,
|
||||
audio
|
||||
}: MediaRoomProps) => {
|
||||
const { user } = useUser();
|
||||
const [token, setToken] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.firstName || !user?.lastName) return;
|
||||
|
||||
const name = `${user.firstName} ${user.lastName}`;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch (`/api/livekit?room=${chatId}&username=${name}`);
|
||||
const data = await resp.json();
|
||||
setToken(data.token);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
})()
|
||||
}, [user?.firstName, user?.lastName, chatId]);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 justify-center items-center">
|
||||
<Loader2
|
||||
className="h-7 w-7 text-zinc-500 animate-spin my-4"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LiveKitRoom
|
||||
data-lk-theme="default"
|
||||
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
|
||||
token={token}
|
||||
connect={true}
|
||||
video={video}
|
||||
audio={audio}
|
||||
>
|
||||
<VideoConference />
|
||||
|
||||
</LiveKitRoom>
|
||||
)
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { Menu } from "lucide-react"
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger
|
||||
} from "@/components/ui/sheet";
|
||||
import { Button } from "./ui/button";
|
||||
import { NavigationSidebar } from "@/components/navigation/navigation-sidebar";
|
||||
import { ServerSidebar } from "@/components/server/server-sidebar";
|
||||
|
||||
export const MobileToggle = ({
|
||||
serverId
|
||||
}: {
|
||||
serverId: string;
|
||||
}) => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Menu/>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0 flex gap-0">
|
||||
<div className="w-[72px]">
|
||||
<NavigationSidebar/>
|
||||
</div>
|
||||
<ServerSidebar serverId={serverId}/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import qs from "query-string";
|
||||
import axios from "axios";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ChannelType } from "@prisma/client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import{
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Channel name is required",
|
||||
}).refine(
|
||||
name => name !== "general",
|
||||
{
|
||||
message: "Channel name cannot be 'general'",
|
||||
}
|
||||
),
|
||||
type: z.nativeEnum(ChannelType),
|
||||
});
|
||||
|
||||
export const CreateChannelModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const isModalOpen = isOpen && type === "createChannel";
|
||||
const { channelType } = data;
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
type: channelType || ChannelType.TEXT,
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (channelType) {
|
||||
form.setValue("type", channelType);
|
||||
}
|
||||
else{
|
||||
form.setValue("type", ChannelType.TEXT);
|
||||
}
|
||||
}, [channelType, form])
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
const url = qs.stringifyUrl({
|
||||
url: "/api/channels",
|
||||
query: {
|
||||
serverId: params?.serverId
|
||||
}
|
||||
});
|
||||
await axios.post(url, values);
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Create Channel
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-8 px-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
className="uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70"
|
||||
>
|
||||
Channel name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="bg-zinc-300/50 border-0
|
||||
focus-visible:ring-0 text-black
|
||||
focus-visible:ring-offset-0"
|
||||
placeholder="Enter channel name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel Type</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger
|
||||
className="bg-zinc-300/50 border-0 focus:ring-0 text-black ring-offset-0 focus:ring-offset-0 capitalize outline-none"
|
||||
>
|
||||
<SelectValue placeholder="Select a channel type"/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.values(ChannelType).map((type) => (
|
||||
<SelectItem key={type} value={type} className="capitalize">
|
||||
{type.toLocaleLowerCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<Button variant={"primary"} disabled={isLoading}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import{
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileUpload } from "@/components/file-upload";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Server name is required",
|
||||
}),
|
||||
imageUrl: z.string().min(1, {
|
||||
message: "Server image is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const CreateServerModal = () => {
|
||||
const { isOpen, onClose, type } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "createServer";
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
imageUrl: "",
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
await axios.post("/api/servers", values);
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Customize your server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Give your server a personality with a name and an image. You can always change it later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-8 px-6">
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imageUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<FileUpload
|
||||
endpoint="serverImage"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
className="uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70"
|
||||
>
|
||||
Server name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="bg-zinc-300/50 border-0
|
||||
focus-visible:ring-0 text-black
|
||||
focus-visible:ring-offset-0"
|
||||
placeholder="Enter server name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<Button variant={"primary"} disabled={isLoading}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import qs from "query-string";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
||||
export const DeleteChannelModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "deleteChannel";
|
||||
const { server, channel } = data;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClick = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
setIsLoading(true);
|
||||
|
||||
const url = qs.stringifyUrl({
|
||||
url: `/api/channels/${channel?.id}`,
|
||||
query: {
|
||||
serverId: server?.id
|
||||
},
|
||||
})
|
||||
|
||||
await axios.delete(url);
|
||||
|
||||
onClose();
|
||||
router.refresh();
|
||||
router.push(`/servers/${server?.id}`);
|
||||
|
||||
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Delete Channel
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Are you sure you want to do this? <br/>
|
||||
<span className="font-semibold text-indigo-500" >
|
||||
#{channel?.name}
|
||||
</span> will be permenantly deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Button disabled={isLoading} onClick={onClose} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={onClick} variant="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import qs from "query-string";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
||||
export const DeleteMessageModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
|
||||
const isModalOpen = isOpen && type === "deleteMessage";
|
||||
const { apiUrl, query } = data;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClick = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
setIsLoading(true);
|
||||
|
||||
const url = qs.stringifyUrl({
|
||||
url: apiUrl || "",
|
||||
query,
|
||||
})
|
||||
|
||||
await axios.delete(url);
|
||||
|
||||
onClose();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Delete Message
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Are you sure you want to do this? <br/>
|
||||
The message will be permenantly deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Button disabled={isLoading} onClick={onClose} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={onClick} variant="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
||||
export const DeleteServerModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "deleteServer";
|
||||
const { server } = data;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClick = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
setIsLoading(true);
|
||||
|
||||
await axios.delete(`/api/servers/${server?.id}`);
|
||||
|
||||
onClose();
|
||||
router.refresh();
|
||||
router.push("/");
|
||||
|
||||
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Delete Server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Are you sure you want to do this? <br/>
|
||||
<span className="font-semibold text-indigo-500" >
|
||||
{server?.name}
|
||||
</span> will be permenantly deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Button disabled={isLoading} onClick={onClose} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={onClick} variant="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import qs from "query-string";
|
||||
import axios from "axios";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ChannelType } from "@prisma/client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import{
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Channel name is required",
|
||||
}).refine(
|
||||
name => name !== "general",
|
||||
{
|
||||
message: "Channel name cannot be 'general'",
|
||||
}
|
||||
),
|
||||
type: z.nativeEnum(ChannelType),
|
||||
});
|
||||
|
||||
export const EditChannelModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "editChannel";
|
||||
const { channel, server } = data;
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
type: channel?.type || ChannelType.TEXT,
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
form.setValue("name", channel.name),
|
||||
form.setValue("type", channel.type)
|
||||
}
|
||||
}, [form, channel])
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
const url = qs.stringifyUrl({
|
||||
url: `/api/channels/${channel?.id}`,
|
||||
query: {
|
||||
serverId: server?.id
|
||||
}
|
||||
});
|
||||
await axios.patch(url, values);
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Edit Channel
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-8 px-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
className="uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70"
|
||||
>
|
||||
Channel name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="bg-zinc-300/50 border-0
|
||||
focus-visible:ring-0 text-black
|
||||
focus-visible:ring-offset-0"
|
||||
placeholder="Enter channel name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel Type</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger
|
||||
className="bg-zinc-300/50 border-0 focus:ring-0 text-black ring-offset-0 focus:ring-offset-0 capitalize outline-none"
|
||||
>
|
||||
<SelectValue placeholder="Select a channel type"/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.values(ChannelType).map((type) => (
|
||||
<SelectItem key={type} value={type} className="capitalize">
|
||||
{type.toLocaleLowerCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<Button variant={"primary"} disabled={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import{
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileUpload } from "@/components/file-upload";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Server name is required",
|
||||
}),
|
||||
imageUrl: z.string().min(1, {
|
||||
message: "Server image is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const EditServerModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "editServer";
|
||||
|
||||
const { server } = data;
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
imageUrl: "",
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
form.setValue("name", server.name),
|
||||
form.setValue("imageUrl", server.imageUrl)
|
||||
|
||||
}
|
||||
}, [server, form]);
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
await axios.patch(`/api/servers/${server?.id}`, values);
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Customize your server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Give your server a personality with a name and an image. You can always change it later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-8 px-6">
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imageUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<FileUpload
|
||||
endpoint="serverImage"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
className="uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70"
|
||||
>
|
||||
Server name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="bg-zinc-300/50 border-0
|
||||
focus-visible:ring-0 text-black
|
||||
focus-visible:ring-offset-0"
|
||||
placeholder="Enter server name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<Button variant={"primary"} disabled={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import{
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileUpload } from "@/components/file-upload";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Server name is required",
|
||||
}),
|
||||
imageUrl: z.string().min(1, {
|
||||
message: "Server image is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const InitialModal = () => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
imageUrl: "",
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
await axios.post("/api/servers", values);
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Customize your server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Give your server a personality with a name and an image. You can always change it later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-8 px-6">
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imageUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<FileUpload
|
||||
endpoint="serverImage"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
className="uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70"
|
||||
>
|
||||
Server name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
className="bg-zinc-300/50 border-0
|
||||
focus-visible:ring-0 text-black
|
||||
focus-visible:ring-offset-0"
|
||||
placeholder="Enter server name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<Button variant={"primary"} disabled={isLoading}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { Check, Copy, RefreshCw } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useOrigin } from "@/hooks/user-origin";
|
||||
|
||||
|
||||
export const InviteModal = () => {
|
||||
const { onOpen, isOpen, onClose, type, data } = useModal();
|
||||
const origin = useOrigin();
|
||||
|
||||
const isModalOpen = isOpen && type === "invite";
|
||||
const { server } = data;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const inviteUrl = `${origin}/invite/${server?.inviteCode}`;
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(inviteUrl);
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const onNew = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await axios.patch(`/api/servers/${server?.id}/invite-code`);
|
||||
|
||||
onOpen("invite", {server: response.data});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally{
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Invite Friends
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-6">
|
||||
<Label className="uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70">
|
||||
Server invite link
|
||||
</Label>
|
||||
<div className="flex items-center mt-2 gap-x-2">
|
||||
<Input className="bg-zinc-300/50 border-0 focus-visible:ring-0 text-black focus-visible:ring-offset-0 "
|
||||
disabled={isLoading}
|
||||
value={inviteUrl}
|
||||
/>
|
||||
<Button disabled={isLoading} onClick={onCopy} size="icon">
|
||||
{copied ? <Check className="w-4 h-4"/> : <Copy className="w-4 h-4"/>}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onNew}
|
||||
disabled={isLoading}
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-xs text-zinc-500 mt-4"
|
||||
>
|
||||
Generate a new link
|
||||
<RefreshCw className="w-4 h-4 ml-2"/>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
||||
export const LeaveServerModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "leaveServer";
|
||||
const { server } = data;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClick = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
setIsLoading(true);
|
||||
|
||||
await axios.patch(`/api/servers/${server?.id}/leave`);
|
||||
|
||||
onClose();
|
||||
router.refresh();
|
||||
router.push("/");
|
||||
|
||||
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Leave Server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Are you sure you wan to leave
|
||||
<span className="font-semibold text-indigo-500" >
|
||||
{server?.name}
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Button disabled={isLoading} onClick={onClose} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={onClick} variant="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import qs from "query-string";
|
||||
import { Check, Gavel, Loader2, MoreVertical, Shield, ShieldAlert, ShieldCheck, ShieldQuestion } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
import { ServerWithMembersWithProfiles } from "@/types";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSubTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
const roleIconMap = {
|
||||
"GUEST": null,
|
||||
"MODERATOR": <ShieldCheck className="w-4 h-4 ml-2 text-indigo-500"/>,
|
||||
"ADMIN": <ShieldAlert className="w-4 h-4 text-rose-500"/>
|
||||
}
|
||||
|
||||
export const MembersModal = () => {
|
||||
const router = useRouter();
|
||||
const { onOpen, isOpen, onClose, type, data } = useModal();
|
||||
const [loadingId, setloadingId] = useState("");
|
||||
|
||||
const isModalOpen = isOpen && type === "members";
|
||||
const { server } = data as { server: ServerWithMembersWithProfiles };
|
||||
|
||||
const onKick = async (memberId: string) => {
|
||||
try {
|
||||
setloadingId(memberId);
|
||||
const url = qs.stringifyUrl({
|
||||
url: `/api/members/${memberId}`,
|
||||
query: {
|
||||
serverId: server?.id,
|
||||
}
|
||||
});
|
||||
|
||||
const repsponse = await axios.delete(url);
|
||||
|
||||
router.refresh();
|
||||
onOpen("members", {server: repsponse.data});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setloadingId("");
|
||||
}
|
||||
}
|
||||
|
||||
const onRoleChange = async (memberId: string, role: MemberRole) => {
|
||||
try {
|
||||
setloadingId(memberId);
|
||||
const url = qs.stringifyUrl({
|
||||
url: `/api/members/${memberId}`,
|
||||
query: {
|
||||
serverId: server?.id,
|
||||
}
|
||||
});
|
||||
|
||||
const response = await axios.patch(url, { role });
|
||||
|
||||
router.refresh();
|
||||
onOpen("members", {server: response.data});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setloadingId("");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-white dark:bg-[#313338] text-black dark:text-white overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Manage Members
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500 dark:text-zinc-300">
|
||||
{server?.members?.length} Members
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="mt-8 max-h-[420px] pr-6">
|
||||
{server?.members?.map((member) => (
|
||||
<div key={member.id} className="flex items-center gap-x-2 mb-6">
|
||||
<UserAvatar src={member.profile.imageUrl}/>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="text-xs font-semibold flex items-center gap-x-1">
|
||||
{member.profile.name}
|
||||
{roleIconMap[member.role]}
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-300">
|
||||
{member.profile.email}
|
||||
</p>
|
||||
</div>
|
||||
{server.profileId !== member.profileId && loadingId !== member.id && (
|
||||
<div className="ml-auto">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<MoreVertical className="h-4 w-4 text-zinc-500 dark:text-zinc-300">
|
||||
|
||||
</MoreVertical>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="left">
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="flex items-center">
|
||||
<ShieldQuestion className="w-4 h-4 mr-2"/>
|
||||
<span>Role</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => onRoleChange(member.id, "GUEST")}>
|
||||
<Shield className="w-4 h-4 mr-2"/>
|
||||
Guest
|
||||
{member.role === "GUEST" && (
|
||||
<Check className="w-4 h-4 ml-auto"/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onRoleChange(member.id, "MODERATOR")}>
|
||||
<ShieldCheck className="w-4 h-4 mr-2"/>
|
||||
Moderator
|
||||
{member.role === "MODERATOR" && (
|
||||
<Check className="w-4 h-4 ml-auto"/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator/>
|
||||
<DropdownMenuItem onClick={() => onKick(member.id)}>
|
||||
<Gavel className="h-4 w-4 mr-2"/>
|
||||
Kick
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{loadingId === member.id && (
|
||||
<Loader2 className="animate-spin text-zinc-500 ml-auto w-4 h-4"/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import axios from "axios";
|
||||
import qs from "query-string";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
} from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileUpload } from "@/components/file-upload";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
|
||||
const formSchema = z.object({
|
||||
fileUrl: z.string().min(1, {
|
||||
message: "Attachment is required."
|
||||
})
|
||||
});
|
||||
|
||||
export const MessageFileModal = () => {
|
||||
const { isOpen, onClose, type, data } = useModal();
|
||||
const router = useRouter();
|
||||
|
||||
const isModalOpen = isOpen && type === "messageFile";
|
||||
const { apiUrl, query } = data;
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
fileUrl: "",
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
const url = qs.stringifyUrl({
|
||||
url: apiUrl || "",
|
||||
query,
|
||||
});
|
||||
|
||||
await axios.post(url, {
|
||||
...values,
|
||||
content: values.fileUrl,
|
||||
});
|
||||
|
||||
form.reset();
|
||||
router.refresh();
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-white text-black p-0 overflow-hidden">
|
||||
<DialogHeader className="pt-8 px-6">
|
||||
<DialogTitle className="text-2xl text-center font-bold">
|
||||
Add an attachment
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-zinc-500">
|
||||
Send a file as a message
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-8 px-6">
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<FileUpload
|
||||
endpoint="messageFile"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="bg-gray-100 px-6 py-4">
|
||||
<Button variant="primary" disabled={isLoading}>
|
||||
Send
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -18,7 +18,7 @@ export function ModeToggle() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="bg-transparrent border-0" variant="outline" size="icon">
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
|
@ -1,33 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import { ActionTooltip } from "@/components/action-tooltip"
|
||||
import { useModal } from "@/hooks/use-modal-store"
|
||||
|
||||
export const NavigationAction = () => {
|
||||
|
||||
const { onOpen } = useModal();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ActionTooltip
|
||||
side="right"
|
||||
align="center"
|
||||
label="Add a server"
|
||||
>
|
||||
<button
|
||||
onClick={() => onOpen("createServer")}
|
||||
className="group flex items-center"
|
||||
>
|
||||
<div className="flex mx-3 h-[48px] w-[48px] rounded-[24px] group-hover:rounded-[16px] transition-all overflow-hidding items-center justify-center bg-background dark:bg-neutral-700 group-hover:bg-emerald-500">
|
||||
<Plus
|
||||
className="group-hover:text-white transition text-emerald-500"
|
||||
size={25}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</ActionTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ActionTooltip } from "@/components/action-tooltip"
|
||||
|
||||
interface NavigationItemProps {
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const NavigationItem = ({
|
||||
id,
|
||||
imageUrl,
|
||||
name
|
||||
}: NavigationItemProps) => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const onClick = () => {
|
||||
router.push(`/servers/${id}`);
|
||||
}
|
||||
return (
|
||||
<ActionTooltip
|
||||
side="right"
|
||||
align="center"
|
||||
label={name}
|
||||
>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="group relative flex items-center"
|
||||
>
|
||||
<div className={cn(
|
||||
"absolute left-0 bg-primary rounded-r-full transition-all w-[4px]",
|
||||
params?.serverId === id && "group-hover:h-[20px]",
|
||||
params?.serverId === id ? "h-[36px]" : "h-[8px]"
|
||||
)}/>
|
||||
<div className={cn(
|
||||
"relative group flex mx-3 h-[48px] w-[48px] rounded-[24px] group-hover:rounded-[16px] transition-all overflow-hidden",
|
||||
params?.serverId === id && "bg-primary/10 text-primary rounded-[16px]"
|
||||
)}>
|
||||
<Image
|
||||
fill
|
||||
src={imageUrl}
|
||||
alt="Channel"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</ActionTooltip>
|
||||
)
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
import { NavigationAction } from "./navigation-action";
|
||||
import { NavigationItem } from "./navigation-item";
|
||||
import { UserButton } from "@clerk/nextjs";
|
||||
|
||||
|
||||
export const NavigationSidebar = async () => {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const servers = await db.server.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-4 flex flex-col items-center h-full text-primary w-full bg-[#E3E5E8] dark:bg-[#1E1F22] py-3"
|
||||
>
|
||||
<NavigationAction/>
|
||||
<Separator
|
||||
className="h-[2px] bg-zinc-300 dark:bg-zinc-700 rounded-md w-10 mx-auto"
|
||||
/>
|
||||
<ScrollArea className="flex-1 w-full">
|
||||
{servers.map((server) => (
|
||||
<div key={server.id} className="mb-4">
|
||||
<NavigationItem
|
||||
id={server.id}
|
||||
name={server.name}
|
||||
imageUrl={server.imageUrl}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
<div className="pb-3 mt-auto flex items-center flex-col gap-y-4">
|
||||
<ModeToggle/>
|
||||
<UserButton
|
||||
afterSignOutUrl="/"
|
||||
appearance={{
|
||||
elements: {
|
||||
avatarBox: "h-[48px] w-[48px]"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { EditServerModal } from "@/components/modals/edit-server-modal";
|
||||
import { CreateServerModal } from "@/components/modals/create-server-modal";
|
||||
import { InviteModal } from "@/components/modals/invite-modal";
|
||||
import { MembersModal } from "@/components/modals/members-modal";
|
||||
import { CreateChannelModal } from "@/components/modals/create-channel-modal";
|
||||
import { LeaveServerModal } from "@/components/modals/leave-server-modal";
|
||||
import { DeleteServerModal } from "@/components/modals/delete-server-modal";
|
||||
import { DeleteChannelModal } from "@/components/modals/delete-channel-modal";
|
||||
import { EditChannelModal } from "@/components/modals/edit-channel-modal";
|
||||
import { MessageFileModal } from "@/components/modals/message-file-modal.tsx";
|
||||
import { DeleteMessageModal } from "@/components/modals/delete-message-modal";
|
||||
|
||||
export const ModalProvider = () => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect (() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateServerModal />
|
||||
<InviteModal />
|
||||
<EditServerModal />
|
||||
<MembersModal/>
|
||||
<CreateChannelModal/>
|
||||
<LeaveServerModal/>
|
||||
<DeleteServerModal/>
|
||||
<DeleteChannelModal/>
|
||||
<EditChannelModal/>
|
||||
<MessageFileModal/>
|
||||
<DeleteMessageModal/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
}from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
export const QueryProvider = ({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
import { io as ClientIO } from "socket.io-client";
|
||||
|
||||
type SocketContextType = {
|
||||
socket: any | null;
|
||||
isConnected: boolean;
|
||||
};
|
||||
|
||||
const SocketContext = createContext<SocketContextType>({
|
||||
socket: null,
|
||||
isConnected: false,
|
||||
});
|
||||
|
||||
export const useSocket = () => {
|
||||
return useContext(SocketContext);
|
||||
};
|
||||
|
||||
export const SocketProvider = ({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [socket, setSocket] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const socketInstance = new (ClientIO as any)(process.env.NEXT_PUBLIC_SITE_URL!, {
|
||||
path: "/api/socket/io",
|
||||
addTrailingSlash: false,
|
||||
});
|
||||
|
||||
socketInstance.on("connect", () => {
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on("disconnect", () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
setSocket(socketInstance);
|
||||
|
||||
return () => {
|
||||
socketInstance.disconnect();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={{ socket, isConnected }}>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
)
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Channel, ChannelType, MemberRole, Server } from "@prisma/client";
|
||||
import { Edit, Hash, Lock, Mic, Trash, Video } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ActionTooltip } from "@/components/action-tooltip";
|
||||
import { ModalType, useModal } from "@/hooks/use-modal-store";
|
||||
|
||||
interface ServerChannelProps {
|
||||
channel: Channel;
|
||||
server: Server;
|
||||
role?: MemberRole;
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
[ChannelType.TEXT]: Hash,
|
||||
[ChannelType.AUDIO]: Mic,
|
||||
[ChannelType.VIDEO]: Video
|
||||
}
|
||||
|
||||
export const ServerChannel = ({
|
||||
channel,
|
||||
server,
|
||||
role
|
||||
}: ServerChannelProps) => {
|
||||
const { onOpen } = useModal();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const Icon = iconMap[channel.type];
|
||||
|
||||
const onClick = () => {
|
||||
router.push(`/servers/${params?.serverId}/channels/${channel.id}`);
|
||||
}
|
||||
|
||||
const onAction = (e: React.MouseEvent, action: ModalType) => {
|
||||
e.stopPropagation();
|
||||
onOpen(action, { channel, server });
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"group px-2 py-2 rounded-md flex items-center gap-x-2 w-full hover:bg-zinc-700/10 dark:hover:bg-zinc-700/50 transition mb-1",
|
||||
params?.channelId === channel.id && "bg-zinc-700/10 dark:bg-zinc-700"
|
||||
)}
|
||||
>
|
||||
<Icon className="flex-shrink-0 w-5 h-5 text-zinc-500 dark:text-zinc-400"/>
|
||||
<p className={cn(
|
||||
"link-clamp-1 font-semibold text-sm text-zinc-500 group-hover:text-zinc-600 dark:text-zinc-400 dark:group-hover:text-zinc-300 transition",
|
||||
params?.channelId === channel.id && "text-primary dark:text-zinc-200 dark:group-hover:text-white"
|
||||
)}>
|
||||
{channel.name}
|
||||
</p>
|
||||
{channel.name !== "general" && role !== MemberRole.GUEST && (
|
||||
<div className="ml-auto flex items-center gap-x-2">
|
||||
<ActionTooltip label="Edit">
|
||||
<Edit
|
||||
onClick={(e) => onAction(e, "editChannel")}
|
||||
className="hidden group-hover:block w-4 h-4 text-zinc-500 hove:text-zinc-600 dark:text-zinc-400 dark:hove:text-zinc-300 transition"
|
||||
/>
|
||||
</ActionTooltip>
|
||||
<ActionTooltip label="Delete">
|
||||
<Trash
|
||||
onClick={(e) => onAction(e, "deleteChannel")}
|
||||
className="hidden group-hover:block w-4 h-4 text-zinc-500 hove:text-zinc-600 dark:text-zinc-400 dark:hove:text-zinc-300 transition"
|
||||
/>
|
||||
</ActionTooltip>
|
||||
|
||||
</div>
|
||||
)}
|
||||
{channel.name === "general" && (
|
||||
<Lock className="ml-auto w-4 h-4 text-zinc-500 dark:text-zinc-400"/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ServerWithMembersWithProfiles } from "@/types";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { ChevronDown, LogOut, Plus, PlusCircle, Settings, Trash, Users } from "lucide-react";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
|
||||
interface ServerHeaderProps {
|
||||
server: ServerWithMembersWithProfiles;
|
||||
role?: MemberRole;
|
||||
}
|
||||
|
||||
export const ServerHeader = ({server, role}: ServerHeaderProps) => {
|
||||
const { onOpen } = useModal();
|
||||
const isAdmin = role === MemberRole.ADMIN;
|
||||
const isModerator = isAdmin || role === MemberRole.MODERATOR;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="focus:outline-none"
|
||||
asChild
|
||||
>
|
||||
<button className="w-full text-md font-semibold px-3 flex items-center h-12 border-neutral-2 dark:border-neutral-800 border-b-2 hover:bg-zinc-700/10 dark:hover:bg-zinc-700/50 transition"
|
||||
>
|
||||
{server.name}
|
||||
<ChevronDown className="w-5 h-5 ml-auto"/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56 text-xs font-medium text-black dark:text-neutral-400 space-y-[2px]"
|
||||
>
|
||||
{isModerator && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpen("invite", {server})}
|
||||
className="text-indigo-600 dark:text-indigo-400 px-3 py-2 text-sm cursor-pointer"
|
||||
>
|
||||
Invite People
|
||||
<Plus className="w-4 h-4 ml-auto"/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpen("editServer", {server})}
|
||||
className=" px-3 py-2 text-sm cursor-pointer"
|
||||
>
|
||||
Server Settings
|
||||
<Settings className="w-4 h-4 ml-auto"/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isModerator && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpen("members", {server})}
|
||||
className=" px-3 py-2 text-sm cursor-pointer"
|
||||
>
|
||||
Manage Members
|
||||
<Users className="w-4 h-4 ml-auto"/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isModerator && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpen("createChannel")}
|
||||
className=" px-3 py-2 text-sm cursor-pointer"
|
||||
>
|
||||
Create Channel
|
||||
<PlusCircle className="w-4 h-4 ml-auto"/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isModerator && (
|
||||
<DropdownMenuSeparator/>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpen("deleteServer", {server})}
|
||||
className="text-rose-500 px-3 py-2 text-sm cursor-pointer"
|
||||
>
|
||||
Delete Server
|
||||
<Trash className="w-4 h-4 ml-auto"/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpen("leaveServer", {server})}
|
||||
className="text-rose-500 px-3 py-2 text-sm cursor-pointer"
|
||||
>
|
||||
Leave Server
|
||||
<LogOut className="w-4 h-4 ml-auto"/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Member, MemberRole, Profile, Server } from "@prisma/client"
|
||||
import { ShieldAlert, ShieldCheck } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
|
||||
interface ServerMemberProps {
|
||||
member: Member & {
|
||||
profile: Profile;
|
||||
};
|
||||
server: Server;
|
||||
}
|
||||
|
||||
const roleIconMap = {
|
||||
[MemberRole.GUEST]: null,
|
||||
[MemberRole.MODERATOR]: <ShieldCheck className="h-4 w-4 ml-2 text-indigo-500" />,
|
||||
[MemberRole.ADMIN]: <ShieldAlert className="w-4 h-4 ml-2 text-rose-500" />
|
||||
}
|
||||
|
||||
export const ServerMember = ({
|
||||
member,
|
||||
server
|
||||
}: ServerMemberProps) => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const icon = roleIconMap[member.role];
|
||||
|
||||
const onClick = () => {
|
||||
router.push(`/servers/${params?.serverId}/conversations/${member.id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"group px-2 py-2 rounded-md flex items-center gap-x-2 w-full hover:bg-zinc-700/10 dark:hover:bg-zinc-700/50 transition mb-1",
|
||||
params?.memberId === member.id && "bg-zinc-700/20 dark:bg-zinc-700"
|
||||
)}>
|
||||
<UserAvatar src={member.profile.imageUrl}
|
||||
className="h-8 w-8 md:h-8 md:w-8"
|
||||
/>
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-sm text-zinc-500 group-hover:text-zinc-600 dark:text-zinc-400 dark:group-hover:text-zinc-300 transition",
|
||||
params?.memberId === member.id &&
|
||||
"dark:text-zinc-200 dark:group-hove:text-white"
|
||||
)}
|
||||
>
|
||||
{member.profile.name}
|
||||
</p>
|
||||
{icon}
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Search } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandList
|
||||
} from "@/components/ui/command";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
interface ServerSearchProps {
|
||||
data: {
|
||||
label: string;
|
||||
type: "channel" | "member";
|
||||
data: {
|
||||
icon: React.ReactNode;
|
||||
name: string;
|
||||
id: string;
|
||||
}[] | undefined
|
||||
}[];
|
||||
}
|
||||
|
||||
export const ServerSearch = ({
|
||||
data
|
||||
}: ServerSearchProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const down = (e: KeyboardEvent) =>
|
||||
{
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey))
|
||||
{
|
||||
e.preventDefault();
|
||||
setOpen((open) => !open);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
const onClick = ({ id, type }: { id: string, type: "channel" | "member"}) =>
|
||||
{
|
||||
setOpen(false);
|
||||
|
||||
if (type === "member") {
|
||||
return router.push(`/servers/${params?.serverId}/conversations/${id}`);
|
||||
}
|
||||
|
||||
if (type === "channel") {
|
||||
return router.push(`/servers/${params?.serverId}/channels/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="group px-2 py-2 rounded-md flex items-center gap-x-2 w-full hover:bg-zinc-700/10 dark:hover:bg-zinc-700/50 transition">
|
||||
<Search className="w-4 h-4 text-zinc-500 dark:text-zinc-400"/>
|
||||
<p className="font-semiboldd text-sm text-zinc-500 dark:text-zinc-400 group-hover:text-zinc-600 dark:group-hover:text-zinc-300 transition">
|
||||
Search
|
||||
</p>
|
||||
<kbd
|
||||
className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-forground ml-auto"
|
||||
>
|
||||
<span className="text-xs">CMD</span>K
|
||||
</kbd>
|
||||
</button>
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Search all channels" />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
No Results Found
|
||||
</CommandEmpty>
|
||||
{data.map(({ label, type, data }) => {
|
||||
if (!data?.length) return null;
|
||||
|
||||
return (
|
||||
<CommandGroup key={label} heading={label}>
|
||||
{data?.map(({ id, icon, name }) => {
|
||||
return (
|
||||
<CommandItem
|
||||
onSelect={() => onClick({ id, type})}
|
||||
key={id}>
|
||||
{icon}
|
||||
<span>{name}</span>
|
||||
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
)
|
||||
})}
|
||||
</CommandList>
|
||||
|
||||
</CommandDialog>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { ServerWithMembersWithProfiles } from "@/types";
|
||||
import { ChannelType, MemberRole } from "@prisma/client";
|
||||
import { ActionTooltip } from "../action-tooltip";
|
||||
import { Plus, Settings } from "lucide-react";
|
||||
import { useModal } from "@/hooks/use-modal-store";
|
||||
|
||||
interface ServerSectionProps {
|
||||
label: string;
|
||||
role?: MemberRole;
|
||||
sectionType: "channels" | "members";
|
||||
channelType?: ChannelType;
|
||||
server?: ServerWithMembersWithProfiles;
|
||||
}
|
||||
|
||||
export const ServerSection = ({
|
||||
label,
|
||||
role,
|
||||
sectionType,
|
||||
channelType,
|
||||
server
|
||||
}: ServerSectionProps)=> {
|
||||
const {onOpen } = useModal();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<p className="text-xs uppercase font-semibold text-zinc-500 dark:text-zinc-400">
|
||||
{label}
|
||||
</p>
|
||||
{role !== MemberRole.GUEST && sectionType === "channels" && (
|
||||
<ActionTooltip label="Create Channel" side="top">
|
||||
<button onClick={() => onOpen("createChannel", {channelType})} className="text-zinc-500 hove:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300 transition">
|
||||
<Plus className="h-4 w-4"/>
|
||||
</button>
|
||||
</ActionTooltip>
|
||||
)}
|
||||
{role === MemberRole.ADMIN && sectionType === "members" && (
|
||||
<ActionTooltip label="Manage Members" side="top">
|
||||
<button onClick={() => onOpen("members", {server})} className="text-zinc-500 hove:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300 transition">
|
||||
<Settings className="h-4 w-4"/>
|
||||
</button>
|
||||
</ActionTooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
import { ChannelType, MemberRole } from "@prisma/client";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Hash, Mic, ShieldAlert, ShieldCheck, Video } from "lucide-react";
|
||||
|
||||
import { currentProfile } from "@/lib/current-profile";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ServerHeader } from "./server-header";
|
||||
import { ServerSearch } from "./server-search";
|
||||
import { ServerSection } from "./server-section";
|
||||
import { ServerChannel } from "./server-channel";
|
||||
import { ServerMember } from "./server-member";
|
||||
|
||||
|
||||
|
||||
interface ServerSidebarProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
[ChannelType.TEXT]: <Hash className="mr-2 h-4 w-4" />,
|
||||
[ChannelType.AUDIO]: <Mic className="mr-2 h-4 w-4" />,
|
||||
[ChannelType.VIDEO]: <Video className="mr-2 h-4 w-4" />
|
||||
}
|
||||
|
||||
const roleIconMap = {
|
||||
[MemberRole.GUEST]: null,
|
||||
[MemberRole.MODERATOR]: <ShieldCheck className="h-4 w-4 mr-2 text-indigo-500" />,
|
||||
[MemberRole.ADMIN]: <ShieldAlert className="w-4 h-4 mr-2 text-rose-500" />
|
||||
}
|
||||
|
||||
export const ServerSidebar = async ({
|
||||
serverId
|
||||
}: ServerSidebarProps) => {
|
||||
const profile = await currentProfile();
|
||||
|
||||
if (!profile) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const server = await db.server.findUnique({
|
||||
where: {
|
||||
id: serverId,
|
||||
},
|
||||
include: {
|
||||
channels: {
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
profile: true,
|
||||
},
|
||||
orderBy: {
|
||||
role: "asc",
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const textChannels = server?.channels.filter((channel) => channel.type === ChannelType.TEXT)
|
||||
const audioChannels = server?.channels.filter((channel) => channel.type === ChannelType.AUDIO)
|
||||
const videoChannels = server?.channels.filter((channel) => channel.type === ChannelType.VIDEO)
|
||||
const members = server?.members.filter((member) => member.profileId !== profile.id)
|
||||
|
||||
if (!server) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const role = server.members.find((member) => member.profileId === profile.id)?.role;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full text-primary w-full dark:bg-[#2B2D31] bg-[#F2F3F5]">
|
||||
<ServerHeader
|
||||
server={server}
|
||||
role={role}
|
||||
/>
|
||||
<ScrollArea className="flex-1 px-3">
|
||||
<div className="mt-2">
|
||||
<ServerSearch
|
||||
data={[
|
||||
{
|
||||
label: "Text Channels",
|
||||
type: "channel",
|
||||
data: textChannels?.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
icon: iconMap[channel.type],
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: "Voice Channels",
|
||||
type: "channel",
|
||||
data: audioChannels?.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
icon: iconMap[channel.type],
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: "Video Channels",
|
||||
type: "channel",
|
||||
data: videoChannels?.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
icon: iconMap[channel.type],
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: "Members",
|
||||
type: "member",
|
||||
data: members?.map((member) => ({
|
||||
id: member.id,
|
||||
name: member.profile.name,
|
||||
icon: roleIconMap[member.role],
|
||||
}))
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="bg-zinc-200 dark:bg-zinc-700 rounded-md my-2"/>
|
||||
{!!textChannels?.length && (
|
||||
<div className="mb-2">
|
||||
<ServerSection
|
||||
sectionType="channels"
|
||||
channelType={ChannelType.TEXT}
|
||||
role={role}
|
||||
label="Text Channels"
|
||||
/>
|
||||
<div className="space-y-[2px]">
|
||||
{textChannels?.map((channel) => (
|
||||
<ServerChannel
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
role={role}
|
||||
server={server}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!audioChannels?.length && (
|
||||
<div className="mb-2">
|
||||
<ServerSection
|
||||
sectionType="channels"
|
||||
channelType={ChannelType.AUDIO}
|
||||
role={role}
|
||||
label="Voice Channels"
|
||||
/>
|
||||
<div className="space-y-[2px]">
|
||||
{audioChannels?.map((channel) => (
|
||||
<ServerChannel
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
role={role}
|
||||
server={server}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!videoChannels?.length && (
|
||||
<div className="mb-2">
|
||||
<ServerSection
|
||||
sectionType="channels"
|
||||
channelType={ChannelType.VIDEO}
|
||||
role={role}
|
||||
label="Video Channels"
|
||||
/>
|
||||
<div className="space-y-[2px]">
|
||||
{videoChannels?.map((channel) => (
|
||||
<ServerChannel
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
role={role}
|
||||
server={server}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!members?.length && (
|
||||
<div className="mb-2">
|
||||
<ServerSection
|
||||
sectionType="members"
|
||||
channelType={ChannelType.VIDEO}
|
||||
role={role}
|
||||
label="Members"
|
||||
server={server}
|
||||
/>
|
||||
<div className="space-y-[2px]">
|
||||
{members?.map((member) => (
|
||||
<ServerMember
|
||||
key={member.id}
|
||||
member={member}
|
||||
server={server}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSocket } from "@/components/providers/socket-provider";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export const SocketIndicator = () => {
|
||||
const { isConnected } = useSocket();
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-yellow-600 text-white border-none"
|
||||
>
|
||||
Fallback: Polling every 1s
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-emerald-600 text-white border-none"
|
||||
>
|
||||
Live: Real-time Updates
|
||||
</Badge>
|
||||
)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
@ -1,36 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
@ -18,7 +18,6 @@ const buttonVariants = cva(
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
primary: "bg-indigo-500 text-white hover:bg-indigo-500/90"
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
|
@ -1,155 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
@ -1,53 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
className={cn(
|
||||
"relative rounded-full bg-border",
|
||||
orientation === "vertical" && "flex-1"
|
||||
)}
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
@ -1,121 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
@ -1,140 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"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",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
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",
|
||||
right:
|
||||
"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",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
@ -1,18 +0,0 @@
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface UserAvatarProps {
|
||||
src?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const UserAvatar = ({
|
||||
src,
|
||||
className
|
||||
}: UserAvatarProps) => {
|
||||
return (
|
||||
<Avatar className={cn("h-7 w-7 md:h-10 md:w-10", className)}>
|
||||
<AvatarImage src={src} />
|
||||
</Avatar>
|
||||
)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import qs from "query-string";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
|
||||
import { useSocket } from "@/components/providers/socket-provider";
|
||||
|
||||
interface ChatQueryProps {
|
||||
queryKey: string;
|
||||
apiUrl: string;
|
||||
paramKey: "channelId" | "conversationId";
|
||||
paramValue: string;
|
||||
};
|
||||
|
||||
export const useChatQuery = ({
|
||||
queryKey,
|
||||
apiUrl,
|
||||
paramKey,
|
||||
paramValue
|
||||
}: ChatQueryProps) => {
|
||||
const { isConnected } = useSocket();
|
||||
|
||||
const fetchMessages = async ({ pageParam = undefined }) => {
|
||||
const url = qs.stringifyUrl({
|
||||
url: apiUrl,
|
||||
query: {
|
||||
cursor: pageParam,
|
||||
[paramKey]: paramValue,
|
||||
}
|
||||
}, { skipNull: true });
|
||||
|
||||
const res = await fetch(url);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const {
|
||||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
status,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [queryKey],
|
||||
queryFn: fetchMessages,
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor,
|
||||
refetchInterval: isConnected ? false : 1000,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
status,
|
||||
};
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type ChatScrollProps = {
|
||||
chatRef: React.RefObject<HTMLDivElement>;
|
||||
bottomRef: React.RefObject<HTMLDivElement>;
|
||||
shouldLoadMore: boolean;
|
||||
loadMore: () => void;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const useChatScroll = ({
|
||||
chatRef,
|
||||
bottomRef,
|
||||
shouldLoadMore,
|
||||
loadMore,
|
||||
count,
|
||||
}: ChatScrollProps) => {
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const topDiv = chatRef?.current;
|
||||
const hadleScroll = () => {
|
||||
const scrollTop = topDiv?.scrollTop;
|
||||
|
||||
if (scrollTop === 0 && shouldLoadMore) {
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
topDiv?.addEventListener("scroll", hadleScroll);
|
||||
|
||||
return () => {
|
||||
topDiv?.removeEventListener("scroll", hadleScroll);
|
||||
}
|
||||
}, [shouldLoadMore, loadMore, chatRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const bottomDiv = bottomRef?.current;
|
||||
const topDiv = chatRef?.current;
|
||||
const shouldAutoScroll= () => {
|
||||
if (!hasInitialized && bottomDiv){
|
||||
setHasInitialized(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!topDiv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const distanceFromBottom = topDiv.scrollHeight - topDiv.clientHeight;
|
||||
return distanceFromBottom <= 100;
|
||||
}
|
||||
|
||||
if (shouldAutoScroll()) {
|
||||
setTimeout(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
}
|
||||
}, [bottomRef, chatRef, hasInitialized, count]);
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
import { useSocket } from "@/components/providers/socket-provider";
|
||||
import { Member, Message, Profile } from "@prisma/client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type ChatSockerProps = {
|
||||
addKey: string;
|
||||
updateKey: string;
|
||||
queryKey: string;
|
||||
}
|
||||
|
||||
type MessageWithMemberWithProfile = Message & {
|
||||
member: Member & {
|
||||
profile: Profile;
|
||||
}
|
||||
}
|
||||
|
||||
export const useChatSocket = ({ addKey, updateKey, queryKey }: ChatSockerProps) => {
|
||||
const { socket } = useSocket();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
socket.on(addKey, (message: MessageWithMemberWithProfile) => {
|
||||
queryClient.setQueryData([queryKey], ( oldData: any) => {
|
||||
if (!oldData || !oldData.pages || oldData.pages.length === 0) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
const newData = oldData.pages.map((page: any) => {
|
||||
return {
|
||||
...page,
|
||||
items: page.items.map((item: MessageWithMemberWithProfile ) => {
|
||||
if (item.id === message.id) {
|
||||
return message;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
pages: newData,
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
socket.on(addKey, (message: MessageWithMemberWithProfile) => {
|
||||
queryClient.setQueryData([queryKey], (oldData: any) => {
|
||||
if (!oldData || !oldData.pages || oldData.pages.length === 0) {
|
||||
return {
|
||||
pages: [{
|
||||
items: [message],
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const newData = [...oldData.pages];
|
||||
|
||||
newData[0] = {
|
||||
...newData[0],
|
||||
items: [message, ...newData[0].items],
|
||||
};
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
pages: newData,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off(addKey);
|
||||
socket.off(updateKey);
|
||||
}
|
||||
}, [queryClient, socket, addKey, updateKey, queryKey]);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
|
||||
import { Channel, ChannelType, Server } from "@prisma/client";
|
||||
import { create } from "zustand";
|
||||
|
||||
export type ModalType = "createServer" | "invite" | "editServer" | "members" | "createChannel" | "leaveServer" | "deleteServer" | "deleteChannel" | "editChannel" | "messageFile" | "deleteMessage";
|
||||
|
||||
interface ModalData {
|
||||
server?: Server;
|
||||
channel?: Channel
|
||||
channelType?: ChannelType;
|
||||
apiUrl?: string;
|
||||
query?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface ModalStore {
|
||||
type: ModalType | null;
|
||||
data: ModalData;
|
||||
isOpen: boolean;
|
||||
onOpen: (type: ModalType, data?:ModalData) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const useModal = create<ModalStore>((set) => ({
|
||||
type: null,
|
||||
data: {},
|
||||
isOpen: false,
|
||||
onOpen: (type, data= {}) => set({ isOpen: true, type, data }),
|
||||
onClose: () => set({ type: null, isOpen: false }),
|
||||
}));
|
@ -1,17 +0,0 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export const useOrigin = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
if (!mounted) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export const getOrCreateConversation = async (memberOneId: string, memberTwoId: string) => {
|
||||
let conversation = await findConversation(memberOneId, memberTwoId) || await findConversation(memberTwoId, memberOneId);
|
||||
|
||||
if (!conversation)
|
||||
{
|
||||
conversation = await createNewConversation(memberOneId, memberTwoId);
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
const findConversation = async (memberOneId: string, memberTwoId: string) => {
|
||||
try
|
||||
{
|
||||
return await db.conversation.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ memberOneId: memberOneId},
|
||||
{ memberTwoId: memberTwoId},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
memberOne: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
},
|
||||
memberTwo: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const createNewConversation = async (memberOneId: string, memberTwoId: string) => {
|
||||
try
|
||||
{
|
||||
return await db.conversation.create({
|
||||
data:{
|
||||
memberOneId,
|
||||
memberTwoId,
|
||||
},
|
||||
include: {
|
||||
memberOne: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
},
|
||||
memberTwo: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { NextApiRequest } from "next";
|
||||
|
||||
export const currentProfilePages = async (req: NextApiRequest) => {
|
||||
const { userId } = getAuth(req);
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = await db.profile.findUnique({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { auth } from "@clerk/nextjs";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export const currentProfile = async () => {
|
||||
const { userId } = auth();
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = await db.profile.findUnique({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const db = globalThis.prisma || new PrismaClient()
|
||||
export const db = globalThis.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalThis.prisma = db
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = db
|
@ -1,32 +0,0 @@
|
||||
import { currentUser, redirectToSignIn } from "@clerk/nextjs";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export const initialProfile = async () => {
|
||||
const user = await currentUser();
|
||||
|
||||
if (!user) {
|
||||
return redirectToSignIn();
|
||||
}
|
||||
|
||||
const profile = await db.profile.findUnique({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (profile) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
const newProfile = await db.profile.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
imageUrl : user.imageUrl,
|
||||
email: user.emailAddresses[0].emailAddress,
|
||||
},
|
||||
});
|
||||
|
||||
return newProfile;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { generateComponents } from "@uploadthing/react";
|
||||
|
||||
import type { OurFileRouter } from "@/app/api/uploadthing/core";
|
||||
|
||||
export const { UploadButton, UploadDropzone, Uploader } =
|
||||
generateComponents<OurFileRouter>();
|
@ -3,11 +3,8 @@ import { authMiddleware } from "@clerk/nextjs";
|
||||
// This example protects all routes including api/trpc routes
|
||||
// Please edit this to allow other routes to be public as needed.
|
||||
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware
|
||||
export default authMiddleware({
|
||||
publicRoutes: ["/api/uploadthing"]
|
||||
});
|
||||
export default authMiddleware({});
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
|
||||
};
|
||||
|
@ -1,19 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config) => {
|
||||
config.externals.push({
|
||||
"utf-8-validate": "utf-8-validate",
|
||||
"bufferutil": "commonjs bufferutil"
|
||||
})
|
||||
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
domains: [
|
||||
"uploadthing.com",
|
||||
"utfs.io"
|
||||
]
|
||||
}
|
||||
}
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
1446
package-lock.json
generated
1446
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@ -10,60 +10,34 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^4.25.3",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@livekit/components-react": "^1.4.0",
|
||||
"@livekit/components-styles": "^1.0.7",
|
||||
"@prisma/client": "^5.4.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@prisma/client": "^5.4.2",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^4.33.0",
|
||||
"@types/node": "20.5.9",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@uploadthing/react": "^5.6.2",
|
||||
"autoprefixer": "10.4.15",
|
||||
"axios": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"livekit-client": "^1.14.4",
|
||||
"livekit-server-sdk": "^1.2.7",
|
||||
"lucide-react": "^0.274.0",
|
||||
"next": "^13.5.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.31",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"socket.io": "^4.7.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.2.2",
|
||||
"uploadthing": "^5.7.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.4.3"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.5",
|
||||
"prisma": "^5.4.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,145 +0,0 @@
|
||||
import { currentProfilePages } from "@/lib/current-profile-pages";
|
||||
import { db } from "@/lib/db";
|
||||
import { NextApiResponseServerIo } from "@/types";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
import { NextApiRequest } from "next";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponseServerIo) {
|
||||
if (req.method !== "DELETE" && req.method !== "PATCH") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await currentProfilePages(req);
|
||||
const { directMessageId, conversationId } = req.query;
|
||||
const { content } = req.body;
|
||||
|
||||
if (!profile) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!conversationId) {
|
||||
return res.status(400).json({ error: "Conversation ID Missing" });
|
||||
}
|
||||
|
||||
const conversation = await db.conversation.findFirst({
|
||||
where: {
|
||||
id: conversationId as string,
|
||||
OR: [
|
||||
{
|
||||
memberOne: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
{
|
||||
memberTwo: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
include: {
|
||||
memberOne: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
},
|
||||
memberTwo: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({ error: "Conversation not found" });
|
||||
}
|
||||
|
||||
const member = conversation.memberOne.profileId === profile.id ?
|
||||
conversation.memberOne : conversation.memberTwo;
|
||||
|
||||
if (!member) {
|
||||
return res.status(404).json({ error: "Member not found" });
|
||||
}
|
||||
|
||||
let directMessage = await db.directMessage.findFirst({
|
||||
where: {
|
||||
id: directMessageId as string,
|
||||
conversationId: conversationId as string,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!directMessage || directMessage.deleted) {
|
||||
return res.status(404).json({ error: "Message not found" });
|
||||
}
|
||||
|
||||
const isMessageOwner = directMessage.memberId === member.id;
|
||||
const isAdmin = member.role === MemberRole.ADMIN;
|
||||
const isModerator = member.role === MemberRole.MODERATOR;
|
||||
const canModify = isMessageOwner || isAdmin || isModerator;
|
||||
|
||||
if (!canModify) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
directMessage = await db.directMessage.update({
|
||||
where: {
|
||||
id: directMessageId as string,
|
||||
},
|
||||
data: {
|
||||
fileUrl: null,
|
||||
content: "This message has been deleted.",
|
||||
deleted: true,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
if (!isMessageOwner) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
directMessage = await db.directMessage.update({
|
||||
where: {
|
||||
id: directMessageId as string,
|
||||
},
|
||||
data: {
|
||||
content,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const updateKey = `chat:${conversationId}:messages:update`;
|
||||
|
||||
res?.socket?.server?.io?.to(updateKey).emit(updateKey, directMessage);
|
||||
|
||||
return res.status(200).json(directMessage);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ error: "Internal Error" });
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import { currentProfilePages } from "@/lib/current-profile-pages";
|
||||
import { NextApiResponseServerIo } from "@/types";
|
||||
import { NextApiRequest } from "next";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponseServerIo) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await currentProfilePages(req);
|
||||
const { content, fileUrl } = req.body;
|
||||
const { conversationId } = req.query;
|
||||
|
||||
if (!profile) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!conversationId) {
|
||||
return res.status(400).json({ error: "Conversation ID Missing" });
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ error: "Content Missing" });
|
||||
}
|
||||
|
||||
const conversation = await db.conversation.findFirst({
|
||||
where: {
|
||||
id: conversationId as string,
|
||||
OR: [
|
||||
{
|
||||
memberOne: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
{
|
||||
memberTwo: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
include: {
|
||||
memberOne: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
},
|
||||
memberTwo: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({ error: "Conversation not found" });
|
||||
}
|
||||
|
||||
const member = conversation.memberOne.profileId === profile.id ?
|
||||
conversation.memberOne : conversation.memberTwo;
|
||||
|
||||
if (!member) {
|
||||
return res.status(401).json({ error: "Member not found" });
|
||||
}
|
||||
|
||||
const message = await db.directMessage.create({
|
||||
data: {
|
||||
content,
|
||||
fileUrl,
|
||||
conversationId: conversationId as string,
|
||||
memberId: member.id,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const channelKey = `chat:${conversationId}:messages`;
|
||||
|
||||
res?.socket?.server?.io?.emit(channelKey, message);
|
||||
|
||||
return res.status(200).json({ message });
|
||||
}
|
||||
catch (error) {
|
||||
console.log("[DIRECT_MESSAGES_POST]", error);
|
||||
return res.status(500).json({ message: "Internal Error" });
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { Server as NetServer } from 'http';
|
||||
import { NextApiRequest } from 'next';
|
||||
import { Server as ServerIO } from 'socket.io';
|
||||
|
||||
import { NextApiResponseServerIo } from '@/types';
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
}
|
||||
}
|
||||
|
||||
const ioHandler = (req: NextApiRequest, res: NextApiResponseServerIo) => {
|
||||
if (!res.socket.server.io) {
|
||||
const path = "/api/socket/io"
|
||||
const httpServer: NetServer = res.socket.server as any;
|
||||
const io = new ServerIO(httpServer, {
|
||||
path: path,
|
||||
addTrailingSlash: false,
|
||||
});
|
||||
res.socket.server.io = io;
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
|
||||
export default ioHandler;
|
@ -1,143 +0,0 @@
|
||||
import { currentProfilePages } from "@/lib/current-profile-pages";
|
||||
import { db } from "@/lib/db";
|
||||
import { NextApiResponseServerIo } from "@/types";
|
||||
import { MemberRole } from "@prisma/client";
|
||||
import { NextApiRequest } from "next";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponseServerIo) {
|
||||
if (req.method !== "DELETE" && req.method !== "PATCH") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await currentProfilePages(req);
|
||||
const { serverId, channelId, messageId } = req.query;
|
||||
const { content } = req.body;
|
||||
|
||||
if (!profile) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!serverId) {
|
||||
return res.status(400).json({ error: "Server ID Missing" });
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
return res.status(400).json({ error: "Channel ID Missing" });
|
||||
}
|
||||
|
||||
const server= await db.server.findFirst({
|
||||
where: {
|
||||
id: serverId as string,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: "Server not found" });
|
||||
}
|
||||
|
||||
const channel = await db.channel.findFirst({
|
||||
where: {
|
||||
id: channelId as string,
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!channel) {
|
||||
return res.status(404).json({ error: "Channel not found" });
|
||||
}
|
||||
|
||||
const member = server.members.find((member) => member.profileId === profile.id);
|
||||
|
||||
if (!member) {
|
||||
return res.status(404).json({ error: "Member not found" });
|
||||
}
|
||||
|
||||
let message = await db.message.findFirst({
|
||||
where: {
|
||||
id: messageId as string,
|
||||
channelId: channel.id as string,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!message || message.deleted) {
|
||||
return res.status(404).json({ error: "Message not found" });
|
||||
}
|
||||
|
||||
const isMessageOwner = message.memberId === member.id;
|
||||
const isAdmin = member.role === MemberRole.ADMIN;
|
||||
const isModerator = member.role === MemberRole.MODERATOR;
|
||||
const canModify = isMessageOwner || isAdmin || isModerator;
|
||||
|
||||
if (!canModify) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
message = await db.message.update({
|
||||
where: {
|
||||
id: messageId as string,
|
||||
},
|
||||
data: {
|
||||
fileUrl: null,
|
||||
content: "This message has been deleted.",
|
||||
deleted: true,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
if (!isMessageOwner) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
message = await db.message.update({
|
||||
where: {
|
||||
id: messageId as string,
|
||||
},
|
||||
data: {
|
||||
content,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const updateKey = `chat:${channelId}:messages:update`;
|
||||
|
||||
res?.socket?.server?.io?.to(updateKey).emit(updateKey, message);
|
||||
|
||||
return res.status(200).json(message);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ error: "Internal Error" });
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
import { currentProfilePages } from "@/lib/current-profile-pages";
|
||||
import { NextApiResponseServerIo } from "@/types";
|
||||
import { NextApiRequest } from "next";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponseServerIo) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await currentProfilePages(req);
|
||||
const { content, fileUrl } = req.body;
|
||||
const { serverId, channelId } = req.query;
|
||||
|
||||
if (!profile) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!serverId) {
|
||||
return res.status(400).json({ error: "Server ID Missing" });
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
return res.status(400).json({ error: "Channel ID Missing" });
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ error: "Content Missing" });
|
||||
}
|
||||
|
||||
const server= await db.server.findFirst({
|
||||
where: {
|
||||
id: serverId as string,
|
||||
members: {
|
||||
some: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: "Server not found" });
|
||||
}
|
||||
|
||||
const channel = await db.channel.findFirst({
|
||||
where: {
|
||||
id: channelId as string,
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!channel) {
|
||||
return res.status(404).json({ error: "Channel not found" });
|
||||
}
|
||||
|
||||
const member = server.members.find((member) => member.profileId === profile.id);
|
||||
|
||||
if (!member) {
|
||||
return res.status(401).json({ error: "Member not found" });
|
||||
}
|
||||
|
||||
const message = await db.message.create({
|
||||
data: {
|
||||
content,
|
||||
fileUrl,
|
||||
channelId: channelId as string,
|
||||
memberId: member.id,
|
||||
},
|
||||
include: {
|
||||
member: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const channelKey = `chat:${channelId}:messages`;
|
||||
|
||||
res?.socket?.server?.io?.emit(channelKey, message);
|
||||
|
||||
return res.status(200).json({ message });
|
||||
}
|
||||
catch (error) {
|
||||
console.log("[MESSAGES_POST]", error);
|
||||
return res.status(500).json({ message: "Internal Error" });
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ model Profile {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
name String
|
||||
imageUrl String @db.Text
|
||||
imageURL String @db.Text
|
||||
email String @db.Text
|
||||
|
||||
servers Server []
|
||||
@ -20,20 +20,20 @@ model Profile {
|
||||
channels Channel []
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
UpdatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Server {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
imageUrl String @db.Text
|
||||
inviteCode String @unique
|
||||
imageURL String @db.Text
|
||||
inviteCode String @db.Text
|
||||
|
||||
profileId String
|
||||
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
|
||||
members Member[]
|
||||
channels Channel[]
|
||||
channel Channel []
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@ -57,12 +57,6 @@ model Member {
|
||||
serverId String
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
|
||||
messages Message[]
|
||||
directMessages DirectMessage[]
|
||||
|
||||
conversationsInitiated Conversation[] @relation("MemberOne")
|
||||
conversationsReceived Conversation[] @relation("MemberTwo")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@ -87,68 +81,9 @@ model Channel {
|
||||
serverId String
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
|
||||
messages Message[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([profileId])
|
||||
@@index([serverId])
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(uuid())
|
||||
content String @db.Text
|
||||
|
||||
fileUrl String? @db.Text
|
||||
|
||||
memberId String
|
||||
member Member @relation(fields: [memberId], references: [id], onDelete: Cascade)
|
||||
|
||||
channelId String
|
||||
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
|
||||
deleted Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([channelId])
|
||||
@@index([memberId])
|
||||
}
|
||||
|
||||
model Conversation {
|
||||
id String @id @default(uuid())
|
||||
|
||||
memberOneId String
|
||||
memberOne Member @relation("MemberOne", fields: [memberOneId], references: [id], onDelete: Cascade)
|
||||
|
||||
memberTwoId String
|
||||
memberTwo Member @relation("MemberTwo", fields: [memberTwoId], references: [id], onDelete: Cascade)
|
||||
|
||||
directMessages DirectMessage[]
|
||||
|
||||
@@index([memberTwoId])
|
||||
|
||||
@@unique([memberOneId, memberTwoId])
|
||||
}
|
||||
|
||||
model DirectMessage {
|
||||
id String @id @default(uuid())
|
||||
content String @db.Text
|
||||
fileUrl String? @db.Text
|
||||
|
||||
memberId String
|
||||
member Member @relation(fields: [memberId], references: [id], onDelete: Cascade)
|
||||
|
||||
conversationId String
|
||||
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
|
||||
deleted Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([memberId])
|
||||
@@index([conversationId])
|
||||
}
|
16
types.ts
16
types.ts
@ -1,16 +0,0 @@
|
||||
import { Server as NetServer, Socket } from 'net';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
import { Server, Member, Profile } from '@prisma/client';
|
||||
|
||||
export type ServerWithMembersWithProfiles = Server & {
|
||||
members: (Member & { profile: Profile })[];
|
||||
}
|
||||
|
||||
export type NextApiResponseServerIo = NextApiResponse & {
|
||||
socket: Socket & {
|
||||
server: NetServer & {
|
||||
io: SocketIOServer;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user