From 80f0d2784b3ff25d7669d3d8d5503c145a1eaf62 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 6 Nov 2025 19:39:53 -0700 Subject: [PATCH 1/9] Added some basic changes nend to implement route fixes --- shatter-backend/src/models/user_model.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index baf8986..3fb1880 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -9,6 +9,7 @@ import { Schema, model } from 'mongoose'; export interface IUser { name: string; email: string; + password: string; } // Create the Mongoose Schema (the database blueprint) @@ -28,6 +29,11 @@ const UserSchema = new Schema( trim: true, lowercase: true, // converts all emails to lowercase before saving for consistency unique: true // enforce uniqueness, error 11000 if duplicate is detected + }, + password: { + type: String, + required: true, + select: false // exclude password field by default when querying users for security } }, { From 17d11da27ddaef99c01c24bb8817aa767aa8b1cd Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Sat, 15 Nov 2025 18:09:35 -0700 Subject: [PATCH 2/9] Updated the user data model to include passwords. --- shatter-backend/package-lock.json | 6 ++++++ shatter-backend/src/controllers/user_controller.ts | 8 ++++---- shatter-backend/src/models/user.ts | 0 shatter-backend/src/server.ts | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 shatter-backend/src/models/user.ts diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index a80d3cc..90ce0dd 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -470,6 +470,7 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -596,6 +597,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -827,6 +829,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1325,6 +1328,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2035,6 +2039,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3216,6 +3221,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 17189c4..ea0732e 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -24,16 +24,16 @@ export const createUser = async (req: Request, res: Response) => { try { // Destructure the req body sent by the client // The ?? {} ensures we don't get error if req.body is undefined - const { name, email } = req.body ?? {}; + const { name, email, password} = req.body ?? {}; // Basic validation to ensure both name and email are provided // if not respond with bad request and stop further processes - if (!name || !email) { - return res.status(400).json({ error: 'name and email required' }); + if (!name || !email || !password) { + return res.status(400).json({ error: 'name, email required, and password' }); } // create a new user doc in DB using Mongoose's .create() - const user = await User.create({ name, email }); + const user = await User.create({ name, email, password}); // respond with "created" and send back created user as JSON res.status(201).json(user); } catch(err: any) { diff --git a/shatter-backend/src/models/user.ts b/shatter-backend/src/models/user.ts deleted file mode 100644 index e69de29..0000000 diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index c23c38c..44a0e8b 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -21,7 +21,7 @@ async function start() { // start listening for incoming HTTP requests on chosen port app.listen(PORT, () => { - console.log('Server running on http://localhost:${PORT}'); + console.log(`Server running on http://localhost:${PORT}`); }); } catch (err) { // if connection goes wrong, log the error console.error('Failed to start server:', err); From f09713a9ad0d9737b1b9625fa66bc63826407b07 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 6 Nov 2025 19:39:53 -0700 Subject: [PATCH 3/9] Added some basic changes nend to implement route fixes --- shatter-backend/src/models/user_model.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index baf8986..3fb1880 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -9,6 +9,7 @@ import { Schema, model } from 'mongoose'; export interface IUser { name: string; email: string; + password: string; } // Create the Mongoose Schema (the database blueprint) @@ -28,6 +29,11 @@ const UserSchema = new Schema( trim: true, lowercase: true, // converts all emails to lowercase before saving for consistency unique: true // enforce uniqueness, error 11000 if duplicate is detected + }, + password: { + type: String, + required: true, + select: false // exclude password field by default when querying users for security } }, { From 955980865d5e3b3be7bed992ef8703ba6a98c39f Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Sat, 15 Nov 2025 18:09:35 -0700 Subject: [PATCH 4/9] Updated the user data model to include passwords. --- shatter-backend/package-lock.json | 6 ++++++ shatter-backend/src/controllers/user_controller.ts | 8 ++++---- shatter-backend/src/server.ts | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index a80d3cc..90ce0dd 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -470,6 +470,7 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -596,6 +597,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -827,6 +829,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1325,6 +1328,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2035,6 +2039,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3216,6 +3221,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 17189c4..ea0732e 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -24,16 +24,16 @@ export const createUser = async (req: Request, res: Response) => { try { // Destructure the req body sent by the client // The ?? {} ensures we don't get error if req.body is undefined - const { name, email } = req.body ?? {}; + const { name, email, password} = req.body ?? {}; // Basic validation to ensure both name and email are provided // if not respond with bad request and stop further processes - if (!name || !email) { - return res.status(400).json({ error: 'name and email required' }); + if (!name || !email || !password) { + return res.status(400).json({ error: 'name, email required, and password' }); } // create a new user doc in DB using Mongoose's .create() - const user = await User.create({ name, email }); + const user = await User.create({ name, email, password}); // respond with "created" and send back created user as JSON res.status(201).json(user); } catch(err: any) { diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index c23c38c..44a0e8b 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -21,7 +21,7 @@ async function start() { // start listening for incoming HTTP requests on chosen port app.listen(PORT, () => { - console.log('Server running on http://localhost:${PORT}'); + console.log(`Server running on http://localhost:${PORT}`); }); } catch (err) { // if connection goes wrong, log the error console.error('Failed to start server:', err); From 53b233f25023d375edd79d741a729d13ad603315 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 6 Nov 2025 19:39:53 -0700 Subject: [PATCH 5/9] Added some basic changes nend to implement route fixes --- shatter-backend/src/app.ts | 2 + .../src/controllers/event_controller.ts | 63 +++++++++++++++++++ shatter-backend/src/models/event_model.ts | 56 +++++++++++++++++ .../src/models/participant_model.ts | 26 ++++++++ shatter-backend/src/models/user_model.ts | 27 ++------ shatter-backend/src/routes/event_routes.ts | 11 ++++ shatter-backend/src/utils/event_utils.ts | 18 ++++++ 7 files changed, 181 insertions(+), 22 deletions(-) create mode 100644 shatter-backend/src/controllers/event_controller.ts create mode 100644 shatter-backend/src/models/event_model.ts create mode 100644 shatter-backend/src/models/participant_model.ts create mode 100644 shatter-backend/src/routes/event_routes.ts create mode 100644 shatter-backend/src/utils/event_utils.ts diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 11e7050..70d0daa 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,6 +1,7 @@ import express from 'express'; import userRoutes from './routes/user_route'; // these routes define how to handle requests to /api/users import authRoutes from './routes/auth_routes'; +import eventRoutes from './routes/event_routes'; const app = express(); @@ -12,5 +13,6 @@ app.get('/', (_req, res) => { app.use('/api/users', userRoutes); app.use('/api/auth', authRoutes); +app.use('/api/events', eventRoutes); export default app; diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts new file mode 100644 index 0000000..4f06d9b --- /dev/null +++ b/shatter-backend/src/controllers/event_controller.ts @@ -0,0 +1,63 @@ +import { Request, Response } from "express"; +import { Event } from "../models/event_model"; +import "../models/participant_model"; + +import {generateEventId, generateJoinCode} from "../utils/event_utils"; + + +export async function createEvent(req: Request, res: Response) { + try { + const { name, description, startDate, endDate, maxParticipant, currentState, createdBy } = req.body; + + if (!createdBy) { + return res.status(400).json({ success: false, error: "createdBy email is required" }); + } + + const eventId = generateEventId(); + const joinCode = generateJoinCode(); + + const event = new Event({ + eventId, + name, + description, + joinCode, + startDate, + endDate, + maxParticipant, + participants: [], + currentState, + createdBy, // required email field + }); + + const savedEvent = await event.save(); + + res.status(201).json({ success: true, event: savedEvent }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +} + + +export async function getEventByJoinCode(req: Request, res: Response) { + try { + const { joinCode } = req.params; + + if (!joinCode) { + return res.status(400).json({ success: false, error: "joinCode is required" }); + } + + // Find event by joinCode and populate participants + const event = await Event.findOne({ joinCode }).populate("participants"); + + if (!event) { + return res.status(404).json({ success: false, error: "Event not found" }); + } + + res.status(200).json({ + success: true, + event, + }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +} \ No newline at end of file diff --git a/shatter-backend/src/models/event_model.ts b/shatter-backend/src/models/event_model.ts new file mode 100644 index 0000000..fd5d74b --- /dev/null +++ b/shatter-backend/src/models/event_model.ts @@ -0,0 +1,56 @@ +import mongoose, { Schema, model, Document, Types } from "mongoose"; +import {User} from "../models/user_model"; + +import { IParticipant } from "./participant_model"; + +export interface IEvent extends Document { + eventId: string; + name: string; + description: string; + joinCode: string; + startDate: Date; + endDate: Date; + maxParticipant: number; + participants: mongoose.Types.DocumentArray; + currentState: string; + createdBy: string; +} + +const EventSchema = new Schema( + { + eventId: { type: String, required: true, unique: true }, + name: { type: String, required: true }, + description: { type: String, required: true }, + joinCode: { type: String, required: true, unique: true }, + startDate: { type: Date, required: true }, + endDate: { type: Date, required: true }, + maxParticipant: { type: Number, required: true }, + participants: [{ type: Types.ObjectId, ref: "Participant" }], + currentState: { type: String, required: true }, + createdBy: { + type: String, + required: true, + validate: { + validator: async function (email: string) { + const user = await User.findOne({ email }); + return !!user; // true if user exists + }, + message: "User with this email does not exist" + } + } + }, + { + timestamps: true, + } +); + +// Optional validation: ensure endDate is after startDate +EventSchema.pre("save", function (next) { + if (this.endDate <= this.startDate) { + next(new Error("endDate must be after startDate")); + } else { + next(); + } +}); + +export const Event = model("Event", EventSchema); diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts new file mode 100644 index 0000000..8de7400 --- /dev/null +++ b/shatter-backend/src/models/participant_model.ts @@ -0,0 +1,26 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IParticipant extends Document { + participantId: string | null; + name: string; + eventId: string; +} + +const ParticipantSchema = new Schema({ + participantId: { + type: String, + default: null, + }, + + name: { + type: String, + required: true, + }, + + eventId: { + type: String, + required: true, + }, +}); + +export const Participant = model("Participant", ParticipantSchema); diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index 23c1633..a93c6de 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -9,11 +9,7 @@ import { Schema, model } from 'mongoose'; export interface IUser { name: string; email: string; - passwordHash: string; - lastLogin?: Date; - passwordChangedAt?: Date; - createdAt?: Date; - updatedAt?: Date; + password: string; } // Create the Mongoose Schema (the database blueprint) @@ -31,26 +27,13 @@ const UserSchema = new Schema( type: String, required: true, trim: true, - lowercase: true, - unique: true, - index: true, - match: [ - /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, - 'Please provide a valid email address' - ] + lowercase: true, // converts all emails to lowercase before saving for consistency + unique: true // enforce uniqueness, error 11000 if duplicate is detected }, - passwordHash: { + password: { type: String, required: true, - select: false // Don't return in queries by default - }, - lastLogin: { - type: Date, - default: null - }, - passwordChangedAt: { - type: Date, - default: null + select: false // exclude password field by default when querying users for security } }, { diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts new file mode 100644 index 0000000..fe95fb1 --- /dev/null +++ b/shatter-backend/src/routes/event_routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { createEvent, getEventByJoinCode } from '../controllers/event_controller'; + +const router = Router(); + +// POST /api/events - create a new event +router.post('/createEvent', createEvent); +router.get("/event/:joinCode", getEventByJoinCode); + + +export default router; \ No newline at end of file diff --git a/shatter-backend/src/utils/event_utils.ts b/shatter-backend/src/utils/event_utils.ts new file mode 100644 index 0000000..58023d6 --- /dev/null +++ b/shatter-backend/src/utils/event_utils.ts @@ -0,0 +1,18 @@ +import crypto from "crypto"; + +/** + * Generates a random hash string for eventId + * Example: 3f5a9c7d2e8b1a6f4c9d0e2b7a1c8f3d + */ +export function generateEventId(): string { + return crypto.randomBytes(16).toString("hex"); +} + +/** + * Generates a random 8-digit number string for joinCode + * Example: "48392017" + */ +export function generateJoinCode(): string { + const code = Math.floor(10000000 + Math.random() * 90000000); + return code.toString(); +} From 0d5f7c8f9fe5e0ddf8902c7b72c743fbd836b3ad Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 27 Nov 2025 20:08:25 -0700 Subject: [PATCH 6/9] fixing conflicts --- .../src/controllers/user_controller.ts | 61 ++++++++----------- shatter-backend/src/models/user_model.ts | 34 +++++++++-- shatter-backend/src/routes/user_route.ts | 13 +--- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index ea0732e..877b53a 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,50 +1,41 @@ -// import req and res types for type safety -import { Request, Response } from 'express'; -import { User } from '../models/user_model.ts'; // imports user model created with mongoose +import { Request, Response } from "express"; +import { User } from "../models/user_model"; // controller: GET /api/users // This function handles GET reqs to /api/users // It fetches all users from MongoDB and sends them as json export const getUsers = async (_req: Request, res: Response) => { - try { - // retrieves all docs from "users" collection - const users = await User.find().lean(); // .lean() returns plain JS objects instead of Mongoose docs, may change incase we need extra model methods later - res.json(users); // sends list of users back as JSON response - } catch (err) { // log the error if something goes wrong - console.error('GET /api/users error:', err); - res.status(500).json({ error: 'Failed to fetch users' }); - } + try { + const users = await User.find().lean(); + res.json(users); + } catch (err: any) { + console.error("GET /api/users error:", err); + res.status(500).json({ + error: "Failed to fetch users", + message: err.message, + }); + } }; - // controller: POST /api/users // reads data from req body, vailidates it and creates a new user export const createUser = async (req: Request, res: Response) => { - try { - // Destructure the req body sent by the client - // The ?? {} ensures we don't get error if req.body is undefined - const { name, email, password} = req.body ?? {}; + try { + const { name, email } = req.body ?? {}; - // Basic validation to ensure both name and email are provided - // if not respond with bad request and stop further processes - if (!name || !email || !password) { - return res.status(400).json({ error: 'name, email required, and password' }); - } + if (!name || !email) { + return res.status(400).json({ error: "name and email required" }); + } - // create a new user doc in DB using Mongoose's .create() - const user = await User.create({ name, email, password}); - // respond with "created" and send back created user as JSON - res.status(201).json(user); - } catch(err: any) { - // Handle duplicate email error - // Mongo DB rejects duplicat value since we have email marked as 'unique' - if (err?.code === 11000) { - return res.status(409).json({ error: 'email already exists' }); - } - - // for all other errors, log them and return generic 500 response - console.error('POST /api/users error:', err); - res.status(500).json({error: 'Failed to create user' }); + const user = await User.create({ name, email }); + res.status(201).json(user); + } catch (err: any) { + if (err?.code === 11000) { + return res.status(409).json({ error: "email already exists" }); } + + console.error("POST /api/users error:", err); + res.status(500).json({ error: "Failed to create user" }); + } }; diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index 3fb1880..23c1633 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -9,7 +9,11 @@ import { Schema, model } from 'mongoose'; export interface IUser { name: string; email: string; - password: string; + passwordHash: string; + lastLogin?: Date; + passwordChangedAt?: Date; + createdAt?: Date; + updatedAt?: Date; } // Create the Mongoose Schema (the database blueprint) @@ -27,13 +31,26 @@ const UserSchema = new Schema( type: String, required: true, trim: true, - lowercase: true, // converts all emails to lowercase before saving for consistency - unique: true // enforce uniqueness, error 11000 if duplicate is detected + lowercase: true, + unique: true, + index: true, + match: [ + /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, + 'Please provide a valid email address' + ] }, - password: { + passwordHash: { type: String, required: true, - select: false // exclude password field by default when querying users for security + select: false // Don't return in queries by default + }, + lastLogin: { + type: Date, + default: null + }, + passwordChangedAt: { + type: Date, + default: null } }, { @@ -44,6 +61,13 @@ const UserSchema = new Schema( } ); +// Add middleware to auto-update passwordChangedAt +UserSchema.pre('save', function (next) { + if (this.isModified('passwordHash') && !this.isNew) { + this.passwordChangedAt = new Date(); + } + next(); +}); // create and export mongoose model // model is simply a wrapper around schema that gives access to MongoDB opeprations diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index c26b3e8..a86e2cf 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,16 +1,9 @@ -// router is like a mini-express app that allows grouping of related routes together import { Router } from 'express'; -import { getUsers, createUser } from '../controllers/user_controller.ts'; -// Importing controller functions that handle logic for each route -// These function define what happens when a req is received +import { getUsers, createUser } from '../controllers/user_controller'; -// creating new router instance const router = Router(); -// Defining routes for the /api/users path -router.get('/', getUsers); // when GET req is made, run getUsers func -router.post('/', createUser); // when POST req is made, run creatUser func +router.get('/', getUsers); +router.post('/', createUser); -// Export the router so it can be used in app.ts -// app.ts imports router and mounts it under '/api/users' export default router; From cd7581d2c25c674097c0d62cf2a554093a00ddae Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:49:39 -0700 Subject: [PATCH 7/9] refactor models --- shatter-backend/src/models/event_model.ts | 22 ++++----- .../src/models/participant_model.ts | 18 ++++--- shatter-backend/src/models/user_model.ts | 49 +++++++++++-------- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/shatter-backend/src/models/event_model.ts b/shatter-backend/src/models/event_model.ts index fd5d74b..1e6228c 100644 --- a/shatter-backend/src/models/event_model.ts +++ b/shatter-backend/src/models/event_model.ts @@ -1,43 +1,41 @@ import mongoose, { Schema, model, Document, Types } from "mongoose"; -import {User} from "../models/user_model"; +import { User } from "../models/user_model"; import { IParticipant } from "./participant_model"; export interface IEvent extends Document { - eventId: string; name: string; description: string; joinCode: string; startDate: Date; endDate: Date; maxParticipant: number; - participants: mongoose.Types.DocumentArray; + participantIds: Schema.Types.ObjectId[]; currentState: string; createdBy: string; } const EventSchema = new Schema( { - eventId: { type: String, required: true, unique: true }, name: { type: String, required: true }, description: { type: String, required: true }, joinCode: { type: String, required: true, unique: true }, startDate: { type: Date, required: true }, endDate: { type: Date, required: true }, - maxParticipant: { type: Number, required: true }, - participants: [{ type: Types.ObjectId, ref: "Participant" }], + maxParticipant: { type: Number, required: true }, + participantIds: [{ type: Schema.Types.ObjectId, ref: "Participant" }], currentState: { type: String, required: true }, - createdBy: { - type: String, + createdBy: { + type: String, required: true, validate: { validator: async function (email: string) { const user = await User.findOne({ email }); - return !!user; // true if user exists + return !!user; }, - message: "User with this email does not exist" - } - } + message: "User with this email does not exist", + }, + }, }, { timestamps: true, diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts index 8de7400..703c970 100644 --- a/shatter-backend/src/models/participant_model.ts +++ b/shatter-backend/src/models/participant_model.ts @@ -1,26 +1,32 @@ import { Schema, model, Document } from "mongoose"; export interface IParticipant extends Document { - participantId: string | null; + userId: Schema.Types.ObjectId | null; name: string; - eventId: string; + eventId: Schema.Types.ObjectId; } const ParticipantSchema = new Schema({ - participantId: { - type: String, + userId: { + type: Schema.Types.ObjectId, + ref: "User", default: null, }, name: { type: String, + ref: "User Name", required: true, }, eventId: { - type: String, + type: Schema.Types.ObjectId, + ref: "Event", required: true, }, }); -export const Participant = model("Participant", ParticipantSchema); +export const Participant = model( + "Participant", + ParticipantSchema +); diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index 23c1633..a2d29e2 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -1,19 +1,20 @@ // Import Schema and model from the Mongoose library. // - Schema: defines the structure and rules for documents in a collection (like a blueprint). // - model: creates a model (class) that we use in code to read/write those documents. -import { Schema, model } from 'mongoose'; +import { Schema, model } from "mongoose"; // define TS interface for type safety // This helps IDE and compiler know what fields exist on a User export interface IUser { - name: string; - email: string; - passwordHash: string; - lastLogin?: Date; - passwordChangedAt?: Date; - createdAt?: Date; - updatedAt?: Date; + name: string; + email: string; + passwordHash: string; + lastLogin?: Date; + passwordChangedAt?: Date; + createdAt?: Date; + updatedAt?: Date; + eventHistoryIds: Schema.Types.ObjectId[]; } // Create the Mongoose Schema (the database blueprint) @@ -24,8 +25,8 @@ const UserSchema = new Schema( { name: { type: String, - required: true, // field is mandatory; Mongoose will throw error if missing - trim: true // removes extra space at start and end + required: true, // field is mandatory; Mongoose will throw error if missing + trim: true, // removes extra space at start and end }, email: { type: String, @@ -35,35 +36,41 @@ const UserSchema = new Schema( unique: true, index: true, match: [ - /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, - 'Please provide a valid email address' - ] + /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, + "Please provide a valid email address", + ], }, passwordHash: { type: String, required: true, - select: false // Don't return in queries by default + select: false, // Don't return in queries by default }, lastLogin: { type: Date, - default: null + default: null, }, passwordChangedAt: { type: Date, - default: null - } + default: null, + }, + eventHistoryIds: [ + { + type: Schema.Types.ObjectId, + ref: "Event", + }, + ], }, { // timestamps: true automatically adds two fields to each document: // - createdAt: Date when the document was first created // - updatedAt: Date when the document was last modified - timestamps: true + timestamps: true, } ); // Add middleware to auto-update passwordChangedAt -UserSchema.pre('save', function (next) { - if (this.isModified('passwordHash') && !this.isNew) { +UserSchema.pre("save", function (next) { + if (this.isModified("passwordHash") && !this.isNew) { this.passwordChangedAt = new Date(); } next(); @@ -74,4 +81,4 @@ UserSchema.pre('save', function (next) { // "User" is the model name // Mongoose will automatically use "users" as the collection name in MongoDB -export const User = model('User', UserSchema); +export const User = model("User", UserSchema); From 3959463987c47c3dd22d127c0b406c5758da6855 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:50:00 -0700 Subject: [PATCH 8/9] join event api for guess and registered user --- shatter-backend/package-lock.json | 400 +++++++++++++++++- shatter-backend/package.json | 4 + shatter-backend/src/app.ts | 12 + .../src/controllers/event_controller.ts | 185 +++++++- shatter-backend/src/routes/event_routes.ts | 5 +- shatter-backend/src/server.ts | 79 +++- shatter-backend/src/types/express/index.d.ts | 9 + shatter-backend/tsconfig.json | 3 +- 8 files changed, 655 insertions(+), 42 deletions(-) create mode 100644 shatter-backend/src/types/express/index.d.ts diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index 0c4405a..b04a613 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -10,9 +10,12 @@ "license": "ISC", "dependencies": { "bcryptjs": "^3.0.3", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "mongoose": "^8.19.2", + "react-router-dom": "^7.12.0", + "socket.io": "^4.8.1", "zod": "^4.1.12" }, "devDependencies": { @@ -20,6 +23,7 @@ "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.5", "@types/node": "^24.9.2", + "@types/socket.io": "^3.0.1", "eslint": "^9.38.0", "globals": "^16.4.0", "jiti": "^2.6.1", @@ -364,6 +368,12 @@ "node": ">= 8" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -420,6 +430,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -477,9 +496,7 @@ "version": "24.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -531,6 +548,16 @@ "@types/node": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -606,7 +633,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -838,7 +864,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -937,6 +962,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -1175,6 +1209,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1291,6 +1338,95 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1346,7 +1482,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2057,7 +2192,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -2407,6 +2541,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2680,6 +2823,80 @@ "url": "https://opencollective.com/express" } }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2815,6 +3032,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -2865,6 +3089,12 @@ "node": ">= 18" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2972,6 +3202,141 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3239,7 +3604,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3276,7 +3640,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3368,6 +3731,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/shatter-backend/package.json b/shatter-backend/package.json index b3696eb..2b9710f 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -14,9 +14,12 @@ "description": "", "dependencies": { "bcryptjs": "^3.0.3", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "mongoose": "^8.19.2", + "react-router-dom": "^7.12.0", + "socket.io": "^4.8.1", "zod": "^4.1.12" }, "devDependencies": { @@ -24,6 +27,7 @@ "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.5", "@types/node": "^24.9.2", + "@types/socket.io": "^3.0.1", "eslint": "^9.38.0", "globals": "^16.4.0", "jiti": "^2.6.1", diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 70d0daa..98fe109 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,4 +1,6 @@ import express from 'express'; +import cors from "cors"; + import userRoutes from './routes/user_route'; // these routes define how to handle requests to /api/users import authRoutes from './routes/auth_routes'; import eventRoutes from './routes/event_routes'; @@ -7,6 +9,16 @@ const app = express(); app.use(express.json()); +app.use(cors({ + origin: "http://localhost:3000", + credentials: true, +})); + +app.use((req, _res, next) => { + req.io = app.get('socketio'); + next(); +}); + app.get('/', (_req, res) => { res.send('Hello'); }); diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 4f06d9b..c22bfd6 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -1,30 +1,40 @@ import { Request, Response } from "express"; import { Event } from "../models/event_model"; -import "../models/participant_model"; - -import {generateEventId, generateJoinCode} from "../utils/event_utils"; +import "../models/participant_model"; +import { generateJoinCode } from "../utils/event_utils"; +import { Participant } from "../models/participant_model"; +import { User } from "../models/user_model"; +import { Types } from "mongoose"; export async function createEvent(req: Request, res: Response) { try { - const { name, description, startDate, endDate, maxParticipant, currentState, createdBy } = req.body; + const { + name, + description, + startDate, + endDate, + maxParticipant, + currentState, + createdBy, + } = req.body; if (!createdBy) { - return res.status(400).json({ success: false, error: "createdBy email is required" }); + return res + .status(400) + .json({ success: false, error: "createdBy email is required" }); } - const eventId = generateEventId(); const joinCode = generateJoinCode(); const event = new Event({ - eventId, name, description, joinCode, startDate, endDate, maxParticipant, - participants: [], + participantIds: [], currentState, createdBy, // required email field }); @@ -37,17 +47,18 @@ export async function createEvent(req: Request, res: Response) { } } - export async function getEventByJoinCode(req: Request, res: Response) { try { const { joinCode } = req.params; if (!joinCode) { - return res.status(400).json({ success: false, error: "joinCode is required" }); + return res + .status(400) + .json({ success: false, error: "joinCode is required" }); } - // Find event by joinCode and populate participants - const event = await Event.findOne({ joinCode }).populate("participants"); + // const event = await Event.findOne({ joinCode }).populate("participantIds"); + const event = await Event.findOne({ joinCode }); if (!event) { return res.status(404).json({ success: false, error: "Event not found" }); @@ -60,4 +71,152 @@ export async function getEventByJoinCode(req: Request, res: Response) { } catch (err: any) { res.status(500).json({ success: false, error: err.message }); } -} \ No newline at end of file +} + +export async function joinEventAsUser(req: Request, res: Response) { + try { + const { name, userId } = req.body; + const { eventId } = req.params; + + console.log("=== JOIN EVENT START ==="); + console.log("EventId:", eventId); + console.log("UserId:", userId); + console.log("Name:", name); + console.log("req.io exists?", !!req.io); + + if (!userId || !name) + return res.status(400).json({ success: false, msg: "Missing fields" }); + + const event = await Event.findById(eventId); + if (!event) + return res.status(404).json({ success: false, msg: "Event not found" }); + + if (event.participantIds.length >= event.maxParticipant) + return res.status(400).json({ success: false, msg: "Event is full" }); + + let participant = await Participant.findOne({ + userId, + eventId, + }); + + if (participant) { + return res.status(409).json({ success: false, msg: "Already joined" }); + } + + participant = await Participant.create({ + userId, + name, + eventId, + }); + + const participantId = participant._id as Types.ObjectId; + + const eventUpdate = await Event.updateOne( + { _id: eventId }, + { $addToSet: { participantIds: participantId } } + ); + + // If nothing changed โ†’ already joined + if (eventUpdate.modifiedCount === 0) { + return res + .status(400) + .json({ success: false, msg: "Already joined this event" }); + } + + // 2. Add event to user history + await User.updateOne( + { _id: userId }, + { $addToSet: { eventHistoryIds: eventId } } + ); + + console.log("=== EMITTING SOCKET EVENT ==="); + console.log("Room (eventId):", eventId); + console.log("Participant data:", { participantId, name }); + + if (!req.io) { + console.error("ERROR: req.io is undefined!"); + } else { + const room = req.io.to(eventId); + console.log("Room object:", room); + + room.emit("participant-joined", { + participantId, + name, + }); + + console.log("Socket event emitted successfully"); + } + + return res.json({ + success: true, + participant, + }); + } catch (e) { + console.error("JOIN EVENT ERROR:", e); + return res.status(500).json({ success: false, msg: "Internal error" }); + } +} + +export async function joinEventAsGuest(req: Request, res: Response) { + try { + const { name } = req.body; + const { eventId } = req.params; + + if (!name) { + return res + .status(400) + .json({ success: false, msg: "Missing guest name" }); + } + + const event = await Event.findById(eventId); + if (!event) { + return res.status(404).json({ success: false, msg: "Event not found" }); + } + + if (event.participantIds.length >= event.maxParticipant) { + return res.status(400).json({ success: false, msg: "Event is full" }); + } + + // Create guest participant (userId is null) + const participant = await Participant.create({ + userId: null, + name, + eventId, + }); + + const participantId = participant._id as Types.ObjectId; + + // Add participant to event + await Event.updateOne( + { _id: eventId }, + { $addToSet: { participantIds: participantId } } + ); + + // Emit socket + console.log("=== EMITTING SOCKET EVENT ==="); + console.log("Room (eventId):", eventId); + console.log("Participant data:", { participantId, name }); + + if (!req.io) { + console.error("ERROR: req.io is undefined!"); + } else { + const room = req.io.to(eventId); + console.log("Room object:", room); + + room.emit("participant-joined", { + participantId, + name, + }); + + console.log("Socket event emitted successfully"); + } + + return res.json({ + success: true, + participant, + }); + } catch (err) { + console.error("JOIN GUEST ERROR:", err); + return res.status(500).json({ success: false, msg: "Internal error" }); + } +} diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts index fe95fb1..c890daa 100644 --- a/shatter-backend/src/routes/event_routes.ts +++ b/shatter-backend/src/routes/event_routes.ts @@ -1,11 +1,14 @@ import { Router } from 'express'; -import { createEvent, getEventByJoinCode } from '../controllers/event_controller'; +import { createEvent, getEventByJoinCode, joinEventAsUser, joinEventAsGuest } from '../controllers/event_controller'; const router = Router(); // POST /api/events - create a new event router.post('/createEvent', createEvent); router.get("/event/:joinCode", getEventByJoinCode); +router.post("/:eventId/join/user", joinEventAsUser); +router.post("/:eventId/join/guest", joinEventAsGuest); + export default router; \ No newline at end of file diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index 257ba45..15a3808 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,27 +1,68 @@ -import 'dotenv/config'; -import mongoose from 'mongoose'; -import app from './app'; +import 'dotenv/config'; +import mongoose from 'mongoose'; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; +import app from './app'; // config -const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; -const MONGODB_URI = process.env.MONGO_URI; +const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; +const MONGODB_URI = process.env.MONGO_URI; async function start() { - try { - if (!MONGODB_URI) { - throw new Error('MONGODB_URI is not set'); - } - await mongoose.connect(MONGODB_URI); - console.log('Successfully connected to MongoDB'); - - // start listening for incoming HTTP requests on chosen port - app.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); - } catch (err) { - console.error('Failed to start server:', err); - process.exit(1); + try { + if (!MONGODB_URI) { + throw new Error("MONGODB_URI is not set"); } + await mongoose.connect(MONGODB_URI); + console.log("Successfully connected to MongoDB"); + + // // start listening for incoming HTTP requests on chosen port + // app.listen(PORT, () => { + // console.log(`Server running on http://localhost:${PORT}`); + // }); + + // Create HTTP server from Express app + const httpServer = http.createServer(app); + + // Setup Socket.IO + const io = new SocketIOServer(httpServer, { + cors: { + origin: "http://localhost:3000", // React frontend + methods: ["GET", "POST"], + credentials: true, + }, + transports: ["websocket", "polling"], // fallback to polling + }); + + app.set('socketio', io); + + // Socket.IO connection handler + io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + socket.on("join-event-room", (eventId: string) => { + socket.join(eventId); + console.log(`Socket ${socket.id} joined room ${eventId}`); + }); + + socket.on("leave-event-room", (eventId: string) => { + socket.leave(eventId); + console.log(`Socket ${socket.id} left room ${eventId}`); + }); + + socket.on("disconnect", () => { + console.log("Client disconnected:", socket.id); + }); + }); + + // Start server + httpServer.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + }); + } catch (err) { + console.error("Failed to start server:", err); + process.exit(1); + } } start(); diff --git a/shatter-backend/src/types/express/index.d.ts b/shatter-backend/src/types/express/index.d.ts new file mode 100644 index 0000000..9725032 --- /dev/null +++ b/shatter-backend/src/types/express/index.d.ts @@ -0,0 +1,9 @@ +import { Server as SocketIOServer } from "socket.io"; + +declare global { + namespace Express { + interface Request { + io: SocketIOServer; + } + } +} diff --git a/shatter-backend/tsconfig.json b/shatter-backend/tsconfig.json index bfaac61..c997406 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -8,7 +8,8 @@ "sourceMap": true, "outDir": "./dist", "rootDir": "./", - "lib": ["ES2021"] + "lib": ["ES2021"], + "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": ["src/**/*", "api/**/*"], "exclude": ["node_modules", "dist"] From f7a2822b054015a6a157e4b4ea4616f3b0dabbcd Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:53:37 -0700 Subject: [PATCH 9/9] websocket hosting event --- shatter-web/.gitignore | 1 + shatter-web/package-lock.json | 208 ++++++++++++++++-- shatter-web/package.json | 2 + shatter-web/src/App.tsx | 18 +- .../src/components/EventCardComponent.tsx | 29 +++ shatter-web/src/main.tsx | 19 +- shatter-web/src/pages/EventPage.tsx | 142 ++++++++++++ shatter-web/src/pages/HomePage.tsx | 19 ++ shatter-web/src/service/socket.ts | 14 ++ shatter-web/vercel.json | 0 10 files changed, 421 insertions(+), 31 deletions(-) create mode 100644 shatter-web/src/components/EventCardComponent.tsx create mode 100644 shatter-web/src/pages/EventPage.tsx create mode 100644 shatter-web/src/pages/HomePage.tsx create mode 100644 shatter-web/src/service/socket.ts create mode 100644 shatter-web/vercel.json diff --git a/shatter-web/.gitignore b/shatter-web/.gitignore index 3c0740e..729af64 100644 --- a/shatter-web/.gitignore +++ b/shatter-web/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? +.vercel diff --git a/shatter-web/package-lock.json b/shatter-web/package-lock.json index e3cca73..3b52405 100644 --- a/shatter-web/package-lock.json +++ b/shatter-web/package-lock.json @@ -12,6 +12,8 @@ "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-router-dom": "^7.12.0", + "socket.io-client": "^4.8.1", "tailwindcss": "^4.1.16" }, "devDependencies": { @@ -60,7 +62,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1313,6 +1314,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", @@ -1634,7 +1641,6 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1645,7 +1651,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1706,7 +1711,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1959,7 +1963,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2078,7 +2081,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2175,6 +2177,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2238,6 +2253,45 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -2321,7 +2375,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3200,7 +3253,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3420,7 +3472,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3447,6 +3498,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3549,6 +3638,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3572,6 +3667,68 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3664,7 +3821,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3717,7 +3873,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3803,7 +3958,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3895,7 +4049,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3929,6 +4082,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/shatter-web/package.json b/shatter-web/package.json index 73143a3..85cf13d 100644 --- a/shatter-web/package.json +++ b/shatter-web/package.json @@ -14,6 +14,8 @@ "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-router-dom": "^7.12.0", + "socket.io-client": "^4.8.1", "tailwindcss": "^4.1.16" }, "devDependencies": { diff --git a/shatter-web/src/App.tsx b/shatter-web/src/App.tsx index b146904..1fd3308 100644 --- a/shatter-web/src/App.tsx +++ b/shatter-web/src/App.tsx @@ -1,15 +1,13 @@ -import { useState } from "react"; -import TestComponent from "./components/TestComponent.tsx"; -import QRCodeDisplay from "./components/QRCodeDisplay.tsx"; +import { Routes, Route } from "react-router-dom"; +import EventPage from "./pages/EventPage"; +import HomePage from "./pages/HomePage"; + function App() { return ( - <> -
-

