From 674943c6c1dd7a4f0c3dff5f7d968debcc0631ea Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 22 Jul 2026 13:17:20 -0300 Subject: [PATCH 1/4] refactor: uses use callback hook to prevent the creation of the handlers functions --- backend/package-lock.json | 1 + frontend/package-lock.json | 34 +++++++++++++-------------------- frontend/src/pages/HomePage.tsx | 19 +++++++++--------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 26803fa..3795eae 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1012,6 +1012,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 45e6747..47c76f6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -64,6 +64,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -273,27 +274,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1118,6 +1098,7 @@ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -1128,6 +1109,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1187,6 +1169,7 @@ "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/types": "8.63.0", @@ -1417,6 +1400,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1537,6 +1521,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", @@ -1804,6 +1789,7 @@ "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -2880,6 +2866,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2949,6 +2936,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2958,6 +2946,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3179,6 +3168,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3264,6 +3254,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3417,6 +3408,7 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 6df6d0e..8331fd3 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { SyncLoader } from "react-spinners"; @@ -51,7 +51,7 @@ const HomePage = () => { fetchRooms(); }, []); - const handleCreateRoom = async (e: React.FormEvent) => { + const handleCreateRoom = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!clientId) { @@ -66,9 +66,9 @@ const HomePage = () => { setRoomName(""); await loadRooms(clientId); } - }; + }, [clientId, createRoom, roomName, loadRooms]); - const handleJoinCode = async (e: React.FormEvent) => { + const handleJoinCode = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!clientId) { @@ -85,9 +85,9 @@ const HomePage = () => { } setJoinCode(""); - }; + }, [clientId, joinRoom, joinCode, loadRooms]); - const handleEnterRoom = async (e: React.FormEvent) => { + const handleEnterRoom = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!clientId) { @@ -107,9 +107,9 @@ const HomePage = () => { setShowUsername(false); navigate(`/room/${result._id}`); } - }; + }, [clientId, selectedRoomId, enterRoom, username, navigate]); - const handleEditUsername = async (e: React.FormEvent) => { + const handleEditUsername = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!clientId) { @@ -123,7 +123,8 @@ const HomePage = () => { setShowEditUsername(false); navigate(`/`); } - }; + }, [clientId, editUsername, navigate]); + return (
From d0ac98bb6e9cc37afd0813df67580a903831cdc0 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 22 Jul 2026 13:19:37 -0300 Subject: [PATCH 2/4] fix: fixes use effect hooks dependencies --- frontend/src/pages/HomePage.tsx | 2 +- frontend/src/pages/RoomPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 8331fd3..40247df 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -49,7 +49,7 @@ const HomePage = () => { }; fetchRooms(); - }, []); + }, [clientId, loadRooms]); const handleCreateRoom = useCallback(async (e: React.FormEvent) => { e.preventDefault(); diff --git a/frontend/src/pages/RoomPage.tsx b/frontend/src/pages/RoomPage.tsx index b6d563e..83c6fd9 100644 --- a/frontend/src/pages/RoomPage.tsx +++ b/frontend/src/pages/RoomPage.tsx @@ -52,7 +52,7 @@ const RoomPage = () => { }; fetchMessages(); - }, [roomId]); + }, [roomId, loadMessages]); // 1. Start listening first useEffect(() => { @@ -101,7 +101,7 @@ const RoomPage = () => { }; fetchRoomData(); - }, []); + }, [clientId, loadCurrentRoom, roomId]); return (
From a4987e1c6adf785d3b0df9eb7fcfa9ece1dba702 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Jul 2026 12:49:18 -0300 Subject: [PATCH 3/4] refactor: divides homepage to prevent its children to dont rerender --- frontend/src/pages/HomePage.tsx | 353 ------------------ frontend/src/pages/HomePage/Header.tsx | 55 +++ frontend/src/pages/HomePage/RoomsList.tsx | 63 ++++ frontend/src/pages/HomePage/index.tsx | 56 +++ .../pages/HomePage/modals/CreateRoomModal.tsx | 72 ++++ .../HomePage/modals/EditUsernameModal.tsx | 72 ++++ .../pages/HomePage/modals/JoinRoomModal.tsx | 72 ++++ .../pages/HomePage/modals/UsernameModal.tsx | 80 ++++ frontend/src/pages/HomePage/stateSetter.ts | 4 + 9 files changed, 474 insertions(+), 353 deletions(-) delete mode 100644 frontend/src/pages/HomePage.tsx create mode 100644 frontend/src/pages/HomePage/Header.tsx create mode 100644 frontend/src/pages/HomePage/RoomsList.tsx create mode 100644 frontend/src/pages/HomePage/index.tsx create mode 100644 frontend/src/pages/HomePage/modals/CreateRoomModal.tsx create mode 100644 frontend/src/pages/HomePage/modals/EditUsernameModal.tsx create mode 100644 frontend/src/pages/HomePage/modals/JoinRoomModal.tsx create mode 100644 frontend/src/pages/HomePage/modals/UsernameModal.tsx create mode 100644 frontend/src/pages/HomePage/stateSetter.ts diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx deleted file mode 100644 index 40247df..0000000 --- a/frontend/src/pages/HomePage.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { useNavigate } from "react-router"; - -import { SyncLoader } from "react-spinners"; - -import useChatroomStore from "../stores/chatroom.store"; -import useUserStore from "../stores/user.store"; - -const HomePage = () => { - const navigate = useNavigate(); - - const [showCreate, setShowCreate] = useState(false); - const [showJoin, setShowJoin] = useState(false); - const [showEditUsername, setShowEditUsername] = useState(false); - const [showUsername, setShowUsername] = useState(false); - const [selectedRoomId, setSelectedRoomId] = useState(null); - - const { - createRoom, - createChatroomError, - createChatroomLoading, - - loadRooms, - rooms, - joinRoom, - joinRoomLoading, - joinRoomError, - enterRoom, - enterRoomLoading, - enterRoomError, - } = useChatroomStore(); - - const [roomName, setRoomName] = useState(""); - const [joinCode, setJoinCode] = useState(""); - - const [username, setUsername] = useState(""); - const [editUsername, setEditUsername] = useState(""); - - const clientId = localStorage.getItem("clientId"); - - useEffect(() => { - const fetchRooms = async () => { - if (!clientId) { - console.log("ClientId not found"); - return; - } - - await loadRooms(clientId); - }; - - fetchRooms(); - }, [clientId, loadRooms]); - - const handleCreateRoom = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - - if (!clientId) { - console.log("ClientId not found"); - return; - } - - const room = await createRoom(clientId, roomName); - - if (room) { - setShowCreate(false); - setRoomName(""); - await loadRooms(clientId); - } - }, [clientId, createRoom, roomName, loadRooms]); - - const handleJoinCode = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - - if (!clientId) { - console.log("ClientId not found"); - return; - } - - const room = await joinRoom(joinCode, clientId); - - if (room) { - setShowJoin(false); - setJoinCode(""); - await loadRooms(clientId); - } - - setJoinCode(""); - }, [clientId, joinRoom, joinCode, loadRooms]); - - const handleEnterRoom = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - - if (!clientId) { - console.log("ClientId not found"); - return; - } - - if (!selectedRoomId) { - console.log("Room Id not found"); - return; - } - - const result = await enterRoom(clientId, username, selectedRoomId); - - if (result && result !== "USERNAME_REQUIRED") { - setUsername(""); - setShowUsername(false); - navigate(`/room/${result._id}`); - } - }, [clientId, selectedRoomId, enterRoom, username, navigate]); - - const handleEditUsername = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - - if (!clientId) { - console.log("ClientId not found"); - return; - } - - const result = await useUserStore.getState().editUsername(clientId, editUsername); - if (result) { - setEditUsername(""); - setShowEditUsername(false); - navigate(`/`); - } - }, [clientId, editUsername, navigate]); - - return ( -
-
-
-
- - -
-

- Live · Socket.IO -

- -

- Chat rooms -

-
-
- -
- - - - -
-
- -
- {rooms.map((r) => ( -
-
-

{r?.name}

-
- -
- - {r?.joinCode} - - - -
-
- ))} -
-
- - {showCreate && ( -
setShowCreate(false)} - > -
e.stopPropagation()} - onSubmit={(e) => handleCreateRoom(e)} - className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" - > -

Create a new room

- - setRoomName(e.target.value)} - value={roomName} - autoFocus - placeholder="Room name" - className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 text-sm focus:outline-none focus:border-amber-400" - /> - - {createChatroomError && ( -

