Skip to content

Commit

Permalink
implement chat and messages logic and view in app b00tc4mp#84
Browse files Browse the repository at this point in the history
  • Loading branch information
Eden23 committed Aug 26, 2024
1 parent cfdbe79 commit b956ae3
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 22 deletions.
2 changes: 1 addition & 1 deletion staff/marti-herms/project/G-HUB/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ mongoose.connect(process.env.MONGODB_URI)

api.delete('/reviews/:reviewId', jwtVerifier, handle.deleteReview)

api.get('/chat/:targeUserId', jwtVerifier, handle.openChat)
api.get('/chat/:targetUserId', jwtVerifier, handle.openChat)

api.post('/chat/:chatId/messages', jwtVerifier, jsonBodyParser, handle.sendMessage)

Expand Down
29 changes: 29 additions & 0 deletions staff/marti-herms/project/G-HUB/app/logic/getChatMessages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { validate, errors } from 'com'

const { SystemError } = errors

export default (chatId) => {
validate.string(chatId, 'chatId')

return fetch(`${import.meta.env.VITE_API_URL}/chat/${chatId}/messages`, {
headers: { Authorization: `Bearer ${sessionStorage.token}` }
})
.catch(error => { throw new SystemError(error.message) })
.then(response => {
const { status } = response

if (status === 200) {
return response.json()
.then(messages => messages)
}

return response.json()
.then(body => {
const { error, message } = body

const constructor = errors[error]

throw new constructor(message)
})
})
}
6 changes: 6 additions & 0 deletions staff/marti-herms/project/G-HUB/app/logic/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ import getUserAvatar from './getUserAvatar.js'
import getUser from './getUser.js'
import editUserAvatar from './editUserAvatar.js'
import editUserUsername from './editUserUsername.js'
import openChat from './openChat.js'
import sendMessage from './sendMessage.js'
import getChatMessages from './getChatMessages.js'

