Skip to content

Commit 4f9c0e2

Browse files
committed
finished invit page and generate invite url
1 parent a41bba3 commit 4f9c0e2

File tree

13 files changed

+312
-5
lines changed

13 files changed

+312
-5
lines changed

app/(invite)/(routes)/invite/[inviteCode]/page.tsx

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react'
2+
import { redirect } from 'next/navigation'
3+
import { redirectToSignIn } from '@clerk/nextjs'
4+
5+
import { db } from '@/lib/db'
6+
import { currentProfile } from '@/lib/current-profile'
7+
8+
import ServerSideBar from '@/components/server/server-sidebar'
9+
10+
const ServerIdLayout = async ({ children, params }: { children: React.ReactNode; params: { serverId: string } }) => {
11+
const profile = await currentProfile()
12+
13+
if (!profile) return redirectToSignIn()
14+
15+
const server = await db.server.findUnique({
16+
where: {
17+
id: params.serverId,
18+
members: {
19+
some: {
20+
profileId: profile.id,
21+
},
22+
},
23+
},
24+
})
25+
26+
if (!server) return redirect('/')
27+
28+
return (
29+
<div className={'h-full'}>
30+
<div className={'hidden md:flex h-full w-60 z-20 flex-col fixed inset-y-0'}>
31+
<ServerSideBar serverId={params.serverId} />
32+
</div>
33+
<main className={'h-full md:pl-60'}>{children}</main>
34+
</div>
35+
)
36+
}
37+
38+
export default ServerIdLayout
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { v4 as uuidv4 } from 'uuid'
2+
import { NextResponse } from 'next/server'
3+
4+
import { db } from '@/lib/db'
5+
import { currentProfile } from '@/lib/current-profile'
6+
7+
export async function PATCH(req: Request, { params }: { params: { serverId: string } }) {
8+
try {
9+
const profile = await currentProfile()
10+
11+
if (!profile) return new NextResponse('Unauthorized', { status: 401 })
12+
13+
if (!params.serverId) return new NextResponse('Server ID Missing', { status: 400 })
14+
15+
const server = await db.server.update({
16+
where: {
17+
id: params.serverId,
18+
profileId: profile.id,
19+
},
20+
data: {
21+
invitedCode: uuidv4(),
22+
},
23+
})
24+
25+
return NextResponse.json(server)
26+
} catch (error) {
27+
console.log('[SERVER_ID]', error)
28+
return new NextResponse('Internal Error', { status: 500 })
29+
}
30+
}

app/favicon.ico

-25.3 KB
Binary file not shown.

app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { ThemeProvider } from '@/components/providers/theme-provider'
1212
const font = Open_Sans({ subsets: ['latin'] })
1313

1414
export const metadata: Metadata = {
15-
title: 'Team chat application',
16-
description: 'Generated by create next app',
15+
title: 'Discord | Your place for communication and relaxation',
16+
description: 'Generated by Abdul basit',
1717
}
1818

