Skip to content

Commit

Permalink
Merge pull request #96 from team-nabi/NABI-271
Browse files Browse the repository at this point in the history
🎉 채팅 관련 UI 및 로직
  • Loading branch information
oaoong authored Nov 23, 2023
2 parents 21c792a + c93439f commit 7886844
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 70 deletions.
13 changes: 13 additions & 0 deletions .hintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": [
"development"
],
"hints": {
"axe/structure": [
"default",
{
"list": "off"
}
]
}
}
6 changes: 6 additions & 0 deletions public/images/send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions src/app/(root)/(routes)/chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useState } from 'react'
import Image from 'next/image'
import Button from '@/components/ui/button'
import Input from '@/components/ui/input'
import Assets from '@/config/assets'

const ChatInput = ({
onSubmit,
}: {
onSubmit: (_newMessage: string) => void
}) => {
const [newMessage, setNewMessage] = useState<string>('')

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setNewMessage(e.target.value)
}

const onSubmitMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!newMessage.trim()) return

onSubmit(newMessage)
setNewMessage('')
}

return (
<form
onSubmit={onSubmitMessage}
className="absolute bottom-0 grid items-center w-full grid-cols-5 p-4 align-middle h-chat_input"
>
<div className="col-span-1" />
<Input
onChange={onChange}
value={newMessage}
type="text"
placeholder="메세지를 입력하세요."
className="col-span-3"
/>
<div className="flex justify-center col-span-1">
<Button
type="submit"
variant={null}
className="transition-opacity hover:opacity-70"
>
<Image src={Assets.sendIcon} alt="발송" />
</Button>
</div>
</form>
)
}

export default ChatInput
79 changes: 79 additions & 0 deletions src/app/(root)/(routes)/chat/components/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { forwardRef, memo } from 'react'
import { TYPOGRAPHY } from '@/styles/sizes'
import type { Message } from '@/types/message'
import { cn } from '@/utils'

type ChatListProps = {
messages: Message[]
currentUserNickname?: string
}

type ChatProps = {
message: Message
isMyMessage: boolean
}

const ChatList = forwardRef<HTMLDivElement, ChatListProps>(
({ messages, currentUserNickname }, ref) => {
return (
<ul className="flex flex-col w-full h-full gap-1">
{messages.map((message: Message) => {
return (
<Chat
key={message.id}
message={message}
isMyMessage={message.sender === currentUserNickname}
/>
)
})}
{messages.length === 0 && (
<h1 className="text-red-500">채팅을 시작해보세요!</h1>
)}
<div className="invisible" ref={ref} />
</ul>
)
},
)
ChatList.displayName = 'ChatList'

export default memo(ChatList)

const Chat = ({ message, isMyMessage }: ChatProps) => {
return isMyMessage ? (
<MyChat message={message} />
) : (
<OtherChat message={message} />
)
}

const MyChat = ({ message }: Pick<ChatProps, 'message'>) => {
return (
<li className={cn('flex flex-row gap-2 justify-end')}>
<p className={cn('mt-auto break-keep', TYPOGRAPHY.description)}>
{message.createdAt.toDate().toLocaleTimeString().slice(0, -3)}
</p>
<div
className={
'p-2 text-black rounded-lg max-w-[75%] w-fit bg-background-secondary-color'
}
>
{message.text}
</div>
</li>
)
}

const OtherChat = ({ message }: Pick<ChatProps, 'message'>) => {
return (
<li className={cn('flex flex-row gap-2 mr-auto justify-start')}>
<div
className={'p-2 text-white rounded-lg max-w-[75%] bg-primary-color '}
>
{message.text}
</div>
<p className={cn('mt-auto break-keep', TYPOGRAPHY.description)}>
{message.createdAt.toDate().toLocaleTimeString().slice(0, -3)}
</p>
</li>
)
}
95 changes: 49 additions & 46 deletions src/app/(root)/(routes)/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,64 @@
'use client'

import React from 'react'
import { collection, limit, orderBy, query, addDoc } from 'firebase/firestore'
import useFirestoreQuery, { Message } from '@/hooks/useFirestoreQuery'
import { db, getMessageRef } from '@/lib/firebase'
import React, { useEffect, useRef } from 'react'
import { limit, orderBy, query, addDoc } from 'firebase/firestore'
import PageTitle from '@/components/domain/page-title'
import { CHAT_LIMIT } from '@/config/firebaseConfig'
import { useAuth } from '@/contexts/AuthProvider'
import useFirestoreQuery from '@/hooks/useFirestoreQuery'
import { useToast } from '@/hooks/useToast'
import { getMessageRef } from '@/lib/firebase'
import ChatInput from './components/ChatInput'
import ChatList from './components/ChatList'

