I am trying to use web sockets in my queue management app. There are issues with updating the data in real time. Often, I need to refresh to get the update or refresh to use different functionality again.
Ignore a lot of other red flags (i.e. I’m currently storing classroom data locally in an object instead of a server).
server.js
const express = require("express");
const app = express();
const http = require("http");
const socketIo = require("socket.io");
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "http://localhost:3001",
methods: ["GET", "POST"],
credentials: true,
},
pingTimeout: 18000000,
});
const cors = require("cors");
const path = require("path");
const uuid = require("uuid");
const fs = require("fs");
app.use(cors());
app.use(express.json());
const dataFilePath = path.join(__dirname, "classrooms.json");
let classrooms = {};
if (fs.existsSync(dataFilePath)) {
const data = fs.readFileSync(dataFilePath, "utf8");
classrooms = JSON.parse(data);
}
function saveClassroomsToFile() {
fs.writeFileSync(dataFilePath, JSON.stringify(classrooms), "utf8");
}
io.on("connection", (socket) => {
socket.on("joinClassroom", (classroomId) => {
socket.join(classroomId);
// Send initial data to the client
socket.emit("initialData", {
queue: classrooms[classroomId].queue,
attended: classrooms[classroomId].attended,
selectedStudent: classrooms[classroomId].selectedStudent,
participation: classrooms[classroomId].participation,
name: classrooms[classroomId].name, // CHANGED
});
});
socket.on("raiseHand", ({ studentId, classroomId, studentName }) => {
if (
classrooms[classroomId] &&
!classrooms[classroomId].queue.some((student) => student.id === studentId)
) {
classrooms[classroomId].queue.push({ id: studentId, name: studentName });
io.to(classroomId).emit("queueUpdate", classrooms[classroomId].queue);
}
});
socket.on("lowerHand", ({ studentId, classroomId }) => {
if (classrooms[classroomId]) {
classrooms[classroomId].queue = classrooms[classroomId].queue.filter(
(student) => student.id !== studentId
);
io.to(classroomId).emit("queueUpdate", classrooms[classroomId].queue);
}
});
socket.on("selectStudent", ({ classroomId, isColdCall }) => {
if (!classrooms[classroomId]) {
return;
}
let selectedStudent;
if (isColdCall) {
const attendedStudents = classrooms[classroomId].attended;
if (attendedStudents.length === 0) {
return;
}
const randomIndex = Math.floor(Math.random() * attendedStudents.length);
selectedStudent = attendedStudents[randomIndex];
} else {
if (classrooms[classroomId].queue.length === 0) {
return;
}
const sortedQueue = [...classrooms[classroomId].queue].sort(
(a, b) =>
(classrooms[classroomId].participation[a.id] || 0) -
(classrooms[classroomId].participation[b.id] || 0)
);
selectedStudent = sortedQueue[0];
classrooms[classroomId].queue = classrooms[classroomId].queue.filter(
(student) => student.id !== selectedStudent.id
);
}
classrooms[classroomId].selectedStudent = selectedStudent;
classrooms[classroomId].participation[selectedStudent.id] =
(classrooms[classroomId].participation[selectedStudent.id] || 0) + 1;
saveClassroomsToFile();
io.to(classroomId).emit("studentSelected", selectedStudent);
io.to(classroomId).emit("queueUpdate", classrooms[classroomId].queue);
io.to(classroomId).emit("participationUpdate", {
student: selectedStudent,
count: classrooms[classroomId].participation[selectedStudent.id],
});
});
socket.on("resetQueue", ({ classroomId }) => {
if (classrooms[classroomId]) {
classrooms[classroomId].queue = [];
classrooms[classroomId].selectedStudent = null;
io.to(classroomId).emit("queueUpdate", []);
io.to(classroomId).emit("clearSelectedStudent");
}
});
});
app.post("/create-classroom", (req, res) => {
const classroomId = uuid.v4().slice(0, 6);
const { name } = req.body; // CHANGED
classrooms[classroomId] = {
name: name, // CHANGED
queue: [],
participation: {},
attended: [],
selectedStudent: null,
};
saveClassroomsToFile();
res.status(200).json({ classroomId });
});
app.post("/log-attendance", (req, res) => {
const { classroomId, studentName, studentId } = req.body;
if (!classrooms[classroomId]) {
return res.status(404).json({ message: "Classroom not found" });
}
if (
!classrooms[classroomId].attended.some(
(student) => student.id === studentId
)
) {
classrooms[classroomId].attended.push({ id: studentId, name: studentName });
saveClassroomsToFile();
res.status(200).json({ message: "Attendance logged" });
io.to(classroomId).emit(
"attendanceUpdate",
classrooms[classroomId].attended
);
} else {
res.status(200).json({ message: "Student already logged" });
}
});
app.post("/verify-classroom", (req, res) => {
const { classroomId } = req.body;
if (!classrooms[classroomId]) {
return res.status(404).json({ message: "Classroom not found" });
}
const classroomName = classrooms[classroomId].name;
res
.status(200)
.json({ message: "Classroom found", classroomName: classroomName }); // CHANGED
});
app.use(express.static(path.join(__dirname, "frontend/public")));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "frontend/public", "index.html"));
});
server.listen(3000, () => {
console.log("Server listening on port 3000");
});
Classroom.js
import React, { useEffect, useState } from "react";
import io from "socket.io-client";
import { useParams } from "react-router-dom";
import "./Classroom.css";
import { useLocation } from "react-router-dom";
const socket = io("http://localhost:3000");
const Classroom = () => {
const { classroomId } = useParams();
const [queue, setQueue] = useState([]);
const [selectedStudent, setSelectedStudent] = useState(null);
const [attendanceList, setAttendanceList] = useState([]);
const location = useLocation(); // CHANGED
const [classroomName, setClassroomName] = useState(
location.state?.classroomName || ""
); // CHANGED
useEffect(() => {
socket.emit("joinClassroom", classroomId);
socket.on("initialData", (data) => {
setQueue(data.queue);
setAttendanceList(data.attended);
setSelectedStudent(data.selectedStudent);
setClassroomName(data.name); // CHANGED
});
socket.on("queueUpdate", (newQueue) => {
console.log("Queue update received:", newQueue);
setQueue(newQueue);
});
socket.on("attendanceUpdate", (newAttendance) => {
console.log("Attendance update received:", newAttendance);
setAttendanceList(newAttendance);
});
socket.on("studentSelected", (newSelectedStudent) => {
console.log("Student selected:", newSelectedStudent);
setSelectedStudent(newSelectedStudent);
});
socket.on("clearSelectedStudent", () => {
setSelectedStudent(null);
});
return () => {
socket.off("initialData");
socket.off("queueUpdate");
socket.off("attendanceUpdate");
socket.off("studentSelected");
socket.off("clearSelectedStudent");
};
}, [classroomId]);
const selectStudent = (isColdCall = false) => {
socket.emit("selectStudent", { classroomId, isColdCall });
};
const resetQueue = () => {
socket.emit("resetQueue", { classroomId });
setSelectedStudent(null);
};
return (
<>
<nav className="nav-bar">
<div>
<span className="nav-title">{classroomName}</span>
<span className="classroom-id">ID: {classroomId}</span>
</div>
</nav>
<div className="container">
<div className="attendance-stats">
<div className="attendance-list">
{attendanceList
.slice(-20)
.reverse()
.map((student, index) => (
<span key={index}>{student.name}</span>
))}
</div>
<div className="class-stats">
<span>Total Students: {attendanceList.length}</span>
</div>
</div>
<div className="main-content">
<div className="card">
<h2>Queue</h2>
<div className="queue-container">
<ul className="queue-list">
{queue.map((student, index) => (
<li key={index} className="fade-in">
<span className="student-icon">{student.name[0]}</span>
{student.name}
</li>
))}
</ul>
<div className="button-container">
<button onClick={() => selectStudent(false)}>Select</button>
<button onClick={() => selectStudent(true)}>Cold Call</button>
<button onClick={resetQueue}>Reset</button>
</div>
</div>
</div>
<div className="card">
<h2>Selected Student</h2>
{selectedStudent ? (
<div className="selected-student slide-in">
<div className="selected-student-icon">
{selectedStudent.name[0]}
</div>
<div className="selected-student-name">
{selectedStudent.name}
</div>
</div>
) : (
<p>No student selected</p>
)}
</div>
</div>
</div>
</>
);
};
export default Classroom;
Student.js
import React, { useState, useEffect } from "react";
import { useParams, useLocation } from "react-router-dom";
import io from "socket.io-client";
import "./Student.css";
const socket = io("http://localhost:3000");
const Student = () => {
const { classroomId } = useParams();
const location = useLocation();
const { studentName, studentId, classroomName } = location.state;
const [participationCount, setParticipationCount] = useState(0);
const [isHandRaised, setIsHandRaised] = useState(false);
const [isSelected, setIsSelected] = useState(false);
useEffect(() => {
console.log("Joining classroom:", classroomId);
socket.emit("joinClassroom", classroomId);
socket.on("initialData", (data) => {
setIsHandRaised(data.queue.some((student) => student.id === studentId));
setParticipationCount(data.participation[studentId] || 0);
});
socket.on("participationUpdate", ({ student, count }) => {
console.log("Participation update received:", student, count);
if (student.id === studentId) {
setParticipationCount(count);
}
});
socket.on("queueUpdate", (newQueue) => {
console.log("Queue update received:", newQueue);
const handRaised = newQueue.some((student) => student.id === studentId);
console.log("Is hand raised?", handRaised);
setIsHandRaised(handRaised);
});
socket.on("studentSelected", (selectedStudent) => {
if (selectedStudent.id === studentId) {
setIsHandRaised(false);
setIsSelected(true);
console.log("You've been selected!");
}
});
socket.on("clearSelectedStudent", () => {
setIsSelected(false);
});
return () => {
socket.off("initialData");
socket.off("participationUpdate");
socket.off("queueUpdate");
socket.off("studentSelected");
socket.off("clearSelectedStudent");
};
}, [classroomId, studentId]);
const raiseHand = () => {
socket.emit("raiseHand", { studentId, classroomId, studentName });
};
const lowerHand = () => {
socket.emit("lowerHand", { studentId, classroomId });
};
const closeModal = () => {
setIsSelected(false);
};
return (
<div className="student-container">
<nav className="student-nav-bar">
<span className="student-nav-title">{classroomName}</span>
<span className="student-classroom-id">Class ID: {classroomId}</span>
</nav>
<div className="student-info-card">
<div className="student-avatar">{studentName[0]}</div>
<h1 className="student-name">{studentName}</h1>
<p className="student-participation-count">
Times Participated: {participationCount}
</p>
<button
className="student-hand-button"
onClick={isHandRaised ? lowerHand : raiseHand}
>
{isHandRaised ? "Lower Hand" : "Raise Hand"}
</button>
</div>
{isSelected && (
<div className="student-selected-modal">
<div className="student-selected-modal-content">
<button className="close-modal-button" onClick={closeModal}>
×
</button>
<h2>You've been selected!</h2>
<p>It's your turn to participate.</p>
</div>
</div>
)}
</div>
);
};
export default Student;
HomePage.js
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import Modal from "react-modal";
import "./HomePage.css";
Modal.setAppElement("#root");
const HomePage = () => {
const navigate = useNavigate();
const [modalIsOpen, setModalIsOpen] = useState(false);
const [joinClassroomId, setJoinClassroomId] = useState("");
const [studentName, setStudentName] = useState("");
const [studentId, setStudentId] = useState("");
const [errorMessage, setErrorMessage] = useState("");
// New state variables for sign-in and registration
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isSignIn, setIsSignIn] = useState(true);
const [showSignInModal, setShowSignInModal] = useState(false);
const [classroomName, setClassroomName] = useState("");
const [showClassroomNameInput, setShowClassroomNameInput] = useState(false);
const createClassroom = async () => {
setShowSignInModal(true);
};
const handleSignIn = async (e) => {
e.preventDefault();
// FUTURE VALIDATION HERE against database
if (email && password) {
setShowSignInModal(false);
setShowClassroomNameInput(true);
} else {
setErrorMessage("Please enter both email and password");
}
};
const handleRegistration = async (e) => {
e.preventDefault();
// FUTURE SEND DATA TO BACKEND HERE
if (email && password) {
// Store credentials in local storage (Note: This is not secure for production)
localStorage.setItem(
"userCredentials",
JSON.stringify({ email, password })
);
setShowSignInModal(false);
setShowClassroomNameInput(true);
} else {
setErrorMessage("Please enter both email and password");
}
};
const handleClassroomNameSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post(
"http://localhost:3000/create-classroom",
{ name: classroomName }
);
navigate(`/classroom/${response.data.classroomId}`, {
state: { classroomName },
});
} catch (error) {
console.error("Error creating classroom:", error);
setErrorMessage("Failed to create classroom. Please try again.");
}
};
const openModal = () => setModalIsOpen(true);
const closeModal = () => {
setModalIsOpen(false);
setErrorMessage("");
};
const joinClassroom = async () => {
if (!joinClassroomId || !studentName || !studentId) {
setErrorMessage("Please fill in all fields");
return;
}
try {
const response = await axios.post(
"http://localhost:3000/verify-classroom",
{
classroomId: joinClassroomId,
}
);
const { classroomName } = response.data;
await axios.post("http://localhost:3000/log-attendance", {
classroomId: joinClassroomId,
studentName,
studentId,
});
navigate(`/student/${joinClassroomId}`, {
state: { studentName, studentId, classroomName },
});
closeModal();
} catch (error) {
setErrorMessage("Classroom ID not found. Please enter a valid ID.");
}
};
return (
<div className="homepage-container">
<h1 className="homepage-title">Welcome to the Classroom App</h1>
<p className="homepage-subtitle">Choose an option to get started:</p>
<div className="options-container">
<div className="option-card" onClick={createClassroom}>
<div className="option-icon">🏫</div>
<h2 className="option-title">Create Classroom</h2>
<p className="option-description">
Start a new virtual classroom for your students
</p>
<button className="option-button create-button">Create Now</button>
</div>
<div className="option-card" onClick={openModal}>
<div className="option-icon">🚪</div>
<h2 className="option-title">Join Classroom</h2>
<p className="option-description">
Enter an existing classroom with your details
</p>
<button className="option-button join-button">Join Now</button>
</div>
</div>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Join Classroom Modal"
className="modal"
overlayClassName="modal-overlay"
>
<h2 className="modal-title">Join Classroom</h2>
<input
type="text"
value={joinClassroomId}
onChange={(e) => setJoinClassroomId(e.target.value)}
placeholder="Enter Classroom ID"
className="modal-input"
/>
<input
type="text"
value={studentName}
onChange={(e) => setStudentName(e.target.value)}
placeholder="Enter your Name"
className="modal-input"
/>
<input
type="text"
value={studentId}
onChange={(e) => setStudentId(e.target.value)}
placeholder="Enter your Student ID"
className="modal-input"
/>
{errorMessage && <p className="error-message">{errorMessage}</p>}
<div className="modal-button-group">
<button className="create-button" onClick={joinClassroom}>
Join
</button>
<button className="join-button" onClick={closeModal}>
Cancel
</button>
</div>
</Modal>
<Modal
isOpen={showSignInModal}
onRequestClose={() => setShowSignInModal(false)}
contentLabel="Sign In Modal"
className="modal"
overlayClassName="modal-overlay"
>
<h2 className="modal-title">
{isSignIn ? "Sign In" : "Create Account"}
</h2>
<form onSubmit={isSignIn ? handleSignIn : handleRegistration}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="modal-input"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="modal-input"
required
/>
{errorMessage && <p className="error-message">{errorMessage}</p>}
<div className="modal-button-group">
<button type="submit" className="create-button">
{isSignIn ? "Sign In" : "Create Account"}
</button>
<button
type="button"
className="join-button"
onClick={() => setIsSignIn(!isSignIn)}
>
{isSignIn
? "Need an account? Sign Up"
: "Already have an account? Sign In"}
</button>
</div>
</form>
</Modal>
<Modal
isOpen={showClassroomNameInput}
onRequestClose={() => setShowClassroomNameInput(false)}
contentLabel="Classroom Name Input"
className="modal"
overlayClassName="modal-overlay"
>
<h2 className="modal-title">Create Classroom</h2>
<form onSubmit={handleClassroomNameSubmit}>
<input
type="text"
value={classroomName}
onChange={(e) => setClassroomName(e.target.value)}
placeholder="Enter Classroom Name"
className="modal-input"
required
/>
<div className="modal-button-group">
<button type="submit" className="create-button">
Create
</button>
<button
type="button"
className="join-button"
onClick={() => setShowClassroomNameInput(false)}
>
Cancel
</button>
</div>
</form>
</Modal>
</div>
);
};
export default HomePage;
I have tried a lot.
New contributor
Ethan O’Brien is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1