1919
export default function RootLayout({ children }: { children: React.ReactNode }) {

components/modals/create-server-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const CreateServerModal = () => {
4545

4646
form.reset()
4747
router.refresh()
48+
onClose()
4849
} catch (error) {
4950
console.log(error)
5051
}

components/modals/invite-modal.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client'
2+
3+
import axios from 'axios'
4+
5+
import { useState } from 'react'
6+
import { useRouter } from 'next/navigation'
7+
8+
import { Check, Copy, RefreshCcw } from 'lucide-react'
9+
10+
import { useOrigin } from '@/hooks/use-origin'
11+
import { useModal } from '@/hooks/use-modal.store'
12+
13+
import { Input } from '@/components/ui/input'
14+
import { Label } from '@/components/ui/label'
15+
import { Button } from '@/components/ui/button'
16+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
17+
18+
const InviteModal = () => {
19+
const { onOpen, isOpen, onClose, type, data } = useModal()
20+
const origin = useOrigin()
21+
22+
const { server } = data
23+
24+
const [copied, setCopied] = useState(false)
25+
const [isLoading, setIsLoading] = useState(false)
26+
27+
const inviteUrl = `${origin}/invite/${server?.invitedCode}`
28+
29+
const onCopy = () => {
30+
navigator.clipboard.writeText(inviteUrl)
31+
setCopied(true)
32+
33+
setTimeout(() => {
34+
setCopied(false)
35+
}, 1000)
36+
}
37+
38+
const onNew = async () => {
39+
try {
40+
setIsLoading(true)
41+
const response = await axios.patch(`/api/servers/${server?.id}/invite-code`)
42+
43+
onOpen('invite', { server: response.data })
44+
} catch (error) {
45+
console.log(error)
46+
} finally {
47+
setIsLoading(false)
48+
}
49+
}
50+
51+
const router = useRouter()
52+
53+
const isModalOpen = isOpen && type === 'invite'
54+
55+
return (
56+
<Dialog open={isModalOpen} onOpenChange={onClose}>
57+
<DialogContent className={'bg-white text-black p-0 overflow-hidden'}>
58+
<DialogHeader className={'pt-8 px-6'}>
59+
<DialogTitle className={'text-2xl text-center font-bold'}>Invite Friends</DialogTitle>
60+
</DialogHeader>
61+
<div className={'p-6'}>
62+
<Label className={'uppercase text-xs font-bold text-zinc-500 dark:text-secondary/70'}>Server invite link</Label>
63+
<div className={'flex items-center mt-2 gap-x-2'}>
64+
<Input disabled={isLoading} className={'bg-zinc-300/50 border-0 focus-visible:ring-0 text-black focus-visible:ring-offset-0'} value={inviteUrl} />
65+
<Button disabled={isLoading} onClick={onCopy} size={'icon'}>
66+
{copied ? <Check className={'w-4 h-4 text-green-500'} /> : <Copy className={'w-4 h-4'} />}
67+
</Button>
68+
</div>
69+
<Button onClick={onNew} disabled={isLoading} variant={'link'} size={'sm'} className={'text-xs text-zinc-500 mt-4'}>
70+
Generate a new link
71+
<RefreshCcw className={'w-4 h-4 ml-2'} />
72+
</Button>
73+
</div>
74+
</DialogContent>
75+
</Dialog>
76+
)
77+
}
78+
79+
export default InviteModal

components/providers/modal-provider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useEffect, useState } from 'react'
44

5+
import InviteModal from '@/components/modals/invite-modal'
56
import CreateServerModal from '../../components/modals/create-server-modal'
67

