diff --git a/backend/src/types.ts b/backend/src/types.ts index 5cc7b35..9b78f22 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -97,4 +97,13 @@ export type AppEventType = GameEventTypes | ChatEventTypes; // Represent all the events that can be sent to the client // a workaround for now to make things work - this will be refactored later export type AppEvent = GameEvent; + +export type Message = { + content: string; + ref?: string | null; + atMentions?: string[]; + reactions?: [string, string][]; + playerName: string; +}; + //todo: Add more events diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ea6e907..98f5579 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,10 +9,12 @@ "version": "0.0.0", "dependencies": { "autoprefixer": "^10.4.19", - "postcss": "^8.4.38", + "emoji-picker-react": "^4.10.0", + "postcss": "^8.4.38", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.23.1", + "react-icons": "^5.2.1", + "react-router-dom": "^6.23.1", "tailwindcss": "^3.4.3" }, "devDependencies": { @@ -1776,7 +1778,21 @@ "integrity": "sha512-i/A2UB0sxYViMN0M2zIotQFRIOt1jLuVXudACHBDiJ5gGuAUzf/crZxwlBTdA0O52Hy4CNtTzS7AKRAacs/08Q==", "license": "ISC" }, - "node_modules/emoji-regex": { + "node_modules/emoji-picker-react": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.10.0.tgz", + "integrity": "sha512-EfvOsGbyweMNcJ1F99XUv+XPdfkpa2NRAYkhwdIeYS6DWeISu3kHWX+iwvFLUVAc533aWbsGpETbxwbhzsiMnw==", + "dependencies": { + "flairup": "0.0.39" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", @@ -2118,7 +2134,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { + "node_modules/flairup": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-0.0.39.tgz", + "integrity": "sha512-UVPkzZmZeBWBx1+Ovo++kYKk9Wi32Jxt+c7HsxnEY80ExwFV54w+NyquFziqMLS0BnGVE43yGD4OvIwaAm/WiQ==" + }, + "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", @@ -3084,7 +3105,15 @@ "react": "^18.3.1" } }, - "node_modules/react-router": { + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-router": { "version": "6.23.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", diff --git a/frontend/package.json b/frontend/package.json index 9d44d66..e8232da 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,10 +13,12 @@ }, "dependencies": { "autoprefixer": "^10.4.19", - "postcss": "^8.4.38", + "emoji-picker-react": "^4.10.0", + "postcss": "^8.4.38", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.23.1", + "react-icons": "^5.2.1", + "react-router-dom": "^6.23.1", "tailwindcss": "^3.4.3" }, "devDependencies": { diff --git a/frontend/src/library/chatbox/Chatbox.tsx b/frontend/src/library/chatbox/Chatbox.tsx new file mode 100644 index 0000000..73687e8 --- /dev/null +++ b/frontend/src/library/chatbox/Chatbox.tsx @@ -0,0 +1,63 @@ +import React, { useState, useEffect } from 'react'; +import MessageList from './MessageList'; +import MessageInput from './MessageInput'; +import { Message as MessageType } from '../../../../backend/src/types'; +import { FaComments } from 'react-icons/fa'; + +const Chatbox: React.FC = () => { + const [messages, setMessages] = useState([]); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const handleReact = (event: CustomEvent) => { + const { content, emoji } = event.detail; + setMessages((prevMessages) => + prevMessages.map((message) => + message.content === content + ? { + ...message, + reactions: [ + ...message.reactions!, + [emoji, 'Player 1'], + ], + } + : message + ) + ); + }; + }, []); + + return ( +
+
+
+
+ +
+
+ +
+
+
+ +
+ ); +}; + +export default Chatbox; diff --git a/frontend/src/library/chatbox/Message.tsx b/frontend/src/library/chatbox/Message.tsx new file mode 100644 index 0000000..fa83b54 --- /dev/null +++ b/frontend/src/library/chatbox/Message.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'; +import { Message as MessageType } from '../../../../backend/src/types'; +import { FaRegSmile } from 'react-icons/fa'; + +interface MessageProps { + message: MessageType; +} + +const Message: React.FC = ({ message }) => { + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + + const handleEmojiClick = (emojiData: EmojiClickData) => { + const event = new CustomEvent('MESSAGE_REACT', { + detail: { content: message.content, emoji: emojiData.emoji }, + }); + window.dispatchEvent(event); + setShowEmojiPicker(false); + }; + + return ( +
+
+ {message.playerName} +
+
{message.content}
+
+
+ {message.reactions?.map(([emoji], index) => ( + + {emoji} + + ))} + + {showEmojiPicker && ( +
+ +
+ )} +
+
+ ); +}; + +export default Message; diff --git a/frontend/src/library/chatbox/MessageInput.tsx b/frontend/src/library/chatbox/MessageInput.tsx new file mode 100644 index 0000000..4873b0f --- /dev/null +++ b/frontend/src/library/chatbox/MessageInput.tsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'; +import Button from '../button'; +import { Message as MessageType } from '../../../../backend/src/types'; + +interface MessageInputProps { + setMessages: React.Dispatch>; +} + +const MessageInput: React.FC = () => { + const [content, setContent] = useState(''); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + + const handleSend = async () => { + if (content.trim() !== '') { + const newMessage: MessageType = { + content, + ref: null, + atMentions: [], + reactions: [], + playerName: 'Player 1', + }; + + // Simulate an API call to send the message + setContent(''); + } + }; + + const onEmojiClick = (emojiData: EmojiClickData) => { + setContent(content + emojiData.emoji); + setShowEmojiPicker(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.ctrlKey && e.key === 'Enter') { + handleSend(); + } + }; + + return ( +
+ + {showEmojiPicker && ( +
+ +
+ )} + setContent(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-0.5 px-1 py-1 border-2 border-gray-500 rounded-xl font-kavoon text-sm" + placeholder="Type a message..." + /> + +
+ ); +}; + +// Simulate an API call to send the message + +export default MessageInput; diff --git a/frontend/src/library/chatbox/MessageList.tsx b/frontend/src/library/chatbox/MessageList.tsx new file mode 100644 index 0000000..e8aa1c8 --- /dev/null +++ b/frontend/src/library/chatbox/MessageList.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Message from './Message'; +import { Message as MessageType } from '../../../../backend/src/types'; +interface MessageListProps { + messages: MessageType[]; +} + +const MessageList: React.FC = ({ messages }) => { + return ( +
+ {messages.map((message, index) => ( + + ))} +
+ ); +}; + +export default MessageList; diff --git a/frontend/src/pages/Game.tsx b/frontend/src/pages/Game.tsx index b319e80..ee804e0 100644 --- a/frontend/src/pages/Game.tsx +++ b/frontend/src/pages/Game.tsx @@ -4,6 +4,7 @@ import Button from '../library/button'; import { useModal } from '../library/modal/ModalContext'; import { useToast } from '../library/toast/toast-context'; import CopyButton from '../library/copyButton'; +import Chatbox from '../library/chatbox/Chatbox'; interface GameProps { currentGame: string; @@ -177,6 +178,8 @@ const Game: React.FC = ({ currentGame }) => { + + ); };