diff --git a/frontend/public/style.css b/frontend/public/style.css
index 7d87f2e..05b4ee4 100644
--- a/frontend/public/style.css
+++ b/frontend/public/style.css
@@ -503,6 +503,29 @@ header svg {
color: var(--mp-panel-accent);
}
+.leaveRoomBtn {
+ border-radius: 8px;
+ border: 1px solid var(--mp-panel-border);
+ background: var(--mp-panel-input-bg);
+ color: var(--font-color);
+ font-size: 0.65rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ padding: 0.35rem 0.65rem;
+ cursor: pointer;
+ opacity: 0.65;
+ transition: 150ms ease;
+}
+
+.leaveRoomBtn:hover {
+ opacity: 1;
+ border-color: var(--mp-panel-accent);
+}
+
+.leaveRoomBtn:active {
+ transform: translate(-0.5px, 1px);
+}
+
.roomSettings {
display: flex;
flex-direction: column;
@@ -1177,12 +1200,14 @@ footer {
height: 250px;
background-color: inherit;
color: inherit;
+ overflow: hidden;
}
.stats .bg {
width: 100%;
position: absolute;
z-index: 0;
+ height: 100%;
}
.statsText {
diff --git a/frontend/src/firebase/auth.js b/frontend/src/firebase/auth.js
index 158fd39..70ef45b 100644
--- a/frontend/src/firebase/auth.js
+++ b/frontend/src/firebase/auth.js
@@ -31,8 +31,10 @@ if (isDevelopment)
export const signIn = async ({ email, password }) => {
try {
const userCred = await signInWithEmailAndPassword(auth, email, password);
+ return { ok: true, userCred };
} catch (error) {
alert(error.code);
+ return { ok: false, error };
}
};
@@ -59,8 +61,10 @@ export const signUp = async ({ email, password, username }) => {
};
setUserData(data);
});
+ return { ok: true, userCred };
} catch (error) {
alert(error.code);
+ return { ok: false, error };
}
};
@@ -76,15 +80,30 @@ export const logout = () => {
export const authState = () => {
onAuthStateChanged(auth, async (user) => {
if (user) {
- user = await getUserData();
- if (user && user.wordCount == null) {
- const updatedUser = { ...user, wordCount: DEFAULT_WORD_COUNT };
+ let userData = await getUserData();
+ if (!userData) {
+ const displayName =
+ user.displayName ||
+ (user.email ? user.email.split("@")[0] : "player");
+ userData = {
+ uId: user.uid,
+ userName: displayName,
+ topSpeed: 0,
+ theme: defaultTheme,
+ punctuationMode,
+ smallCaseMode,
+ wordCount: DEFAULT_WORD_COUNT,
+ lastSpeed: 0,
+ };
+ setUserData(userData);
+ } else if (userData.wordCount == null) {
+ const updatedUser = { ...userData, wordCount: DEFAULT_WORD_COUNT };
setUserData(updatedUser);
- user = updatedUser;
+ userData = updatedUser;
}
- handleMenu(user);
- handleStats(user);
- username.innerText = user.userName;
+ handleMenu(userData);
+ handleStats(userData);
+ username.innerText = userData.userName || "";
} else {
handleMenu(user);
handleStats(user);
diff --git a/frontend/src/socket/roomHandling.js b/frontend/src/socket/roomHandling.js
index 7465c6a..3fa038f 100644
--- a/frontend/src/socket/roomHandling.js
+++ b/frontend/src/socket/roomHandling.js
@@ -36,7 +36,7 @@ export async function createRoom() {
setIsHost(true);
}
-export function renderPlayers(playerList) {
+export function renderPlayers(playerList, winnerId = null) {
const list = Array.isArray(playerList) ? playerList : [];
if (list.length === 0) {
clearPlayerArea();
@@ -46,7 +46,7 @@ export function renderPlayers(playerList) {
return;
}
- const winnerKey = getWinnerKey(list);
+ const winnerKey = getWinnerKey(list, winnerId);
renderTrackPlayers(list, winnerKey);
renderLobbyPlayers(list, winnerKey);
announceWinnerIfAny(list, winnerKey);
@@ -77,14 +77,17 @@ function renderTrackPlayers(playerList, winnerKey) {
}
row.innerHTML =
'
';
requestAnimationFrame(() => {
row.classList.remove("is-new");
});
}
- row.classList.toggle("is-winner", key === winnerKey);
+ if (key === winnerKey) {
+ row.classList.add("is-winner");
+ } else {
+ row.classList.remove("is-winner");
+ }
const nameContainer = row.querySelector(".playerName");
const nameEl = row.querySelector(".playerNameText");
@@ -99,6 +102,7 @@ function renderTrackPlayers(playerList, winnerKey) {
const progress = Number(player.progress) || 0;
if (nameContainer) {
+ syncCrown(nameContainer, key === winnerKey);
nameContainer.style.left = `${progress}%`;
nameContainer.style.transform = `translateX(-${progress}%)`;
}
@@ -146,7 +150,7 @@ function renderLobbyPlayers(playerList, winnerKey) {
@@ -165,13 +169,21 @@ function renderLobbyPlayers(playerList, winnerKey) {
const speed =
player.speed === undefined || player.speed === null ? 0 : player.speed;
- row.classList.toggle("is-winner", key === winnerKey);
+ if (key === winnerKey) {
+ row.classList.add("is-winner");
+ } else {
+ row.classList.remove("is-winner");
+ }
const nameEl = row.querySelector(".lobbyPlayerNameText");
+ const nameContainer = row.querySelector(".lobbyPlayerName");
if (nameEl) {
nameEl.textContent = name;
} else {
row.querySelector(".lobbyPlayerName").textContent = name;
}
+ if (nameContainer) {
+ syncCrown(nameContainer, key === winnerKey);
+ }
row.querySelector(".lobbyPlayerAvatar").textContent = getInitials(name);
row.querySelector(".lobbyPlayerTag").textContent = getPlayerTag(player, key);
row.querySelector(".lobbyPlayerSpeed").textContent = `Speed: ${speed}`;
@@ -199,8 +211,9 @@ function clearLobbyPlayers() {
}
function getPlayerKey(player, index) {
- const keyBase = player.id || player.name || `player-${index}`;
- return String(keyBase);
+ if (player && player.id) return String(player.id);
+ const namePart = player && player.name ? String(player.name) : "player";
+ return `${namePart}-${index}`;
}
function getPlayerName(player, index) {
@@ -221,6 +234,18 @@ function getPlayerTag(player, fallbackKey) {
return `#${String(fallbackKey).slice(0, 4)}`;
}
+function syncCrown(container, isWinner) {
+ if (!container) return;
+ const existing = container.querySelector(".playerCrown");
+ if (isWinner) {
+ if (!existing) {
+ container.insertAdjacentHTML("afterbegin", crownSvg);
+ }
+ } else if (existing) {
+ existing.remove();
+ }
+}
+
function announceWinnerIfAny(playerList, winnerKey) {
if (!winnerKey || winnerAnnounced) return;
@@ -237,12 +262,20 @@ function announceWinnerIfAny(playerList, winnerKey) {
handlePopup(`${getPlayerName(winner, winnerIndex)} wins!`, 2500);
}
-function getWinnerKey(playerList) {
- const winnerIndex = playerList.findIndex(
+function getWinnerKey(playerList, winnerId) {
+ if (winnerId) {
+ const winnerIndex = playerList.findIndex(
+ (player) => player && player.id === winnerId
+ );
+ if (winnerIndex !== -1) {
+ return getPlayerKey(playerList[winnerIndex], winnerIndex);
+ }
+ }
+ const fallbackIndex = playerList.findIndex(
(player) => (Number(player.progress) || 0) >= 100
);
- if (winnerIndex === -1) return null;
- return getPlayerKey(playerList[winnerIndex], winnerIndex);
+ if (fallbackIndex === -1) return null;
+ return getPlayerKey(playerList[fallbackIndex], fallbackIndex);
}
export function resetWinnerState() {
diff --git a/frontend/src/socket/socket.js b/frontend/src/socket/socket.js
index 1a56df3..86fa9fa 100644
--- a/frontend/src/socket/socket.js
+++ b/frontend/src/socket/socket.js
@@ -10,6 +10,8 @@ import {
roomSettingsList,
roomHeader,
textContainer,
+ lobbyPlayersWrap,
+ roomId,
} from "../ui/uiElements";
import {
isHost,
@@ -40,6 +42,7 @@ socket.on("connect", () => {
});
socket.on("disconnect", () => {
+ resetLobbyUI();
setIsHost(false);
setMultiplayerMode(false);
console.log("you are disconnected");
@@ -75,15 +78,20 @@ socket.on("unknownCode", () => {
socket.on("tooManyPlayers", () => {
handlePopup("room is full 😞", 1000);
});
+socket.on("leftRoom", () => {
+ resetLobbyUI();
+});
let you, showTimer, newTextTimeout;
-socket.on("playerList", (playerList) => {
+socket.on("playerList", (payload) => {
+ const playerList = Array.isArray(payload) ? payload : payload.players || [];
+ const winnerId = Array.isArray(payload) ? null : payload.winnerId || null;
let index = playerList.findIndex((user) => user.id == socket.id);
if (index !== -1) {
you = playerList[index];
}
- renderPlayers(playerList);
+ renderPlayers(playerList, winnerId);
});
socket.on("newText", (data) => {
@@ -151,6 +159,20 @@ function closeMpArea() {
}, 500);
}
+function resetLobbyUI() {
+ setIsHost(false);
+ setMultiplayerMode(false);
+ resetWinnerState();
+ renderPlayers([]);
+ if (joinRoomForm) joinRoomForm.classList.remove("hide");
+ if (lobbyStats) lobbyStats.classList.remove("hide");
+ if (roomHeader) roomHeader.classList.add("hide");
+ if (roomSettings) roomSettings.classList.add("hide");
+ if (roomSettingsList) roomSettingsList.innerHTML = "";
+ if (lobbyPlayersWrap) lobbyPlayersWrap.classList.add("hide");
+ if (roomId) roomId.textContent = "";
+}
+
function renderRoomSettings(settings) {
if (!roomSettings || !roomSettingsList) return;
const chips = [];
@@ -177,4 +199,9 @@ function renderRoomSettings(settings) {
}
}
+export function leaveRoom() {
+ resetLobbyUI();
+ socket.emit("leaveRoom");
+}
+
export default socket;
diff --git a/frontend/src/ui/uiElements.js b/frontend/src/ui/uiElements.js
index 88dbda3..ccf8781 100644
--- a/frontend/src/ui/uiElements.js
+++ b/frontend/src/ui/uiElements.js
@@ -49,6 +49,7 @@ export const loginBtn = document.querySelector(".loginBtn");
export const signinForm = document.querySelector(".profile>.routs>.signIn");
export const updateBtn = document.querySelector(".updateBtn");
export const updateForm = document.querySelector(".profile>.routs>.update");
+export const updateCancel = document.querySelector(".updateCancel");
//logout
export const logoutBtn = document.querySelector(".logoutBtn");
@@ -90,6 +91,7 @@ export const playersCount = document.querySelector(".playersCount");
export const roomHeader = document.querySelector(".roomHeader");
export const copyRoomId = document.querySelector(".copyRoomId");
export const copyRoomLink = document.querySelector(".copyRoomLink");
+export const leaveRoomBtn = document.querySelector(".leaveRoomBtn");
export const roomId = document.querySelector("span.roomId");
export const mpClose = document.querySelector(".mpClose");
export const lobbyPlayersWrap = document.querySelector(".lobbyPlayersWrap");
diff --git a/frontend/src/ui/uiListeners.js b/frontend/src/ui/uiListeners.js
index 72ded70..0dd5307 100644
--- a/frontend/src/ui/uiListeners.js
+++ b/frontend/src/ui/uiListeners.js
@@ -26,6 +26,7 @@ import {
updateBtn,
signUpinfo,
loader,
+ updateCancel,
leaderBoardBtn,
leaderBoard,
updateForm,
@@ -40,6 +41,7 @@ import {
copyRoomLink,
roomId,
mpClose,
+ leaveRoomBtn,
} from "./uiElements";
import saveStats from "../functions/saveStats";
import { logout, signIn, signUp, updateUser } from "../firebase/auth";
@@ -51,6 +53,7 @@ import {
normalizeWordCount,
} from "../functions/userDefault";
import { createRoom, joinRoom } from "../socket/roomHandling";
+import { leaveRoom } from "../socket/socket";
import handlePopup from "../functions/handlePopup";
import { multiplayerMode } from "../functions/userDefault";
import { handleInput } from "../functions/start";
@@ -233,20 +236,29 @@ saveStatsBtn.addEventListener("click", () => {
});
//signupform and signIn form handler
-signupForm.addEventListener("submit", (e) => {
+signupForm.addEventListener("submit", async (e) => {
e.preventDefault();
- signUp({
+ handleProfile(loader);
+ const result = await signUp({
email: e.target.email.value,
username: e.target.username.value,
password: e.target.password.value,
});
- handleProfile(loader);
+ if (!result || !result.ok) {
+ handleProfile(signupForm);
+ }
});
-signinForm.addEventListener("submit", (e) => {
+signinForm.addEventListener("submit", async (e) => {
e.preventDefault();
- signIn({ email: e.target.email.value, password: e.target.password.value });
handleProfile(loader);
+ const result = await signIn({
+ email: e.target.email.value,
+ password: e.target.password.value,
+ });
+ if (!result || !result.ok) {
+ handleProfile(signinForm);
+ }
});
// to show signup and login window
@@ -268,6 +280,12 @@ updateBtn.addEventListener("click", () => {
handleProfile(updateForm);
});
+if (updateCancel) {
+ updateCancel.addEventListener("click", () => {
+ handleProfile(stats);
+ });
+}
+
updateForm.addEventListener("submit", (e) => {
e.preventDefault();
let username = e.target.username.value;
@@ -376,6 +394,12 @@ if (mpClose) {
});
}
+if (leaveRoomBtn) {
+ leaveRoomBtn.addEventListener("click", () => {
+ leaveRoom();
+ });
+}
+
//create room
createRoomBtn.addEventListener("click", (e) => {
createRoom();