- {createChatroomError} -

- )} - - -
-
- )} - - {showJoin && ( -
setShowJoin(false)} - > -
e.stopPropagation()} - onSubmit={(e) => handleJoinCode(e)} - className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" - > -

Join a room

- - setJoinCode(e.target.value.toUpperCase())} - value={joinCode} - autoFocus - maxLength={6} - placeholder="Enter code" - className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 font-mono text-lg tracking-[0.2em] text-center uppercase focus:outline-none focus:border-teal-300" - /> - - {joinRoomError && ( -

- {joinRoomError} -

- )} - - -
-
- )} - - {showEditUsername && ( -
setShowEditUsername(false)} - > -
e.stopPropagation()} - onSubmit={(e) => handleEditUsername(e)} - className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" - > -

Enter your new username

- - setEditUsername(e.target.value)} - value={editUsername} - autoFocus - placeholder="Username" - className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 text-sm focus:outline-none focus:border-amber-400" - /> - - -
-
- )} - - {showUsername && ( -
setShowUsername(false)} - > -
e.stopPropagation()} - onSubmit={(e) => handleEnterRoom(e)} - className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" - > -

Enter a username

- - setUsername(e.target.value)} - value={username} - autoFocus - placeholder="Username" - className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 text-sm focus:outline-none focus:border-amber-400" - /> - - {enterRoomError && ( -