const logic = {
deleteReview,
editUserAvatar,
editUserUsername,
getChatMessages,
getDevUserGames,
getGameById,
getGameReviews,
Expand All @@ -41,10 +45,12 @@ const logic = {
loginUser,
logoutUser,
makeReview,
openChat,
registerGame,
registerUser,
searchGame,
searchUser,
sendMessage,
toggleAddGame,
toggleFavGame,
toggleFollowUser,
Expand Down
29 changes: 29 additions & 0 deletions staff/marti-herms/project/G-HUB/app/logic/openChat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { validate, errors } from 'com'

const { SystemError } = errors

export default (targetUserId) => {
validate.string(targetUserId, 'targetUserId')

return fetch(`${import.meta.env.VITE_API_URL}/chat/${targetUserId}`, {
headers: { Authorization: `Bearer ${sessionStorage.token}` }
})
.catch(error => { throw new SystemError(error.message) })
.then(response => {
const { status } = response

if (status === 200) {
return response.json()
.then(chatId => chatId)
}

return response.json()
.then(body => {
const { error, message } = body

const constructor = errors[error]

throw new constructor(message)
})
})
}
31 changes: 31 additions & 0 deletions staff/marti-herms/project/G-HUB/app/logic/sendMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { validate, errors } from 'com'

const { SystemError } = errors

export default (chatId, content) => {
validate.string(chatId, 'chatId')
validate.string(content, 'content')

if (content.trim().length === 0) throw new Error('empty content')

return fetch(`${import.meta.env.VITE_API_URL}/chat/${chatId}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${sessionStorage.token}` },
body: JSON.stringify({ content })
})
.catch(error => { throw new SystemError(error.message) })
.then(response => {
const { status } = response

if (status === 200) return

return response.json()
.then(body => {
const { error, message } = body

const constructor = errors[error]

throw new constructor(message)
})
})
}
2 changes: 1 addition & 1 deletion staff/marti-herms/project/G-HUB/app/src/home/AddGame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function AddGame({ onAddGame }) {
<Input id='name-input' type='text' placeholder='Name' />
{image && <img src={image} className='w-56 h-auto rounded' alt="" />}
<Input id='image-input' type='text' placeholder='Image' onChange={handleChange} />
<textarea className='w-9/12 rounded p-1 h-20 text-2xl' id='description-input' type='text' placeholder='Description' />
<textarea className='w-9/12 rounded-lg p-1 h-20 text-2xl' id='description-input' type='text' placeholder='Description' />
<Input id='link-input' type='text' placeholder='Link' />
<Button variant='contained' type='submit'>Add Game</Button>
</Form>
Expand Down
141 changes: 141 additions & 0 deletions staff/marti-herms/project/G-HUB/app/src/home/Chat.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { IoIosSend as SendIcon } from 'react-icons/io'

import Container from '../library/Container'
import Form from '../library/Form'
import Input from '../library/Input'
import Button from '../library/Button'
import Avatar from '../library/Avatar'
import Paragraph from '../library/Paragraph'

import Message from './Message'

import useContext from '../context'
import extractPayloadFromToken from '../../util/extractPayloadFromToken'

import defaultAvatar from '../../images/defaultAvatar.svg'

import logic from '../../logic'

export default function Chat({ onOpenChat }) {
const [users, setUsers] = useState([])
const [chat, setChat] = useState(null)
const [messages, setMessages] = useState([])
const { alert } = useContext()

const { sub: loggedInUser } = extractPayloadFromToken(sessionStorage.token)

const { userId } = useParams()

useEffect(() => {
try {
if (userId === loggedInUser) {
logic.getUserFollowing(userId)
.then(users => setUsers(users))
.catch(error => {
console.error(error)

alert(error.message)
})
} else {
logic.openChat(userId)
.then(chatId => setChat(chatId))
.catch(error => {
console.error(error)

alert(error.message)
})
}
} catch (error) {
console.error(error)

alert(error.message)
}
}, [userId])

useEffect(() => {
let intervalId
if (chat) {
loadMessages()
intervalId = setInterval(() => {
loadMessages()
}, 1000)
}
return () => {
if (intervalId) {
clearInterval(intervalId)
setChat(null)
}
}
}, [chat])

const handleSendMessage = (event) => {
event.preventDefault()

const form = event.target

const messageInput = form['message-input']

const message = messageInput.value

try {
logic.sendMessage(chat, message)
.then(() => {
loadMessages()
})
.catch(error => {
console.error(error)

alert(error)
})
} catch (error) {
console.error(error)

alert(error.message)
}
}

const loadMessages = () => {
try {
logic.getChatMessages(chat)
.then(messages => setMessages(messages.reverse()))
.catch(error => {
console.error(error)

alert(error.message)
})
} catch (error) {
console.error(error)

alert(error.message)
}
}

const handleOpenChat = (userId) => {
onOpenChat(userId)
}

return <Container className='w-full h-full flex flex-col pb-[110px]' >
{userId !== loggedInUser ? <>
<Container className='w-full h-full flex flex-col'>
{messages.map(message => <Message key={message.id} message={message} />)}
</Container>
<Form className='fixed w-full h-[50px] bottom-[60px] dark:bg-[#1e1e1e] flex flex-row justify-center items-center gap-2' onSubmit={handleSendMessage}>
<Input id='message-input' />
<Button className='bg-blue-800 rounded-full h-[40px] aspect-square' ><SendIcon className='w-8 h-8 ml-0.5 mt-1 text-white' /></Button>
</Form>
</> :
<Container className='flex flex-col items-center w-full h-[50px]'>
{users.map(user =>
<article key={user.id} className='flex flex-row w-full items-center justify-between p-3 border-y border-solid border-slate-700 dark:bg-black'>
<Container className='flex flex-row items-center'>
<Avatar className='w-2/12 h-2/12' url={user.avatar || defaultAvatar} />
<Paragraph className='text-xl font-bold'>{user.username}</Paragraph>
</Container>
<Button className='h-[40px]' onClick={() => handleOpenChat(user.id)} ><SendIcon className='w-8 h-8 ml-0.5 mt-1 text-white' /></Button>
</article>
)}
</Container>}
</Container >
}
23 changes: 18 additions & 5 deletions staff/marti-herms/project/G-HUB/app/src/home/Footer.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { Route, Routes } from 'react-router-dom'
import { Route, Routes, useLocation } from 'react-router-dom'
import { MdScreenSearchDesktop as SearchIcon, MdAddComment as ReviewIcon, MdAddBox as AddGameIcon, MdCancelPresentation as CancelIcon } from 'react-icons/md'
import { GoHome as HomeIcon } from 'react-icons/go'

import { BsChatRightText as ChatIcon } from 'react-icons/bs'

import Button from '../library/Button'
import NavigationButton from '../library/NavigationButton'

import extractPayloadFromToken from '../../util/extractPayloadFromToken.js'
import paths from '../../util/paths.js'

export default function Footer({ makeReviewVisibility, onSearchGame, onAddGame, onHome, onAddReview, onCancel }) {
export default function Footer({ makeReviewVisibility, onSearchGame, onAddGame, onHome, onAddReview, onCancel, onChat }) {
const { role } = extractPayloadFromToken(sessionStorage.token)
const location = useLocation()

let userId = location.pathname.slice(location.pathname.indexOf('/', 1) + 1)
if (userId.includes('/')) {
userId = userId.slice(0, userId.indexOf('/'))
}

const handleChatClick = () => {
onChat(userId)
}

return <footer className='fixed w-screen h-[7%] bottom-0 left-0 flex flex-row justify-evenly items-center border-t border-solid border-t-black z-10 bg-slate-700'>
<Routes>
Expand All @@ -23,7 +32,11 @@ export default function Footer({ makeReviewVisibility, onSearchGame, onAddGame,
{makeReviewVisibility ? <Button onClick={onCancel}><CancelIcon className='w-8 h-8 dark:text-white' /></Button> :
<Button onClick={onAddReview}><ReviewIcon className='w-8 h-8 dark:text-white' /></Button>}
</>} />
<Route path={paths.profile + '*'} element={<Button onClick={onHome}><HomeIcon className='w-8 h-8 dark:text-white' /></Button>} />
<Route path={paths.profile + ':userId'} element={<>
<Button onClick={onHome}><HomeIcon className='w-8 h-8 dark:text-white' /></Button>
<Button onClick={handleChatClick}><ChatIcon className='w-7 h-7 dark:text-white' /></Button>
</>
} />
<Route path={paths.addGame} element={<Button onClick={onHome}><HomeIcon className='w-8 h-8 dark:text-white' /></Button>} />
<Route path={paths.search} element={<Button onClick={onHome}><HomeIcon className='w-8 h-8 dark:text-white' /></Button>} />
<Route path={paths.followers + ':userId'} element={<Button onClick={onHome}><HomeIcon className='w-8 h-8 dark:text-white' /></Button>} />
Expand Down
22 changes: 22 additions & 0 deletions staff/marti-herms/project/G-HUB/app/src/home/Message.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Container from '../library/Container'
import Paragraph from '../library/Paragraph'

import extractPayloadFromToken from '../../util/extractPayloadFromToken'

export default function Message({ message }) {
const { sub: userId } = extractPayloadFromToken(sessionStorage.token)

return (message.author.id === userId) ?
<>
<Paragraph className='text-xs relative top-[25px] self-end mr-5 opacity-50'>{message.author.username}</Paragraph>
<Container className='max-w-[60%] min-w-[20%] h-auto p-1 rounded-full mt-3 mx-2 bg-blue-800 self-end' >
<Paragraph>{message.content}</Paragraph>
</Container>
</> :
<>
<Paragraph className='text-xs relative top-[25px] self-start ml-5 opacity-50'>{message.author.username}</Paragraph>
<Container className='max-w-[60%] min-w-[20%] h-auto p-1 rounded-full mt-3 mx-2 bg-green-700 self-start' >
<Paragraph>{message.content}</Paragraph>
</Container>
</>
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import useContext from '../context'
import GameBanner from './GameBanner'
import UserBanner from './UserBanner'

export default function SearchResults({ refreshStamp, onGameClick, onUserClick }) {
export default function SearchResults({ onGameClick, onUserClick }) {
const { alert } = useContext()

const [searchParams] = useSearchParams()
Expand All @@ -25,7 +25,7 @@ export default function SearchResults({ refreshStamp, onGameClick, onUserClick }
clearTimeout(debounceTimer)
setResults([])
}
}, [refreshStamp, q])
}, [q])

const loadGames = () => {
try {
Expand Down
4 changes: 2 additions & 2 deletions staff/marti-herms/project/G-HUB/app/src/home/UserList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export default function UserList({ onUserClick }) {
const [users, setUsers] = useState([])

useEffect(() => {
if (listType === 'following')
if (listType === 'following' || listType === 'chat')
loadFollowing()
else
else if (listType === 'followers')
loadFollowers()
}, [])

Expand Down
Loading

0 comments on commit b956ae3

Please sign in to comment.