Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
resetUser,
setUser,
} from "./user.js";
import { setFinishers } from "./race.js";
import { getWinner, resetFinishers, resetWinner, setFinishers } from "./race.js";

import dotenv from "dotenv";
dotenv.config();
Expand Down Expand Up @@ -111,12 +111,33 @@ io.on("connection", (socket) => {
createRoom(userName, room, settings)
);
socket.on("joinRoom", (userName, room) => joinRoom(userName, room));
socket.on("leaveRoom", () => {
const room = Array.from(socket.rooms).find((id) => id !== socket.id);
removeUser(socket.id);
if (room) {
socket.leave(room);
const remainingUsers = getUsersInRoom(room);
io.to(room).emit("playerList", {
players: remainingUsers,
winnerId: getWinner(room),
});
if (remainingUsers.length === 0) {
roomSettings.delete(room);
resetWinner(room);
}
emitLobbyStats();
}
socket.emit("leftRoom");
});

socket.on("typing", (user) => {
console.log(`[INFO] User ${user.name} typing in room ${user.room} - Progress: ${user.progress}%`);
setUser(user);
const playerList = getUsersInRoom(user.room);
io.to(user.room).emit("playerList", playerList);
io.to(user.room).emit("playerList", {
players: playerList,
winnerId: getWinner(user.room),
});
});

function createRoom(userName, room, settings) {
Expand All @@ -141,16 +162,24 @@ io.on("connection", (socket) => {

addUser(tempUser);
const playerList = getUsersInRoom(roomId);
io.to(roomId).emit("playerList", playerList);
io.to(roomId).emit("playerList", {
players: playerList,
winnerId: getWinner(roomId),
});
emitLobbyStats();
}

socket.on("getText", (room) => {
console.log(`[INFO] Fetching new text for room: ${room}`);
const settings = roomSettings.get(room) || normalizeRoomSettings();
getText(io, room, settings);
resetFinishers(room);
resetWinner(room);
const playerList = resetUser(room);
io.to(room).emit("playerList", playerList);
io.to(room).emit("playerList", {
players: playerList,
winnerId: getWinner(room),
});
});

function joinRoom(userName, roomName) {
Expand Down Expand Up @@ -191,7 +220,10 @@ io.on("connection", (socket) => {
};
addUser(tempUser);
const playerList = getUsersInRoom(roomName);
io.to(roomName).emit("playerList", playerList);
io.to(roomName).emit("playerList", {
players: playerList,
winnerId: getWinner(roomName),
});
emitLobbyStats();

if (numClients === 1) {
Expand All @@ -207,16 +239,25 @@ io.on("connection", (socket) => {
socket.on("finished", (user) => {
console.log(`[INFO] User ${user.name} finished race in room ${user.room} - Speed: ${user.speed} WPM`);
setFinishers(user, io);
const playerList = getUsersInRoom(user.room);
io.to(user.room).emit("playerList", {
players: playerList,
winnerId: getWinner(user.room),
});
});

socket.on("disconnect", () => {
const user = removeUser(socket.id);
if (user) {
console.log(`[INFO] User ${user.name} disconnected from room: ${user.room}`);
const remainingUsers = getUsersInRoom(user.room);
io.to(user.room).emit("playerList", remainingUsers);
io.to(user.room).emit("playerList", {
players: remainingUsers,
winnerId: getWinner(user.room),
});
if (remainingUsers.length === 0) {
roomSettings.delete(user.room);
resetWinner(user.room);
}
emitLobbyStats();
} else {
Expand Down
13 changes: 13 additions & 0 deletions backend/race.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { getUsersInRoom } from "./user.js";

const finishers = new Map();
const winners = new Map();

export function setFinishers(user, io) {
let players = finishers.get(user.room);
if (players) players.push(user);
else players = new Array(user);

if (!winners.has(user.room)) {
winners.set(user.room, user.id);
}

let totalUsers = getUsersInRoom(user.room);
console.log(`[INFO] Finisher count: ${players.length}/${totalUsers.length} in room ${user.room}`);

Expand All @@ -33,6 +38,14 @@ export function getFinishers(room) {
return finishers.get(room);
}

export function getWinner(room) {
return winners.get(room) || null;
}

export function resetFinishers(room) {
finishers.set(room, []);
}

export function resetWinner(room) {
winners.delete(room);
}
4 changes: 4 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
<form class="update hide">
<input type="text" name="username" placeholder="username" required />
<input type="submit" name="submit" value="submit" class="btn" />
<button type="button" class="btn updateCancel">back</button>
</form>
<form class="signIn hide">
<input type="email" name="email" placeholder="email" required />
Expand Down Expand Up @@ -258,6 +259,9 @@
<div class="roomActions">
<span class="copyRoomId" aria-label="Copy room id">📋</span>
<span class="copyRoomLink" aria-label="Copy room link">🔗</span>
<button class="leaveRoomBtn" type="button" aria-label="Exit room">
Exit
</button>
</div>
</div>
<div class="roomSettings hide">
Expand Down
25 changes: 25 additions & 0 deletions frontend/public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
33 changes: 26 additions & 7 deletions frontend/src/firebase/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
};

Expand All @@ -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 };
}
};

Expand All @@ -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);
Expand Down
57 changes: 45 additions & 12 deletions frontend/src/socket/roomHandling.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -77,14 +77,17 @@ function renderTrackPlayers(playerList, winnerKey) {
}
row.innerHTML =
'<div class="playerProgress"><div class="playerName">' +
crownSvg +
'<span class="playerNameText"></span></div></div><div class="playerSpeed"></div>';
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");
Expand All @@ -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}%)`;
}
Expand Down Expand Up @@ -146,7 +150,7 @@ function renderLobbyPlayers(playerList, winnerKey) {
<div class="lobbyPlayerProfile">
<div class="lobbyPlayerAvatar"></div>
<div class="lobbyPlayerIdentity">
<div class="lobbyPlayerName">${crownSvg}<span class="lobbyPlayerNameText"></span></div>
<div class="lobbyPlayerName"><span class="lobbyPlayerNameText"></span></div>
<div class="lobbyPlayerTag"></div>
</div>
</div>
Expand All @@ -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}`;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;

Expand All @@ -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() {
Expand Down
Loading
Loading