- {enterRoomError} -

- )} - - -
-
- )} -
- ); -}; - -export default HomePage; diff --git a/frontend/src/pages/HomePage/Header.tsx b/frontend/src/pages/HomePage/Header.tsx new file mode 100644 index 0000000..89d3f93 --- /dev/null +++ b/frontend/src/pages/HomePage/Header.tsx @@ -0,0 +1,55 @@ +import type { StateSetter } from "./stateSetter"; + +interface HeaderProps { + setShowEditUsername: StateSetter; + setShowJoin: StateSetter; + setShowCreate: StateSetter; +} + +const Header = ({ + setShowEditUsername, + setShowJoin, + setShowCreate, +}: HeaderProps) => { + return ( +
+
+ + +
+

+ Live · Socket.IO +

+ +

+ Chat rooms +

+
+
+ +
+ + + + +
+
+ ); +}; + +export default Header; diff --git a/frontend/src/pages/HomePage/RoomsList.tsx b/frontend/src/pages/HomePage/RoomsList.tsx new file mode 100644 index 0000000..41bf094 --- /dev/null +++ b/frontend/src/pages/HomePage/RoomsList.tsx @@ -0,0 +1,63 @@ +import { useNavigate } from "react-router"; +import type { StateSetter } from "./stateSetter"; +import useChatroomStore from "../../stores/chatroom.store"; + +interface RoomsListProps { + clientId: string | null; + setSelectedRoomId: StateSetter; + setShowUsername: StateSetter; +} + +const RoomsList = ({ + clientId, + setSelectedRoomId, + setShowUsername, +}: RoomsListProps) => { + const navigate = useNavigate(); + + const { rooms, enterRoom } = useChatroomStore(); + + return ( +
+ {rooms.map((r) => ( +
+
+

{r?.name}

+
+ +
+ + {r?.joinCode} + + + +
+
+ ))} +
+ ); +}; + +export default RoomsList; diff --git a/frontend/src/pages/HomePage/index.tsx b/frontend/src/pages/HomePage/index.tsx new file mode 100644 index 0000000..4e8e14f --- /dev/null +++ b/frontend/src/pages/HomePage/index.tsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import Header from "./Header"; +import RoomsList from "./RoomsList"; +import CreateRoomModal from "./modals/CreateRoomModal"; +import JoinRoomModal from "./modals/JoinRoomModal"; +import EditUsernameModal from "./modals/EditUsernameModal"; +import UsernameModal from "./modals/UsernameModal"; + +const HomePage = () => { + const [showCreate, setShowCreate] = useState(false); + const [showJoin, setShowJoin] = useState(false); + const [showEditUsername, setShowEditUsername] = useState(false); + const [showUsername, setShowUsername] = useState(false); + const [selectedRoomId, setSelectedRoomId] = useState(null); + + const clientId = localStorage.getItem("clientId"); + + return ( +
+
+
+ +
+ + {showCreate && ( + + )} + {showJoin && ( + + )} + {showEditUsername && ( + + )} + {showUsername && ( + + )} +
+ ); +}; + +export default HomePage; diff --git a/frontend/src/pages/HomePage/modals/CreateRoomModal.tsx b/frontend/src/pages/HomePage/modals/CreateRoomModal.tsx new file mode 100644 index 0000000..ac45928 --- /dev/null +++ b/frontend/src/pages/HomePage/modals/CreateRoomModal.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import useChatroomStore from "../../../stores/chatroom.store"; +import { SyncLoader } from "react-spinners"; +import type { StateSetter } from "../stateSetter"; + +interface CreateRoomModalProps { + setShowCreate: StateSetter; + clientId: string | null; +} + +const CreateRoomModal = ({ setShowCreate, clientId }: CreateRoomModalProps) => { + const [roomName, setRoomName] = useState(""); + + const { createRoom, loadRooms, createChatroomError, createChatroomLoading } = + useChatroomStore(); + + const handleCreateRoom = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!clientId) { + console.log("ClientId not found"); + return; + } + + const room = await createRoom(clientId, roomName); + + if (room) { + setShowCreate(false); + setRoomName(""); + await loadRooms(clientId); + } + }; + + return ( +
setShowCreate(false)} + > +
e.stopPropagation()} + onSubmit={(e) => handleCreateRoom(e)} + className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" + > +

