Skip to content

Commit

Permalink
add conversation box
Browse files Browse the repository at this point in the history
  • Loading branch information
ngquyduc committed Jul 18, 2023
1 parent a1e9ca8 commit 03750d2
Show file tree
Hide file tree
Showing 13 changed files with 434 additions and 75 deletions.
38 changes: 38 additions & 0 deletions app/actions/getConversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import prisma from '@/app/libs/prismadb'
import getCurrentUser from './getCurrentUser'

const getConversations = async () => {
const currentUser = await getCurrentUser()

if (!currentUser?.id) {
return []
}

try {
const conversations = await prisma.conversation.findMany({
orderBy: {
lastMessageAt: 'desc',
},
where: {
userIds: {
has: currentUser.id,
},
},
include: {
users: true,
messages: {
include: {
sender: true,
seen: true,
},
},
},
})

return conversations
} catch (error: any) {
return []
}
}

export default getConversations
86 changes: 86 additions & 0 deletions app/api/conversations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import getCurrentUser from '@/app/actions/getCurrentUser'
import prisma from '@/app/libs/prismadb'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
try {
const currentUser = await getCurrentUser()
const body = await request.json()
const { userId, isGroup, members, name } = body

if (!currentUser?.id || !currentUser?.email) {
return new NextResponse('Unauthorized', { status: 401 })
}

if (isGroup && (!members || members.length < 2 || !name)) {
return new NextResponse('Invalid Data', { status: 400 })
}

if (isGroup) {
const newConversation = await prisma.conversation.create({
data: {
name,
isGroup,
users: {
connect: [
...members.map((member: { value: string }) => ({
id: member.value,
})),
{ id: currentUser.id },
],
},
},
include: {
users: true,
},
})

return NextResponse.json(newConversation)
} else {
const existingConversations = await prisma.conversation.findMany({
where: {
OR: [
{
userIds: {
equals: [currentUser.id, userId],
},
},
{
userIds: {
equals: [userId, currentUser.id],
},
},
],
},
})

const singleConversation = existingConversations[0]

if (singleConversation) {
return NextResponse.json(singleConversation)
}

const newConversation = await prisma.conversation.create({
data: {
users: {
connect: [
{
id: currentUser.id,
},
{
id: userId,
},
],
},
},
include: {
users: true,
},
})

return NextResponse.json(newConversation)
}
} catch (error) {
return new NextResponse('Internal Error', { status: 500 })
}
}
98 changes: 98 additions & 0 deletions app/conversations/components/ConversationBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client'

import Avatar from '@/app/components/Avatar'
import useOtherUser from '@/app/hooks/useOtherUser'
import { FullConversationType } from '@/app/types'
import { Conversation, Message, User } from '@prisma/client'
import clsx from 'clsx'
import { format } from 'date-fns'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useCallback, useMemo } from 'react'

interface ConversationBoxProps {
data: FullConversationType
selected?: boolean
}

const ConversationBox: React.FC<ConversationBoxProps> = ({
data,
selected,
}) => {
const otherUser = useOtherUser(data)
const session = useSession()
const router = useRouter()

const handleClick = useCallback(() => {
router.push(`/conversations/${data.id}`)
}, [data.id, router])

const lastMessage = useMemo(() => {
const messages = data.messages || []

return messages[messages.length - 1]
}, [data.messages])

const userEmail = useMemo(() => {
return session.data?.user?.email
}, [session.data?.user?.email])

const hasSeen = useMemo(() => {
if (!lastMessage) return false

const seenArray = lastMessage.seen || []

if (!userEmail) return false

return seenArray.filter((user) => user.email === userEmail).length !== 0
}, [userEmail, lastMessage])

const lastMessageText = useMemo(() => {
if (lastMessage?.image) {
return 'Sent an image'
}

if (lastMessage?.body) {
return lastMessage.body
}

return 'Started a conversation'
}, [lastMessage])

return (
<div
onClick={handleClick}
className={clsx(
`w-full relative flex items-center space-x-3
hover:bg-neutral-100 rounded-lg transition cursor-pointer p-3`,
selected ? 'bg-neutral-100' : 'bg-white'
)}
>
<Avatar user={otherUser} />
<div className="min-w-0 flex-1">
<div className="focus:outline-none">
<div className="flex justify-between items-center mb-1">
<p className="text-md font-medium text-gray-900">
{data.name || otherUser.name}
</p>
{lastMessage?.createdAt && (
<p className="text-xs text-gray-400 font-light">
{format(new Date(lastMessage.createdAt), 'p')}
</p>
)}
</div>
<p
className={clsx(
`truncate text-sm`,
hasSeen ? 'text-gray-500' : 'text-black font-medium'
)}
>
{lastMessageText}
</p>
</div>
</div>
</div>
)
}