This is the first component

- - -
- + + } /> + } /> + ); } diff --git a/shatter-web/src/components/EventCardComponent.tsx b/shatter-web/src/components/EventCardComponent.tsx new file mode 100644 index 0000000..2a72341 --- /dev/null +++ b/shatter-web/src/components/EventCardComponent.tsx @@ -0,0 +1,29 @@ +import { Link } from "react-router-dom"; + +interface EventCardComponentProps { + name: string; + joinCode: string; +} + +const EventCardComponent = ({ name, joinCode }: EventCardComponentProps) => { + return ( +
+

{name}

+ +

+ Join Code: {joinCode} +

+ +
+ + Event + +
+
+ ); +}; + +export default EventCardComponent; diff --git a/shatter-web/src/main.tsx b/shatter-web/src/main.tsx index bef5202..85a16d5 100644 --- a/shatter-web/src/main.tsx +++ b/shatter-web/src/main.tsx @@ -1,10 +1,13 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import "./index.css"; +import App from "./App.tsx"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( - - , -) + + + + +); diff --git a/shatter-web/src/pages/EventPage.tsx b/shatter-web/src/pages/EventPage.tsx new file mode 100644 index 0000000..f5297d5 --- /dev/null +++ b/shatter-web/src/pages/EventPage.tsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; + +import { socket } from "../service/socket"; + +interface Participant { + participantId: string; + userId: string; + name: string; +} + +interface EventResponse { + success: boolean; + event: { + _id: string; + participantIds: Participant[]; + }; +} + +export default function EventPage() { + const { joinCode } = useParams<{ joinCode: string }>(); + + const [participants, setParticipants] = useState([]); + const [eventId, setEventId] = useState(null); + const [loading, setLoading] = useState(true); + + async function loadEventData() { + console.log("๐Ÿ“ก Loading event data for:", joinCode); + const res = await fetch( + `http://localhost:4000/api/events/event/${joinCode}`, + { + cache: "no-store", + } + ); + const data: EventResponse = await res.json(); + console.log("๐Ÿ“ฆ Event data received:", data); + + if (data.success) { + setParticipants(data.event.participantIds); + setEventId(data.event._id); + console.log("Using eventId from API:", data.event._id); + } + setLoading(false); + } + + useEffect(() => { + loadEventData(); + }, [joinCode]); + + useEffect(() => { + if (!eventId) { + console.log("Waiting for eventId from API..."); + return; + } + + console.log("=== Setting up socket with eventId:", eventId, "==="); + + const handleConnect = () => { + console.log("๐Ÿ”Œ Socket CONNECTED, ID:", socket.id); + console.log("๐Ÿ“ค Emitting join-event-room for:", eventId); + socket.emit("join-event-room", eventId); + }; + + const handleRoomJoined = (data: any) => { + console.log("โœ… ROOM JOINED CONFIRMATION:", data); + }; + + const handleParticipantJoined = (p: Participant) => { + console.log("๐ŸŽ‰ PARTICIPANT JOINED EVENT RECEIVED"); + console.log("Type of p:", typeof p); + console.log("Value of p:", p); + console.log("JSON.stringify(p):", JSON.stringify(p)); + console.log("p.name:", p?.name); + console.log("p.participantId:", p?.participantId); + + if (!p || !p.participantId || !p.name) { + console.error("โŒ Invalid participant data received:", p); + return; + } + + setParticipants((prev) => { + if ( + prev.some( + (participant) => participant.participantId === p.participantId + ) + ) { + console.log("Participant already exists"); + return prev; + } + console.log("Adding new participant:", p); + return [...prev, p]; + }); + }; + + // Register listeners + socket.on("connect", handleConnect); + socket.on("room-joined", handleRoomJoined); + socket.on("participant-joined", handleParticipantJoined); + + // Log ALL events + socket.onAny((eventName, ...args) => { + console.log("๐Ÿ“จ Socket event received:", eventName, args); + }); + + // If already connected, join immediately + if (socket.connected) { + handleConnect(); + } else { + console.log("โš ๏ธ Socket not connected, connecting..."); + socket.connect(); + } + + return () => { + console.log("๐Ÿงน Cleanup: leaving room", eventId); + socket.off("connect", handleConnect); + socket.off("room-joined", handleRoomJoined); + socket.off("participant-joined", handleParticipantJoined); + socket.offAny(); + socket.emit("leave-event-room", eventId); + }; + }, [eventId]); // Trigger when eventId from API is set + + if (loading) { + return
Loading event...
; + } + + if (!eventId) { + return
Event not found
; + } + + return ( +
+

Participants ({participants.length})

+

Code: {joinCode}

+
    + {participants.map((p) => ( +
  • {p.name}
  • + ))} +
+
+ ); +} diff --git a/shatter-web/src/pages/HomePage.tsx b/shatter-web/src/pages/HomePage.tsx new file mode 100644 index 0000000..1026f84 --- /dev/null +++ b/shatter-web/src/pages/HomePage.tsx @@ -0,0 +1,19 @@ +import TestComponent from "../components/TestComponent"; +import QRCodeDisplay from "../components/QRCodeDisplay"; +import EventCardComponent from "../components/EventCardComponent"; + +const HomePage = () => { + return ( +
+

This is the first component

+ + + +
+ ); +}; + +export default HomePage; diff --git a/shatter-web/src/service/socket.ts b/shatter-web/src/service/socket.ts new file mode 100644 index 0000000..05c72f7 --- /dev/null +++ b/shatter-web/src/service/socket.ts @@ -0,0 +1,14 @@ +import { io } from 'socket.io-client'; + +export const socket = io('http://localhost:4000', { + autoConnect: true, + transports: ['websocket', 'polling'], +}); + +socket.on('connect', () => { + console.log('Socket connected:', socket.id); +}); + +socket.on('disconnect', () => { + console.log('Socket disconnected'); +}); \ No newline at end of file diff --git a/shatter-web/vercel.json b/shatter-web/vercel.json new file mode 100644 index 0000000..e69de29