Create a new room

+ + setRoomName(e.target.value)} + value={roomName} + autoFocus + placeholder="Room name" + className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 text-sm focus:outline-none focus:border-amber-400" + /> + + {createChatroomError && ( +

+ {createChatroomError} +

+ )} + + +
+
+ ); +}; + +export default CreateRoomModal; diff --git a/frontend/src/pages/HomePage/modals/EditUsernameModal.tsx b/frontend/src/pages/HomePage/modals/EditUsernameModal.tsx new file mode 100644 index 0000000..5ea5833 --- /dev/null +++ b/frontend/src/pages/HomePage/modals/EditUsernameModal.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import useUserStore from "../../../stores/user.store"; +import { useNavigate } from "react-router"; +import useChatroomStore from "../../../stores/chatroom.store"; +import { SyncLoader } from "react-spinners"; +import type { StateSetter } from "../stateSetter"; + +interface EditUsernameModalProps { + clientId: string | null; + setShowEditUsername: StateSetter; +} + +const EditUsernameModal = ({ + clientId, + setShowEditUsername, +}: EditUsernameModalProps) => { + const navigate = useNavigate(); + + const [editUsername, setEditUsername] = useState(""); + + const { enterRoomLoading } = useChatroomStore(); + + const handleEditUsername = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!clientId) { + console.log("ClientId not found"); + return; + } + + const result = await useUserStore + .getState() + .editUsername(clientId, editUsername); + if (result) { + setEditUsername(""); + setShowEditUsername(false); + navigate(`/`); + } + }; + + return ( +
setShowEditUsername(false)} + > +
e.stopPropagation()} + onSubmit={(e) => handleEditUsername(e)} + className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" + > +

Enter your new username

+ + setEditUsername(e.target.value)} + value={editUsername} + autoFocus + placeholder="Username" + className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 text-sm focus:outline-none focus:border-amber-400" + /> + + +
+
+ ); +}; + +export default EditUsernameModal; diff --git a/frontend/src/pages/HomePage/modals/JoinRoomModal.tsx b/frontend/src/pages/HomePage/modals/JoinRoomModal.tsx new file mode 100644 index 0000000..75c0cf6 --- /dev/null +++ b/frontend/src/pages/HomePage/modals/JoinRoomModal.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import useChatroomStore from "../../../stores/chatroom.store"; +import type { StateSetter } from "../stateSetter"; +import { SyncLoader } from "react-spinners"; + +interface JoinRoomModal { + clientId: string | null; + setShowJoin: StateSetter; +} + +const JoinRoomModal = ({ clientId, setShowJoin }: JoinRoomModal) => { + const [joinCode, setJoinCode] = useState(""); + + const { joinRoom, loadRooms, joinRoomError, joinRoomLoading } = + useChatroomStore(); + + const handleJoinCode = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!clientId) { + console.log("ClientId not found"); + return; + } + + const room = await joinRoom(joinCode, clientId); + + if (room) { + setShowJoin(false); + setJoinCode(""); + await loadRooms(clientId); + } + + setJoinCode(""); + }; + + return ( +
setShowJoin(false)} + > +
e.stopPropagation()} + onSubmit={(e) => handleJoinCode(e)} + className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" + > +