export default ConversationBox
55 changes: 55 additions & 0 deletions app/conversations/components/ConversationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client'

import useConversation from '@/app/hooks/useConversation'
import { FullConversationType } from '@/app/types'
import { Conversation } from '@prisma/client'
import clsx from 'clsx'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { MdOutlineGroupAdd } from 'react-icons/md'
import ConversationBox from './ConversationBox'

interface ConversationListProps {
initialItems: FullConversationType[]
}

const ConversationList: React.FC<ConversationListProps> = ({
initialItems,
}) => {
const [items, setItems] = useState(initialItems)

const router = useRouter()

const { conversationId, isOpen } = useConversation()

return (
<aside
className={clsx(
`fixed inset-y-0 pb-20 lg:pb-0 lg:left-20 lg:w-80
lg:block overflow-y-auto border-r border-gray-200`,
isOpen ? 'hidden' : 'block w-full left-0'
)}
>
<div className="px-5">
<div className="flex justify-between mb-4 pt-4">
<div className="text-2xl font-bold text-neutral-800">Messages</div>
<div
className="rounded-full p-2 bg-gray-100 text-gray-600
cursor-pointer hover:opacity-75 transition"
>
<MdOutlineGroupAdd size={20} />
</div>
</div>
{items.map((item) => (
<ConversationBox
key={item.id}
data={item}
selected={conversationId === item.id}
/>
))}
</div>
</aside>
)
}

export default ConversationList
19 changes: 19 additions & 0 deletions app/conversations/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import getConversations from '../actions/getConversations'
import Sidebar from '../components/sidebar/Sidebar'
import ConversationList from './components/ConversationList'

export default async function ConversationsLayout({
children,
}: {
children: React.ReactNode
}) {
const conversations = await getConversations()
return (
<Sidebar>
<div className="h-full">
<ConversationList initialItems={conversations} />
{children}
</div>
</Sidebar>
)
}
19 changes: 19 additions & 0 deletions app/conversations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'

import clsx from 'clsx'
import EmptyState from '../components/EmptyState'
import useConversation from '../hooks/useConversation'

const Home = () => {
const { isOpen } = useConversation()

return (
<div
className={clsx('lg:pl-80 h-full lg:block', isOpen ? 'block' : 'hidden')}
>
<EmptyState />
</div>
)
}

export default Home
27 changes: 27 additions & 0 deletions app/hooks/useOtherUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { User } from '@prisma/client'
import { useSession } from 'next-auth/react'
import { useMemo } from 'react'
import { FullConversationType } from '../types'

const useOtherUser = (
conversation:
| FullConversationType
| {
users: User[]
}
) => {
const session = useSession()
const otherUser = useMemo(() => {
const currentUserEmail = session.data?.user?.email

const otherUser = conversation.users.filter(
(user) => user.email !== currentUserEmail
)

return otherUser[0]
}, [session.data?.user?.email, conversation.users])

return otherUser
}

export default useOtherUser
10 changes: 7 additions & 3 deletions app/hooks/useRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { signOut } from 'next-auth/react'
import { usePathname } from 'next/navigation'
import { useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import { HiChat } from 'react-icons/hi'
import { HiArrowLeftOnRectangle, HiUsers } from 'react-icons/hi2'

Expand All @@ -10,19 +10,23 @@ const useRoutes = () => {
const pathname = usePathname()
const { conversationId } = useConversation()

useEffect(() => {
console.log('pathname', pathname)
}, [pathname, conversationId])

const routes = useMemo(
() => [
{
label: 'Chat',
href: '/conversations',
icon: HiChat,
active: pathname === 'conversations' || !!conversationId,
active: pathname === '/conversations' || !!conversationId,
},
{
label: 'Users',
href: '/users',
icon: HiUsers,
active: pathname === 'users',
active: pathname === '/users',
},
{
label: 'Logout',
Expand Down
11 changes: 11 additions & 0 deletions app/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Conversation, Message, User } from '@prisma/client'

export type FullMessageType = Message & {
sender: User
seen: User[]
}

export type FullConversationType = Conversation & {
users: User[]
messages: FullMessageType[]
}
Loading

0 comments on commit 03750d2

Please sign in to comment.