const ChatPage = () => {
const messageRef = getMessageRef('room2')
const { currentUser } = useAuth()
const { toast } = useToast()

const messageRef = getMessageRef('room2') // TODO: room id를 받아서 처리
const messages = useFirestoreQuery(
query(messageRef, orderBy('createdAt', 'asc'), limit(200)),
query(messageRef, orderBy('createdAt', 'asc'), limit(CHAT_LIMIT)),
)

const [newMessage, setNewMessage] = React.useState<string>('')
const chatBottomRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (chatBottomRef.current) {
chatBottomRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [messageRef])

const onSubmitMessage = async (message: string) => {
chatBottomRef.current?.scrollIntoView({ behavior: 'smooth' })

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setNewMessage(e.target.value)
try {
await addDoc(messageRef, {
text: message,
sender: currentUser?.nickname ?? '익명',
createdAt: new Date(),
})
} catch (e) {
toast({
title: '메세지 전송에 실패했습니다.',
variant: 'destructive',
duration: 3000,
})
}
}

return (
<div className="flex flex-col items-center w-full gap-10">
<h1 className="text-4xl">chat</h1>
<h1>new message</h1>
<form
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log(newMessage)
addDoc(collection(db, 'chats', 'room2', 'messages'), {
text: newMessage,
sender: 'me',
createdAt: new Date().toISOString(),
}).then(() => {
setNewMessage('')
})
}}
>
<input
onChange={onChange}
value={newMessage}
type="text"
placeholder="메세지를 입력하세요."
<main className="relative flex flex-col items-center w-full gap-10 h-page pb-chat_input">
<PageTitle title="채팅방" />
<section className="flex flex-col items-center px-2 overflow-scroll overflow-x-hidden">
<ChatList
messages={messages}
currentUserNickname={currentUser?.nickname}
ref={chatBottomRef}
/>
<button type="submit">send</button>
</form>
<div className="flex flex-col items-center w-full">
{messages.map((item: Message, idx: number) => {
console.log(item)
return (
<div key={idx} className="flex flex-row gap-2">
<h2>{`${item.sender}:`} </h2>
<h2>{item.text}</h2>
</div>
)
})}
{messages.length === 0 && <h1 className="text-red-500">no data</h1>}
</div>
</div>
</section>
<ChatInput onSubmit={onSubmitMessage} />
<div className="w-full h-0 border border-t-background-secondary-color" />
</main>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const NewCardButton = () => {
if (isLoggedIn) router.push(AppPath.newCard())
}

// TODO: 챗 페이지에서는 안보이게 처리
if (!isLoggedIn || path === AppPath.newCard()) return <></>

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/domain/page-title/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Assets from '@/config/assets'

const PageTitle = ({ title }: { title: string }) => {
return (
<div className="flex grid items-center justify-between w-full h-8 grid-cols-3 my-4">
<div className="grid items-center justify-between w-full h-8 grid-cols-3 my-4">
<Image
src={Assets.arrowLeftIcon}
alt="이전 아이콘"
Expand Down
2 changes: 2 additions & 0 deletions src/config/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import VMoreIcon from '/public/images/more-vertical.svg'
import NoData from '/public/images/no-data-img.svg'
import QuitCircle from '/public/images/quit-circle.svg'
import Search from '/public/images/search.svg'
import SendIcon from '/public/images/send.svg'
import TradeComplete from '/public/images/trade-complete.svg'
import UnavailableIcon from '/public/images/unavailable.png'
import XIcon from '/public/images/x-icon.svg'
Expand Down Expand Up @@ -84,6 +85,7 @@ const Assets = {
allCardIcon: AllCards,
headerLogo: HeaderLogo,
noDataIcon: NoData,
sendIcon: SendIcon,
} as const

export default Assets
13 changes: 13 additions & 0 deletions src/config/firebaseConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_APP_ID,
}

const CHAT_LIMIT = 200 //TODO: 제한 개수 수정 또는 불러오는 법?

export default firebaseConfig
export { CHAT_LIMIT }
8 changes: 1 addition & 7 deletions src/hooks/useFirestoreQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

import { useEffect, useRef, useState } from 'react'
import { Query, onSnapshot, queryEqual } from 'firebase/firestore'

export interface Message {
text: string
createdAt: Date
sender: string
id: string
}
import { Message } from '@/types/message'

const isMessageType = (arg: any): arg is Message => {
return (
Expand Down
17 changes: 1 addition & 16 deletions src/lib/firebase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@ import {
getFirestore,
collection,
} from 'firebase/firestore'

export const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_APP_ID,
}
import firebaseConfig from '@/config/firebaseConfig'

const fb = initializeApp(firebaseConfig)
const db = getFirestore(fb)
Expand All @@ -21,11 +13,4 @@ const getMessageRef = (roomId: string): CollectionReference => {
return collection(db, 'chats', roomId, 'messages')
}

// async function getData(db: Firestore) {
// const dataCol = collection(db, 'chats', 'room2', 'messages')
// const dataSnap = await getDocs(dataCol)
// const dataList = dataSnap.docs.map((doc) => doc.data())
// return dataList
// }

export { db, getMessageRef }
3 changes: 3 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
@layer base {
:root {
--nav-height: 3.5rem;
--page-height: calc(100vh - var(--nav-height));
--chat-input-height: 4.5rem;

--page-min-width: 320px;
--page-max-width: 640px;
}
Expand Down
10 changes: 10 additions & 0 deletions src/types/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Timestamp } from 'firebase/firestore'

interface Message {
text: string
createdAt: Timestamp
sender: string
id: string
}

export type { Message }
5 changes: 5 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ module.exports = {
height: {
...HEIGHT,
nav: 'var(--nav-height)',
page: 'var(--page-height)',
chat_input: 'var(--chat-input-height)',
},
borderRadius: {
...BORDER_RADIUS,
Expand All @@ -36,6 +38,9 @@ module.exports = {
fontFamily: {
pretendard: ['Pretendard'],
},
padding: {
chat_input: 'var(--chat-input-height)',
},
},
},
plugins: [
Expand Down

0 comments on commit 7886844

Please sign in to comment.