Join a room

+ + setJoinCode(e.target.value.toUpperCase())} + value={joinCode} + autoFocus + maxLength={6} + placeholder="Enter code" + className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 font-mono text-lg tracking-[0.2em] text-center uppercase focus:outline-none focus:border-teal-300" + /> + + {joinRoomError && ( +

{joinRoomError}

+ )} + + +
+
+ ); +}; + +export default JoinRoomModal; diff --git a/frontend/src/pages/HomePage/modals/UsernameModal.tsx b/frontend/src/pages/HomePage/modals/UsernameModal.tsx new file mode 100644 index 0000000..64ddef2 --- /dev/null +++ b/frontend/src/pages/HomePage/modals/UsernameModal.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import useChatroomStore from "../../../stores/chatroom.store"; +import { useNavigate } from "react-router"; +import { SyncLoader } from "react-spinners"; +import type { StateSetter } from "../stateSetter"; + +interface UsernameModalProps { + clientId: string | null; + selectedRoomId: string | null; + setShowUsername: StateSetter; +} + +const UsernameModal = ({ + clientId, + selectedRoomId, + setShowUsername, +}: UsernameModalProps) => { + const [username, setUsername] = useState(""); + const navigate = useNavigate(); + + const { enterRoom, enterRoomError, enterRoomLoading } = useChatroomStore(); + + const handleEnterRoom = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!clientId) { + console.log("ClientId not found"); + return; + } + + if (!selectedRoomId) { + console.log("Room Id not found"); + return; + } + + const result = await enterRoom(clientId, username, selectedRoomId); + + if (result && result !== "USERNAME_REQUIRED") { + setUsername(""); + setShowUsername(false); + navigate(`/room/${result._id}`); + } + }; + + return ( +
setShowUsername(false)} + > +
e.stopPropagation()} + onSubmit={(e) => handleEnterRoom(e)} + className="bg-neutral-900 border border-neutral-800 rounded-2xl p-7 w-full max-w-sm flex flex-col gap-4" + > +

Enter a username

