diff --git a/.gitignore b/.gitignore index fc9158a..34a7f16 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ frontend/node_modules/ # testing /coverage - # next.js frontend/.next/ /out/ diff --git a/frontend/components/chat/chatContainer.jsx b/frontend/components/chat/chatContainer.jsx deleted file mode 100644 index 11137d5..0000000 --- a/frontend/components/chat/chatContainer.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from "react"; -import Image from "next/image"; -import moment from "moment"; -import { useState } from "react"; -import Avatar1 from "../../assets/avatars/avatar_1.svg"; -import Avatar2 from "../../assets/avatars/avatar_2.svg"; -import Avatar3 from "../../assets/avatars/avatar_3.svg"; -import Avatar4 from "../../assets/avatars/avatar_4.svg"; -import Avatar5 from "../../assets/avatars/avatar_5.svg"; -import Avatar6 from "../../assets/avatars/avatar_6.svg"; -import Avatar7 from "../../assets/avatars/avatar_7.svg"; -import Avatar8 from "../../assets/avatars/avatar_8.svg"; -import Avatar9 from "../../assets/avatars/avatar_9.svg"; -import Avatar10 from "../../assets/avatars/avatar_10.svg"; -import Avatar11 from "../../assets/avatars/avatar_11.svg"; -import Avatar12 from "../../assets/avatars/avatar_12.svg"; -import Avatar13 from "../../assets/avatars/avatar_13.svg"; -import Avatar14 from "../../assets/avatars/avatar_14.svg"; -import Avatar15 from "../../assets/avatars/avatar_15.svg"; -import getAvatar from "../../utils/session/getAvatar"; - -export default function ChatContainer({ messages, messagesEndRef }) { - const formatTime = (timestamp) => { - const date = new Date(timestamp * 1000); - return moment(date).format("hh:mm A"); - }; - - const AvatarList = [ - Avatar1, - Avatar2, - Avatar3, - Avatar4, - Avatar5, - Avatar6, - Avatar7, - Avatar8, - Avatar9, - Avatar10, - Avatar11, - Avatar12, - Avatar13, - Avatar14, - Avatar15, - ]; - const [Avatar, setAvatar] = useState(Avatar1); - - React.useEffect(() => { - if (messagesEndRef.current) { - messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - async function loadAvatar() { - const AvatarId = await getAvatar(); - const Avatar = AvatarList[AvatarId]; - setAvatar(Avatar); - } - loadAvatar(); - }, [messages]); - - return ( -
- -
-
- ); -} \ No newline at end of file diff --git a/frontend/components/chat/chatContainer.tsx b/frontend/components/chat/chatContainer.tsx new file mode 100644 index 0000000..bf76cee --- /dev/null +++ b/frontend/components/chat/chatContainer.tsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect } from "react"; +import Echofy from "../../assets/logo.svg"; +import Image from "next/image"; +import Avatar1 from "../../assets/avatars/avatar_1.svg"; +import Avatar2 from "../../assets/avatars/avatar_2.svg"; +import Avatar3 from "../../assets/avatars/avatar_3.svg"; +import Avatar4 from "../../assets/avatars/avatar_4.svg";`` +import Avatar5 from "../../assets/avatars/avatar_5.svg"; +import Avatar6 from "../../assets/avatars/avatar_6.svg"; +import Avatar7 from "../../assets/avatars/avatar_7.svg"; +import Avatar8 from "../../assets/avatars/avatar_8.svg"; +import Avatar9 from "../../assets/avatars/avatar_9.svg"; +import Avatar10 from "../../assets/avatars/avatar_10.svg"; +import Avatar11 from "../../assets/avatars/avatar_11.svg"; +import Avatar12 from "../../assets/avatars/avatar_12.svg"; +import Avatar13 from "../../assets/avatars/avatar_13.svg"; +import Avatar14 from "../../assets/avatars/avatar_14.svg"; +import Avatar15 from "../../assets/avatars/avatar_15.svg"; +import parseMessageText from "../../utils/chatbot_formatting/parseMessageText"; +import getAvatar from "../../utils/session/getAvatar"; +import { ChatContainerProps, Message } from "../../interface/interface"; + +export default function ChatContainer({ messages, messagesEndRef }: ChatContainerProps): React.JSX.Element { + const [filteredMessage, setFilteredMessage] = useState([]); + + const AvatarList = [ + Avatar1, + Avatar2, + Avatar3, + Avatar4, + Avatar5, + Avatar6, + Avatar7, + Avatar8, + Avatar9, + Avatar10, + Avatar11, + Avatar12, + Avatar13, + Avatar14, + Avatar15, + ]; + const [Avatar, setAvatar] = useState(Avatar1.src); + + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + + async function loadAvatar() { + const AvatarId = await getAvatar(); + const Avatar = AvatarList[AvatarId]; + setAvatar(Avatar.src); + } + loadAvatar(); + }, [messages]); + + useEffect(() => { + const filterMessages = () => { + const newMessages = messages.filter((message) => { + // message is already an object, no need to parse it + return !(message.userID && message.userID.startsWith("chatbot")); + }); + return newMessages; + }; + const newFilteredMessages = filterMessages(); + setFilteredMessage(newFilteredMessages as Message[]); + }, [messages]); + + return ( +
+
    + + {filteredMessage?.map((message, index) => { + return ( +
  • +
    +
    +
    +
    + +
    +
    {message.username}
    +
    +
    +
    +
    + {message.isSent ? message.text : parseMessageText(message.text)} +
    +
    +
    +
    +
    +
  • + ); + })} +
+
+
+ ); +} diff --git a/frontend/components/chat/chatInputBox.jsx b/frontend/components/chat/chatInputBox.jsx deleted file mode 100644 index 6835f88..0000000 --- a/frontend/components/chat/chatInputBox.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useState, useRef } from "react"; -import sendLogo from "../../assets/send.svg"; -import Image from "next/image"; -export default function ChatInputBox({ socketRef }) { - const [newMessage, setNewMessage] = useState(""); - const [isTimeout, setIsTimeout] = useState(false); - const messageTimesRef = useRef([]); - - function handleInputChange(event) { - setNewMessage(event.target.value); - } - - function handleSendClick() { - if (isTimeout) return; - - if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { - socketRef.current.send(newMessage); - setNewMessage(""); - messageTimesRef.current.push(Date.now()); - checkForTimeout(); - } else { - //todo : add an alert in case of websocket is not connected, redirect user to login screen - } - } - - function handleKeyPress(event) { - if (event.key === "Enter") { - handleSendClick(); - } - } - - const checkForTimeout = () => { - const now = Date.now(); - messageTimesRef.current = messageTimesRef.current.filter( - (t) => now - t < 3000, - ); - - if (messageTimesRef.current.length >= 3) { - setIsTimeout(true); - setTimeout(() => { - setIsTimeout(false); - messageTimesRef.current = []; - }, 5000); - } - }; - - return ( - <> -
-
- -
- sendButton -
-
-
- - ); -} \ No newline at end of file diff --git a/frontend/components/chat/chatInputBox.tsx b/frontend/components/chat/chatInputBox.tsx new file mode 100644 index 0000000..bb0c3b6 --- /dev/null +++ b/frontend/components/chat/chatInputBox.tsx @@ -0,0 +1,87 @@ +import React, { useState, useRef } from "react"; +import sendLogo from "../../assets/send.svg"; +import Image from "next/image"; +import { ChatInputBoxProps } from "../../interface/interface"; + + +const ChatInputBox: React.FC = ({ socketRef }) => { + const [newMessage, setNewMessage] = useState(""); + const [isTimeout, setIsTimeout] = useState(false); + const messageTimesRef = useRef([]); + + const handleInputChange = (event: React.ChangeEvent) => { + setNewMessage(event.target.value); + }; + + const handleSendClick = () => { + if (isTimeout) return; + + const socket = socketRef.current; + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(newMessage); + setNewMessage(""); + messageTimesRef.current.push(Date.now()); + checkForTimeout(); + } else { + // Handle websocket not connected (e.g., alert, redirect) + console.error("WebSocket is not connected."); + } + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + handleSendClick(); + } + }; + + const checkForTimeout = () => { + const now = Date.now(); + messageTimesRef.current = messageTimesRef.current.filter( + (t) => now - t < 3000 + ); + + if (messageTimesRef.current.length >= 3) { + setIsTimeout(true); + setTimeout(() => { + setIsTimeout(false); + messageTimesRef.current = []; + }, 5000); // Timeout for 5 seconds + } + }; + + return ( +
+
+ + + +
+
+ ); +}; + +export default ChatInputBox; diff --git a/frontend/components/chat/loginModal.jsx b/frontend/components/chat/loginModal.tsx similarity index 58% rename from frontend/components/chat/loginModal.jsx rename to frontend/components/chat/loginModal.tsx index a82c539..283faae 100644 --- a/frontend/components/chat/loginModal.jsx +++ b/frontend/components/chat/loginModal.tsx @@ -5,59 +5,76 @@ import getSessionUserId from "../../utils/session/getSessionUserId"; import setSessionUser from "../../utils/session/setSessionUser"; import removeSessionUserId from "../../utils/session/removeSessionUserId"; import checkAndPromptSessionChange from "../../utils/alerts/checkAndPromptSessionChange"; - -const LoginModal = ({ onClose, redirect }) => { - const popupRef = useRef(); +import { toast } from "react-toastify"; +import {LoginModalProps} from "../../interface/interface" +const LoginModal: React.FC = ({ onClose, redirect }) => { + const popupRef = useRef(null); const [username, setUsername] = useState(""); const router = useRouter(); + const inputRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (popupRef.current && !popupRef.current.contains(event.target as Node))   + { + onClose(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside);   + + }, [onClose]); + + useEffect(()   => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); - function handleUsernameChange(event) { + const handleUsernameChange = (event: React.ChangeEvent) => { setUsername(event.target.value); - } + }; - function handleEnterClick(event) { + const handleEnterClick = (event: React.KeyboardEvent) => { if (event.key === "Enter") { handleChatWithUsClick(); } - } + }; - useEffect(() => { - function handleClickOutside(event) { - if (popupRef.current && !popupRef.current.contains(event.target)) { - onClose(); - } + const handleChatWithUsClick = async () => { + if (!username.trim()) { + toast.error("Please enter a username."); + return; } - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [popupRef, onClose]); - - async function handleChatWithUsClick() { const chatType = redirect; const currentUser = getSessionUser(); const currentUserId = getSessionUserId(); - const query={channel:chatType}; - if (currentUser && currentUserId) { - if (currentUser === username) { - router.push({pathname:'/chat',query}); - } else { - const hasChanged = await checkAndPromptSessionChange( - currentUser, - username, - () => { - removeSessionUserId(); - setSessionUser(username); - }, - ); - if (hasChanged) { - router.push({pathname:'/chat',query}); - } - } + const queryParams = new URLSearchParams({ channel: chatType }); + + if (currentUser && currentUserId) { + if (currentUser === username) { + router.push(`/chat?${queryParams.toString()}`); } else { - setSessionUser(username); - router.push({pathname:'/chat',query}); + const hasChanged = await checkAndPromptSessionChange({ + currentUser: currentUser, + username: username, + onConfirm: () => { + removeSessionUserId(); + setSessionUser(username); + }, + }); + if (hasChanged) { + router.push(`/chat?${queryParams.toString()}`); + } } - } + } else { + setSessionUser(username); + router.push(`/chat?${queryParams.toString()}`); + } + }; + const closeModal = ()=>{onClose()} return (
{ />
CHAT WITH US diff --git a/frontend/components/chatbot/chatbotContainer.jsx b/frontend/components/chatbot/chatbotContainer.tsx similarity index 96% rename from frontend/components/chatbot/chatbotContainer.jsx rename to frontend/components/chatbot/chatbotContainer.tsx index 48d2403..1911001 100644 --- a/frontend/components/chatbot/chatbotContainer.jsx +++ b/frontend/components/chatbot/chatbotContainer.tsx @@ -18,8 +18,9 @@ import Avatar14 from "../../assets/avatars/avatar_14.svg"; import Avatar15 from "../../assets/avatars/avatar_15.svg"; import parseMessageText from "../../utils/chatbot_formatting/parseMessageText"; import getAvatar from "../../utils/session/getAvatar"; +import { ChatContainerProps } from "../../interface/interface"; -export default function ChatContainer({ messages, messagesEndRef }) { +export default function ChatContainer({ messages, messagesEndRef }:ChatContainerProps): React.JSX.Element { const [filteredMessage, setFilteredMessage] = useState([]); const AvatarList = [ @@ -58,7 +59,7 @@ export default function ChatContainer({ messages, messagesEndRef }) { const filterMessages = () => { const newMessages = messages.filter((message) => { try { - message = JSON.parse(message); + // message = JSON.parse(message); const messageData = JSON.parse(message.text); return !( messageData.userID && messageData.userID.startsWith("chatbot") diff --git a/frontend/components/chatbot/chatbotLoginModal.jsx b/frontend/components/chatbot/chatbotLoginModal.tsx similarity index 75% rename from frontend/components/chatbot/chatbotLoginModal.jsx rename to frontend/components/chatbot/chatbotLoginModal.tsx index 9dc3ce4..d4cac22 100644 --- a/frontend/components/chatbot/chatbotLoginModal.jsx +++ b/frontend/components/chatbot/chatbotLoginModal.tsx @@ -7,47 +7,59 @@ import removeSessionUserId from "../../utils/session/removeSessionUserId"; import checkAndPromptSessionChange from "../../utils/alerts/checkAndPromptSessionChange"; import { TopicDropdown } from "../topicDropdown"; -const ChatBotLoginModal = ({ onClose }) => { - const popupRef = useRef(); - const [username, setUsername] = useState(""); - const [topic, setTopic] = useState("SELECT A TOPIC"); +interface ChatBotLoginModalProps { + onClose: () => void; + redirect; +} + +interface DropdownProps { + topic: string; + setTopic: React.Dispatch>; + login?: boolean; +} + +const ChatBotLoginModal: React.FC = ({ onClose , redirect}) => { + const popupRef = useRef(null); + const [username, setUsername] = useState(""); + const [topic, setTopic] = useState("SELECT A TOPIC"); const router = useRouter(); - function handleUsernameChange(event) { + const handleUsernameChange = (event: React.ChangeEvent) => { setUsername(event.target.value); - } + }; - function handleEnterClick(event) { + const handleEnterClick = (event: React.KeyboardEvent) => { if (event.key === "Enter") { handleChatWithUsClick(); } - } + }; useEffect(() => { - function handleClickOutside(event) { - if (popupRef.current && !popupRef.current.contains(event.target)) { + const handleClickOutside = (event: MouseEvent) => { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { onClose(); } - } + }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); - }, [popupRef, onClose]); + }, [onClose]); - async function handleChatWithUsClick() { + const handleChatWithUsClick = async () => { const currentUser = getSessionUser(); const currentUserId = getSessionUserId(); + if (currentUser && currentUserId) { if (currentUser === username) { router.push(`/chat_bot?topic=${encodeURIComponent(topic)}`); } else { - const hasChanged = await checkAndPromptSessionChange( + const hasChanged = await checkAndPromptSessionChange({ currentUser, username, - () => { + onConfirm: () => { removeSessionUserId(); setSessionUser(username); }, - ); + }); if (hasChanged) { router.push(`/chat_bot?topic=${encodeURIComponent(topic)}`); } @@ -56,7 +68,8 @@ const ChatBotLoginModal = ({ onClose }) => { setSessionUser(username); router.push(`/chat_bot?topic=${encodeURIComponent(topic)}`); } - } + }; + return (
{ onKeyDown={handleEnterClick} />
- +
{
); }; -export default ChatBotLoginModal; \ No newline at end of file + +export default ChatBotLoginModal; diff --git a/frontend/components/layout.jsx b/frontend/components/layout.tsx similarity index 94% rename from frontend/components/layout.jsx rename to frontend/components/layout.tsx index cb04d58..f0734ae 100644 --- a/frontend/components/layout.jsx +++ b/frontend/components/layout.tsx @@ -3,11 +3,11 @@ import Image from "next/image"; import styles from "./layout.module.css"; import utilStyles from "../styles/utils.module.css"; import Link from "next/link"; - +import { LayoutProps } from "../interface/interface"; const name = "Choose the Chat Mode"; export const siteTitle = "Jinora"; -export default function Layout({ children, home }) { +export default function Layout({ children, home }:LayoutProps) { return (
diff --git a/frontend/components/mail.jsx b/frontend/components/mail.tsx similarity index 79% rename from frontend/components/mail.jsx rename to frontend/components/mail.tsx index 7eff024..020138a 100644 --- a/frontend/components/mail.jsx +++ b/frontend/components/mail.tsx @@ -1,32 +1,36 @@ -import React from "react"; +import React, { useEffect, useRef } from "react"; import { useState } from "react"; import subscribe from "../services/api/subscribeApi"; import getSessionUser from "../utils/session/getSessionUser"; import getSessionUserId from "../utils/session/getSessionUserId"; +import { MailProps } from "../interface/interface"; + + export default function Mail({ isOpen, onClose, channel, -}) { - const [email, setEmail] = useState(""); - const popupRef = React.useRef(); +} : MailProps) { + const [email, setEmail] = useState(""); + const popupRef = useRef(null); - const handleSubmit = async (e) => { + const handleSubmit = async (e: React.FormEvent) => { let userId = getSessionUserId(); let username = getSessionUser(); + let timestamp = Date.now(); e.preventDefault(); try { - await subscribe(email, username, userId, channel, userId); + await subscribe({email, username, userId, channel, timestamp}); onClose(); } catch (error) { //todo -> enable sentry logger here } }; - React.useEffect(() => { - function handleClickOutside(event) { - if (popupRef.current && !popupRef.current.contains(event.target)) { + useEffect(() => { + function handleClickOutside(event:MouseEvent) { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { onClose(); } } diff --git a/frontend/components/mdgBox.jsx b/frontend/components/mdgBox.tsx similarity index 96% rename from frontend/components/mdgBox.jsx rename to frontend/components/mdgBox.tsx index ddec593..6822978 100644 --- a/frontend/components/mdgBox.jsx +++ b/frontend/components/mdgBox.tsx @@ -4,8 +4,11 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import { fetchProjects } from "../services/api/projectsApi"; import { ProjectList } from "./projects/projectList"; +import { BoxProps } from "../interface/interface"; -export default function Box({ channel }) { + + +export default function Box({ channel }: BoxProps) { const router = useRouter(); const arr = ["public", "private", "chatbot"]; const newArr = arr.filter((item) => item !== channel); @@ -51,7 +54,7 @@ export default function Box({ channel }) { } }; const [topic, setTopic] = useState(" "); - const handleDivClick = (e) => { + const handleDivClick = (e: any) => { const content = e.target.textContent; setTopic(content); router.push({ diff --git a/frontend/components/modal.jsx b/frontend/components/modal.tsx similarity index 80% rename from frontend/components/modal.jsx rename to frontend/components/modal.tsx index 7a6f5f3..053ca66 100644 --- a/frontend/components/modal.jsx +++ b/frontend/components/modal.tsx @@ -1,7 +1,8 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; +import { ModalProps } from "../interface/interface"; -export default function Modal({ isOpen, onClose, children }) { +export default function Modal({ isOpen, onClose, children }:ModalProps) { const router = useRouter(); useEffect(() => { diff --git a/frontend/components/navbar.jsx b/frontend/components/navbar.tsx similarity index 83% rename from frontend/components/navbar.jsx rename to frontend/components/navbar.tsx index 1197abd..c253177 100644 --- a/frontend/components/navbar.jsx +++ b/frontend/components/navbar.tsx @@ -5,13 +5,16 @@ import Image from "next/image"; import jinoraLogo from "../assets/logo.svg"; import slack from ".././assets/slack_blue.svg"; import { TopicDropdown } from "./topicDropdown"; +import { NavbarProps } from "../interface/interface"; -export const Navbar = ({ currentPage, currentTopic }) => { - const [isMailOpen, setIsMailOpen] = useState(false); - const [logo, setLogo] = useState(jinoraLogo); - const [leftText, setLeftText] = useState(""); - const [toShow, setToShow] = useState(false); - const [topic,setTopic]=useState(currentTopic); + + +export const Navbar = ({ currentPage, currentTopic }: NavbarProps) => { + const [isMailOpen, setIsMailOpen] = useState(false); + const [logo, setLogo] = useState(jinoraLogo); + const [leftText, setLeftText] = useState(""); + const [toShow, setToShow] = useState(false); + const [topic,setTopic]=useState(currentTopic); function openMail() { setIsMailOpen(true); diff --git a/frontend/components/projects/projectCard.jsx b/frontend/components/projects/projectCard.tsx similarity index 96% rename from frontend/components/projects/projectCard.jsx rename to frontend/components/projects/projectCard.tsx index 29edb40..41169a8 100644 --- a/frontend/components/projects/projectCard.jsx +++ b/frontend/components/projects/projectCard.tsx @@ -3,8 +3,11 @@ import ps from "../../assets/Playstore.svg"; import as from "../../assets/Apple_logo_grey.svg"; import Image from "next/image"; import { useRouter } from "next/router"; +import { ProjectCardProps } from "../../interface/interface"; -export const ProjectCard = ({ + + +export const ProjectCard : React.FC = ({ name, shortDesc, ImageLink, diff --git a/frontend/components/projects/projectData.jsx b/frontend/components/projects/projectData.tsx similarity index 100% rename from frontend/components/projects/projectData.jsx rename to frontend/components/projects/projectData.tsx diff --git a/frontend/components/projects/projectList.jsx b/frontend/components/projects/projectList.tsx similarity index 82% rename from frontend/components/projects/projectList.jsx rename to frontend/components/projects/projectList.tsx index 3a84f23..b2536b5 100644 --- a/frontend/components/projects/projectList.jsx +++ b/frontend/components/projects/projectList.tsx @@ -1,6 +1,8 @@ +import { ProjectListProps } from "../../interface/interface"; import { ProjectCard } from "./projectCard"; -export const ProjectList = ({ projects, category, heightDecrease }) => { + +export const ProjectList : React.FC = ({ projects, category, heightDecrease }) => { return (
diff --git a/frontend/components/rightPane.jsx b/frontend/components/rightPane.tsx similarity index 95% rename from frontend/components/rightPane.jsx rename to frontend/components/rightPane.tsx index faf4abf..58dccef 100644 --- a/frontend/components/rightPane.jsx +++ b/frontend/components/rightPane.tsx @@ -7,13 +7,15 @@ import Image from "next/image"; import SettingsPopup from "./settingsPopup"; import React, { useState } from "react"; import { useRouter } from "next/router"; +import { SettingsPopupProps } from "../interface/interface"; export default function RightPane({ + onClose, soundEnabled, setSoundEnabled, notificationsEnabled, setNotificationsEnabled, -}) { +}:SettingsPopupProps) { const [showSettings, setShowSettings] = useState(false); const router = useRouter(); const hoverEffectClasses ="hover:scale-125 hover:cursor-pointer transition-transform duration-300 ease-in-out"; diff --git a/frontend/components/settingsPopup.jsx b/frontend/components/settingsPopup.tsx similarity index 91% rename from frontend/components/settingsPopup.jsx rename to frontend/components/settingsPopup.tsx index 0f0a242..f9b49aa 100644 --- a/frontend/components/settingsPopup.jsx +++ b/frontend/components/settingsPopup.tsx @@ -7,6 +7,8 @@ import { MdClose, MdAudiotrack, } from "react-icons/md"; +import { SettingsPopupProps } from "../interface/interface"; + const SettingsPopup = ({ onClose, @@ -14,12 +16,12 @@ const SettingsPopup = ({ setSoundEnabled, notificationsEnabled, setNotificationsEnabled, -}) => { - const popupRef = useRef(); +}: SettingsPopupProps) => { + const popupRef = useRef(null); useEffect(() => { - function handleClickOutside(event) { - if (popupRef.current && !popupRef.current.contains(event.target)) { + function handleClickOutside(event:MouseEvent) { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { onClose(); } } diff --git a/frontend/components/topicDropdown.jsx b/frontend/components/topicDropdown.tsx similarity index 72% rename from frontend/components/topicDropdown.jsx rename to frontend/components/topicDropdown.tsx index e955bfa..37d9d08 100644 --- a/frontend/components/topicDropdown.jsx +++ b/frontend/components/topicDropdown.tsx @@ -1,50 +1,52 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { fetchProjects } from "../services/api/projectsApi"; -import { useEffect, useRef } from "react"; +import { Project,TopicDropdownProps} from "../interface/interface"; -export const TopicDropdown = ({ topic, setTopic,login }) => { - const popupRef = useRef(); +export const TopicDropdown: React.FC = ({ topic, setTopic, login }) => { + const popupRef = useRef(null); const [isOpen, setIsOpen] = useState(false); - const [projects, setProjects] = useState([]); - const projectList = projects.filter( - (project) => project.Category === "Projects", - ); + const [projects, setProjects] = useState([]); + + const projectList = projects.filter((project) => project.Category === "Projects"); const eventList = projects.filter((project) => project.Category === "Events"); const toggleDropdown = () => { setIsOpen(!isOpen); }; - const handleClick = (e) => { - const content = e.target.textContent; + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); // Prevent default link behavior + const content = event.currentTarget.textContent || ""; // Get text content safely setTopic(content); - if(!login){ + setIsOpen(false); + if (!login) { window.location.href = `/chat_bot?topic=${encodeURIComponent(content)}`; } }; useEffect(() => { - async function fetchProjectsData() { + const fetchProjectsData = async () => { const data = await fetchProjects(); - if (data!=null){ - setProjects(data); - }else{ - setProjects([]); - } - } + setProjects(data || []); // Handle null response + }; + fetchProjectsData(); - }, []); + }, []); useEffect(() => { - function handleClickOutside(event) { - if (popupRef.current && !popupRef.current.contains(event.target)) { - setIsOpen(false); + const handleClickOutside = (event: MouseEvent) => { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { + setIsOpen(false);   + } - } + }; document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [popupRef, isOpen]); + return () => document.removeEventListener("mousedown",   + handleClickOutside); + }, [popupRef]);   + // No need for isOpen in dependency array + return (
{
{`${topic}`} {
    -
  • +
  • Projects
  • {projectList.map((project, index) => ( @@ -94,7 +96,7 @@ export const TopicDropdown = ({ topic, setTopic,login }) => {
    -
  • +
  • Events
  • {eventList.map((event, index) => ( diff --git a/frontend/components/topicSelectionModal.jsx b/frontend/components/topicSelectionModal.tsx similarity index 65% rename from frontend/components/topicSelectionModal.jsx rename to frontend/components/topicSelectionModal.tsx index b210bc5..1921a4c 100644 --- a/frontend/components/topicSelectionModal.jsx +++ b/frontend/components/topicSelectionModal.tsx @@ -1,18 +1,21 @@ import React from "react"; +import { TopicSelectionModalProps } from "../interface/interface"; -const TopicSelectionModal = ({ onClose }) => { + +const TopicSelectionModal: React.FC = ({ onClose }) => { return (

    Please select a topic from the left pane.

    -
    +
); }; -export default TopicSelectionModal; \ No newline at end of file + +export default TopicSelectionModal; diff --git a/frontend/components/usernameInput.jsx b/frontend/components/usernameInput.jsx deleted file mode 100644 index 4805abd..0000000 --- a/frontend/components/usernameInput.jsx +++ /dev/null @@ -1,13 +0,0 @@ -
-
-
- -
-
-
; \ No newline at end of file diff --git a/frontend/components/usernameInput.tsx b/frontend/components/usernameInput.tsx new file mode 100644 index 0000000..18428e5 --- /dev/null +++ b/frontend/components/usernameInput.tsx @@ -0,0 +1,27 @@ +import React, { useState, ChangeEvent } from 'react'; +import { UsernameInputProps } from '../interface/interface'; + + +const UsernameInput: React.FC = ({ value, onChange }) => { + const handleInputChange = (event: ChangeEvent) => { + onChange(event.target.value); + }; + + return ( +
+
+
+ +
+
+
+ ); +}; + +export default UsernameInput; diff --git a/frontend/context/userContext.js b/frontend/context/userContext.js deleted file mode 100644 index f1ee3f2..0000000 --- a/frontend/context/userContext.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { createContext, useState } from "react"; - -export const UserContext = createContext(); - -export const UserProvider = ({ children }) => { - const [userName, setUserName] = useState(null); - return ( - - {children} - - ); -}; diff --git a/frontend/context/userContext.tsx b/frontend/context/userContext.tsx new file mode 100644 index 0000000..1e16ce0 --- /dev/null +++ b/frontend/context/userContext.tsx @@ -0,0 +1,16 @@ +import React, { createContext, useState, ReactNode } from "react"; +import { UserContextType, UserProviderProps } from "../interface/interface"; + + +export const UserContext = createContext(undefined); + +export const UserProvider: React.FC = ({ children }) => { + const [userName, setUserName] = useState(null); + + return ( + + {children} +   + + ); +}; \ No newline at end of file diff --git a/frontend/global.d.ts b/frontend/global.d.ts new file mode 100644 index 0000000..ce579d2 --- /dev/null +++ b/frontend/global.d.ts @@ -0,0 +1,5 @@ +declare module "*.mp3" { + const src: string; + export default src; + } + \ No newline at end of file diff --git a/frontend/hooks/useLeaveChat.jsx b/frontend/hooks/useLeaveChat.jsx deleted file mode 100644 index 3c85cb1..0000000 --- a/frontend/hooks/useLeaveChat.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect } from "react"; -import getSessionUserId from "../utils/session/getSessionUserId"; -import removeSessionUserId from "../utils/session/removeSessionUserId"; -import { leaveChat } from "../services/api/leaveChatApi"; - -const useLeaveChat=(router)=>{ - useEffect(() => { - const leaveChatOnNavigation = () => { - leaveChat(getSessionUserId()); - removeSessionUserId(); - }; - const handleBeforeUnload = (e) => { - leaveChat(getSessionUserId()); - }; - - router.events.on("routeChangeStart", leaveChatOnNavigation); - window.addEventListener("beforeunload", handleBeforeUnload); - - return () => { - router.events.off("routeChangeStart", leaveChatOnNavigation); - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }, [router]); -} -export default useLeaveChat; \ No newline at end of file diff --git a/frontend/hooks/useLeaveChat.tsx b/frontend/hooks/useLeaveChat.tsx new file mode 100644 index 0000000..2b514ea --- /dev/null +++ b/frontend/hooks/useLeaveChat.tsx @@ -0,0 +1,35 @@ +// useLeaveChat.ts +import { useEffect } from "react"; +import getSessionUserId from "../utils/session/getSessionUserId"; +import removeSessionUserId from "../utils/session/removeSessionUserId"; +import { leaveChat } from "../services/api/leaveChatApi"; +import { NextRouter } from "next/router"; + +const useLeaveChat = (router: NextRouter) => { + useEffect(() => { + const leaveChatOnNavigation = () => { + const userId = getSessionUserId(); + if (userId) { + leaveChat(userId); + removeSessionUserId(); + } + }; + + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + const userId = getSessionUserId(); + if (userId) { + leaveChat(userId); + } + }; + + router.events.on("routeChangeStart", leaveChatOnNavigation); + window.addEventListener("beforeunload", handleBeforeUnload); + + return () => { + router.events.off("routeChangeStart", leaveChatOnNavigation); + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [router]); +}; + +export default useLeaveChat; diff --git a/frontend/hooks/useLoadSettings.jsx b/frontend/hooks/useLoadSettings.jsx deleted file mode 100644 index 1eecb9b..0000000 --- a/frontend/hooks/useLoadSettings.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import {useEffect} from 'react'; - -const useLoadSetting=(setSoundEnabled,setNotificationsEnabled)=>{ - useEffect(() => { - const savedSoundEnabled = localStorage.getItem("soundEnabled"); - const savedNotificationsEnabled = localStorage.getItem( - "notificationsEnabled", - ); - - if (savedSoundEnabled !== null) { - setSoundEnabled(JSON.parse(savedSoundEnabled)); - } - if (savedNotificationsEnabled !== null) { - setNotificationsEnabled(JSON.parse(savedNotificationsEnabled)); - } - }, []); -} -export default useLoadSetting; \ No newline at end of file diff --git a/frontend/hooks/useLoadSettings.tsx b/frontend/hooks/useLoadSettings.tsx new file mode 100644 index 0000000..990e8f7 --- /dev/null +++ b/frontend/hooks/useLoadSettings.tsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { UseLoadSettingProps } from '../interface/interface'; + +const useLoadSetting = ({ setSoundEnabled, setNotificationsEnabled }: UseLoadSettingProps) => { + useEffect(() => { + const savedSoundEnabled = localStorage.getItem("soundEnabled"); + const savedNotificationsEnabled = localStorage.getItem("notificationsEnabled"); + + if (savedSoundEnabled !== null) { + setSoundEnabled(JSON.parse(savedSoundEnabled)); + } + + if (savedNotificationsEnabled !== null) { + setNotificationsEnabled(JSON.parse(savedNotificationsEnabled)); + } + }, [setSoundEnabled, setNotificationsEnabled]); +}; + +export default useLoadSetting; + diff --git a/frontend/hooks/useSettings.jsx b/frontend/hooks/useSettings.jsx deleted file mode 100644 index 2f9a7ff..0000000 --- a/frontend/hooks/useSettings.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useEffect } from "react"; - -const useSettings=(soundEnabled,notificationsEnabled)=>{ - useEffect(() => { - localStorage.setItem("soundEnabled", JSON.stringify(soundEnabled)); - }, [soundEnabled]); - - useEffect(() => { - localStorage.setItem( - "notificationsEnabled", - JSON.stringify(notificationsEnabled), - ); - }, [notificationsEnabled]); -} -export default useSettings; \ No newline at end of file diff --git a/frontend/hooks/useSettings.tsx b/frontend/hooks/useSettings.tsx new file mode 100644 index 0000000..344eb49 --- /dev/null +++ b/frontend/hooks/useSettings.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { UseSettingsProps } from "../interface/interface"; + +const useSettings = ({ soundEnabled, notificationsEnabled }: UseSettingsProps) => { + useEffect(() => { + localStorage.setItem("soundEnabled", JSON.stringify(soundEnabled)); + }, [soundEnabled]); + + useEffect(() => { + localStorage.setItem("notificationsEnabled", JSON.stringify(notificationsEnabled)); + }, [notificationsEnabled]); +}; + +export default useSettings; diff --git a/frontend/hooks/useVisibilityChange.jsx b/frontend/hooks/useVisibilityChange.jsx deleted file mode 100644 index 89d2b7e..0000000 --- a/frontend/hooks/useVisibilityChange.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect } from "react"; - -const useVisibilityChange=(setUnreadCount)=>{ - useEffect(() => { - const handleVisibilityChange = () => { - if (!document.hidden) { - setUnreadCount(0); - } - }; - window.addEventListener("visibilitychange", handleVisibilityChange); - window.addEventListener("focus", handleVisibilityChange); - return () => { - window.removeEventListener("visibilitychange", handleVisibilityChange); - window.removeEventListener("focus", handleVisibilityChange); - }; - }, []); -} -export default useVisibilityChange; \ No newline at end of file diff --git a/frontend/hooks/useVisibilityChange.tsx b/frontend/hooks/useVisibilityChange.tsx new file mode 100644 index 0000000..1cbc47b --- /dev/null +++ b/frontend/hooks/useVisibilityChange.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { UseVisibilityChangeProps } from '../interface/interface'; + + + +const useVisibilityChange = ({ setUnreadCount }: UseVisibilityChangeProps) => { + useEffect(() => { + const handleVisibilityChange = () => { + if (!document.hidden) { + setUnreadCount(0); + } + }; + + window.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('focus', handleVisibilityChange); + + return () => { + window.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('focus', handleVisibilityChange); + }; + }, [setUnreadCount]); +}; + +export default useVisibilityChange; diff --git a/frontend/hooks/useWebSocketForChatBot.jsx b/frontend/hooks/useWebSocketForChatBot.tsx similarity index 89% rename from frontend/hooks/useWebSocketForChatBot.jsx rename to frontend/hooks/useWebSocketForChatBot.tsx index 6349eac..957c19e 100644 --- a/frontend/hooks/useWebSocketForChatBot.jsx +++ b/frontend/hooks/useWebSocketForChatBot.tsx @@ -8,8 +8,9 @@ import { handleWebSocketError } from "../utils/websocket/handleWebSocketError"; import processWebSocketMessage from "../utils/websocket/processWebSocketMessage"; import { buildWebSocketURL } from "../services/url-builder/url-builder"; import { initializeWebSocketConnection } from "../services/api/api"; +import { useWebsocketForChatbotProps } from "../interface/interface"; -const useWebsocketForChatbot=(socketRef,setMessages,router)=>{ +const useWebsocketForChatbot=({socketRef,setMessages,router}:useWebsocketForChatbotProps)=>{ useEffect(() => { const username = getSessionUser(); if (!username || username === "null" || username === "undefined") { @@ -31,16 +32,18 @@ const useWebsocketForChatbot=(socketRef,setMessages,router)=>{ //todo-> toast connected to server }; const handleMessage = (event) => - processWebSocketMessage(event, setMessages, () => router.push("/"), true); + processWebSocketMessage({event, setMessages, navigateToLogin:() => router.push("/"), isChatbot:true}); const handleClose = (event) => handleWebSocketClose(event, () => router.push("/")); const handleError = handleWebSocketError; const socket = initializeWebSocketConnection( url, + { handleOpen, handleMessage, handleClose, handleError, + } ); socketRef.current = socket; diff --git a/frontend/hooks/useWebsocket.jsx b/frontend/hooks/useWebsocket.tsx similarity index 82% rename from frontend/hooks/useWebsocket.jsx rename to frontend/hooks/useWebsocket.tsx index dcae442..fc852f8 100644 --- a/frontend/hooks/useWebsocket.jsx +++ b/frontend/hooks/useWebsocket.tsx @@ -1,4 +1,4 @@ -import { useEffect} from "react"; +import { MutableRefObject, useEffect} from "react"; import getSessionUser from "../utils/session/getSessionUser"; import getSessionUserId from "../utils/session/getSessionUserId"; import handleWebSocketClose from "../utils/websocket/handleWebSocketClose"; @@ -7,8 +7,26 @@ import processWebSocketMessage from "../utils/websocket/processWebSocketMessage" import { buildWebSocketURL } from "../services/url-builder/url-builder"; import { initializeWebSocketConnection } from "../services/api/api"; import playSound from "../utils/playSound"; +import { Message } from "../interface/interface" +import { NextRouter } from "next/router"; -const useWebsocket=(soundEnabled,channel,socketRef,setMessages,router,setUnreadCount)=>{ +interface UseWebSocketProps { + soundEnabled: boolean, + channel: string, + socketRef: MutableRefObject, + setMessages: React.Dispatch>, + router: NextRouter, + setUnreadCount: React.Dispatch> +} + +const useWebsocket=({ + soundEnabled, + channel, + socketRef, + setMessages, + router, + setUnreadCount, +}: UseWebSocketProps)=>{ useEffect(() => { const username = getSessionUser(); if (!username || username === "null" || username === "undefined") { @@ -20,27 +38,29 @@ const useWebsocket=(soundEnabled,channel,socketRef,setMessages,router,setUnreadC //todo-> toast connected to server }; const handleMessage = (event) => - processWebSocketMessage( + processWebSocketMessage({ event, setMessages, - () => router.push("/"), - false, + navigateToLogin:() => router.push("/"), + isChatbot:false,} ); const handleClose = (event) => handleWebSocketClose(event, () => router.push("/")); const handleError = handleWebSocketError; const socket = initializeWebSocketConnection( url, + { handleOpen, handleMessage, handleClose, handleError, + } ); socketRef.current = socket; socket.addEventListener("message", (event) => { try { - let data = ""; + let data:any = ""; if ( event.data != "Message send successful" && event.data != "Welcome to MDG Chat!" @@ -83,6 +103,7 @@ const useWebsocket=(soundEnabled,channel,socketRef,setMessages,router,setUnreadC username: data.sender, timestamp: parseFloat(data.timestamp), avatar: data.url, + userID: data.userID, }, ]); if (soundEnabled) playSound(isSent); diff --git a/frontend/interface/interface.ts b/frontend/interface/interface.ts new file mode 100644 index 0000000..a4c1c19 --- /dev/null +++ b/frontend/interface/interface.ts @@ -0,0 +1,299 @@ +import React, { Dispatch, MutableRefObject, ReactNode, SetStateAction, useEffect, useState } from "react"; +import { useRouter } from 'next/navigation'; +import alertServerError from "../utils/alerts/alertServerError"; + +export interface SettingsPopupProps { + onClose: () => void; + soundEnabled: boolean; + setSoundEnabled: (enabled: boolean) => void; + notificationsEnabled: boolean; + setNotificationsEnabled: (enabled: boolean) => void; + } + +export interface RightPaneProps extends SettingsPopupProps {} + + interface IconProps { + src: string; + alt: string; + onClick?: () => void; + } + +export interface LayoutProps { + children: React.ReactNode; + home: boolean; +} + +export interface TopicDropdownProps { + topic: string; + setTopic: (topic: string) => void; + login: boolean; + } + +export interface Project { + Category: string; + Name: string; + ShortDesc: string; + LongDesc: string; + ImageLink: string; + AppStoreLink: string; + GithubLink: string; + PlayStoreLink: string; + } + + export interface TopicSelectionModalProps { + onClose: () => void; + } + + export interface UsernameInputProps { + value: string; + onChange: (value: string) => void; + } + +export interface NavbarProps{ + currentPage: string; + currentTopic:string; + } + +export interface ModalProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; + } + + export interface BoxProps { + channel: "public" | "private" | "chatbot"; + } + +export interface MailProps { + isOpen?: boolean; + onClose: () => void; + channel: "public" | "private" | "chatbot"; + } + export interface ProjectListProps { + projects: Project[]; + category: string; + heightDecrease?: boolean; + } + + export interface ProjectCardProps { + name: string; + shortDesc: string; + ImageLink: string; + Github?: string; + PlayStore?: string; + AppStore?: string; + } + + export interface LoginModalProps { + onClose: () => void; + redirect: string; + } + + export interface ChatBotLoginModalProps { + onClose: () => void; // Function to close the modal + } + export type Topic = "SELECT A TOPIC" | "Option 1" | "Option 2" | string ; + + export interface Message { + isSent: boolean; + avatar?: string; + username: string; + text: string; + timestamp: number; + userID: string; + } + export interface ChatContainerProps { + messages: Message[]; // Assuming messages are serialized JSON strings + messagesEndRef: React.RefObject; // Ref to scroll to the bottom + } + + + + + + export interface UserContextType { + userName: string | null; + setUserName: React.Dispatch>; + } + + export interface UserProviderProps { + children: React.ReactNode; + } + + export interface UseLoadSettingProps { + setSoundEnabled: React.Dispatch>; + setNotificationsEnabled: React.Dispatch>; +} + + export interface UseSettingsProps { + soundEnabled: boolean; + notificationsEnabled: boolean; + } + + export interface UseVisibilityChangeProps { + setUnreadCount: Dispatch> +} + +export interface UseWebsocketProps { + soundEnabled: boolean; + channel: string; + socketRef: React.MutableRefObject; + setMessages: React.Dispatch>; + router: any; + setUnreadCount: React.Dispatch>; +} + +export interface useWebsocketForChatbotProps { + socketRef: MutableRefObject; + setMessages: Dispatch>; + router: any; +} + +interface UseLoadSettingHook { + (setSoundEnabled: (enabled: boolean) => void, setNotificationsEnabled: (enabled: boolean) => void): void; +} + +interface UseSettingsHook { + (soundEnabled: boolean, notificationsEnabled: boolean): void; +} + +interface UseWebsocketForChatbotHook { + ( + socketRef: React.MutableRefObject, + setMessages: React.Dispatch>, + router: typeof useRouter, + messagesEndRef: React.MutableRefObject + ): void; +} + +interface UseVisibilityChangeHook { + (setUnreadCount: React.Dispatch>): void; +} + +interface UseLeaveChatHook { + (router: typeof useRouter, socketRef: React.MutableRefObject): void; +} + +interface ChatbotContainerProps { + messages: Message[]; + messagesEndRef: React.MutableRefObject; +} + +export interface ProcessWebSocketMessageProps { + event: MessageEvent; + setMessages: React.Dispatch>; + navigateToLogin: () => void; + isChatbot: boolean; +} + +export interface DataFromServer { + userID?: string; + Message?: string; + Delete?: string; +} + + + + + +export interface WebSocketHandlers { + onOpen: (event: Event) => void; + onMessage: (event: MessageEvent) => void; + onClose: (event: CloseEvent) => void; + onError: (event: Event) => void; +} + + +export type AlertSameUserProps = (reason: string, navigate: NavigateFunction) => void; +export type AlertBadRequestProps = (reason: string, navigate: NavigateFunction) => void; +export type AlertServerErrorProps = (reason: string, navigate: NavigateFunction) => void; + +export interface WebSocketURLParams { + userId: string; + username: string; + channel: string; + topic?: string; +} + +export interface URLBuilderParams { + host: string; + port: string; + protocol: string; +} + +export interface LeaveChatURLParams { + userID: string; +} + +export interface ChatContainerProps { + messages: Message[]; // An array of message objects + messagesEndRef: React.RefObject; // A ref for auto-scrolling to the bottom of the chat +} + +export interface AvatarListProps { + AvatarList: string[]; // List of avatar image paths/URLs +} + +export interface ChatInputBoxProps { + socketRef: React.MutableRefObject; +} + + + +export type CheckAndPromptSessionChange = ( + currentUser: string, + newUsername: string, + onSessionChange: () => void +) => Promise; + +export type GetSessionUser = () => string | null; +export type GetSessionUserId = () => string | null; +export type SetSessionUser = (username: string) => void; +export type RemoveSessionUserId = () => void; + +export interface LeaveChatResponse { + // Define the expected properties of the response here + success: boolean; + message?: string; +} + +// Define the return type of the fetchProjects function +export type FetchProjectsResponse = Project[]; // Array of projects + +// Define the interface for a project +export interface Project { + id: string; // Adjust the type as per your actual API response + name: string; // Example property + description?: string; // Optional property + // Add more properties as needed based on your project data structure +} + +// Define the interface for the subscribe response +export interface SubscribeResponse { + code: number; + message: string; +} + +// Define the parameters for the subscribe function +export interface SubscribeParams { + email: string; + username: string; + userId: string; + channel: string; + timestamp: number; +} + +// Define an interface for the environment variables +export interface BackendEnvironment { + NEXT_PUBLIC_BACKEND_HOST: string; + NEXT_PUBLIC_BACKEND_PORT: string; + NEXT_PUBLIC_BACKEND_ENVIRONMENT: 'development' | 'production'; +} + +// Create a type for the optional topic parameter + +export type NavigateFunction = (path: string) => void; + +export type AlertBannedUser = (reason: string, navigate: NavigateFunction) => void; + +export type AlertAbnormalCloseProps = (reason: string, navigate: NavigateFunction) => void; diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba56355..dd8f4d3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,19 +9,26 @@ "axios": "^1.6.8", "file-loader": "^6.2.0", "moment": "^2.29.4", - "next": "latest", + "next": "^14.2.5", "postcss": "^8.4.31", "prettier": "^3.2.5", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.12.0", + "react-router-dom": "^6.26.2", "react-switch": "^7.0.0", + "react-toastify": "^10.0.5", "sharp": "^0.33.1", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", "start": "^5.1.0", "sweetalert2": "^11.10.1", "tailwindcss": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^22.6.1", + "@types/react": "^18.3.3", + "typescript": "5.5.4" } }, "node_modules/@alloc/quick-lru": { @@ -529,17 +536,19 @@ } }, "node_modules/@next/env": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", - "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", - "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -549,12 +558,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", - "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -564,12 +574,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", - "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -579,12 +590,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", - "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -594,12 +606,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", - "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -609,12 +622,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", - "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -624,12 +638,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", - "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -639,12 +654,13 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", - "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -654,12 +670,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", - "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -700,16 +717,33 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -758,11 +792,30 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/node": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", - "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" } }, "node_modules/@webassemblyjs/ast": { @@ -1158,9 +1211,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001543", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", - "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==", + "version": "1.0.30001646", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz", + "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==", "funding": [ { "type": "opencollective", @@ -1174,7 +1227,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chokidar": { "version": "3.5.3", @@ -1227,6 +1281,15 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1319,6 +1382,13 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1667,7 +1737,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true }, "node_modules/graceful-fs": { "version": "4.2.11", @@ -1979,37 +2050,39 @@ "peer": true }, "node_modules/next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", - "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "license": "MIT", "dependencies": { - "@next/env": "13.5.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.14.0" + "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.4", - "@next/swc-darwin-x64": "13.5.4", - "@next/swc-linux-arm64-gnu": "13.5.4", - "@next/swc-linux-arm64-musl": "13.5.4", - "@next/swc-linux-x64-gnu": "13.5.4", - "@next/swc-linux-x64-musl": "13.5.4", - "@next/swc-win32-arm64-msvc": "13.5.4", - "@next/swc-win32-ia32-msvc": "13.5.4", - "@next/swc-win32-x64-msvc": "13.5.4" + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -2018,10 +2091,7 @@ "@opentelemetry/api": { "optional": true }, - "fibers": { - "optional": true - }, - "node-sass": { + "@playwright/test": { "optional": true }, "sass": { @@ -2320,6 +2390,38 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-switch": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", @@ -2332,6 +2434,19 @@ "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2857,10 +2972,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.0.13", @@ -2916,6 +3046,7 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" diff --git a/frontend/package.json b/frontend/package.json index d026958..a68540c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,18 +11,25 @@ "axios": "^1.6.8", "file-loader": "^6.2.0", "moment": "^2.29.4", - "next": "latest", + "next": "^14.2.5", "postcss": "^8.4.31", "prettier": "^3.2.5", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.12.0", + "react-router-dom": "^6.26.2", "react-switch": "^7.0.0", + "react-toastify": "^10.0.5", "sharp": "^0.33.1", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", "start": "^5.1.0", "sweetalert2": "^11.10.1", "tailwindcss": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^22.6.1", + "@types/react": "^18.3.3", + "typescript": "5.5.4" } } diff --git a/frontend/pages/_app.jsx b/frontend/pages/_app.tsx similarity index 56% rename from frontend/pages/_app.jsx rename to frontend/pages/_app.tsx index ac0e0ab..ac4f267 100644 --- a/frontend/pages/_app.jsx +++ b/frontend/pages/_app.tsx @@ -1,10 +1,14 @@ +import React from 'react'; +import { AppProps } from 'next/app'; import { UserProvider } from "../context/userContext"; import "../styles/globals.css"; -export default function App({ Component, pageProps }) { +function App({ Component, pageProps }: AppProps) { return ( ); } + +export default App; diff --git a/frontend/pages/chat.jsx b/frontend/pages/chat.tsx similarity index 83% rename from frontend/pages/chat.jsx rename to frontend/pages/chat.tsx index 9afbf65..7cb1552 100644 --- a/frontend/pages/chat.jsx +++ b/frontend/pages/chat.tsx @@ -10,21 +10,24 @@ import useSettings from "../hooks/useSettings"; import useWebsocket from "../hooks/useWebsocket"; import useLeaveChat from "../hooks/useLeaveChat"; import useVisibilityChange from "../hooks/useVisibilityChange"; +import { Message } from "../interface/interface"; + + export default function Home(){ - const [messages, setMessages] = useState([]); - const [unreadCount, setUnreadCount] = useState(0); - const [soundEnabled, setSoundEnabled] = useState(true); - const [notificationsEnabled, setNotificationsEnabled] = useState(true); - const router = useRouter() - const {channel}=router.query; - const socketRef = useRef(null); - const messagesEndRef = useRef(null); + const [messages, setMessages] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [soundEnabled, setSoundEnabled] = useState(true); + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + const router = useRouter(); + const {channel}=router.query as { channel?: string };; + const socketRef = useRef(null); + const messagesEndRef = useRef(null); - useLoadSetting(setSoundEnabled,setNotificationsEnabled); - useSettings(soundEnabled,notificationsEnabled); - useWebsocket(soundEnabled,channel,socketRef,setMessages,router,setUnreadCount) + useLoadSetting({setSoundEnabled,setNotificationsEnabled}); + useSettings({soundEnabled,notificationsEnabled}); + useWebsocket({soundEnabled,channel,socketRef,setMessages,router,setUnreadCount}) useLeaveChat(router); - useVisibilityChange(setUnreadCount); + useVisibilityChange({setUnreadCount}); useEffect(() => {}, [messages]); diff --git a/frontend/pages/chat_bot.jsx b/frontend/pages/chat_bot.tsx similarity index 87% rename from frontend/pages/chat_bot.jsx rename to frontend/pages/chat_bot.tsx index c88924f..0798206 100644 --- a/frontend/pages/chat_bot.jsx +++ b/frontend/pages/chat_bot.tsx @@ -1,4 +1,4 @@ -+"use client"; +"use client"; import ChatInputBox from "../components/chat/chatInputBox"; import Box from "../components/mdgBox"; import { useState, useEffect, useRef } from "react"; @@ -11,25 +11,25 @@ import useWebsocketForChatbot from "../hooks/useWebSocketForChatBot"; import useVisibilityChange from "../hooks/useVisibilityChange"; import useLeaveChat from "../hooks/useLeaveChat"; + export default function Home() { const [messages, setMessages] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [soundEnabled, setSoundEnabled] = useState(true); const [notificationsEnabled, setNotificationsEnabled] = useState(true); - const [topic, setTopic] = useState("Appetizer"); + const [topic, setTopic] = useState("Appetizer"); const router = useRouter(); const socketRef = useRef(null); const messagesEndRef = useRef(null); - useLoadSetting(setSoundEnabled,setNotificationsEnabled); - useSettings(soundEnabled,notificationsEnabled); - useWebsocketForChatbot(socketRef,setMessages,router); - useVisibilityChange(setUnreadCount); + useLoadSetting({setSoundEnabled,setNotificationsEnabled}); + useSettings({soundEnabled,notificationsEnabled}); + useWebsocketForChatbot({socketRef,setMessages,router}); + useVisibilityChange({setUnreadCount}); useLeaveChat(router); - useEffect(() => { - setTopic(router.query.topic ?? "Appetizer"); - }, [router.query]); + useEffect(() => setTopic(router.query.topic as string ?? "Appetizer"), [router.query]); + useEffect(() => {}, [messages]); diff --git a/frontend/pages/index.jsx b/frontend/pages/index.tsx similarity index 88% rename from frontend/pages/index.jsx rename to frontend/pages/index.tsx index d09a811..77ea60b 100644 --- a/frontend/pages/index.jsx +++ b/frontend/pages/index.tsx @@ -7,11 +7,11 @@ import LoginModal from "../components/chat/loginModal"; import ChatBotLoginModal from "../components/chatbot/chatbotLoginModal"; export default function Home() { - const [isModalOpen, setIsModalOpen] = useState(false); - const [isChatBotModalOpen, setISChatBotModalOpen] = useState(false); - const [redirect, setRedirect] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isChatBotModalOpen, setISChatBotModalOpen] = useState(false); + const [redirect, setRedirect] = useState(""); - const openModal = (redirect) => { + const openModal = (redirect:string) => { setIsModalOpen(true); setRedirect(redirect); }; @@ -45,14 +45,14 @@ export default function Home() {
- +
{isModalOpen && ( )} {isChatBotModalOpen && ( - + )}
@@ -76,7 +76,7 @@ export default function Home() { onClick={goToChatbot} >

- + {"chat TALK TO OUR CHATBOT

@@ -85,7 +85,7 @@ export default function Home() { onClick={goToPrivateChat} >

- + {"slack PRIVATE CHAT ON SLACK

@@ -94,7 +94,7 @@ export default function Home() { onClick={goToPublicChat} >

- + {"slack PUBLIC MDG CHAT FORUM

diff --git a/frontend/services/api/api.js b/frontend/services/api/api.js deleted file mode 100644 index 58a88a8..0000000 --- a/frontend/services/api/api.js +++ /dev/null @@ -1,14 +0,0 @@ -export function initializeWebSocketConnection( - url, - onOpen, - onMessage, - onClose, - onError, -) { - const socket = new WebSocket(url); - socket.onopen = onOpen; - socket.onmessage = onMessage; - socket.onclose = onClose; - socket.onerror = onError; - return socket; -} \ No newline at end of file diff --git a/frontend/services/api/api.ts b/frontend/services/api/api.ts new file mode 100644 index 0000000..516175c --- /dev/null +++ b/frontend/services/api/api.ts @@ -0,0 +1,20 @@ +interface WebSocketHandlers { + handleOpen: (event: Event) => void; + handleMessage: (event: MessageEvent) => void; + handleClose: (event: CloseEvent) => void; + handleError: (event: Event) => void; +} + +export function initializeWebSocketConnection( + url: string, + handlers: WebSocketHandlers +): WebSocket { + const socket = new WebSocket(url); + + socket.onopen = handlers.handleOpen; + socket.onmessage = handlers.handleMessage; + socket.onclose = handlers.handleClose; + socket.onerror = handlers.handleError; + + return socket; +} diff --git a/frontend/services/api/leaveChatApi.js b/frontend/services/api/leaveChatApi.js deleted file mode 100644 index 9452cef..0000000 --- a/frontend/services/api/leaveChatApi.js +++ /dev/null @@ -1,13 +0,0 @@ -import axios from "axios"; -import { leaveChatURLbuildr } from "../url-builder/url-builder"; - -export async function leaveChat(userID) { - if (userID) { - try { - const url = leaveChatURLbuildr(userID); - const response = await axios.post(url, {}); - } catch (error) { - // console.error("Error in leaving chat:", error) - } - } -} \ No newline at end of file diff --git a/frontend/services/api/leaveChatApi.ts b/frontend/services/api/leaveChatApi.ts new file mode 100644 index 0000000..2a4a120 --- /dev/null +++ b/frontend/services/api/leaveChatApi.ts @@ -0,0 +1,17 @@ +import axios from "axios"; +import { leaveChatURLbuildr } from "../url-builder/url-builder"; +import { LeaveChatResponse } from "../../interface/interface"; + +export async function leaveChat(userID: string): Promise { + if (userID) { + try { + const url = leaveChatURLbuildr(userID); + const response = await axios.post(url, {}); + + return response.data; // Assuming the API returns a response with the properties defined in LeaveChatResponse + } catch (error) { + console.error("Error in leaving chat:", error); + return undefined; // Or handle the error as needed + } + } +} diff --git a/frontend/services/api/projectsApi.js b/frontend/services/api/projectsApi.js deleted file mode 100644 index b7365cd..0000000 --- a/frontend/services/api/projectsApi.js +++ /dev/null @@ -1,11 +0,0 @@ -import axios from "axios"; -import { projectURLbuildr } from "../url-builder/url-builder"; -export const fetchProjects = async () => { - try { - const url = projectURLbuildr(); - const res = await axios.get(url); - return res.data; - } catch (error) { - //todo -> enable sentry logger here - } -}; \ No newline at end of file diff --git a/frontend/services/api/projectsApi.ts b/frontend/services/api/projectsApi.ts new file mode 100644 index 0000000..fb8905e --- /dev/null +++ b/frontend/services/api/projectsApi.ts @@ -0,0 +1,17 @@ +import axios from "axios"; +import { projectURLbuildr } from "../url-builder/url-builder"; + +import { FetchProjectsResponse, Project } from "../../interface/interface"; + +export const fetchProjects = async (): Promise => { + try { + const url = projectURLbuildr(); + const res = await axios.get(url); + return res.data; + } catch (error) { + console.error("Error fetching projects:", error); + // todo -> enable sentry logger here + return undefined; // Handle error as needed + } +}; + diff --git a/frontend/services/api/subscribeApi.js b/frontend/services/api/subscribeApi.js deleted file mode 100644 index 2767e0c..0000000 --- a/frontend/services/api/subscribeApi.js +++ /dev/null @@ -1,30 +0,0 @@ -import axios from "axios"; -import { subscribeURLbuildr } from "../url-builder/url-builder"; - -const subscribe = async (email, username, userId, channel, timestamp) => { - const url = subscribeURLbuildr(); - try { - const response = await axios.post( - url, - { - email: email, - username: username, - userId: userId, - channel: channel, - timestamp: timestamp, - }, - { - headers: { - "Content-Type": "multipart/form-data", - }, - }, - ); - - if (response.status === 200) { - return response.data; - } - } catch (error) { - throw error.response.data; - } -}; -export default subscribe; \ No newline at end of file diff --git a/frontend/services/api/subscribeApi.ts b/frontend/services/api/subscribeApi.ts new file mode 100644 index 0000000..e362411 --- /dev/null +++ b/frontend/services/api/subscribeApi.ts @@ -0,0 +1,45 @@ +import axios from "axios"; +import { subscribeURLbuildr } from "../url-builder/url-builder"; + +import { SubscribeResponse ,SubscribeParams } from "../../interface/interface"; + + +// Convert the subscribe function to TypeScript +const subscribe = async ({ + email, + username, + userId, + channel, + timestamp, +}: SubscribeParams): Promise => { + const url = subscribeURLbuildr(); + + try { + const response = await axios.post( + url, + { + email, + username, + userId, + channel, + timestamp, + }, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + + if (response.status === 200) { + return response.data; + } else { + throw new Error("Subscription failed"); + } + } catch (error: any) { + // Here, we throw a new error with a message from the server if available + throw new Error(error.response?.data?.message || "An error occurred during subscription"); + } +}; + +export default subscribe; diff --git a/frontend/services/url-builder/url-builder.js b/frontend/services/url-builder/url-builder.js deleted file mode 100644 index 991266f..0000000 --- a/frontend/services/url-builder/url-builder.js +++ /dev/null @@ -1,53 +0,0 @@ -export function buildWebSocketURL(userId, username, channel, topic) { - const host = process.env.NEXT_PUBLIC_BACKEND_HOST; - const port = process.env.NEXT_PUBLIC_BACKEND_PORT; - const protocol = - process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" - ? "ws" - : "wss"; - const baseUrl = `${protocol}://${host}:${port}/chat`; - const params = new URLSearchParams({ - channel: channel || "public", - name: username, - userID: userId || "0", - topic: topic || null, - }); - return `${baseUrl}?${params.toString()}`; -} - -export function projectURLbuildr() { - const host = process.env.NEXT_PUBLIC_BACKEND_HOST; - const port = process.env.NEXT_PUBLIC_BACKEND_PORT; - const protocol = - process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" - ? "http" - : "https"; - const baseUrl = `${protocol}://${host}:${port}/projects`; - return baseUrl; -} - -export function leaveChatURLbuildr(userID) { - const host = process.env.NEXT_PUBLIC_BACKEND_HOST; - const port = process.env.NEXT_PUBLIC_BACKEND_PORT; - const protocol = - process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" - ? "http" - : "https"; - const baseUrl = `${protocol}://${host}:${port}/chat/leave`; - const params = new URLSearchParams({ - userID: userID, - }); - - return `${baseUrl}?${params.toString()}`; -} - -export function subscribeURLbuildr() { - const host = process.env.NEXT_PUBLIC_BACKEND_HOST; - const port = process.env.NEXT_PUBLIC_BACKEND_PORT; - const protocol = - process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" - ? "http" - : "https"; - const baseUrl = `${protocol}://${host}:${port}/subscribe`; - return baseUrl; -} \ No newline at end of file diff --git a/frontend/services/url-builder/url-builder.ts b/frontend/services/url-builder/url-builder.ts new file mode 100644 index 0000000..a5940a9 --- /dev/null +++ b/frontend/services/url-builder/url-builder.ts @@ -0,0 +1,74 @@ + +import { BackendEnvironment , Topic } from "../../interface/interface"; +// Function to build WebSocket URL +export function buildWebSocketURL( + userId: string, + username: string, + channel: string, + topic?: Topic +): string { + const env: BackendEnvironment = { + NEXT_PUBLIC_BACKEND_HOST: process.env.NEXT_PUBLIC_BACKEND_HOST || 'localhost', + NEXT_PUBLIC_BACKEND_PORT: process.env.NEXT_PUBLIC_BACKEND_PORT || '1323', + NEXT_PUBLIC_BACKEND_ENVIRONMENT: process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT as 'development' | 'production', + }; + + const protocol = env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" ? "ws" : "wss"; + const baseUrl = `${protocol}://${env.NEXT_PUBLIC_BACKEND_HOST}:${env.NEXT_PUBLIC_BACKEND_PORT}/chat`; + + const params = new URLSearchParams({ + channel: channel || "public", + name: username, + userID: userId || "0", + topic: topic || '', + }); + + return `${baseUrl}?${params.toString()}`; +} + + +// Function to build project URL +export function projectURLbuildr(): string { + const env: BackendEnvironment = { + NEXT_PUBLIC_BACKEND_HOST: process.env.NEXT_PUBLIC_BACKEND_HOST || 'localhost', + NEXT_PUBLIC_BACKEND_PORT: process.env.NEXT_PUBLIC_BACKEND_PORT || '1323', + NEXT_PUBLIC_BACKEND_ENVIRONMENT: process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT as 'development' | 'production', + }; + + const protocol = env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" ? "http" : "https"; + const baseUrl = `${protocol}://${env.NEXT_PUBLIC_BACKEND_HOST}:${env.NEXT_PUBLIC_BACKEND_PORT}/projects`; + + return baseUrl; +} + +// Function to build leave chat URL +export function leaveChatURLbuildr(userID: string): string { + const env: BackendEnvironment = { + NEXT_PUBLIC_BACKEND_HOST: process.env.NEXT_PUBLIC_BACKEND_HOST || 'localhost', + NEXT_PUBLIC_BACKEND_PORT: process.env.NEXT_PUBLIC_BACKEND_PORT || '1323', + NEXT_PUBLIC_BACKEND_ENVIRONMENT: process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT as 'development' | 'production', + }; + + const protocol = env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" ? "http" : "https"; + const baseUrl = `${protocol}://${env.NEXT_PUBLIC_BACKEND_HOST}:${env.NEXT_PUBLIC_BACKEND_PORT}/chat/leave`; + + const params = new URLSearchParams({ + userID: userID, + }); + + return `${baseUrl}?${params.toString()}`; +} + + +// Function to build subscribe URL +export function subscribeURLbuildr(): string { + const env: BackendEnvironment = { + NEXT_PUBLIC_BACKEND_HOST: process.env.NEXT_PUBLIC_BACKEND_HOST || 'localhost', + NEXT_PUBLIC_BACKEND_PORT: process.env.NEXT_PUBLIC_BACKEND_PORT || '1323', + NEXT_PUBLIC_BACKEND_ENVIRONMENT: process.env.NEXT_PUBLIC_BACKEND_ENVIRONMENT as 'development' | 'production', + }; + + const protocol = env.NEXT_PUBLIC_BACKEND_ENVIRONMENT === "development" ? "http" : "https"; + const baseUrl = `${protocol}://${env.NEXT_PUBLIC_BACKEND_HOST}:${env.NEXT_PUBLIC_BACKEND_PORT}/subscribe`; + return baseUrl; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..09d1f3f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" +, "context/userContext.ts" ], + "exclude": [ + "node_modules" + ] +} diff --git a/frontend/utils/alerts/alertAbnormalClose.js b/frontend/utils/alerts/alertAbnormalClose.js deleted file mode 100644 index 84d6ecd..0000000 --- a/frontend/utils/alerts/alertAbnormalClose.js +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; -import Swal from "sweetalert2"; - -export default function alertAbnormalClose(reason, navigateToLogin) { - try { - Swal.fire({ - title: "Connection lost", - text: `Please try again or with a different username ${reason}`, - icon: "warning", - - iconColor: "#3670F5", - confirmButtonColor: "#3670F5", - confirmButtonText: "OK", - didOpen: (popup) => { - popup.style.borderRadius = "1rem"; - }, - }).then((result) => { - try { - if (result.isConfirmed) navigateToLogin(); - } catch (error) {} - }); - } catch (error) {} - } \ No newline at end of file diff --git a/frontend/utils/alerts/alertAbnormalClose.ts b/frontend/utils/alerts/alertAbnormalClose.ts new file mode 100644 index 0000000..48f33eb --- /dev/null +++ b/frontend/utils/alerts/alertAbnormalClose.ts @@ -0,0 +1,32 @@ +"use client"; +import Swal from 'sweetalert2'; +import { Dispatch, SetStateAction } from 'react'; // Import types for state setters +import { AlertAbnormalCloseProps } from '../../interface/interface'; +import { navigateToLogin } from '../websocket/handleWebSocketClose'; + +const alertAbnormalClose: AlertAbnormalCloseProps = async (reason, navigate) => { // Make the function async + try { + const result = await Swal.fire({ // Corrected line + title: "Connection lost", + text: `Please try again or with a different username. Reason: ${reason}`, + icon: "warning", + iconColor: "#3670F5", + confirmButtonColor: "#3670F5", + confirmButtonText: "OK", + didOpen: (popup: HTMLElement) => { + popup.style.borderRadius = "1rem"; + }, + }); + + if (result.isConfirmed) { + navigate("/"); + } + } catch (error) { + // More specific error handling here, e.g., log the error + console.error("Error in SweetAlert:", error); + } + + return null; // This component doesn't render anything directly, it's just a function +}; + +export default alertAbnormalClose; diff --git a/frontend/utils/alerts/alertBadRequest.js b/frontend/utils/alerts/alertBadRequest.ts similarity index 56% rename from frontend/utils/alerts/alertBadRequest.js rename to frontend/utils/alerts/alertBadRequest.ts index 8f1d643..f003dd5 100644 --- a/frontend/utils/alerts/alertBadRequest.js +++ b/frontend/utils/alerts/alertBadRequest.ts @@ -1,7 +1,8 @@ "use client"; import Swal from "sweetalert2"; - -export default function alertBadRequest(reason, navigateToLogin) { +import { navigateToLogin } from "../websocket/handleWebSocketClose"; +import { AlertBadRequestProps } from "../../interface/interface"; +const alertBadRequest: AlertBadRequestProps = (reason, navigate) => { try { Swal.fire({ title: "Bad request", @@ -11,13 +12,15 @@ export default function alertBadRequest(reason, navigateToLogin) { iconColor: "#3670F5", confirmButtonColor: "#3670F5", confirmButtonText: "OK", - didOpen: (popup) => { + didOpen: (popup: HTMLElement) => { popup.style.borderRadius = "1rem"; }, }).then((result) => { try { - if (result.isConfirmed) navigateToLogin(); + if (result.isConfirmed) navigate("/"); } catch (error) {} }); } catch (error) {} - } \ No newline at end of file + } + + export default alertBadRequest; \ No newline at end of file diff --git a/frontend/utils/alerts/alertBannedUser.js b/frontend/utils/alerts/alertBannedUser.ts similarity index 59% rename from frontend/utils/alerts/alertBannedUser.js rename to frontend/utils/alerts/alertBannedUser.ts index ac5adb6..27af9a4 100644 --- a/frontend/utils/alerts/alertBannedUser.js +++ b/frontend/utils/alerts/alertBannedUser.ts @@ -1,7 +1,8 @@ "use client"; import Swal from "sweetalert2"; - -export default function alertBannedUser(reason, navigateToLogin) { +import {NavigateFunction , AlertBannedUser } from "../../interface/interface"; +import { navigateToLogin } from "../websocket/handleWebSocketClose"; +const alertBannedUser: AlertBannedUser = (reason, navigate) => { try { Swal.fire({ title: "You have been banned", @@ -11,7 +12,7 @@ export default function alertBannedUser(reason, navigateToLogin) { confirmButtonColor: "#3670F5", iconColor: "#3670F5", confirmButtonText: "OK", - didOpen: (popup) => { + didOpen: (popup: HTMLElement) => { popup.style.borderRadius = "1rem"; }, customClass: { @@ -19,8 +20,10 @@ export default function alertBannedUser(reason, navigateToLogin) { }, }).then((result) => { try { - if (result.isConfirmed) navigateToLogin(); + if (result.isConfirmed) navigate("/"); } catch (error) {} }); } catch (error) {} - } \ No newline at end of file + } + +export default alertBannedUser; \ No newline at end of file diff --git a/frontend/utils/alerts/alertSameUsername.js b/frontend/utils/alerts/alertSameUsername.ts similarity index 59% rename from frontend/utils/alerts/alertSameUsername.js rename to frontend/utils/alerts/alertSameUsername.ts index 6f0fc5d..c2decae 100644 --- a/frontend/utils/alerts/alertSameUsername.js +++ b/frontend/utils/alerts/alertSameUsername.ts @@ -1,7 +1,8 @@ "use client"; import Swal from "sweetalert2"; - -export default function alertSameUsername(reason, navigateToLogin) { +import { navigateToLogin } from "../websocket/handleWebSocketClose"; +import {AlertSameUserProps } from "../../interface/interface"; +const alertSameUsername: AlertSameUserProps = (reason, navigate) => { try { Swal.fire({ title: "Username already exists", @@ -12,13 +13,15 @@ export default function alertSameUsername(reason, navigateToLogin) { imageAlt: "Username Taken", confirmButtonColor: "#3670F5", confirmButtonText: "OK", - didOpen: (popup) => { + didOpen: (popup:HTMLElement) => { popup.style.borderRadius = "1rem"; }, }).then((result) => { try { - if (result.isConfirmed) navigateToLogin(); + if (result.isConfirmed) navigate("/"); } catch (error) {} }); } catch (error) {} - } \ No newline at end of file + } + + export default alertSameUsername; \ No newline at end of file diff --git a/frontend/utils/alerts/alertServerError.js b/frontend/utils/alerts/alertServerError.ts similarity index 56% rename from frontend/utils/alerts/alertServerError.js rename to frontend/utils/alerts/alertServerError.ts index 981350f..338b400 100644 --- a/frontend/utils/alerts/alertServerError.js +++ b/frontend/utils/alerts/alertServerError.ts @@ -1,7 +1,8 @@ "use client"; import Swal from "sweetalert2"; - -export default function alertServerError(reason, navigateToLogin) { +import { AlertServerErrorProps } from "../../interface/interface"; +import { navigateToLogin } from "../websocket/handleWebSocketClose"; +const alertServerError: AlertServerErrorProps = (reason, navigate) => { try { Swal.fire({ title: "Server error", @@ -11,13 +12,15 @@ export default function alertServerError(reason, navigateToLogin) { iconColor: "#3670F5", confirmButtonColor: "#3670F5", confirmButtonText: "OK", - didOpen: (popup) => { + didOpen: (popup:HTMLElement) => { popup.style.borderRadius = "1rem"; }, }).then((result) => { try { - if (result.isConfirmed) navigateToLogin(); + if (result.isConfirmed) navigate("/"); } catch (error) {} }); } catch (error) {} - } \ No newline at end of file + } + + export default alertServerError; \ No newline at end of file diff --git a/frontend/utils/alerts/checkAndPromptSessionChange.js b/frontend/utils/alerts/checkAndPromptSessionChange.js deleted file mode 100644 index ca356ba..0000000 --- a/frontend/utils/alerts/checkAndPromptSessionChange.js +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; -import Swal from "sweetalert2"; - -export default async function checkAndPromptSessionChange( - currentUsername, - inputUsername, - onConfirm, - ) { - if (currentUsername && currentUsername !== inputUsername) { - try { - const result = await Swal.fire({ - title: "Change Username?", - text: `You already have a running session with the username "${currentUsername}". Do you want to change your username?`, - icon: "question", - iconColor: "#3670F5", - - showCancelButton: true, - confirmButtonColor: "3670F5", - cancelButtonColor: "#d33", - confirmButtonText: "Yes, change it!", - }); - - if (result.isConfirmed && inputUsername.length < 20) { - onConfirm(); - return true; - } else { - if (inputUsername.length > 20) { - Swal.fire({ - title: "Username too long", - text: `Please choose a username with less than 20 characters`, - icon: "warning", - - iconColor: "#3670F5", - confirmButtonColor: "3670F5", - confirmButtonText: "OK", - didOpen: (popup) => { - popup.style.borderRadius = "1rem"; - }, - }); - } - return false; - } - } catch (error) { - console.error("Error with SweetAlert2:", error); - return false; - } - } - return false; - } \ No newline at end of file diff --git a/frontend/utils/alerts/checkAndPromptSessionChange.ts b/frontend/utils/alerts/checkAndPromptSessionChange.ts new file mode 100644 index 0000000..05a8a7b --- /dev/null +++ b/frontend/utils/alerts/checkAndPromptSessionChange.ts @@ -0,0 +1,58 @@ +"use client"; +import Swal, { SweetAlertResult } from "sweetalert2"; + +// Interface for the function parameters +interface CheckAndPromptSessionChangeProps { + currentUser: string | null; + username: string | null; + onConfirm: () => void; +} + +// Function to check and prompt session change using SweetAlert2 +export default async function checkAndPromptSessionChange({ + currentUser, + username, + onConfirm, +}: CheckAndPromptSessionChangeProps): Promise { + // Check if the current username exists and differs from the input username + if (currentUser && currentUser !== username) { + try { + const result: SweetAlertResult = await Swal.fire({ + title: "Change Username?", + text: `You already have a running session with the username "${currentUser}". Do you want to change your username?`, + icon: "question", + iconColor: "#3670F5", + showCancelButton: true, + confirmButtonColor: "#3670F5", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, change it!", + }); + + // If the user confirms and the new username is less than 20 characters + if (result.isConfirmed && username.length < 20) { + onConfirm(); + return true; + } else { + // If the input username exceeds the length limit, display a warning + if (username.length > 20) { + Swal.fire({ + title: "Username too long", + text: `Please choose a username with less than 20 characters`, + icon: "warning", + iconColor: "#3670F5", + confirmButtonColor: "#3670F5", + confirmButtonText: "OK", + didOpen: (popup: HTMLElement) => { + popup.style.borderRadius = "1rem"; + }, + }); + } + return false; + } + } catch (error) { + console.error("Error with SweetAlert2:", error); + return false; + } + } + return false; +} diff --git a/frontend/utils/chatbot_formatting/formatChatbotUserText.js b/frontend/utils/chatbot_formatting/formatChatbotUserText.ts similarity index 50% rename from frontend/utils/chatbot_formatting/formatChatbotUserText.js rename to frontend/utils/chatbot_formatting/formatChatbotUserText.ts index 93fb920..6d6e5de 100644 --- a/frontend/utils/chatbot_formatting/formatChatbotUserText.js +++ b/frontend/utils/chatbot_formatting/formatChatbotUserText.ts @@ -1,4 +1,4 @@ -export default function formatChatbotUserText(message) { +export default function formatChatbotUserText(message: string): string | undefined { if (message.split(":")[0] === "You") { return message.split(":")[1]; } diff --git a/frontend/utils/chatbot_formatting/getIsSentForChatBot.js b/frontend/utils/chatbot_formatting/getIsSentForChatBot.ts similarity index 59% rename from frontend/utils/chatbot_formatting/getIsSentForChatBot.js rename to frontend/utils/chatbot_formatting/getIsSentForChatBot.ts index 41ec026..e57ddbc 100644 --- a/frontend/utils/chatbot_formatting/getIsSentForChatBot.js +++ b/frontend/utils/chatbot_formatting/getIsSentForChatBot.ts @@ -1,4 +1,4 @@ -export default function getIsSentForChatBot(message) { +export default function getIsSentForChatBot(message: string): boolean { if (message.split(":")[0] === "You") { return true; } else { diff --git a/frontend/utils/chatbot_formatting/parseMessageText.jsx b/frontend/utils/chatbot_formatting/parseMessageText.tsx similarity index 73% rename from frontend/utils/chatbot_formatting/parseMessageText.jsx rename to frontend/utils/chatbot_formatting/parseMessageText.tsx index 5b64f54..0235889 100644 --- a/frontend/utils/chatbot_formatting/parseMessageText.jsx +++ b/frontend/utils/chatbot_formatting/parseMessageText.tsx @@ -1,17 +1,18 @@ "use client"; +import React from "react"; -export default function parseMessageText(text) { - const replaceURLs = (message) => { +export default function parseMessageText(text: string): JSX.Element { + const replaceURLs = (message: string): string => { const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi; return message.replace(urlRegex, (url) => `${url}`); }; - const replaceBoldText = (message) => { + const replaceBoldText = (message: string): string => { return message.replace(/\*\*(.*?)\*\*/g, "$1"); }; - const replaceCodeText = (message) => { + const replaceCodeText = (message: string): string => { return message.replace( /`(.*?)`/g, '$1', diff --git a/frontend/utils/getTimestampFromDate.js b/frontend/utils/getTimestampFromDate.js deleted file mode 100644 index 67afced..0000000 --- a/frontend/utils/getTimestampFromDate.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function getTimestampFromDate(dateString) { - const date = new Date(dateString); - const timestamp = Math.floor(date.getTime() / 1000); - return timestamp; - } \ No newline at end of file diff --git a/frontend/utils/getTimestampFromDate.ts b/frontend/utils/getTimestampFromDate.ts new file mode 100644 index 0000000..e46f260 --- /dev/null +++ b/frontend/utils/getTimestampFromDate.ts @@ -0,0 +1,5 @@ +export default function getTimestampFromDate(dateString: string): number { + const date = new Date(dateString); + const timestamp = Math.floor(date.getTime() / 1000); + return timestamp; +} diff --git a/frontend/utils/playSound.js b/frontend/utils/playSound.ts similarity index 69% rename from frontend/utils/playSound.js rename to frontend/utils/playSound.ts index fba1100..08bb7d8 100644 --- a/frontend/utils/playSound.js +++ b/frontend/utils/playSound.ts @@ -2,13 +2,14 @@ import notif from "../assets/sounds/notif.mp3"; import notifRecieve from "../assets/sounds/notif-recieve.mp3"; import { useCallback } from "react"; -const playSound = (soundEnabled)=>{ - useCallback( - (isSent) => { +const playSound = (soundEnabled: boolean): void => { + useCallback( + (isSent: boolean) => { const sound = isSent ? new Audio(notif) : new Audio(notifRecieve); sound.play(); }, [soundEnabled], ); } -export default playSound; \ No newline at end of file + +export default playSound; diff --git a/frontend/utils/session/getAvatar.js b/frontend/utils/session/getAvatar.js deleted file mode 100644 index 3590906..0000000 --- a/frontend/utils/session/getAvatar.js +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -export default async function getAvatar() { - let avatarId = sessionStorage.getItem("avatarId"); - if (avatarId == null || isNaN(avatarId) || avatarId < 0 || avatarId > 14) { - avatarId = Math.floor(Math.random() * 15); - sessionStorage.setItem("avatarId", avatarId); - } - return avatarId; - } \ No newline at end of file diff --git a/frontend/utils/session/getAvatar.ts b/frontend/utils/session/getAvatar.ts new file mode 100644 index 0000000..74b6e57 --- /dev/null +++ b/frontend/utils/session/getAvatar.ts @@ -0,0 +1,15 @@ +"use client"; + +export default async function getAvatar() { + + let avatarIdString = sessionStorage.getItem("avatarId"); + let avatarId: number; + + if (avatarIdString === null || isNaN(Number(avatarIdString)) || Number(avatarIdString) < 0 || Number(avatarIdString) > 14) { + avatarId = Math.floor(Math.random() * 15); + sessionStorage.setItem("avatarId", avatarId.toString()); + } else { + avatarId = Number(avatarIdString); + } + return avatarId; + } \ No newline at end of file diff --git a/frontend/utils/session/getSessionUser.js b/frontend/utils/session/getSessionUser.ts similarity index 100% rename from frontend/utils/session/getSessionUser.js rename to frontend/utils/session/getSessionUser.ts diff --git a/frontend/utils/session/getSessionUserId.js b/frontend/utils/session/getSessionUserId.ts similarity index 100% rename from frontend/utils/session/getSessionUserId.js rename to frontend/utils/session/getSessionUserId.ts diff --git a/frontend/utils/session/removeSessionUserId.js b/frontend/utils/session/removeSessionUserId.ts similarity index 52% rename from frontend/utils/session/removeSessionUserId.js rename to frontend/utils/session/removeSessionUserId.ts index 24c9730..517de64 100644 --- a/frontend/utils/session/removeSessionUserId.js +++ b/frontend/utils/session/removeSessionUserId.ts @@ -1,5 +1,5 @@ "use client"; -export default function removeSessionUserId() { +export default function removeSessionUserId():void { sessionStorage.removeItem("userID"); } \ No newline at end of file diff --git a/frontend/utils/session/setSessionUser.js b/frontend/utils/session/setSessionUser.ts similarity index 100% rename from frontend/utils/session/setSessionUser.js rename to frontend/utils/session/setSessionUser.ts diff --git a/frontend/utils/websocket/handleWebSocketClose.js b/frontend/utils/websocket/handleWebSocketClose.js deleted file mode 100644 index 2880c73..0000000 --- a/frontend/utils/websocket/handleWebSocketClose.js +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; -import { - isUserBanned, - isSameUsername, - isBadRequest, - isServerError, - isAbnormalClose, - } from "./websocketHelpers"; -import alertBannedUser from "../alerts/alertBannedUser"; -import alertSameUsername from "../alerts/alertSameUsername"; -import alertBadRequest from "../alerts/alertBadRequest"; -import alertServerError from "../alerts/alertServerError"; -import alertAbnormalClose from "../alerts/alertAbnormalClose"; - -export default function handleWebSocketClose(event, navigateToLogin) { - if (isUserBanned(event.code)) { - alertBannedUser(event.reason, navigateToLogin); - } - if (isSameUsername(event.code)) { - alertSameUsername(event.reason, navigateToLogin); - } - if (isBadRequest(event.code)) { - alertBadRequest(event.reason, navigateToLogin); - } - if (isServerError(event.code)) { - alertServerError(event.reason, navigateToLogin); - } - if (isAbnormalClose(event.code)) { - alertAbnormalClose(event.reason, navigateToLogin); - } - } \ No newline at end of file diff --git a/frontend/utils/websocket/handleWebSocketClose.ts b/frontend/utils/websocket/handleWebSocketClose.ts new file mode 100644 index 0000000..bd85615 --- /dev/null +++ b/frontend/utils/websocket/handleWebSocketClose.ts @@ -0,0 +1,49 @@ +"use client"; +import { + isUserBanned, + isSameUsername, + isBadRequest, + isServerError, + isAbnormalClose, +} from "./websocketHelpers"; +import alertBannedUser from "../alerts/alertBannedUser"; +import alertSameUsername from "../alerts/alertSameUsername"; // Ensure all alert imports are correctly defined +import alertBadRequest from "../alerts/alertBadRequest"; +import alertServerError from "../alerts/alertServerError"; +import alertAbnormalClose from "../alerts/alertAbnormalClose"; +import {useNavigate} from "react-router-dom"; +// Define the type for the WebSocket close event +interface WebSocketCloseEvent { + code: number; + reason: string; +} + + +export function navigateToLogin(): void { + const navigate = useNavigate(); // Import the useNavigate hook from react-router-dom + navigate("/"); // Adjust the path as needed +} + +// Define the type for the navigate function +type NavigateFunction = (path: string) => void; + +export default function handleWebSocketClose( + event: WebSocketCloseEvent, + navigateToLogin: NavigateFunction +): void { + if (isUserBanned(event.code)) { + alertBannedUser(event.reason, navigateToLogin); + } + if (isSameUsername(event.code)) { + alertSameUsername(event.reason, navigateToLogin); + } + if (isBadRequest(event.code)) { + alertBadRequest(event.reason, navigateToLogin); + } + if (isServerError(event.code)) { + alertServerError(event.reason, navigateToLogin); + } + if (isAbnormalClose(event.code)) { + alertAbnormalClose(event.reason, navigateToLogin); + } +} diff --git a/frontend/utils/websocket/handleWebSocketError.js b/frontend/utils/websocket/handleWebSocketError.ts similarity index 100% rename from frontend/utils/websocket/handleWebSocketError.js rename to frontend/utils/websocket/handleWebSocketError.ts diff --git a/frontend/utils/websocket/processWebSocketMessage.js b/frontend/utils/websocket/processWebSocketMessage.ts similarity index 53% rename from frontend/utils/websocket/processWebSocketMessage.js rename to frontend/utils/websocket/processWebSocketMessage.ts index 5438017..5938093 100644 --- a/frontend/utils/websocket/processWebSocketMessage.js +++ b/frontend/utils/websocket/processWebSocketMessage.ts @@ -1,14 +1,11 @@ "use client"; +import { DataFromServer, Message, ProcessWebSocketMessageProps } from "../../interface/interface"; import alertBannedUser from "../alerts/alertBannedUser"; export default function processWebSocketMessage( - event, - setMessages, - navigateToLogin, - isChatbot, - ) { +{ event, setMessages, navigateToLogin, isChatbot }: ProcessWebSocketMessageProps) { if (isChatbot) { - const userIdRegex = /\buserID\b/; + const userIdRegex = /\buserID\b/; if (userIdRegex.test(event.data)) { handleUserID(JSON.parse(event.data)); } @@ -36,25 +33,23 @@ export default function processWebSocketMessage( console.error("Error parsing or handling the message:", error); } - function handleUserID(data) { + function handleUserID(data: DataFromServer) { if (data.userID) { - sessionStorage.setItem("userID", data.userID); + sessionStorage.setItem("userID", data.userID); } - } - - function handleBannedUser(data, navigateToLogin) { + } + + function handleBannedUser(data: DataFromServer, navigateToLogin: () => void) { if (data.Message && data.Message === "You are banned now") { - alertBannedUser(data.Message); - navigateToLogin(); - return true; + alertBannedUser(data.Message,navigateToLogin); + navigateToLogin(); + return true; } return false; - } - - function handleDeleteMessage(data, setMessages) { - const deleteTimestamp = parseFloat(data.Delete); - setMessages((prevMessages) => - prevMessages.filter((message) => message.timestamp !== deleteTimestamp), - ); - } - } \ No newline at end of file + } + + function handleDeleteMessage(data: DataFromServer, setMessages: React.Dispatch>) { + const deleteTimestamp = parseFloat(data.Delete!); + setMessages((prevMessages) => prevMessages.filter((message) => message.timestamp !== deleteTimestamp)); + } +} \ No newline at end of file diff --git a/frontend/utils/websocket/websocketHelpers.js b/frontend/utils/websocket/websocketHelpers.ts similarity index 100% rename from frontend/utils/websocket/websocketHelpers.js rename to frontend/utils/websocket/websocketHelpers.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..10a3adc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,42 @@ +{ + "name": "echofy", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "react": "^18.3.1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e1f4bc9 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "react": "^18.3.1" + } +} diff --git a/semantic.yml b/semantic.yml new file mode 100644 index 0000000..e69de29