diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index b537f61..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,7 +496,6 @@ "version": "24.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -530,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", @@ -934,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", @@ -1172,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", @@ -1288,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", @@ -2402,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", @@ -2675,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", @@ -2810,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", @@ -2860,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", @@ -2967,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", @@ -3270,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": { @@ -3362,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 11e7050..98fe109 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,16 +1,30 @@ 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'; 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'); }); 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..c22bfd6 --- /dev/null +++ b/shatter-backend/src/controllers/event_controller.ts @@ -0,0 +1,222 @@ +import { Request, Response } from "express"; +import { Event } from "../models/event_model"; +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; + + if (!createdBy) { + return res + .status(400) + .json({ success: false, error: "createdBy email is required" }); + } + + const joinCode = generateJoinCode(); + + const event = new Event({ + name, + description, + joinCode, + startDate, + endDate, + maxParticipant, + participantIds: [], + 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" }); + } + + // 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" }); + } + + res.status(200).json({ + success: true, + event, + }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +} + +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/models/event_model.ts b/shatter-backend/src/models/event_model.ts new file mode 100644 index 0000000..1e6228c --- /dev/null +++ b/shatter-backend/src/models/event_model.ts @@ -0,0 +1,54 @@ +import mongoose, { Schema, model, Document, Types } from "mongoose"; +import { User } from "../models/user_model"; + +import { IParticipant } from "./participant_model"; + +export interface IEvent extends Document { + name: string; + description: string; + joinCode: string; + startDate: Date; + endDate: Date; + maxParticipant: number; + participantIds: Schema.Types.ObjectId[]; + currentState: string; + createdBy: string; +} + +const EventSchema = new Schema( + { + 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 }, + participantIds: [{ type: Schema.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; + }, + 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..703c970 --- /dev/null +++ b/shatter-backend/src/models/participant_model.ts @@ -0,0 +1,32 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IParticipant extends Document { + userId: Schema.Types.ObjectId | null; + name: string; + eventId: Schema.Types.ObjectId; +} + +const ParticipantSchema = new Schema({ + userId: { + type: Schema.Types.ObjectId, + ref: "User", + default: null, + }, + + name: { + type: String, + ref: "User Name", + required: true, + }, + + eventId: { + type: Schema.Types.ObjectId, + ref: "Event", + 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..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); diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts new file mode 100644 index 0000000..c890daa --- /dev/null +++ b/shatter-backend/src/routes/event_routes.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; +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/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(); +} 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"] 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