+ + setUsername(e.target.value)} + value={username} + autoFocus + placeholder="Username" + className="bg-neutral-800 border border-neutral-700 rounded-lg px-3.5 py-3 text-sm focus:outline-none focus:border-amber-400" + /> + + {enterRoomError && ( +

{enterRoomError}

+ )} + + +
+
+ ); +}; + +export default UsernameModal; diff --git a/frontend/src/pages/HomePage/stateSetter.ts b/frontend/src/pages/HomePage/stateSetter.ts new file mode 100644 index 0000000..4f0f688 --- /dev/null +++ b/frontend/src/pages/HomePage/stateSetter.ts @@ -0,0 +1,4 @@ +// This type is an generic for future needs +type StateSetter = React.Dispatch>; + +export { type StateSetter }; From dc3ec708ed827ca7d090c8985c1f71ac9c2acc12 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Jul 2026 13:34:45 -0300 Subject: [PATCH 4/4] refactor: divides roompage to prevent its children to dont rerender --- frontend/src/pages/RoomPage.tsx | 222 ------------------ frontend/src/pages/RoomPage/Header.tsx | 56 +++++ frontend/src/pages/RoomPage/MessagesInput.tsx | 75 ++++++ frontend/src/pages/RoomPage/MessagesList.tsx | 40 ++++ frontend/src/pages/RoomPage/index.tsx | 93 ++++++++ 5 files changed, 264 insertions(+), 222 deletions(-) delete mode 100644 frontend/src/pages/RoomPage.tsx create mode 100644 frontend/src/pages/RoomPage/Header.tsx create mode 100644 frontend/src/pages/RoomPage/MessagesInput.tsx create mode 100644 frontend/src/pages/RoomPage/MessagesList.tsx create mode 100644 frontend/src/pages/RoomPage/index.tsx diff --git a/frontend/src/pages/RoomPage.tsx b/frontend/src/pages/RoomPage.tsx deleted file mode 100644 index 83c6fd9..0000000 --- a/frontend/src/pages/RoomPage.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router"; - -import socket from "../lib/socket"; - -import useChatroomStore from "../stores/chatroom.store"; - -const RoomPage = () => { - console.log("ROOM PAGE RENDERED"); - const { roomId } = useParams(); - const clientId = localStorage.getItem("clientId"); - - const [onlineCount, setOnlineCount] = useState(0); - - const navigate = useNavigate(); - - const { loadCurrentRoom, currentRoom, sendMessage, loadMessages } = - useChatroomStore(); - - const [message, setMessage] = useState(""); - - type Message = { - _id: string; - message: string; - username: string; - userId: string; - }; - - const [messages, setMessages] = useState([]); - - useEffect(() => { - socket.on("new-message", (data) => { - console.log("NEW MESSAGE:", data); - - setMessages((prev) => [...prev, data]); - }); - - return () => { - socket.off("new-message"); - }; - }, []); - - useEffect(() => { - const fetchMessages = async () => { - if (!roomId) return; - - const oldMessages = await loadMessages(roomId); - - if (oldMessages) { - setMessages(oldMessages); - } - }; - - fetchMessages(); - }, [roomId, loadMessages]); - - // 1. Start listening first - useEffect(() => { - socket.on("room-online-updated", (data) => { - console.log("RECEIVED ONLINE UPDATE:", data); - - if (data.roomId === roomId) { - setOnlineCount(data.onlineCount); - } - }); - - return () => { - socket.off("room-online-updated"); - }; - }, [roomId]); - - // 2. Then join the room - useEffect(() => { - if (!roomId || !clientId) return; - - if (!socket.connected) { - socket.connect(); - } - - console.log("JOINING ROOM:", roomId, clientId); - - socket.emit("join-room", { - roomId, - clientId, - }); - - return () => { - socket.emit("leave-room", { - roomId, - clientId, - }); - }; - }, [roomId, clientId]); - - useEffect(() => { - const fetchRoomData = async () => { - if (!roomId) return; - if (!clientId) return; - - await loadCurrentRoom(roomId, clientId); - }; - - fetchRoomData(); - }, [clientId, loadCurrentRoom, roomId]); - - return ( -
- {/* Header */} -
-
-

- Live Chat -

- -

- Room name: {currentRoom?.name} -