78
const ModalProvider = () => {
@@ -16,6 +17,7 @@ const ModalProvider = () => {
1617
return (
1718
<>
1819
<CreateServerModal />
20+
<InviteModal />
1921
</>
2022
)
2123
}

components/server/server-header.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use client'
2+
3+
import { MemberRole, Server } from '@prisma/client'
4+
import { ServerWithMembersWithProfiles } from '@/types'
5+
6+
import { useModal } from '@/hooks/use-modal.store'
7+
import { ChevronDown, LogOut, PlusCircle, Settings, Trash, UserPlus, Users } from 'lucide-react'
8+
9+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
10+
11+
interface ServerHeaderProps {
12+
server: ServerWithMembersWithProfiles
13+
role?: MemberRole
14+
}
15+
16+
const ServerHeader = ({ role, server }: ServerHeaderProps) => {
17+
const { onOpen } = useModal()
18+
19+
const isAdmin = role === MemberRole.ADMIN
20+
const isModerator = isAdmin || role === MemberRole.MODERATOR
21+
22+
return (
23+
<DropdownMenu>
24+
<DropdownMenuTrigger className={'focus:outline-none'} asChild>
25+
<button className={'w-full text-md font-semibold px-3 flex items-center h-12 border-neutral-200 dark:border-neutral-800 border-b-2 hover:bg-zinc-700/10 dark:hover:bg-zinc-700/50 transition'}>
26+
{server.name}
27+
<ChevronDown className={'h-5 w-5 ml-auto'} />
28+
</button>
29+
</DropdownMenuTrigger>
30+
<DropdownMenuContent className={'w-56 text-xs font-medium text-black dark:text-neutral-400 space-y-[2px]'}>
31+
{isModerator && (
32+
<DropdownMenuItem onClick={() => onOpen('invite', { server })} className={'text-indigo-600 dark:text-indigo-400 px-3 py-2 text-sm cursor-pointer'}>
33+
Invite People
34+
<UserPlus className={'h-4 w-4 ml-auto'} />
35+
</DropdownMenuItem>
36+
)}
37+
{isAdmin && (
38+
<DropdownMenuItem className={'px-3 py-2 text-sm cursor-pointer'}>
39+
Server Settings
40+
<Settings className={'h-4 w-4 ml-auto'} />
41+
</DropdownMenuItem>
42+
)}
43+
{isAdmin && (
44+
<DropdownMenuItem className={'px-3 py-2 text-sm cursor-pointer'}>
45+
Manage Members
46+
<Users className={'h-4 w-4 ml-auto'} />
47+
</DropdownMenuItem>
48+
)}
49+
{isModerator && (
50+
<DropdownMenuItem className={'px-3 py-2 text-sm cursor-pointer'}>
51+
Create Channel
52+
<PlusCircle className={'h-4 w-4 ml-auto'} />
53+
</DropdownMenuItem>
54+
)}
55+
{isModerator && <DropdownMenuSeparator />}
56+
{isAdmin && (
57+
<DropdownMenuItem className={'text-rose-500 px-3 py-2 text-sm cursor-pointer'}>
58+
Delete Channel
59+
<Trash className={'h-4 w-4 ml-auto'} />
60+
</DropdownMenuItem>
61+
)}
62+
{!isAdmin && (
63+
<DropdownMenuItem className={'text-rose-500 px-3 py-2 text-sm cursor-pointer'}>
64+
Leave Channel
65+
<LogOut className={'h-4 w-4 ml-auto'} />
66+
</DropdownMenuItem>
67+
)}
68+
</DropdownMenuContent>
69+
</DropdownMenu>
70+
)
71+
}
72+
73+
export default ServerHeader

components/server/server-sidebar.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ChannelType } from '@prisma/client'
2+
import { redirect } from 'next/navigation'
3+
4+
import { db } from '@/lib/db'
5+
import { currentProfile } from '@/lib/current-profile'
6+
7+
import ServerHeader from '@/components/server/server-header'
8+
9+
interface ServerSidebarProps {
10+
serverId: string
11+
}
12+
13+
const ServerSideBar = async ({ serverId }: ServerSidebarProps) => {
14+
const profile = await currentProfile()
15+
16+
if (!profile) return redirect('/')
17+
18+
const server = await db.server.findUnique({
19+
where: {
20+
id: serverId,
21+
},
22+
include: {
23+
channels: {
24+
orderBy: {
25+
createdAt: 'asc',
26+
},
27+
},
28+
members: {
29+
include: {
30+
profile: true,
31+
},
32+
orderBy: {
33+
role: 'asc',
34+
},
35+
},
36+
},
37+
})
38+
39+
const textChannels = server?.channels.filter((channel) => channel.type === ChannelType.TEXT)
40+
const audioChannels = server?.channels.filter((channel) => channel.type === ChannelType.AUDIO)
41+
const videoChannels = server?.channels.filter((channel) => channel.type === ChannelType.VIDEO)
42+
const members = server?.members.filter((member) => member.profileId !== profile.id)
43+
44+
if (!server) {
45+
return redirect('/')
46+
}
47+
48+
const role = server.members.find((member) => member.profileId === profile.id)?.role
49+
50+
return (
51+
<div className={'flex flex-col h-full text-primary w-full dark:bg-[#2B2D31] bg-[#F2F3F5]'}>
52+
<ServerHeader server={server} role={role} />
53+
</div>
54+
)
55+
}
56+
57+
export default ServerSideBar

hooks/use-modal.store.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { create } from 'zustand'
2+
import { Server } from '@prisma/client'
23

3-
export type ModalType = 'createServer'
4+
export type ModalType = 'createServer' | 'invite'
5+
6+
interface ModalData {
7+
server?: Server
8+
}
49

510
interface ModalStore {
611
type: ModalType | null
12+
data: ModalData
713
isOpen: boolean
8-
onOpen: (type: ModalType) => void
14+
onOpen: (type: ModalType, data?: ModalData) => void
915
onClose: () => void
1016
}
1117

1218
export const useModal = create<ModalStore>((set) => ({
1319
type: null,
20+
data: {},
1421
isOpen: false,
15-
onOpen: (type) => set({ isOpen: true, type }),
22+
onOpen: (type, data = {}) => set({ isOpen: true, type, data }),
1623
onClose: () => set({ type: null, isOpen: false }),
1724
}))

hooks/use-origin.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useEffect, useState } from 'react'
2+
3+
export const useOrigin = () => {
4+
const [mounted, setMounted] = useState(false)
5+
6+
useEffect(() => {
7+
setMounted(true)
8+
}, [])
9+
10+
const origin = typeof window !== 'undefined' && window.location.origin ? window.location.origin : ''
11+
12+
if (!mounted) return ''
13+
14+
return origin
15+
}

types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Member, Profile, Server } from '@prisma/client'
2+
3+
export type ServerWithMembersWithProfiles = Server & {
4+
members: Member & { profile: Profile }[]
5+
}

0 commit comments

Comments
 (0)