-
- -
-
- - {onlineCount} Online -
- - -
-
- - {/* Messages */} -
- {messages.map((msg) => ( -
-

- {msg.username} -

- -

{msg.message}

-
- ))} -
- - {/* Message Input */} -
-
- setMessage(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - sendMessage(roomId!, clientId!, message); - setMessage(""); - } - }} - type="text" - placeholder="Type a message..." - className=" - flex-1 - bg-neutral-900 - border - border-neutral-800 - rounded-xl - px-5 - py-3 - text-neutral-100 - placeholder:text-neutral-500 - focus:outline-none - focus:border-teal-400 - transition - " - /> - - -
-
-
- ); -}; - -export default RoomPage; diff --git a/frontend/src/pages/RoomPage/Header.tsx b/frontend/src/pages/RoomPage/Header.tsx new file mode 100644 index 0000000..e37dbfa --- /dev/null +++ b/frontend/src/pages/RoomPage/Header.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import useChatroomStore from "../../stores/chatroom.store"; +import { useNavigate, useParams } from "react-router"; +import socket from "../../lib/socket"; + +const Header = () => { + const [onlineCount, setOnlineCount] = useState(0); + const navigate = useNavigate(); + const { roomId } = useParams(); + + const { currentRoom } = useChatroomStore(); + + useEffect(() => { + socket.on("room-online-updated", (data) => { + console.log("RECEIVED ONLINE UPDATE:", data); + + if (data.roomId === roomId) { + setOnlineCount(data.onlineCount); + } + }); + + return () => { + socket.off("room-online-updated"); + }; + }, [roomId]); + + return ( +
+
+

+ Live Chat +

+ +

+ Room name: {currentRoom?.name} +

+
+ +
+
+ + {onlineCount} Online +
+ + +
+
+ ); +}; + +export default Header; diff --git a/frontend/src/pages/RoomPage/MessagesInput.tsx b/frontend/src/pages/RoomPage/MessagesInput.tsx new file mode 100644 index 0000000..eea799e --- /dev/null +++ b/frontend/src/pages/RoomPage/MessagesInput.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import useChatroomStore from "../../stores/chatroom.store"; + +interface MessageInputProps { + roomId: string | undefined; + clientId: string | null; +} + +const MessageInput = ({ roomId, clientId }: MessageInputProps) => { + const [message, setMessage] = useState(""); + const { sendMessage } = useChatroomStore(); + + return ( +
+
+ setMessage(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + sendMessage(roomId!, clientId!, message); + setMessage(""); + } + }} + type="text" + placeholder="Type a message..." + className=" + flex-1 + bg-neutral-900 + border + border-neutral-800 + rounded-xl + px-5 + py-3 + text-neutral-100 + placeholder:text-neutral-500 + focus:outline-none + focus:border-teal-400 + transition + " + /> + + +
+
+ ); +}; + +export default MessageInput; diff --git a/frontend/src/pages/RoomPage/MessagesList.tsx b/frontend/src/pages/RoomPage/MessagesList.tsx new file mode 100644 index 0000000..3c29de3 --- /dev/null +++ b/frontend/src/pages/RoomPage/MessagesList.tsx @@ -0,0 +1,40 @@ +type Message = { + _id: string; + message: string; + username: string; + userId: string; +}; + +interface MessagesListProps { + messages: Message[]; + clientId: string | null; +} + +const MessagesList = ({ messages, clientId }: MessagesListProps) => { + return ( +
+ {messages.map((msg) => ( +
+

+ {msg.username} +

+ +

{msg.message}

+
+ ))} +
+ ); +}; + +export default MessagesList; diff --git a/frontend/src/pages/RoomPage/index.tsx b/frontend/src/pages/RoomPage/index.tsx new file mode 100644 index 0000000..c4bd38a --- /dev/null +++ b/frontend/src/pages/RoomPage/index.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from "react"; +import Header from "./Header"; +import MessagesList from "./MessagesList"; +import { useParams } from "react-router"; +import MessageInput from "./MessagesInput"; +import socket from "../../lib/socket"; +import useChatroomStore from "../../stores/chatroom.store"; + +type Message = { + _id: string; + message: string; + username: string; + userId: string; +}; + +const RoomPage = () => { + const [messages, setMessages] = useState([]); + const { roomId } = useParams(); + const { loadMessages, loadCurrentRoom } = useChatroomStore(); + + const clientId = localStorage.getItem("clientId"); + + useEffect(() => { + socket.on("new-message", (data) => { + console.log("NEW MESSAGE:", data); + + setMessages((prev) => [...prev, data]); + }); + + return () => { + socket.off("new-message"); + }; + }, []); + + useEffect(() => { + const fetchMessages = async () => { + if (!roomId) return; + + const oldMessages = await loadMessages(roomId); + + if (oldMessages) { + setMessages(oldMessages); + } + }; + + fetchMessages(); + }, [roomId, loadMessages]); + + useEffect(() => { + if (!roomId || !clientId) return; + + if (!socket.connected) { + socket.connect(); + } + + console.log("JOINING ROOM:", roomId, clientId); + + socket.emit("join-room", { + roomId, + clientId, + }); + + return () => { + socket.emit("leave-room", { + roomId, + clientId, + }); + }; + }, [roomId, clientId]); + + useEffect(() => { + const fetchRoomData = async () => { + if (!roomId) return; + if (!clientId) return; + + await loadCurrentRoom(roomId, clientId); + }; + + fetchRoomData(); + }, [clientId, loadCurrentRoom, roomId]); + + return ( +
+
+ + + + +
+ ); +}; + +export default RoomPage;