From 91842646f0a04c19e3a6c4dc3f1556cdf1464c0e Mon Sep 17 00:00:00 2001 From: Abiram070207 Date: Thu, 18 Jun 2026 17:56:04 +0530 Subject: [PATCH] Merged two websites --- RIT-EMS-main/frontend/index.html | 2 + RIT-EMS-main/frontend/package-lock.json | 239 ++++++- RIT-EMS-main/frontend/package.json | 5 +- RIT-EMS-main/frontend/src/App.tsx | 20 + .../frontend/src/components/Sidebar.tsx | 3 +- .../components/dashboard/StudentDashboard.tsx | 529 +++++++++++++++ .../dashboard/StudentRegistrationsView.tsx | 622 ++++++++++++++++++ .../components/student/AboutHubSection.tsx | 73 ++ .../src/components/student/AboutSection.tsx | 134 ++++ .../student/AccreditationsSection.tsx | 34 + .../src/components/student/CategoryGrid.tsx | 107 +++ .../src/components/student/ContactSection.tsx | 114 ++++ .../components/student/DomainSelection.tsx | 65 ++ .../src/components/student/EventCard.tsx | 547 +++++++++++++++ .../src/components/student/EventList.tsx | 126 ++++ .../components/student/EventsHeroSlider.tsx | 180 +++++ .../src/components/student/Footer.tsx | 167 +++++ .../frontend/src/components/student/Hero.tsx | 49 ++ .../src/components/student/HomeDashboard.tsx | 164 +++++ .../src/components/student/Navbar.tsx | 93 +++ .../components/student/PortalAnimation.tsx | 109 +++ .../src/components/student/ProfileView.tsx | 426 ++++++++++++ .../components/student/RegistrationsView.tsx | 413 ++++++++++++ .../student/SpecialEventsBanner.tsx | 183 ++++++ .../src/components/student/StatsSection.tsx | 101 +++ .../components/student/StatusTrackerView.tsx | 583 ++++++++++++++++ .../student/UpcomingEventsSlider.tsx | 163 +++++ .../src/components/student/constants.tsx | 199 ++++++ .../frontend/src/context/AuthContext.tsx | 8 +- RIT-EMS-main/frontend/src/index.css | 98 +++ .../frontend/src/lib/firebaseBackend.ts | 280 ++++++++ RIT-EMS-main/frontend/src/lib/pdfGenerator.ts | 153 +++++ RIT-EMS-main/frontend/src/pages/LoginPage.tsx | 602 +++++++++++++---- RIT-EMS-main/frontend/src/types.ts | 153 +++++ 34 files changed, 6613 insertions(+), 131 deletions(-) create mode 100644 RIT-EMS-main/frontend/src/components/dashboard/StudentDashboard.tsx create mode 100644 RIT-EMS-main/frontend/src/components/dashboard/StudentRegistrationsView.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/AboutHubSection.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/AboutSection.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/AccreditationsSection.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/CategoryGrid.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/ContactSection.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/DomainSelection.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/EventCard.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/EventList.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/EventsHeroSlider.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/Footer.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/Hero.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/HomeDashboard.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/Navbar.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/PortalAnimation.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/ProfileView.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/RegistrationsView.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/SpecialEventsBanner.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/StatsSection.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/StatusTrackerView.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/UpcomingEventsSlider.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/constants.tsx create mode 100644 RIT-EMS-main/frontend/src/lib/pdfGenerator.ts create mode 100644 RIT-EMS-main/frontend/src/types.ts diff --git a/RIT-EMS-main/frontend/index.html b/RIT-EMS-main/frontend/index.html index 46fd13b..34ce75a 100644 --- a/RIT-EMS-main/frontend/index.html +++ b/RIT-EMS-main/frontend/index.html @@ -5,6 +5,8 @@ RIT EMS - Event Management System + +
diff --git a/RIT-EMS-main/frontend/package-lock.json b/RIT-EMS-main/frontend/package-lock.json index a749584..1f67ebc 100644 --- a/RIT-EMS-main/frontend/package-lock.json +++ b/RIT-EMS-main/frontend/package-lock.json @@ -13,7 +13,10 @@ "date-fns": "^4.1.0", "firebase": "^12.15.0", "framer-motion": "^12.38.0", - "lucide-react": "^1.8.0", + "html-to-image": "^1.11.13", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "lucide-react": "^0.454.0", "react": "^19.2.5", "react-dom": "^19.2.5", "tailwind-merge": "^3.5.0", @@ -244,6 +247,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1897,6 +1909,19 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1917,6 +1942,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -2368,6 +2400,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", @@ -2457,6 +2498,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -2551,6 +2612,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -2578,6 +2651,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2630,6 +2712,16 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.343", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", @@ -2884,6 +2976,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -2914,6 +3017,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3145,6 +3254,25 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-parser-js": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", @@ -3194,6 +3322,12 @@ "node": ">=0.8.19" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3310,6 +3444,23 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3641,12 +3792,12 @@ } }, "node_modules/lucide-react": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", - "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", + "version": "0.454.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz", + "integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "node_modules/magic-string": { @@ -3777,6 +3928,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3810,6 +3967,13 @@ "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3909,6 +4073,16 @@ "node": ">=6" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/re2js": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/re2js/-/re2js-0.4.3.tgz", @@ -3936,6 +4110,13 @@ "react": "^19.2.5" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3955,6 +4136,16 @@ "node": ">=4" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", @@ -4077,6 +4268,16 @@ "node": ">=0.8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4129,6 +4330,16 @@ "node": ">=8" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -4160,6 +4371,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -4294,6 +4514,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", diff --git a/RIT-EMS-main/frontend/package.json b/RIT-EMS-main/frontend/package.json index c269d48..cf7debc 100644 --- a/RIT-EMS-main/frontend/package.json +++ b/RIT-EMS-main/frontend/package.json @@ -15,7 +15,10 @@ "date-fns": "^4.1.0", "firebase": "^12.15.0", "framer-motion": "^12.38.0", - "lucide-react": "^1.8.0", + "html-to-image": "^1.11.13", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "lucide-react": "^0.454.0", "react": "^19.2.5", "react-dom": "^19.2.5", "tailwind-merge": "^3.5.0", diff --git a/RIT-EMS-main/frontend/src/App.tsx b/RIT-EMS-main/frontend/src/App.tsx index 051cd33..a7e14a3 100644 --- a/RIT-EMS-main/frontend/src/App.tsx +++ b/RIT-EMS-main/frontend/src/App.tsx @@ -20,6 +20,8 @@ import { ClubInstitutionalChecklist } from './components/dashboard/ClubInstituti import { AutomationView } from './components/dashboard/AutomationView'; import { Overview } from './components/dashboard/Overview'; import { ExcelImport } from './components/dashboard/ExcelImport'; +import { StudentDashboard } from './components/dashboard/StudentDashboard'; +import { StudentRegistrationsView } from './components/dashboard/StudentRegistrationsView'; const AppContent: React.FC = () => { const { isAuthenticated, user } = useAuth(); @@ -104,6 +106,22 @@ const AppContent: React.FC = () => { ); } + if (user?.role === 'STUDENT') { + return ( + + + + + + ); + } + const renderContent = () => { const isDashboard = ( { return (user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? : isDashboard; case 'import-excel': return ; + case 'student-registrations': + return (user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? : isDashboard; case 'dashboard': return isDashboard; default: diff --git a/RIT-EMS-main/frontend/src/components/Sidebar.tsx b/RIT-EMS-main/frontend/src/components/Sidebar.tsx index 14dee55..69aa745 100644 --- a/RIT-EMS-main/frontend/src/components/Sidebar.tsx +++ b/RIT-EMS-main/frontend/src/components/Sidebar.tsx @@ -38,7 +38,8 @@ export const Sidebar: React.FC = ({ activeItem, onItemClick }) => { icon: HistoryIcon, label: 'History', id: 'history' }, ...((user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? [{ icon: Zap, label: 'Automation', id: 'automation' }] : []), ...(user?.role === 'ADMIN' ? [{ icon: BookOpen, label: 'Classes', id: 'classes' }] : []), - ...(user?.role === 'ADMIN' ? [{ icon: Users, label: 'Manage Users', id: 'user-management' }] : []) + ...(user?.role === 'ADMIN' ? [{ icon: Users, label: 'Manage Users', id: 'user-management' }] : []), + ...((user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? [{ icon: Users, label: 'Student Hub', id: 'student-registrations' }] : []) ]; return ( diff --git a/RIT-EMS-main/frontend/src/components/dashboard/StudentDashboard.tsx b/RIT-EMS-main/frontend/src/components/dashboard/StudentDashboard.tsx new file mode 100644 index 0000000..05a2ad0 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/StudentDashboard.tsx @@ -0,0 +1,529 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import type { UserRole, DashboardView, Event, Announcement, SpecialEvent } from '../../types'; +import { useAuth } from '../../context/AuthContext'; +import { API_BASE_URL } from '../../lib/config'; + +// Import Site 2 components +import Navbar from '../student/Navbar'; +import Hero from '../student/Hero'; +import UpcomingEventsSlider from '../student/UpcomingEventsSlider'; +import AboutHubSection from '../student/AboutHubSection'; +import StatsSection from '../student/StatsSection'; +import HomeDashboard from '../student/HomeDashboard'; +import AboutSection from '../student/AboutSection'; +import ContactSection from '../student/ContactSection'; +import EventList from '../student/EventList'; +import RegistrationsView from '../student/RegistrationsView'; +import ProfileView from '../student/ProfileView'; +import StatusTrackerView from '../student/StatusTrackerView'; +import Footer from '../student/Footer'; + +const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return createPortal(children, document.body); +}; + +export const StudentDashboard: React.FC = () => { + const { user, logout } = useAuth(); + + // Tab Routing + const [activeView, setActiveView] = useState('HOME'); + const [selectedCategory, setSelectedCategory] = useState(null); + const [trackedEvent, setTrackedEvent] = useState(null); + + // States + const [rawEvents, setRawEvents] = useState([]); + const [announcements, setAnnouncements] = useState([]); + const [allRegistrations, setAllRegistrations] = useState([]); + const [specialEvents, setSpecialEvents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // Team Modals + const [showCreateModal, setShowCreateModal] = useState<{eventId: string, title: string} | null>(null); + const [showJoinModal, setShowJoinModal] = useState<{eventId: string, title: string} | null>(null); + const [teamName, setTeamName] = useState(''); + const [teamCode, setTeamCode] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + + // Map raw Firebase event object to Site 2 Event structure + const mapDbToEvent = useCallback((dbEvent: any, registrations: any[] = []): Event => { + const eventRegs = registrations.filter(r => String(r.eventId || r.event_id) === String(dbEvent.id)); + const deptCounts: Record = {}; + const deptSectionCounts: Record> = {}; + + eventRegs.forEach(reg => { + const dept = reg.dept || reg.department; + if (dept) { + deptCounts[dept] = (deptCounts[dept] || 0) + 1; + if (reg.section) { + if (!deptSectionCounts[dept]) { + deptSectionCounts[dept] = {}; + } + deptSectionCounts[dept][reg.section] = (deptSectionCounts[dept][reg.section] || 0) + 1; + } + } + }); + + return { + id: String(dbEvent.id), + title: dbEvent.title || 'Untitled Event', + location: dbEvent.location || dbEvent.venue || 'TBD', + date: dbEvent.startDate || dbEvent.date || new Date().toISOString(), + category: dbEvent.category || 'TECHNICAL', + domain: dbEvent.domain || dbEvent.department || '', + pricingType: dbEvent.hasRegistrationFee ? 'PAID' : 'FREE', + coordinator: dbEvent.proposer?.fullName || dbEvent.coordinator || 'Faculty Coordinator', + image: dbEvent.image || 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=800', + status: dbEvent.status || 'APPROVED', + maxParticipants: dbEvent.maxParticipants || dbEvent.max_participants, + registrationDeadline: dbEvent.registrationDeadline || dbEvent.endDate, + durationDays: dbEvent.durationDays || 1, + club: dbEvent.club || '', + deptLimits: dbEvent.deptLimits || {}, + deptSectionLimits: dbEvent.deptSectionLimits || {}, + event_summary: dbEvent.description || dbEvent.event_summary || '', + isTeamEvent: !!dbEvent.isTeamEvent, + teamSizeLimit: dbEvent.teamSizeLimit || dbEvent.team_size_limit, + teamComposition: dbEvent.teamComposition || dbEvent.team_composition, + currentParticipants: eventRegs.length, + currentDeptCounts: deptCounts, + currentDeptSectionCounts: deptSectionCounts, + created_by: dbEvent.proposer?.email || dbEvent.created_by || '', + participantType: dbEvent.participantType || 'BOTH', + verificationStatus: dbEvent.status === 'COMPLETED' ? 'APPROVED' : 'APPROVED' + }; + }, []); + + // Sync data from Firestore + const fetchData = async () => { + if (!user) return; + setIsLoading(true); + try { + const [eventsRes, regsRes, annRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/events`), + fetch(`${API_BASE_URL}/api/registrations`), + fetch(`${API_BASE_URL}/api/announcements`) + ]); + + let eventsData = []; + let regsData = []; + let annData = []; + + if (eventsRes.ok) eventsData = await eventsRes.json(); + if (regsRes.ok) regsData = await regsRes.json(); + if (annRes.ok) annData = await annRes.json(); + + setRawEvents(eventsData); + setAllRegistrations(regsData); + setAnnouncements(annData); + + // Fetch special events safely (defaulting to empty array if not supported) + try { + const seRes = await fetch(`${API_BASE_URL}/api/special-events`); + if (seRes.ok) { + const seData = await seRes.json(); + setSpecialEvents(seData); + } + } catch { + setSpecialEvents([]); + } + } catch (err) { + console.error("Failed to fetch student dashboard data from Firestore:", err); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [user?.id]); + + // Compute computed lists + const events = useMemo(() => { + const list = rawEvents.map(e => mapDbToEvent(e, allRegistrations)); + // Students only see live (approved / ongoing / completed) events + return list.filter(e => e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing' || e.status === 'ONGOING'); + }, [rawEvents, allRegistrations, mapDbToEvent]); + + const userRegistrations = useMemo(() => { + return allRegistrations.filter(r => String(r.userId || r.user_id) === String(user?.id)); + }, [allRegistrations, user?.id]); + + const bookedEventIds = useMemo(() => { + return userRegistrations.map(r => String(r.eventId || r.event_id)); + }, [userRegistrations]); + + // Actions + const handleToggleBooking = async (eventId: string) => { + if (!user) return; + const isBooked = bookedEventIds.includes(eventId); + setIsProcessing(true); + + try { + if (isBooked) { + // Cancel registration + const regId = `${user.id}_${eventId}`; + const res = await fetch(`${API_BASE_URL}/api/registrations/${regId}`, { + method: 'DELETE' + }); + if (res.ok) { + await fetchData(); + } else { + alert("Failed to cancel registration."); + } + } else { + // Block booking if there's an unfinalized completed event (missing proof or missing OD approval) + const unfinalized = userRegistrations.find(reg => { + const ev = events.find(e => String(e.id) === String(reg.eventId || reg.event_id)); + if (ev && ev.status === 'COMPLETED') { + const hasUploadedCert = !!(reg.certificationUrl || reg.certification_url); + const hasOd = !!(reg.odUrl || reg.od_url); + return !hasUploadedCert || !hasOd; + } + return false; + }); + + if (unfinalized) { + const ev = events.find(e => String(e.id) === String(unfinalized.eventId || unfinalized.event_id)); + alert(`Registration Blocked: You must upload your certificate and obtain OD approval for your completed event "${ev?.title || 'Past Event'}" before registering for another event.`); + setIsProcessing(false); + return; + } + + const targetEvent = events.find(e => String(e.id) === String(eventId)); + if (!targetEvent) return; + + // Register booking + const payload = { + userId: user.id, + user_id: user.id, + eventId: Number(eventId), + event_id: Number(eventId), + userEmail: user.email, + userName: user.fullName, + regNo: user.regNo, + phone: user.phone, + gender: user.gender, + dept: user.department, + section: user.section, + year: user.year, + college: user.collegeName || 'Rajalakshmi Institute of Technology' + }; + + const res = await fetch(`${API_BASE_URL}/api/registrations`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + await fetchData(); + setActiveView('REGISTRATIONS'); + } else { + const err = await res.json(); + alert(err.message || "Registration failed."); + } + } + } catch (err) { + console.error("Booking handler error:", err); + } finally { + setIsProcessing(false); + } + }; + + const generateCode = () => Math.random().toString(36).substring(2, 8).toUpperCase(); + + const handleCreateTeam = async (eventId: string) => { + if (!teamName.trim()) return alert("Enter team name"); + setIsProcessing(true); + try { + const code = generateCode(); + const registrationId = `${user?.id}_${eventId}`; + const res = await fetch(`${API_BASE_URL}/api/registrations/${registrationId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + teamCode: code, + teamName: teamName.trim(), + isTeamLeader: true + }) + }); + + if (!res.ok) throw new Error("Failed to create team"); + + setShowCreateModal(null); + setTeamName(''); + await fetchData(); + } catch (err) { + console.error(err); + alert("Failed to form team"); + } finally { + setIsProcessing(false); + } + }; + + const handleJoinTeam = async (eventId: string) => { + if (!teamCode.trim()) return alert("Enter team code"); + setIsProcessing(true); + try { + // Fetch team registrations to validate + const res = await fetch(`${API_BASE_URL}/api/registrations?eventId=${eventId}&teamCode=${teamCode.trim().toUpperCase()}`); + if (!res.ok) throw new Error("Failed to fetch team details"); + const teamRegs = await res.json(); + + if (!teamRegs || teamRegs.length === 0) { + alert("Invalid team code for this event."); + setIsProcessing(false); + return; + } + + const leader = teamRegs.find((r: any) => r.isTeamLeader || r.is_team_leader); + const eventVal = events.find(e => String(e.id) === String(eventId)); + + if (eventVal?.teamSizeLimit && teamRegs.length >= eventVal.teamSizeLimit) { + alert("Team is already full."); + setIsProcessing(false); + return; + } + + if (eventVal?.teamComposition === 'INTER_DEPT' && leader && user?.department !== (leader.dept || leader.department)) { + alert(`This event requires INTER-DEPARTMENT teams. You must join a team from department ${(leader.dept || leader.department)}`); + setIsProcessing(false); + return; + } + + const registrationId = `${user?.id}_${eventId}`; + const joinRes = await fetch(`${API_BASE_URL}/api/registrations/${registrationId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + teamCode: teamCode.trim().toUpperCase(), + teamName: leader?.teamName || leader?.team_name || 'Team', + isTeamLeader: false + }) + }); + + if (!joinRes.ok) throw new Error("Join failed"); + + setShowJoinModal(null); + setTeamCode(''); + await fetchData(); + } catch (err) { + console.error(err); + alert("Failed to join team"); + } finally { + setIsProcessing(false); + } + }; + + const handleUploadCertificate = async (eventId: string, base64: string): Promise => { + const regId = `${user?.id}_${eventId}`; + const res = await fetch(`${API_BASE_URL}/api/registrations/${regId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + certificationUrl: base64, + certificationStatus: 'PENDING_APPROVAL' + }) + }); + if (!res.ok) throw new Error("Failed to upload screenshot proof"); + await fetchData(); + return base64; + }; + + const handleTrackStatus = (event: Event) => { + setTrackedEvent(event); + setActiveView('STATUS_TRACKER'); + }; + + // Render Sub View + const renderContent = () => { + switch (activeView) { + case 'HOME': + return ( + <> + + + + + setActiveView('EVENTS')} + specialEvents={specialEvents} + /> + + ); + case 'ABOUT': + return ( +
+ +
+ ); + case 'CONTACT': + return ( +
+ +
+ ); + case 'EVENTS': + return ( + + ); + case 'REGISTRATIONS': + return ( + + ); + case 'PROFILE': + return ; + case 'STATUS_TRACKER': + const registration = userRegistrations.find(r => String(r.eventId || r.event_id) === String(trackedEvent?.id)); + return trackedEvent ? ( + setActiveView('REGISTRATIONS')} + onUploadCertificate={(data) => handleUploadCertificate(trackedEvent.id, data)} + onShowCreateTeam={() => setShowCreateModal({ eventId: trackedEvent.id, title: trackedEvent.title })} + onShowJoinTeam={() => setShowJoinModal({ eventId: trackedEvent.id, title: trackedEvent.title })} + /> + ) : null; + default: + return null; + } + }; + + if (isLoading) { + return ( +
+
+

Syncing with Firestore Database...

+
+ ); + } + + return ( +
+ { + setActiveView(view); + setTrackedEvent(null); + }} + onLogout={logout} + /> +
+ {renderContent()} +
+
+ + {/* Team Management Modals */} + {showCreateModal && ( + +
+
+
+
+ RIT Logo +
+

Form a Team

+

{showCreateModal.title}

+ +
+
+ + setTeamName(e.target.value)} + autoFocus + /> +
+ +
+ + +
+
+
+
+
+ )} + + {showJoinModal && ( + +
+
+
+
+ RIT Logo +
+

Join Alliance

+

{showJoinModal.title}

+ +
+
+ + setTeamCode(e.target.value)} + maxLength={6} + autoFocus + /> +
+ +
+ + +
+
+
+
+
+ )} +
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/StudentRegistrationsView.tsx b/RIT-EMS-main/frontend/src/components/dashboard/StudentRegistrationsView.tsx new file mode 100644 index 0000000..26b8c11 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/StudentRegistrationsView.tsx @@ -0,0 +1,622 @@ +import { API_BASE_URL } from '../../lib/config'; +import React, { useState, useEffect } from 'react'; +import { + Users, + CheckSquare, + Upload, + FileCheck, + XCircle, + CheckCircle, + FileText, + Search, + ChevronRight, + Eye, + Award +} from 'lucide-react'; +import { useAuth } from '../../context/AuthContext'; +import { cn } from '../../lib/utils'; + +interface Event { + id: number; + title: string; + startDate: string; + endDate: string; + type: string; + location: string; + category: string; + department: string; + status: string; +} + +interface Registration { + id: string; + userId: string; + eventId: number; + userName: string; + userEmail: string; + regNo: string; + phone: string; + gender: string; + dept: string; + section: string; + year: string; + college: string; + paymentStatus: 'PENDING' | 'COMPLETED'; + teamCode: string | null; + teamName: string | null; + isTeamLeader: boolean; + certificationUrl: string | null; + certificationStatus: 'NOT_SUBMITTED' | 'PENDING_APPROVAL' | 'APPROVED'; + odUrl: string | null; +} + +export const StudentRegistrationsView: React.FC = () => { + const { user } = useAuth(); + const [events, setEvents] = useState([]); + const [selectedEvent, setSelectedEvent] = useState(null); + const [registrations, setRegistrations] = useState([]); + const [attendanceLogs, setAttendanceLogs] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // Search and view states + const [searchQuery, setSearchQuery] = useState(''); + const [activeSubTab, setActiveSubTab] = useState<'roster' | 'attendance' | 'od' | 'certificates'>('roster'); + + // Audit modals + const [inspectCertReg, setInspectCertReg] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + + // OD Upload states + const [odFileBase64, setOdFileBase64] = useState(null); + + // Attendance checking state (maps registrationId -> dayLabel -> isPresent) + const [attendanceGrid, setAttendanceGrid] = useState>>({}); + + const daysList = ["Day 1", "Day 2", "Day 3"]; // Standard slots + + useEffect(() => { + fetchEvents(); + }, []); + + useEffect(() => { + if (selectedEvent) { + fetchRegistrations(selectedEvent.id); + } + }, [selectedEvent]); + + const fetchEvents = async () => { + setIsLoading(true); + try { + const res = await fetch(API_BASE_URL + '/api/events'); + if (res.ok) { + const data = await res.json(); + // Faculty can manage their proposed events, Admins/HODs can manage all + let filtered = data.filter((e: Event) => e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing'); + if (user?.role === 'FACULTY') { + filtered = filtered.filter((e: Event) => e.department === user.department); + } + setEvents(filtered); + if (filtered.length > 0) { + setSelectedEvent(filtered[0]); + } + } + } catch (err) { + console.error(err); + } finally { + setIsLoading(false); + } + }; + + const fetchRegistrations = async (eventId: number) => { + setIsLoading(true); + try { + const [regsRes, attRes] = await Promise.all([ + fetch(API_BASE_URL + `/api/registrations?eventId=${eventId}`), + fetch(API_BASE_URL + `/api/attendance?eventId=${eventId}`) + ]); + + if (regsRes.ok && attRes.ok) { + const regsData = await regsRes.json(); + const attData = await attRes.json(); + + setRegistrations(regsData); + setAttendanceLogs(attData); + + // Pre-populate attendance grid from attendanceLogs + const grid: Record> = {}; + regsData.forEach((r: Registration) => { + grid[r.id] = {}; + daysList.forEach(day => { + const match = attData.find((a: any) => String(a.registrationId || a.registration_id) === String(r.id) && a.dayLabel === day); + grid[r.id][day] = match ? !!match.isPresent : false; + }); + }); + setAttendanceGrid(grid); + } + } catch (err) { + console.error(err); + } finally { + setIsLoading(false); + } + }; + + // Toggle Payment Status Manually + const handleTogglePayment = async (reg: Registration) => { + setIsProcessing(true); + const newStatus = reg.paymentStatus === 'COMPLETED' ? 'PENDING' : 'COMPLETED'; + try { + const res = await fetch(`${API_BASE_URL}/api/registrations/${reg.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paymentStatus: newStatus }) + }); + if (res.ok) { + await fetchRegistrations(selectedEvent!.id); + } + } catch (err) { + console.error(err); + } finally { + setIsProcessing(false); + } + }; + + // Toggle Attendance Cell local state + const handleToggleAttendance = (regId: string, day: string) => { + setAttendanceGrid(prev => ({ + ...prev, + [regId]: { + ...prev[regId], + [day]: !prev[regId][day] + } + })); + }; + + // Save Attendance Grid to Firestore + const handleSaveAttendance = async () => { + setIsProcessing(true); + try { + const payload: any[] = []; + Object.keys(attendanceGrid).forEach(regId => { + daysList.forEach(day => { + payload.push({ + registrationId: regId, + eventId: selectedEvent!.id, + dayLabel: day, + batchLabel: "Slot 1", + isPresent: attendanceGrid[regId][day], + date: new Date().toLocaleDateString() + }); + }); + }); + + const res = await fetch(API_BASE_URL + '/api/attendance', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + alert("Attendance records synchronized successfully!"); + await fetchRegistrations(selectedEvent!.id); + } else { + alert("Failed to save attendance."); + } + } catch (err) { + console.error(err); + } finally { + setIsProcessing(false); + } + }; + + // Handle OD PDF File selection + const handleODFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onloadend = () => { + setOdFileBase64(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + // Upload OD File to all registered students + const handleUploadODFile = async () => { + if (!odFileBase64) return alert("Please select an On-Duty file first."); + setIsProcessing(true); + try { + // Loop over and update all registrations for this event + for (const reg of registrations) { + await fetch(`${API_BASE_URL}/api/registrations/${reg.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ odUrl: odFileBase64 }) + }); + } + alert("OD sheet broadcasted to all registered students successfully!"); + setOdFileBase64(null); + await fetchRegistrations(selectedEvent!.id); + } catch (err) { + console.error(err); + } finally { + setIsProcessing(false); + } + }; + + // Approve or Reject screenshot proof + const handleAuditCertificate = async (regId: string, status: 'APPROVED' | 'NOT_SUBMITTED') => { + setIsProcessing(true); + try { + const res = await fetch(`${API_BASE_URL}/api/registrations/${regId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ certificationStatus: status }) + }); + + if (res.ok) { + setInspectCertReg(null); + alert(status === 'APPROVED' ? "Certificate approved!" : "Certificate rejected."); + await fetchRegistrations(selectedEvent!.id); + } + } catch (err) { + console.error(err); + } finally { + setIsProcessing(false); + } + }; + + const filteredRoster = registrations.filter(r => + r.userName.toLowerCase().includes(searchQuery.toLowerCase()) || + r.regNo.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( +
+ {/* Header Info */} +
+
+

Student Engagement Hub

+

Review registration rosters, log session attendance checklists, release OD files, and audit certificate proofs.

+
+ + {/* Event Selector Dropdown */} +
+ + +
+
+ + {selectedEvent ? ( +
+ {/* Quick Metrics Panels */} +
+
+ Passes Secured +

{registrations.length}

+
+
+ Teamed Up +

{registrations.filter(r => r.teamCode).length}

+
+
+ In Review Proofs +

{registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').length}

+
+
+ Verified Certificates +

{registrations.filter(r => r.certificationStatus === 'APPROVED').length}

+
+
+ + {/* Sub Navigation Tabs */} +
+ + + + +
+ + {/* Sub Tab Contents */} +
+ + {/* SUB TAB 1: ROSTER & MANUAL FEE */} + {activeSubTab === 'roster' && ( +
+
+

Registration Roster

+ + {/* Search Roster */} +
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ + + + + + + + + + + + {filteredRoster.length === 0 ? ( + + + + ) : ( + filteredRoster.map((reg) => ( + + + + + + + + )) + )} + +
Student DetailsClassCollegeAlliance/TeamPayment Status
No registered students matched the search.
+

{reg.userName}

+

{reg.regNo} | {reg.phone}

+
+ {reg.year} Year / SEC {reg.section} +

{reg.dept}

+
{reg.college} + {reg.teamCode ? ( +
+

{reg.teamName}

+

Code: {reg.teamCode}

+
+ ) : ( + Individual + )} +
+ +
+
+
+ )} + + {/* SUB TAB 2: SESSION ATTENDANCE GRID */} + {activeSubTab === 'attendance' && ( +
+
+

Mark Slot Presence

+ +
+ +
+ + + + + + {daysList.map(day => )} + + + + {registrations.length === 0 ? ( + + + + ) : ( + registrations.map((reg) => ( + + + + {daysList.map(day => ( + + ))} + + )) + )} + +
StudentRegister No{day}
No students registered yet.
{reg.userName}{reg.regNo} + handleToggleAttendance(reg.id, day)} + className="w-4.5 h-4.5 rounded border-slate-300 text-brand-indigo focus:ring-brand-indigo cursor-pointer" + /> +
+
+
+ )} + + {/* SUB TAB 3: ON-DUTY UPLOADER */} + {activeSubTab === 'od' && ( +
+
+

On-Duty Official Release

+

Upload the unified signed On-Duty letter for this event. It will be released to all registered students automatically.

+
+ +
+ +
+ + +
+ {odFileBase64 && ( +

File captured ready to broadcast

+ )} +
+ + +
+ )} + + {/* SUB TAB 4: CERTIFICATE AUDITOR */} + {activeSubTab === 'certificates' && ( +
+
+

Audit Student Proofs

+

Inspect uploaded completion screenshot proofs and approve them to unlock certificate downloads.

+
+ +
+ {registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').length === 0 ? ( +
+ + All certificate proofs have been successfully audited! +
+ ) : ( + registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').map((reg) => ( +
+
+

{reg.userName}

+

{reg.regNo} | {reg.dept}

+
+ +
+ )) + )} +
+
+ )} + +
+
+ ) : ( +
+ +

No Live Events Available

+

Create/approve events in your main dashboard first to access student hub audits.

+
+ )} + + {/* INSPECT SCREENSHOT MODAL */} + {inspectCertReg && ( +
+
+
+
+

Inspect Screenshot

+

{inspectCertReg.userName} | {inspectCertReg.regNo}

+
+ +
+ + {/* Proof Body */} +
+ {inspectCertReg.certificationUrl ? ( + Completion proof + ) : ( +

No proof uploaded

+ )} +
+ + {/* Actions Bar */} +
+ + +
+
+
+ )} +
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/student/AboutHubSection.tsx b/RIT-EMS-main/frontend/src/components/student/AboutHubSection.tsx new file mode 100644 index 0000000..b01c8df --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/AboutHubSection.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +const AboutHubSection: React.FC = () => { + return ( +
+
+ + {/* Left Side: Image & Floating Card */} +
+ + + {/* Floating Card */} + +

+ INNOVATION +

+

+ Where creativity meets execution. Fueling the future of tech. +

+
+
+ + {/* Right Side: Content */} +
+ +

+ Our Legacy +

+

+ What is RIT EVENTS HUB? +

+
+ + + The RIT Events Hub is your centralized gateway to campus life, designed to simplify how you discover and participate in events. Whether it's a technical hackathon, a cultural fest, or a workshop, our platform brings everything to your fingertips. With seamless registration, real-time updates, and personalized recommendations, we ensure you never miss an opportunity to learn, compete, and grow. Join a community where accessing innovation is as easy as a single click. + +
+ +
+
+ ); +}; + +export default AboutHubSection; + diff --git a/RIT-EMS-main/frontend/src/components/student/AboutSection.tsx b/RIT-EMS-main/frontend/src/components/student/AboutSection.tsx new file mode 100644 index 0000000..ee7e1dc --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/AboutSection.tsx @@ -0,0 +1,134 @@ +import React from 'react'; + +const AboutSection: React.FC = () => { + return ( +
+
+
+

+ ABOUT + +

+

+ Rajalakshmi Institute of Technology (RIT) +

+
+ +
+
+

+ Rajalakshmi Institute of Technology ( An Autonomous Institution) is one of the best engineering colleges in Chennai and is part of Rajalakshmi Institutions, which has been synonymous with providing excellence in higher education to students for many years. +

+

+ Rajalakshmi Institute of Technology was established in 2008. RIT is accredited with highest grade of A++ by NAAC. RIT is affiliated with Anna University Chennai. It is one of the AICTE-approved colleges in Chennai New Delhi, and also offers NBA-approved courses. +

+
+
+ Rajalakshmi Institute of Technology +
+
+ + {/* Graduate Programmes Section */} +
+

+ Graduate Programmes offered +

+ +
+
+ RIT Students +
+ +
+
+

UG Programmes

+
    +
  • B.E. Computer Science & Engineering
  • +
  • B.E. Computer Science & Engineering(AI&ML)
  • +
  • B.E. Computer & Communication Engineering
  • +
  • B.E. Electronics & Communication Engineering
  • +
  • B.E. Mechanical Engineering
  • +
  • B.E. Electronic Engineering (VLSI)
  • +
  • B.Tech. Artificial Intelligence & Data Science
  • +
  • B.Tech. Computer Science and Business Systems
  • +
  • B.Tech Bio Technology
  • +
+
+ +
+

PG Programmes

+
    +
  • M.E. Electronics and Communication Engineering (VLSI Design)
  • +
+
+ +
+

Anna University Approved Research Institute

+

+ Ph.D. Programmes are offered across all Engineering, Technology, Science & Humanities disciplines +

+
+
+
+
+ + {/* Campus Life & Events Section */} +
+

+ Campus Life & Events +

+ +
+ {/* Left Column: YouTube Videos */} +
+
+ +
+
+ +
+
+ + {/* Right Column: Instagram Post */} +
+ +
+
+
+
+
+ ); +}; + +export default AboutSection; + diff --git a/RIT-EMS-main/frontend/src/components/student/AccreditationsSection.tsx b/RIT-EMS-main/frontend/src/components/student/AccreditationsSection.tsx new file mode 100644 index 0000000..e68e87a --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/AccreditationsSection.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const AccreditationsSection: React.FC = () => { + // No longer needed: list of individual logos replaced by a single unified picture. + // const accreditations = [...]; + + return ( +
+
+ +
+
+

Our Strategic Partners

+

Global Accreditations & Collaborations

+
+
+ +
+
+ Global Accreditations & Collaborations +
+
+
+
+ ); +}; + +export default AccreditationsSection; + diff --git a/RIT-EMS-main/frontend/src/components/student/CategoryGrid.tsx b/RIT-EMS-main/frontend/src/components/student/CategoryGrid.tsx new file mode 100644 index 0000000..78f03b0 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/CategoryGrid.tsx @@ -0,0 +1,107 @@ + +import React from 'react'; +import { CATEGORIES } from './constants'; + +interface CategoryGridProps { + onSelectCategory: (id: string) => void; + title?: string; +} + +const CategoryGrid: React.FC = ({ onSelectCategory }) => { + const threeCategories = CATEGORIES.filter(c => c.id !== 'CENTRE-ACTIVITY'); + const centreActivityCategory = CATEGORIES.find(c => c.id === 'CENTRE-ACTIVITY'); + + return ( +
+

+ Discover by Category +

+ + {/* Three Standard Categories */} +
+ {threeCategories.map((cat) => ( +
onSelectCategory(cat.id)} + className="group relative h-[380px] md:h-[500px] rounded-[3rem] overflow-hidden cursor-pointer shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-700 hover:scale-[1.02] hover:shadow-[0_40px_80px_rgba(0,0,0,0.25)]" + > + {/* Background Image with Zoom Effect */} + {cat.name} + + {/* Premium Multi-layer Overlay */} +
+
+ + {/* Border Glow on Hover */} +
+ + {/* Rich Typography and Content */} +
+
+ + Explore Events + +

+ {cat.name} +

+
+
+ +
+ +
+
+
+ ))} +
+ + {/* Centre Activity Wide Card */} + {centreActivityCategory && ( +
+
onSelectCategory(centreActivityCategory.id)} + className="group relative h-[320px] md:h-[420px] rounded-[3.5rem] overflow-hidden cursor-pointer shadow-[0_20px_50px_rgba(0,0,0,0.15)] transition-all duration-700 hover:scale-[1.01] hover:shadow-[0_40px_80px_rgba(0,0,0,0.3)] border-[3px] border-transparent hover:border-white/20" + > + {/* Background Image with Zoom Effect */} + {centreActivityCategory.name} + + {/* Overlays */} +
+
+ + {/* Rich Typography and Content */} +
+
+ + Special Hub Activities + +

+ {centreActivityCategory.name} +

+

+ Engage in multidisciplinary innovation, advanced research hubs, incubation cells, and collaborative special projects driving academic excellence. +

+
+
+ +
+ +
+
+
+
+ )} +
+ ); +}; + +export default CategoryGrid; + diff --git a/RIT-EMS-main/frontend/src/components/student/ContactSection.tsx b/RIT-EMS-main/frontend/src/components/student/ContactSection.tsx new file mode 100644 index 0000000..f94e1a4 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/ContactSection.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { MapPin, Phone, Mail, Building } from 'lucide-react'; + +const ContactSection: React.FC = () => { + return ( +
+
+
+

+ CONTACT US + +

+

+ Get in Touch +

+
+ +
+ {/* Contact Info */} +
+ {/* College Address */} +
+
+
+ +
+

College

+
+ +
+
+ +

+ Rajalakshmi Institute of Technology,
+ Poonamallee, Chennai - 600 124. +

+
+ +
+ +

+ 91 44 6718 1600 / 91 44 6718 1601 /
+ 8925977445 +

+
+ + +
+
+ + {/* Administrative Office */} +
+
+
+ +
+

Administrative Office

+
+ +
+
+ +

+ #69, New Avadi Road, Kilpauk,
+ Chennai - 600 010. +

+
+ +
+ +
+

Tel : 91 44 2644 2472

+

91 44 2646 1316

+

91 44 2646 0124

+
+
+ + +
+
+
+ + {/* Map */} +
+ +
+
+
+
+ ); +}; + +export default ContactSection; + diff --git a/RIT-EMS-main/frontend/src/components/student/DomainSelection.tsx b/RIT-EMS-main/frontend/src/components/student/DomainSelection.tsx new file mode 100644 index 0000000..c7bef65 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/DomainSelection.tsx @@ -0,0 +1,65 @@ +import React, { useState, useEffect } from 'react'; +import { DOMAIN_MAP } from './constants'; + +interface DomainSelectionProps { + category: string; + onSelectDomain: (domainId: string, domainName?: string) => void; + onBack: () => void; +} + +const DomainSelection: React.FC = ({ category, onSelectDomain, onBack }) => { + const [domains, setDomains] = useState([]); + + useEffect(() => { + const mappedDomains = DOMAIN_MAP[category] || []; + setDomains([...mappedDomains].sort((a: any, b: any) => a.name.localeCompare(b.name))); + }, [category]); + + return ( +
+ + +

+ CHOOSE YOUR DOMAIN +

+ +
+ {domains.map((domain) => ( +
onSelectDomain(domain.name, domain.name)} + className="group relative h-[350px] rounded-[2rem] overflow-hidden cursor-pointer shadow-[0_10px_30px_rgba(0,0,0,0.1)] transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl" + > + {domain.name} +
+ +
+ + Domain + +

+ {domain.name} +

+
+ +
+ +
+
+ ))} +
+
+ ); +}; + +export default DomainSelection; diff --git a/RIT-EMS-main/frontend/src/components/student/EventCard.tsx b/RIT-EMS-main/frontend/src/components/student/EventCard.tsx new file mode 100644 index 0000000..a00400e --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/EventCard.tsx @@ -0,0 +1,547 @@ +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { toPng } from 'html-to-image'; +import type { Event } from '../../types'; +import { useAuth } from '../../context/AuthContext'; +import { API_BASE_URL } from '../../lib/config'; +import { CLUBS } from './constants'; + +interface EventCardProps { + event: Event; + isBooked: boolean; + onToggle: () => void; + onTrackStatus?: (event: Event) => void; + currentUserName?: string; + userRole?: string; + registration?: any; +} + +const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return createPortal(children, document.body); +}; + +const EventCard: React.FC = ({ event, isBooked, onToggle, onTrackStatus, currentUserName = 'Student', userRole, registration }) => { + const [showConfirm, setShowConfirm] = useState(false); + const clubInfo = useMemo(() => CLUBS.find(c => c.name === event.club), [event.club]); + const [showCancelConfirm, setShowCancelConfirm] = useState(false); + const [showTicket, setShowTicket] = useState(false); + const [showSummary, setShowSummary] = useState(false); + const [userDept, setUserDept] = useState(''); + const [userYear, setUserYear] = useState(''); + const [userSection, setUserSection] = useState(''); + const [now, setNow] = useState(new Date()); + const [isDownloading, setIsDownloading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const ticketRef = useRef(null); + const [persistedTicket, setPersistedTicket] = useState<{id: string, qr: string} | null>(null); + + useEffect(() => { + if (showTicket && registration?.id) { + const initializeTicket = async () => { + // 1. Check if already persisted + if (registration.ticket_id && registration.ticket_qrcode) { + setPersistedTicket({ id: registration.ticket_id, qr: registration.ticket_qrcode }); + return; + } + + // 2. Otherwise generate and save + try { + const newId = `RIT-EVT-${Math.random().toString(36).substring(2, 6).toUpperCase()}-${registration.id.substring(0, 4).toUpperCase()}`; + const newQr = `${window.location.origin}/?verify=${registration.id}`; + + const res = await fetch(`${API_BASE_URL}/api/registrations/${registration.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ticket_id: newId, + ticket_qrcode: newQr + }) + }); + + if (res.ok) { + setPersistedTicket({ id: newId, qr: newQr }); + } + } catch (err) { + console.error("Ticket initialization failed:", err); + } + }; + initializeTicket(); + } + }, [showTicket, registration?.id]); + + const { user } = useAuth(); + + useEffect(() => { + const timer = setInterval(() => setNow(new Date()), 1000); + if (user) { + setUserDept(user.department || ''); + setUserYear(user.year || ''); + setUserSection(user.section || ''); + } + return () => clearInterval(timer); + }, [user]); + + const admissionStatus = useMemo(() => { + if (event.status === 'Completed') return 'EVENT_ENDED'; + + if (event.registrationDeadline) { + const deadline = new Date(event.registrationDeadline); + if (now > deadline) return 'DEADLINE_PASSED'; + } + + if (userDept && event.deptLimits?.[userDept]) { + const sectionLimits = event.deptSectionLimits?.[userDept]; + const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0; + + if (hasSectionLimits) { + const limit = sectionLimits[userSection]; + if (!limit || limit <= 0) { + return 'SECTION_NOT_ALLOWED'; + } + const currentSectionCount = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0; + if (currentSectionCount >= limit) { + return 'SECTION_FULL'; + } + } else { + const currentDeptCount = event.currentDeptCounts?.[userDept] || 0; + if (currentDeptCount >= event.deptLimits[userDept]) return 'DEPT_FULL'; + } + } + + if (event.maxParticipants && (event.currentParticipants || 0) >= event.maxParticipants) { + return 'TOTAL_FULL'; + } + + return 'OPEN'; + }, [event, now, userDept, userSection]); + + const timeLeft = useMemo(() => { + const targetDate = new Date(event.date).getTime(); + const distance = targetDate - now.getTime(); + if (isNaN(targetDate) || distance <= 0) return { d: '00', h: '00', m: '00', s: '00' }; + return { + d: Math.floor(distance / (1000 * 60 * 60 * 24)).toString().padStart(2, '0'), + h: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)).toString().padStart(2, '0'), + m: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0'), + s: Math.floor((distance % (1000 * 60)) / 1000).toString().padStart(2, '0') + }; + }, [event.date, now]); + + const handleDownload = async () => { + if (!ticketRef.current) return; + setIsDownloading(true); + try { + const dataUrl = await toPng(ticketRef.current, { cacheBust: true, quality: 1, backgroundColor: '#1e293b' }); + const link = document.createElement('a'); + link.download = `Pass_${event.title.replace(/\s+/g, '_')}.png`; + link.href = dataUrl; + link.click(); + } catch (err) { + console.error("Capture failed:", err); + } finally { + setIsDownloading(false); + } + }; + + const verificationLink = `${window.location.origin}/?verify=local_${event.id}`; + + return ( +
+
+ {event.title} +
+
{event.category}
+ {event.registrationDeadline && ( +
+ + {admissionStatus === 'DEADLINE_PASSED' ? 'Closed' : `Ends ${new Date(event.registrationDeadline).toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`} +
+ )} +
+
+ +
+
+
+ {clubInfo && ( +
+ {clubInfo.name} + {clubInfo.name} +
+ )} +

{event.title}

+
+
{event.pricingType}
+
+ +
+
+ + {event.date}{event.schedule?.[0]?.start_time ? ` • ${event.schedule[0].start_time}` : ''} +
+ {event.durationDays && event.durationDays > 1 && ( +
+ + Duration: {event.durationDays} Days +
+ )} +
+
+ + {event.location} +
+ {event.event_summary && ( + + )} +
+
+ + + {event.schedule && event.schedule.length > 0 && (() => { + // Group schedule entries by day_idx + const dayMap = new Map(); + event.schedule!.forEach(s => { + if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []); + dayMap.get(s.day_idx)!.push(s); + }); + const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]); + + return ( +
+ Event Schedule +
+ {sortedDays.map(([dayIdx, slots]) => ( +
+
+ Day {dayIdx} + {slots![0]?.date} +
+
+ {slots!.map((slot) => ( +
+ Batch {slot.batch_idx} + {slot.start_time} - {slot.end_time} +
+ ))} +
+
+ ))} +
+
+ ); + })()} + +
+
+ Global Seats + {event.currentParticipants || 0} / {event.maxParticipants || (event.deptLimits && Object.keys(event.deptLimits).length > 0 ? Object.values(event.deptLimits).map(Number).reduce((a, b) => a + b, 0) : '∞')} +
+ {userDept && event.deptLimits?.[userDept] && (() => { + const sectionLimits = event.deptSectionLimits?.[userDept]; + const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0; + if (hasSectionLimits) { + const limit = sectionLimits[userSection] || 0; + const current = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0; + return ( +
+ {userDept} Sec {userSection || 'N/A'} Allocation + + {limit > 0 ? `${current} / ${limit}` : 'RESTRICTED'} + +
+ ); + } + return ( +
+ {userDept} Allocation + + {event.currentDeptCounts?.[userDept] || 0} / {event.deptLimits[userDept]} + +
+ ); + })()} +
+ +
+ {[ {l: 'D', v: timeLeft.d}, {l: 'H', v: timeLeft.h}, {l: 'M', v: timeLeft.m}, {l: 'S', v: timeLeft.s} ].map((t, i) => ( +
+ {t.v} + {t.l} +
+ ))} +
+ +
+ {userRole === 'ADMIN' || userRole === 'COORDINATOR' ? ( + + ) : isBooked ? ( +
+ + {event.status !== 'Completed' && ( + + )} + {event.status !== 'Event Ongoing' && event.status !== 'Completed' && ( + + )} +
+ ) : admissionStatus === 'OPEN' ? ( + + ) : ( + + )} +
+
+ + {showConfirm && ( + +
+
+
+ +
+

+ {event.isTeamEvent ? 'Team Registration' : 'Confirm Pass?'} +

+ + {event.isTeamEvent ? ( +
+
+

+ Important Notice +

+

+ This is a team-based event. You are about to register as an individual, after which you must create or join a team in the registrations section to participate. +

+
+ +
+
+ Max Size + {event.teamSizeLimit || '∞'} Members +
+
+ Type + {event.teamComposition === 'INTER_DEPT' ? 'Inter-Dept' : 'Mixed'} +
+
+ +

+ Proceed with registration for "{event.title}"? +

+
+ ) : ( +

Register for "{event.title}"?

+ )} + +
+ + +
+
+
+
+ )} + + {showCancelConfirm && ( + +
+
+
+

Cancel Booking?

+

Are you sure you want to cancel your registration for "{event.title}"?

+
+ + +
+
+
+
+ )} + + {showTicket && ( + +
+
+ + {/* Header - Now White Theme as Requested */} +
+
+
+ RIT Logo +
+ Secure Entry Pass +
+
+ +
+ +
+
+
+ QR + {persistedTicket?.id && ( +
+ {persistedTicket.id} +
+ )} +
+

{event.title}

+
+
+ +
+
+ Attendee +

{currentUserName}

+
+
+ Year / Section +

+ {registration?.year || userYear || 'N/A'} - {registration?.section || userSection || 'N/A'} +

+
+ {event.isTeamEvent && registration?.team_name && ( +
+ Team Identity +

{registration.team_name}

+
+ )} +
+ Event Date +

{event.date.split(',')[0]}

+
+
+ Dept +

{registration?.dept || userDept || 'Student'}

+
+
+ +
+ +

Digital Ticket ID: {persistedTicket?.id || 'GENERATING...'}

+
+
+
+
+
+
+ )} + + {showSummary && ( + +
+
+ + +
+
+ {clubInfo && ( + + )} +
+ {event.club || 'Organized by'} + + {event.coordinator} + +
+
+ +
+
+
+

Summary

+
+
+ +

+ {event.event_summary || "Details for this session will be provided soon."} +

+
+
+ +
+ +
+
+
+
+
+ )} +
+ ); +}; + +export default EventCard; diff --git a/RIT-EMS-main/frontend/src/components/student/EventList.tsx b/RIT-EMS-main/frontend/src/components/student/EventList.tsx new file mode 100644 index 0000000..6967aa6 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/EventList.tsx @@ -0,0 +1,126 @@ +import React, { useState, useEffect } from 'react'; +import type { Event } from '../../types'; +import EventCard from './EventCard'; +import CategoryGrid from './CategoryGrid'; +import DomainSelection from './DomainSelection'; +import EventsHeroSlider from './EventsHeroSlider'; + +interface EventListProps { + events: Event[]; + selectedCategory: string | null; + bookedEventIds: string[]; + onToggleBooking: (id: string) => void; + onSelectCategory: (id: string | null) => void; + onTrackStatus?: (event: Event) => void; + currentUserName: string; + userRole?: string; + userRegistrations?: any[]; +} +const EventList: React.FC = ({ + events, + selectedCategory, + bookedEventIds, + onToggleBooking, + onSelectCategory, + onTrackStatus, + currentUserName, + userRole, + userRegistrations = [] +}) => { + const [selectedDomain, setSelectedDomain] = useState(null); + const [selectedDomainName, setSelectedDomainName] = useState(null); + + useEffect(() => { + setSelectedDomain(null); + setSelectedDomainName(null); + }, [selectedCategory]); + + if (!selectedCategory) { + const hasActiveEvents = events.some(e => e.status !== 'Completed'); + return ( +
+ + +
+ ); + } + + if (!selectedDomain) { + return ( + { + setSelectedDomain(id); + if (name) setSelectedDomainName(name); + }} + onBack={() => onSelectCategory(null)} + /> + ); + } + + let filteredEvents = events; + if (selectedCategory !== 'ALL') { + filteredEvents = filteredEvents.filter(e => e.category === selectedCategory); + } + if (selectedDomain !== 'ALL') { + filteredEvents = filteredEvents.filter(e => e.domain === selectedDomain); + } + + const categoryName = selectedCategory === 'ALL' ? 'All' : + selectedCategory.charAt(0) + selectedCategory.slice(1).toLowerCase(); + const domainName = selectedDomainName || (selectedDomain === 'ALL' ? 'All Domains' : selectedDomain); + + return ( +
+ + +
+
+

+ {domainName} +

+

+ Exploring {categoryName} Category +

+
+

Showing {filteredEvents.length} events found

+
+ +
+ {filteredEvents.map(event => { + const reg = userRegistrations.find(r => String(r.event_id) === String(event.id)); + return ( + onToggleBooking(event.id)} + onTrackStatus={onTrackStatus} + currentUserName={currentUserName} + userRole={userRole} + registration={reg} + /> + ); + })} +
+ + {filteredEvents.length === 0 && ( +
+
+ +
+

No events found in this domain yet.

+ +
+ )} +
+ ); +}; + +export default EventList; diff --git a/RIT-EMS-main/frontend/src/components/student/EventsHeroSlider.tsx b/RIT-EMS-main/frontend/src/components/student/EventsHeroSlider.tsx new file mode 100644 index 0000000..e75aa87 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/EventsHeroSlider.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import type { Event } from '../../types'; + +interface EventsHeroSliderProps { + events: Event[]; +} + +const EventsHeroSlider: React.FC = ({ events }) => { + const sortedEvents = useMemo(() => { + return [...events] + .filter(e => e.status !== 'Completed') + .sort((a, b) => { + return new Date(a.date).getTime() - new Date(b.date).getTime(); + }); + }, [events]); + + const [currentIndex, setCurrentIndex] = useState(0); + const [timeLeft, setTimeLeft] = useState({ d: '00', h: '00', m: '00', s: '00' }); + + const nextSlide = useCallback(() => { + setCurrentIndex((prev) => (prev + 1) % sortedEvents.length); + }, [sortedEvents.length]); + + const prevSlide = useCallback(() => { + setCurrentIndex((prev) => (prev - 1 + sortedEvents.length) % sortedEvents.length); + }, [sortedEvents.length]); + + useEffect(() => { + if (sortedEvents.length === 0) return; + const interval = setInterval(nextSlide, 5000); + return () => clearInterval(interval); + }, [sortedEvents.length, nextSlide]); + + const updateTimer = useCallback(() => { + const activeEvent = sortedEvents[currentIndex]; + if (!activeEvent) { + setTimeLeft({ d: '00', h: '00', m: '00', s: '00' }); + return; + } + + const targetDate = new Date(activeEvent.date).getTime(); + const now = new Date().getTime(); + const distance = targetDate - now; + + if (isNaN(targetDate) || distance < 0) { + setTimeLeft({ d: '00', h: '00', m: '00', s: '00' }); + return; + } + + setTimeLeft(prev => { + const newD = Math.floor(distance / (1000 * 60 * 60 * 24)).toString().padStart(2, '0'); + const newH = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)).toString().padStart(2, '0'); + const newM = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0'); + const newS = Math.floor((distance % (1000 * 60)) / 1000).toString().padStart(2, '0'); + + if (prev.d === newD && prev.h === newH && prev.m === newM && prev.s === newS) { + return prev; + } + return { d: newD, h: newH, m: newM, s: newS }; + }); + }, [sortedEvents, currentIndex]); + + useEffect(() => { + updateTimer(); + const timerId = setInterval(updateTimer, 1000); + return () => clearInterval(timerId); + }, [updateTimer]); + + if (sortedEvents.length === 0) return null; + + const activeEvent = sortedEvents[currentIndex]; + + return ( +
+ {/* Background Images & Content */} + {sortedEvents.map((event, idx) => { + const isActive = idx === currentIndex; + return ( +
+ {/* Background Image */} +
+ {event.title} +
+ + {/* Top Right Location Button */} +
+
+ + {event.location} +
+
+ + {/* Main Content */} +
+
+
+
+ + JOIN US {event.date} + +
+ +

+ {event.title} +

+
+
+
+ ); + })} + + {/* Countdown Timer Block (Slanted Orange Design) */} +
+
+
+ {timeLeft.d} + Days +
+
+
+ {timeLeft.h} + Hrs +
+
+
+ {timeLeft.m} + Min +
+
+
+ {timeLeft.s} + Sec +
+
+
+ + {/* Navigation Arrows */} + + + + {/* Slider Dots */} +
+ {sortedEvents.map((_, idx) => ( + + ))} +
+
+ ); +}; + +export default EventsHeroSlider; + diff --git a/RIT-EMS-main/frontend/src/components/student/Footer.tsx b/RIT-EMS-main/frontend/src/components/student/Footer.tsx new file mode 100644 index 0000000..f9060c7 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/Footer.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { Youtube, Instagram, Facebook, Linkedin, Twitter, MapPin, Phone, Mail, ExternalLink } from 'lucide-react'; + +const Footer: React.FC = () => { + const currentYear = new Date().getFullYear(); + + const footerLinks = { + col1: [ + { name: 'About The College', href: 'https://ritchennai.org' }, + { name: 'News Room', href: 'https://ritchennai.org' }, + { name: 'Governing Council', href: 'https://ritchennai.org/about-governing-council.php' }, + { name: 'Career @ RIT', href: 'https://ritchennai.org' }, + { name: 'Blog', href: 'https://ritchennai.org' }, + { name: 'In alliance with Rajalakshmi Eduverse (www.myeduverse.net) - Cancellation & Refund Policy', href: 'https://ritchennai.org' }, + ], + col2: [ + { name: 'Courses', href: 'https://www.ritchennai.org/admission-courses-offered.php' }, + { name: 'Admissions', href: 'https://www.ritchennai.org/admission-eligibility.php' }, + { name: 'Eligibility', href: 'https://www.ritchennai.org/admission-eligibility.php' }, + { name: 'Code of Conduct - Students', href: 'https://ritchennai.org' }, + { name: 'Online Fees Payment', href: 'https://ritchennai.org' }, + { name: 'Terms & Conditions', href: 'https://ritchennai.org' }, + ], + col3: [ + { name: 'Library', href: 'https://ritchennai.org' }, + { name: 'Hostel', href: 'https://ritchennai.org' }, + { name: 'Sports', href: 'https://ritchennai.org' }, + { name: 'Transport', href: 'https://ritchennai.org' }, + { name: 'HR Manual', href: 'https://ritchennai.org' }, + { name: 'Patent', href: 'https://ritchennai.org' }, + { name: 'Audited Statements', href: 'https://ritchennai.org' }, + { name: 'Shopping and Delivery Conditions', href: 'https://ritchennai.org' }, + ], + }; + + const socialLinks = [ + { icon: Facebook, href: 'https://www.facebook.com/ritchennai/', color: 'hover:text-blue-600' }, + { icon: Twitter, href: 'https://x.com/rit_chennai', color: 'hover:text-sky-500' }, + { icon: Instagram, href: 'https://www.instagram.com/ritchennai/', color: 'hover:text-pink-600' }, + { icon: Linkedin, href: 'https://www.linkedin.com/school/rajalakshmi-institute-of-technology/', color: 'hover:text-blue-700' }, + { icon: Youtube, href: 'https://youtube.com/@rajalakshmiinstituteoftech4448?si=jRmCDzp9dbwBLVQI', color: 'hover:text-red-600' }, + ]; + + return ( +
+
+
+ + {/* Brand and Social */} +
+
+

+ We are a leading Engineering college in India, Rajalakshmi Institutions. Imparting the Spirit of Excellence and Quality Technical Education that fosters Learning. +

+
+ +
+ {socialLinks.map((social, index) => ( + + + + ))} +
+ +
+ RIT Logo +
+
+ + {/* Links Columns */} +
+
    + {footerLinks.col1.map((link) => ( +
  • + {link.name} +
  • + ))} +
+
+ +
+
    + {footerLinks.col2.map((link) => ( +
  • + {link.name} +
  • + ))} +
+
+ +
+
+ {footerLinks.col3.map((link) => ( + {link.name} + ))} +
+
+ + {/* Contact Info */} +
+
+

+ ADDRESS +

+
+
+

Rajalakshmi Institutions Head Office

+

# 69 New Avadi Road

+

Kilpauk

+

Chennai - 600 010.

+
+
+

College Campus

+

Rajalakshmi Nagar

+

Thandalam

+

Tamil Nadu 602105

+
+
+
+ +
+

+ PHONE +

+

44-26442472 / 44-26461316 / 44-26460124

+
+ + +
+ +
+ + {/* Bottom Bar */} +
+
+ © {currentYear} Rajalakshmi Institute Of Technology +
+ +
+ {/* Links removed as per request */} +
+
+
+
+ ); +}; + +export default Footer; + diff --git a/RIT-EMS-main/frontend/src/components/student/Hero.tsx b/RIT-EMS-main/frontend/src/components/student/Hero.tsx new file mode 100644 index 0000000..2085a54 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/Hero.tsx @@ -0,0 +1,49 @@ + +import React from 'react'; + +interface HeroProps { + events?: any[]; +} + +const Hero: React.FC = () => { + return ( +
+ {/* Optimized Video Background Container */} +
+ +
+ + {/* Centered Content */} +
+

+ The Future is
Built Here +

+

+ Welcome to the RIT Events Hub +

+
+ + {/* Scroll Down Arrow */} +
+
+ Scroll Down + +
+
+
+ ); +}; + +export default Hero; + diff --git a/RIT-EMS-main/frontend/src/components/student/HomeDashboard.tsx b/RIT-EMS-main/frontend/src/components/student/HomeDashboard.tsx new file mode 100644 index 0000000..3873989 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/HomeDashboard.tsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import type { Event, Announcement, SpecialEvent } from '../../types'; +import { motion, AnimatePresence } from 'framer-motion'; +import SpecialEventsBanner from './SpecialEventsBanner'; +import AccreditationsSection from './AccreditationsSection'; + +interface HomeDashboardProps { + events: Event[]; + announcements: Announcement[]; + onNavigateToEvents: () => void; + specialEvents: SpecialEvent[]; +} + +const HomeDashboard: React.FC = ({ events = [], announcements = [], onNavigateToEvents, specialEvents = [] }) => { + const [now, setNow] = useState(new Date()); + + useEffect(() => { + const timer = setInterval(() => setNow(new Date()), 30000); + return () => clearInterval(timer); + }, []); + + const filteredAnnouncements = useMemo(() => { + return (announcements || []).filter(ann => { + if (!ann.expiresAt) return true; + const expiry = new Date(ann.expiresAt); + return expiry > now; + }); + }, [announcements, now]); + + // Generate stable random values for rotation and color based on announcement ID + const getNoteStyle = (id: string) => { + // Simple hash function for stability + const hash = id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + const rotations = [-2, -1, 1, 2, 3, -3]; + const colors = [ + 'bg-[#fdfd96]', // Classic Yellow + 'bg-[#ff7eb9]', // Hot Pink + 'bg-[#7afcff]', // Light Blue + 'bg-[#feff9c]', // Light Yellow + 'bg-[#fff740]' // Darker Yellow + ]; + + return { + rotation: rotations[hash % rotations.length], + color: colors[hash % colors.length] + }; + }; + + return ( +
+
+
+ Events +
+
+

+ Let's get started +

+
+ + Explore Events + +
+
+
+
+ + {/* Notice Board Section */} +
+ {/* Cork texture pattern */} +
+ +
+
+

+ Campus Notice Board +

+

Real-time updates from the administration

+
+
+ +
+ + {filteredAnnouncements.length > 0 ? ( + filteredAnnouncements.slice(0, 6).map((ann) => { + const style = getNoteStyle(ann.id); + return ( + + {/* Pin */} +
+ +
+ + {ann.type || 'NOTICE'} + +
+ +

+ {ann.title} +

+ +

+ {ann.message} +

+ +
+
+ + + {new Date(ann.timestamp).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })} + + + + {new Date(ann.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} + +
+ {ann.expiresAt && Exp: {new Date(ann.expiresAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}} +
+
+ ); + }) + ) : ( + + +

The board is empty right now.

+

Check back later for updates

+
+ )} +
+
+
+ + + + +
+ ); +}; + +export default HomeDashboard; diff --git a/RIT-EMS-main/frontend/src/components/student/Navbar.tsx b/RIT-EMS-main/frontend/src/components/student/Navbar.tsx new file mode 100644 index 0000000..a28d22e --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/Navbar.tsx @@ -0,0 +1,93 @@ + +import React, { useState, useEffect } from 'react'; +import type { DashboardView } from '../../types'; + +interface NavbarProps { + activeView: DashboardView; + onViewChange: (view: DashboardView) => void; + onLogout: () => void; + onBackToCoordinatorHub?: () => void; +} + +const Navbar: React.FC = ({ activeView, onViewChange, onLogout, onBackToCoordinatorHub }) => { + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + const menuItems: { id: DashboardView; label: string }[] = [ + { id: 'HOME', label: 'HOME' }, + { id: 'EVENTS', label: 'EVENTS' }, + { id: 'REGISTRATIONS', label: 'REGISTRATIONS' }, + { id: 'PROFILE', label: 'PROFILE' }, + { id: 'ABOUT', label: 'ABOUT' }, + { id: 'CONTACT', label: 'CONTACT' }, + ]; + + return ( + + ); +}; + +export default Navbar; + diff --git a/RIT-EMS-main/frontend/src/components/student/PortalAnimation.tsx b/RIT-EMS-main/frontend/src/components/student/PortalAnimation.tsx new file mode 100644 index 0000000..c7e64fe --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/PortalAnimation.tsx @@ -0,0 +1,109 @@ + +import React, { useEffect, useRef, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface PortalAnimationProps { + isVisible: boolean; + onComplete: () => void; + targetLink: string; +} + +const PortalAnimation: React.FC = ({ isVisible, onComplete, targetLink }) => { + const videoRef = useRef(null); + const [statusText, setStatusText] = useState("Initializing Port..."); + + useEffect(() => { + if (isVisible) { + // Sequence of status texts for a more high-tech feel + const statusInterval = setInterval(() => { + const texts = ["Synchronizing...", "Establishing Link...", "Calibrating Vortex...", "Dimensional Drift...", "Transmitting..."]; + setStatusText(prev => { + const idx = texts.indexOf(prev); + return texts[(idx + 1) % texts.length]; + }); + }, 1000); + + // 5-second delay for the high-quality transition + const timer = setTimeout(() => { + // Redirection logic starts - the page will unload + // The overlay will naturally stay until the browser replaces the page + window.location.href = targetLink; + }, 5000); + + return () => { + clearTimeout(timer); + clearInterval(statusInterval); + }; + } + }, [isVisible, targetLink]); + + useEffect(() => { + if (isVisible && videoRef.current) { + videoRef.current.play().catch(err => console.error("Video play failed:", err)); + } + }, [isVisible]); + + if (!isVisible) return null; + + return ( + + +
+ {/* High-Quality Native Video - Absolute Full Screen */} +
+
+
+ ); +}; + +export default PortalAnimation; + diff --git a/RIT-EMS-main/frontend/src/components/student/ProfileView.tsx b/RIT-EMS-main/frontend/src/components/student/ProfileView.tsx new file mode 100644 index 0000000..6970afe --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/ProfileView.tsx @@ -0,0 +1,426 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import type { UserRole } from '../../types'; +import { useAuth } from '../../context/AuthContext'; +import { API_BASE_URL } from '../../lib/config'; + +const compressImage = (base64Str: string): Promise => { + return new Promise((resolve) => { + const img = new Image(); + img.src = base64Str; + img.onload = () => { + const canvas = document.createElement('canvas'); + const MAX_WIDTH = 400; + const MAX_HEIGHT = 400; + let width = img.width; + let height = img.height; + if (width > height) { + if (width > MAX_WIDTH) { + height *= MAX_WIDTH / width; + width = MAX_WIDTH; + } + } else { + if (height > MAX_HEIGHT) { + width *= MAX_HEIGHT / height; + height = MAX_HEIGHT; + } + } + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(img, 0, 0, width, height); + resolve(canvas.toDataURL('image/jpeg', 0.7)); + }; + }); +}; + +interface ProfileViewProps { + onLogout?: () => void; + onSupabaseError?: () => void; +} + +const ProfileView: React.FC = ({ onLogout }) => { + const { user, login } = useAuth(); + const [loading, setLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [regCount, setRegCount] = useState(0); + const [isEditing, setIsEditing] = useState(false); + const [editForm, setEditForm] = useState({ name: '', phone: '', profile_photo: '', year: '', section: '' }); + const [showSuccess, setShowSuccess] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const fileInputRef = useRef(null); + const [pastEvents, setPastEvents] = useState([]); + + const fetchProfile = async () => { + if (!user) return; + try { + setEditForm({ + name: user.fullName || '', + phone: user.phone || '', + profile_photo: (user as any).profile_photo || '', + year: user.year || '', + section: user.section || '' + }); + + // Fetch registrations and events to compute counts and completed events + const [regsRes, eventsRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/registrations?userId=${user.id}`), + fetch(`${API_BASE_URL}/api/events`) + ]); + + if (regsRes.ok && eventsRes.ok) { + const regs = await regsRes.json(); + const events = await eventsRes.json(); + setRegCount(regs.length); + + // Find completed events for this student + const completedRegs = regs.filter((r: any) => { + const ev = events.find((e: any) => String(e.id) === String(r.eventId || r.event_id)); + return ev && (ev.status === 'Completed' || ev.status === 'COMPLETED'); + }).map((r: any) => { + const ev = events.find((e: any) => String(e.id) === String(r.eventId || r.event_id)); + return { + events: { + title: ev.title, + category: ev.category + }, + participation_date: r.registeredAt || r.registered_at || new Date().toISOString() + }; + }); + setPastEvents(completedRegs); + } + } catch (err) { + console.error("Failed to load user profile stats:", err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchProfile(); + }, [user?.id]); + + const handleUpdate = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + if (!user) return; + setIsSaving(true); + setErrorMessage(null); + try { + const payload = { + fullName: editForm.name, + phone: editForm.phone, + year: editForm.year, + section: editForm.section, + profile_photo: editForm.profile_photo + }; + + const res = await fetch(`${API_BASE_URL}/api/admin/users/${user.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!res.ok) { + throw new Error("Failed to save profile changes."); + } + + // Update AuthContext to sync globally + login({ + ...user, + fullName: editForm.name, + phone: editForm.phone, + year: editForm.year, + section: editForm.section, + ...payload + }); + + setIsEditing(false); + setShowSuccess(true); + setTimeout(() => setShowSuccess(false), 3000); + } catch (err: any) { + setErrorMessage(err.message || "Failed to update profile."); + } finally { + setIsSaving(false); + } + }; + + const handlePhotoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = async () => { + const compressed = await compressImage(reader.result as string); + setEditForm({ ...editForm, profile_photo: compressed }); + }; + reader.readAsDataURL(file); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + const avatarUrl = editForm.profile_photo || (user as any)?.profile_photo; + + return ( +
+ {showSuccess && createPortal( +
+
+
+ +
+

Profile Updated

+

Your changes have been saved successfully.

+ +
+
, document.body + )} + +
+
+ {/* Left Column: Profile Card */} +
+
+
+
+
+
+ Profile + {isEditing && ( +
fileInputRef.current?.click()} + className="absolute inset-0 bg-black/50 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" + > + +
+ )} +
+ +
+ +
+

{user?.fullName}

+

{user?.role}

+
+ + {user?.email} +
+
+ +
+
+ {regCount} + Registry +
+
+ {pastEvents.length} + Completed +
+
+ + {!isEditing && ( + + )} +
+
+
+ + {/* Right Column: Details & Edit Form */} +
+ {/* Personal Information Card */} +
+
+

Personal Information

+ {!isEditing && user?.role === 'STUDENT' && ( + + )} +
+ +
+ {isEditing ? ( +
+
+
+ + setEditForm({...editForm, name: e.target.value})} + className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all" + placeholder="Enter your full name" + /> +
+
+ + setEditForm({...editForm, phone: e.target.value})} + className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all" + placeholder="Enter your phone number" + /> +
+
+ + +
+
+ + setEditForm({...editForm, section: e.target.value.toUpperCase()})} + className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all" + placeholder="e.g. A, B, C" + /> +
+
+ + {errorMessage && ( +
+ + {errorMessage} +
+ )} + +
+ + +
+
+ ) : ( +
+
+ +

{user?.fullName}

+
+
+ +

{user?.email}

+
+
+ +

{user?.phone || 'Not provided'}

+
+
+ +

+ + {user?.role} +

+
+ {user?.department && ( +
+ +

{user.department}

+
+ )} + {user?.year && ( +
+ +

{user.year}

+
+ )} + {user?.section && ( +
+ +

{user.section}

+
+ )} + {user?.regNo && ( +
+ +

{user.regNo}

+
+ )} +
+ )} +
+
+ + {/* Recent Activity Card */} + {!isEditing && ( +
+
+

Recent Activity

+
+
+ {pastEvents.length > 0 ? ( +
+ {pastEvents.map((pe, idx) => ( +
+
+ +
+
+

{pe.events?.title || 'Unknown Event'}

+

{pe.events?.category} • Completed on {new Date(pe.participation_date).toLocaleDateString()}

+
+
+ ))} +
+ ) : ( +
+
+ +
+

No participation history yet

+

Events you participate in will appear here.

+
+ )} +
+
+ )} +
+
+
+
+ ); +}; + +export default ProfileView; diff --git a/RIT-EMS-main/frontend/src/components/student/RegistrationsView.tsx b/RIT-EMS-main/frontend/src/components/student/RegistrationsView.tsx new file mode 100644 index 0000000..1e13b82 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/RegistrationsView.tsx @@ -0,0 +1,413 @@ +import React from 'react'; +import { createPortal } from 'react-dom'; +import type { Event } from '../../types'; +import EventCard from './EventCard'; +import { API_BASE_URL } from '../../lib/config'; +import { useAuth } from '../../context/AuthContext'; + +interface RegistrationsViewProps { + events: Event[]; + bookedEventIds: string[]; + userRegistrations: any[]; + onToggleBooking: (id: string) => void; + onTrackStatus: (event: Event) => void; + currentUserName: string; + userRole?: string; +} + +const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return createPortal(children, document.body); +}; + +const RegistrationsView: React.FC = ({ events, bookedEventIds, userRegistrations, onToggleBooking, onTrackStatus, currentUserName, userRole }) => { + const { user } = useAuth(); + const [showInspectModal, setShowInspectModal] = React.useState<{eventId: string, title: string, teamCode: string} | null>(null); + const [showRemoveConfirm, setShowRemoveConfirm] = React.useState<{member: any, eventId: string, teamCode: string} | null>(null); + const [showRemoveSuccess, setShowRemoveSuccess] = React.useState<{name: string} | null>(null); + const [showDisbandConfirm, setShowDisbandConfirm] = React.useState<{eventId: string, teamCode: string} | null>(null); + const [showLeaveConfirm, setShowLeaveConfirm] = React.useState<{eventId: string} | null>(null); + const [teamMembers, setTeamMembers] = React.useState([]); + const [isProcessing, setIsProcessing] = React.useState(false); + const [copyingCode, setCopyingCode] = React.useState(null); + + const bookedEvents = events.filter(e => bookedEventIds.includes(e.id)); + + const handleCopyCode = async (code: string) => { + try { + await navigator.clipboard.writeText(code); + setCopyingCode(code); + setTimeout(() => setCopyingCode(null), 2000); + } catch (err) { + console.error("Copy failed", err); + } + }; + + const executeLeaveTeam = async (eventId: string) => { + if (!user) return; + setIsProcessing(true); + try { + const registrationId = `${user.id}_${eventId}`; + const res = await fetch(`${API_BASE_URL}/api/registrations/${registrationId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + team_code: null, + team_name: null, + is_team_leader: false + }) + }); + + if (!res.ok) throw new Error("Failed to leave team"); + + setShowLeaveConfirm(null); + setShowInspectModal(null); + window.location.reload(); + } catch (err) { + console.error(err); + alert("Failed to leave team"); + } finally { + setIsProcessing(false); + } + }; + + const executeDisbandTeam = async (eventId: string, code: string) => { + setIsProcessing(true); + try { + // Fetch all members of this team + const resMembers = await fetch(`${API_BASE_URL}/api/registrations?eventId=${eventId}&teamCode=${code}`); + const members = resMembers.ok ? await resMembers.json() : []; + + // Update all members to disband + for (const m of members) { + const regId = m.id || `${m.userId || m.user_id}_${eventId}`; + await fetch(`${API_BASE_URL}/api/registrations/${regId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + team_code: null, + team_name: null, + is_team_leader: false + }) + }); + } + + setShowDisbandConfirm(null); + setShowInspectModal(null); + window.location.reload(); + } catch (err) { + console.error(err); + alert("Failed to disband team"); + } finally { + setIsProcessing(false); + } + }; + + const handleInspectTeam = async (eventId: string, code: string, title: string) => { + setIsProcessing(true); + try { + const res = await fetch(`${API_BASE_URL}/api/registrations?eventId=${eventId}&teamCode=${code}`); + if (!res.ok) throw new Error("Failed to fetch team roster"); + const members = await res.json(); + + const formattedMembers = members.map((m: any) => ({ + user_id: m.userId || m.user_id, + name: m.userName || m.user_name, + reg_no: m.regNo || m.reg_no, + year: m.year, + department: m.dept || m.department, + section: m.section, + is_team_leader: m.isTeamLeader || m.is_team_leader + })); + + setTeamMembers(formattedMembers); + setShowInspectModal({ eventId, title, teamCode: code }); + } catch (err) { + console.error(err); + alert("Failed to fetch team members"); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+

My Registrations

+ + {bookedEvents.length > 0 ? ( +
+ {bookedEvents.map(event => { + const reg = userRegistrations.find(r => String(r.eventId || r.event_id) === String(event.id)); + const teamCodeVal = reg?.teamCode || reg?.team_code; + const teamNameVal = reg?.teamName || reg?.team_name; + const isLeaderVal = reg?.isTeamLeader || reg?.is_team_leader; + + return ( +
+ onToggleBooking(event.id)} + onTrackStatus={onTrackStatus} + currentUserName={currentUserName} + userRole={userRole} + registration={reg} + /> + + {event.isTeamEvent && ( +
+
+ {teamCodeVal ? ( +
+
+ + Status: Teamed Up + +

{teamNameVal}

+

+ CODE: {teamCodeVal} + +

+
+
+
+ +
+ +
+
+ ) : ( +
+
+
+ Team Required +
+

Please use the "Track Progress" button to manage your team for this event.

+
+ )} +
+ )} +
+ ); + })} +
+ ) : ( +
+ +

You haven't booked any events yet, or your previously registered events have been removed.

+
+ )} + + {showInspectModal && ( + +
+
+
+ +
+
+

Team Roster

+

Code: {showInspectModal.teamCode} | {showInspectModal.title}

+
+ +
+ +
+ {teamMembers.map((member, idx) => { + const isLeader = teamMembers.find(m => m.is_team_leader && m.name === currentUserName); + return ( +
+
+
+ +
+
+
+

{member.name}

+ {member.is_team_leader && Leader} +
+

{member.reg_no}

+
+
+
+
+

{member.department}

+

{member.year} • SEC {member.section}

+
+ {isLeader && !member.is_team_leader && ( + + )} +
+
+ ); + })} +
+ +
+ {teamMembers.find(m => m.is_team_leader && m.name === currentUserName) ? ( + + ) : ( + + )} + +
+ + {/* Confirmation Modals (Portaled to prevent clipping) */} + {showRemoveConfirm && ( + +
+
+
+
+ +
+

Remove Member?

+

Are you sure you want to remove {showRemoveConfirm.member.name} from the team?

+
+
+ + +
+
+
+
+ )} + + {showRemoveSuccess && ( + +
+
+
+
+ +
+

Member Removed

+

{showRemoveSuccess.name} has been removed.

+ +
+
+
+
+ )} + + {showLeaveConfirm && ( + +
+
+
+
+ +
+

Leave Team?

+

Are you sure you want to leave this team?

+
+
+ + +
+
+
+
+ )} + + {showDisbandConfirm && ( + +
+
+
+
+ +
+

Disband Team?

+

Are you sure? This will disband the team for ALL members.

+
+
+ + +
+
+
+
+ )} +
+
+
+ )} + +
+ ); +}; + +export default RegistrationsView; diff --git a/RIT-EMS-main/frontend/src/components/student/SpecialEventsBanner.tsx b/RIT-EMS-main/frontend/src/components/student/SpecialEventsBanner.tsx new file mode 100644 index 0000000..ac41548 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/SpecialEventsBanner.tsx @@ -0,0 +1,183 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import type { SpecialEvent } from '../../types'; + +interface SpecialEventsBannerProps { + specialEvents: SpecialEvent[]; +} + +const SpecialEventsBanner: React.FC = ({ specialEvents }) => { + const [isOpen, setIsOpen] = useState(false); + const hasEvents = specialEvents && specialEvents.length > 0; + + const handleRedirect = (link: string) => { + window.location.href = link; + }; + + const defaultTitle = "The Grand Chronicles of RIT"; + const defaultDescription = "Hark! The tides have brought forth prestigious gatherings and legendary challenges. Seek thy destiny in the links below."; + + return ( +
+ {/* Background glow (Softer and centered) */} +
+
+
+ +
+ {/* New Header: Descriptive text above the scroll */} +
+

+ Special Events Registry +

+

+ Tap the ancient scroll to reveal the current decrees of RIT +

+
+
+ + + {/* Top Roller - Interactive */} + setIsOpen(!isOpen)} + className="relative z-30 w-full h-16 rounded-full shadow-[0_8px_32px_rgba(62,39,35,0.25)] border-[3px] border-[#3e2723]/60 overflow-hidden cursor-pointer active:scale-95 transition-transform" + style={{ + backgroundImage: 'url(/wood_roller_texture.png)', + backgroundSize: '100% 100%', + backgroundPosition: 'center' + }} + > + {/* End Caps: Fixes the 'broken corner' look */} +
+
+ +
+
+
+
+
+ +
+ + {isOpen ? 'Tap to Close' : 'Tap to Open'} + +
+
+ + {/* Parchment Body */} + + {/* Inner Shadow Shadow */} +
+ +
+ +

+ {hasEvents ? defaultTitle : "The Void Chronicles"} +

+
+

+ {hasEvents ? defaultDescription : "The magic portal remains dormant. Check back when the moons align."} +

+
+ +
+ {specialEvents.map((event, index) => ( + handleRedirect(event.link)} + > +
+

+ {event.title} +

+

+ {event.description} +

+
+ + Behold + +
+
+
+ ))} +
+ + {hasEvents && ( +
+ End of Scroll +
+ )} +
+
+ + {/* Bottom Roller - Interactive */} + setIsOpen(!isOpen)} + variants={{ + closed: { y: -64 }, // Perfect overlap (h-16 = 64px) + open: { y: 0 } + }} + transition={{ duration: 0.8, ease: "easeOut" }} + className="relative z-30 w-full h-16 rounded-full shadow-[0_-8px_32px_rgba(62,39,35,0.25)] border-[3px] border-[#3e2723]/60 overflow-hidden cursor-pointer active:scale-95 transition-transform" + style={{ + backgroundImage: 'url(/wood_roller_texture.png)', + backgroundSize: '100% 100%', + backgroundPosition: 'center' + }} + > + {/* End Caps: Fixes the 'broken corner' look */} +
+
+ +
+
+
+
+ + +
+ ); +}; + +export default SpecialEventsBanner; + + diff --git a/RIT-EMS-main/frontend/src/components/student/StatsSection.tsx b/RIT-EMS-main/frontend/src/components/student/StatsSection.tsx new file mode 100644 index 0000000..883c8e0 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/StatsSection.tsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import type { Event } from '../../types'; + +interface StatsSectionProps { + events: Event[]; +} + +const StatsSection: React.FC = ({ events }) => { + const [counts, setCounts] = useState({ + nonTechnical: 0, + technical: 0, + workshops: 0, + totalEvents: 0 + }); + + useEffect(() => { + if (events) { + const nonTech = events.filter(e => e.category === 'NON-TECHNICAL').length; + const tech = events.filter(e => e.category === 'TECHNICAL').length; + const workshops = events.filter(e => e.category === 'WORKSHOP').length; + + setCounts({ + nonTechnical: nonTech, + technical: tech, + workshops: workshops, + totalEvents: events.length + }); + } + }, [events]); + + const stats = [ + { + label: "Active Non-Tech Events", + value: counts.nonTechnical, + suffix: "+", + delay: 0.1 + }, + { + label: "Technical Events", + value: counts.technical, + suffix: "+", + delay: 0.2 + }, + { + label: "Workshops", + value: counts.workshops, + suffix: "+", + delay: 0.3 + }, + { + label: "Total Events", + value: counts.totalEvents, + suffix: "+", + delay: 0.4 + } + ]; + + return ( +
+
+
+ {stats.map((stat, index) => ( + + {/* Corner accents similar to the image */} +
+ + + +
+
+ + + +
+ +
+

+ {stat.value}{stat.suffix} +

+

+ {stat.label} +

+
+
+ ))} +
+
+
+ ); +}; + +export default StatsSection; + diff --git a/RIT-EMS-main/frontend/src/components/student/StatusTrackerView.tsx b/RIT-EMS-main/frontend/src/components/student/StatusTrackerView.tsx new file mode 100644 index 0000000..8869fa4 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/StatusTrackerView.tsx @@ -0,0 +1,583 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import type { Event } from '../../types'; +import { useAuth } from '../../context/AuthContext'; +import { API_BASE_URL } from '../../lib/config'; + +interface StatusTrackerViewProps { + event: Event; + registration?: any; + onBack: () => void; + onUploadCertificate: (data: string) => Promise; + onShowCreateTeam?: () => void; + onShowJoinTeam?: () => void; +} + +const SuccessPopup: React.FC<{ onClose: () => void }> = ({ onClose }) => ( +
+
+
+ +
+

File Captured

+

Proof uploaded successfully

+ +
+
+); + +const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return createPortal(children, document.body); +}; + +const StatusTrackerView: React.FC = ({ event, registration, onBack, onShowCreateTeam, onShowJoinTeam }) => { + const { user } = useAuth(); + const fileInputRef = React.useRef(null); + const [isUploading, setIsUploading] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const [now, setNow] = useState(new Date()); + const [attendanceRecords, setAttendanceRecords] = useState([]); + + const fetchAttendance = async () => { + if (!registration?.id) return; + try { + const res = await fetch(`${API_BASE_URL}/api/attendance?registrationId=${registration.id}`); + if (res.ok) { + const data = await res.json(); + setAttendanceRecords(data); + } + } catch (err) { + console.error("Failed to fetch attendance:", err); + } + }; + + const odUrl = registration?.odUrl || registration?.od_url || registration?.od; + const certUrl = registration?.certificationUrl || registration?.certification_url || registration?.certifications; + const certStatus = registration?.certificationStatus || registration?.certification_status || registration?.certification_approval; + + const [liveOdUrl, setLiveOdUrl] = useState(odUrl); + const [liveCertUrl, setLiveCertUrl] = useState(certUrl); + const [liveCertStatus, setLiveCertStatus] = useState(certStatus); + + useEffect(() => { + if (registration?.id) { + // Fetch latest registration fields (OD/Cert status) dynamically + fetch(`${API_BASE_URL}/api/registrations?userId=${user?.id}`) + .then(res => res.json()) + .then(regs => { + const currentReg = regs.find((r: any) => String(r.id) === String(registration.id)); + if (currentReg) { + setLiveOdUrl(currentReg.odUrl || currentReg.od_url); + setLiveCertUrl(currentReg.certificationUrl || currentReg.certification_url); + setLiveCertStatus(currentReg.certificationStatus || currentReg.certification_status); + } + }) + .catch(err => console.error("Error refreshing registration status:", err)); + + fetchAttendance(); + } + + const timer = setInterval(() => { + setNow(new Date()); + fetchAttendance(); + }, 15000); // refresh every 15s + return () => clearInterval(timer); + }, [registration?.id, event.id, user?.id]); + + const isEventEnded = event.status === 'Completed' || event.status === 'COMPLETED'; + + const progressSteps = useMemo(() => { + const isFree = event.pricingType === 'FREE' || !event.hasRegistrationFee; + const isPaidVerified = registration?.paymentStatus === 'COMPLETED' || registration?.payment_status === 'COMPLETED'; + const isManuallyEnded = event.status === 'Completed' || event.status === 'COMPLETED'; + const isManuallyOngoing = event.status === 'Event Ongoing' || event.status === 'ONGOING'; + + const isEnded = isManuallyEnded; + const isOngoing = isManuallyOngoing; + + const hasUploaded = !!liveCertUrl; + const isApproved = liveCertStatus === 'APPROVED'; + const hasUploadedOd = !!liveOdUrl; + + return [ + { + label: 'Registered', + status: 'completed', + icon: 'fa-user-check', + color: 'bg-emerald-500', + detail: 'Identity Secured' + }, + { + label: 'TEAM', + status: !event.isTeamEvent || registration?.teamCode || registration?.team_code ? 'completed' : 'active', + icon: 'fa-users', + color: 'bg-orange-500', + detail: !event.isTeamEvent ? 'Solo Mode' : ((registration?.teamCode || registration?.team_code) ? `Team: ${registration.teamName || registration.team_name || 'Joined'}` : 'Wait for Team') + }, + { + label: 'Payment', + status: isFree || isPaidVerified ? 'completed' : 'active', + icon: 'fa-credit-card', + color: 'bg-blue-400', + detail: isFree ? 'Waiver Applied' : (isPaidVerified ? 'Funds Verified' : 'Awaiting Payment') + }, + { + label: 'Ticket', + status: isFree || isPaidVerified ? 'completed' : 'pending', + icon: 'fa-ticket-alt', + color: 'bg-amber-500', + detail: isFree || isPaidVerified ? 'Access Granted' : 'Locked' + }, + { + label: 'Ongoing', + status: isEnded ? 'completed' : (isOngoing ? 'active' : 'pending'), + icon: 'fa-play-circle', + color: 'bg-purple-500', + detail: isOngoing ? 'Live Session' : (isEnded ? 'Session Ended' : 'Scheduled') + }, + { + label: 'Ended', + status: isEnded ? 'completed' : 'pending', + icon: 'fa-calendar-check', + color: 'bg-rose-500', + detail: isEnded ? 'Archived' : 'Wait for End' + }, + { + label: 'Certification', + status: isApproved ? 'completed' : (isEnded ? 'active' : 'pending'), + icon: isApproved ? 'fa-check-double' : (hasUploaded ? 'fa-spinner fa-spin' : 'fa-award'), + color: isApproved ? 'bg-emerald-600' : (isEnded ? 'bg-amber-500' : 'bg-indigo-600'), + detail: isApproved ? 'Verified by Faculty' : (hasUploaded ? 'In Review' : (isEnded ? 'Upload Proof' : 'Wait for End')) + }, + { + label: 'OD', + status: hasUploadedOd ? 'completed' : (isApproved ? 'active' : 'pending'), + icon: 'fa-file-signature', + color: 'bg-teal-500', + detail: hasUploadedOd ? 'OD Provided' : (isApproved ? 'Ready for Download' : 'Wait for Approval') + }, + ]; + }, [event, registration, liveOdUrl, liveCertUrl, liveCertStatus]); + + const handleOdDownload = async () => { + if (!liveOdUrl) return; + try { + const response = await fetch(liveOdUrl); + const blob = await response.blob(); + + const isPdf = liveOdUrl.toLowerCase().endsWith('.pdf') || blob.type === 'application/pdf'; + const extension = isPdf ? 'pdf' : 'jpg'; + + const blobUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = blobUrl; + link.download = `OD_${event.title.replace(/\s+/g, '_')}.${extension}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(blobUrl); + } catch (err) { + console.error("OD Download failed:", err); + window.open(liveOdUrl, '_blank'); + } + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file && user && registration) { + setIsUploading(true); + const reader = new FileReader(); + reader.onloadend = async () => { + try { + const base64 = reader.result as string; + const res = await fetch(`${API_BASE_URL}/api/registrations/${registration.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + certificationUrl: base64, + certificationStatus: 'PENDING_APPROVAL' + }) + }); + + if (!res.ok) throw new Error("Update failed"); + + setLiveCertUrl(base64); + setLiveCertStatus('PENDING_APPROVAL'); + setShowSuccess(true); + } catch (err) { + console.error("Upload failed:", err); + alert("Upload failed. Please check your connection."); + } finally { + setIsUploading(false); + } + }; + reader.readAsDataURL(file); + } + }; + + return ( +
+ {showSuccess && { setShowSuccess(false); window.location.reload(); }} />} + +
+
+
+ +

REAL-TIME PROGRESS

+

Tracking Node: {event.title}

+
+
+ +
+
+
+ + {/* Continuous Line Background */} +
+ + {/* Active Line Foreground */} +
s.status === 'active') !== -1 + ? progressSteps.findIndex(s => s.status === 'active') + : Math.max(0, progressSteps.filter(s => s.status === 'completed').length - 1)) + / (progressSteps.length - 1) + } * (100% - ${100 / progressSteps.length}%))` + }} + >
+ + {/* Steps Container */} +
+ {progressSteps.map((step, idx) => ( +
+ + {/* Dot */} +
+ + {/* Icon */} +
+ +
+ + {/* Text */} +
+ {step.label} +

{step.detail}

+
+
+ ))} +
+ + {/* Attendance Tracker Redesign (Node-based) */} +
+
+
+ Attendance Milestones +
+ +
+ {(() => { + const allSessions: { label: string, status: string, day: string, batch: string }[] = []; + if (event.schedule && event.schedule.length > 0) { + const dayMap = new Map(); + event.schedule.forEach(s => { + if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []); + dayMap.get(s.day_idx)!.push(s); + }); + const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]); + sortedDays.forEach(([dayIdx, slots]) => { + slots!.forEach((slot) => { + const dLabel = `Day ${dayIdx}`; + const bLabel = `Batch ${slot.batch_idx}`; + const record = attendanceRecords.find(r => + (r.dayLabel === dLabel || r.day_label === dLabel || r.day_idx === dLabel) && + (r.batchLabel === bLabel || r.batch_label === bLabel || r.batch_idx === bLabel) + ); + allSessions.push({ + label: `D${dayIdx} B${slot.batch_idx}`, + day: dLabel, + batch: bLabel, + status: (record?.isPresent || record?.is_present) ? 'completed' : ((record?.isPresent === false || record?.is_present === false) ? 'absent' : 'pending') + }); + }); + }); + } else { + const record = attendanceRecords.find(r => r.dayLabel === 'Day 1' || r.day_label === 'Day 1'); + allSessions.push({ + label: 'Day 1', + day: 'Day 1', + batch: '', + status: (record?.isPresent || record?.is_present) ? 'completed' : ((record?.isPresent === false || record?.is_present === false) ? 'absent' : 'pending') + }); + } + + return ( +
+ {/* Connecting Line Background */} +
+ + {/* Connecting Line Foreground */} +
s.status === 'pending') !== -1 + ? Math.max(0, allSessions.findIndex(s => s.status === 'pending') - 1) + : Math.max(0, allSessions.filter(s => s.status === 'completed').length - 1)) + / Math.max(1, allSessions.length - 1) + } * (100% - ${100 / allSessions.length}%))` + }} + >
+ + {/* Nodes */} +
+ {allSessions.map((session, sIdx) => ( +
+ + {/* Dot on line */} +
+ + {/* Large Circle Shaped Indicator */} +
+ +
+ + {/* Text */} +
+ {session.label} +

+ {session.status === 'completed' ? 'Attended' : + session.status === 'absent' ? 'Absent' : 'Upcoming'} +

+
+
+ ))} +
+
+ ); + })()} +
+ +
+

+ + {attendanceRecords.filter(r => r.isPresent || r.is_present).length} / {Math.max(1, event.schedule?.length || 1)} Sessions Attended +

+
+
+
+ + {/* Team Management Buttons */} + {event.isTeamEvent && !(registration?.teamCode || registration?.team_code) && ( +
+
+ +
+
+
+
+ Team Management +
+

This is a team event. Form your own team or join an existing alliance to participate in this competition.

+
+ +
+ + +
+
+
+ )} +
+
+ + {/* OD Document Download Section */} + {liveOdUrl && ( +
+
+
+ +
+
+

Official On-Duty Document

+

Authorized by Faculty Coordinator

+
+
+
+ + Inspect + + +
+
+ )} + +
+
+
+

Certification Portal

+

Provide valid proof of attendance or task completion to finalize your official event participation.

+ + + + {!isEventEnded ? ( +
+ +

+ + Upload unlocks once admin marks event as Ended +

+
+ ) : ( + <> + + {liveCertStatus === 'PENDING_APPROVAL' && ( +

+ Awaiting Faculty Review +

+ )} + + )} +
+ +
+ {liveCertUrl ? ( + Proof + ) : ( +
+ +

Preview Area

+
+ )} +
+
+ +
+
+

+ Event Info + +

+
+
+ Conducting Club + {event.club || 'N/A'} +
+
+ Enrollment Status + {registration?.paymentStatus || registration?.payment_status || 'PENDING'} +
+ {event.durationDays && ( +
+ Duration + {event.durationDays} Days +
+ )} + {event.schedule && event.schedule.length > 0 && ( +
+ Sessions + + {event.schedule.length} Total + +
+ )} +
+ Attendance Progress + + {attendanceRecords.filter(r => r.isPresent || r.is_present).length} Marked + +
+
+ Certification + + {liveCertStatus?.replace('_', ' ') || 'NOT SUBMITTED'} + +
+
+ Verification Status + + {liveCertStatus === 'APPROVED' ? 'Finalized' : 'Pending'} + +
+
+
+
+
+
+ ); +}; + +export default StatusTrackerView; diff --git a/RIT-EMS-main/frontend/src/components/student/UpcomingEventsSlider.tsx b/RIT-EMS-main/frontend/src/components/student/UpcomingEventsSlider.tsx new file mode 100644 index 0000000..ade462c --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/UpcomingEventsSlider.tsx @@ -0,0 +1,163 @@ + +import React, { useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import type { Event } from '../../types'; +import { CLUBS } from './constants'; + +const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return createPortal(children, document.body); +}; + +interface UpcomingEventsSliderProps { + events: Event[]; +} + +const UpcomingEventsSlider: React.FC = ({ events }) => { + const [selectedEvent, setSelectedEvent] = useState(null); + const clubInfo = useMemo(() => selectedEvent ? CLUBS.find(c => c.name === selectedEvent.club) : null, [selectedEvent]); + + const sortedEvents = useMemo(() => { + return [...events] + .filter(e => e.status !== 'Completed') + .sort((a, b) => { + return new Date(a.date).getTime() - new Date(b.date).getTime(); + }); + }, [events]); + + // Duplicate events for seamless loop only if we have enough to fill the track + const loopEvents = sortedEvents.length > 2 ? [...sortedEvents, ...sortedEvents] : sortedEvents; + + if (sortedEvents.length === 0) return null; + + return ( +
+ +
+
+

Upcoming Experiences

+
+
+ +
+ {/* Gradient masks removed to eliminate fogginess */} + +
+ {loopEvents.map((event, idx) => ( +
+
+ {event.title} +
+ {event.category} +
+
+
+

+ {event.title} +

+
+
+ + + {event.date} + + {event.schedule?.[0]?.start_time || ''} + +
+
+ + {event.location} +
+
+
+ Limited Access + +
+
+
+ ))} +
+
+ + {selectedEvent && ( + +
+
+ + +
+
+ {clubInfo && ( + + )} +
+ {selectedEvent.club || 'Organized by'} + + {selectedEvent.coordinator} + +
+
+ +
+
+
+

Summary

+
+
+ +

+ {selectedEvent.event_summary || "Details for this session will be provided soon."} +

+
+
+ +
+ +
+
+
+
+
+ )} +
+ ); +}; + +export default UpcomingEventsSlider; + diff --git a/RIT-EMS-main/frontend/src/components/student/constants.tsx b/RIT-EMS-main/frontend/src/components/student/constants.tsx new file mode 100644 index 0000000..55a0258 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/student/constants.tsx @@ -0,0 +1,199 @@ + +import type { Event } from '../../types'; + +const getFutureDate = (days: number, hours: number = 0) => { + const date = new Date(); + date.setDate(date.getDate() + days); + date.setHours(date.getHours() + hours); + const options: Intl.DateTimeFormatOptions = { month: 'short', day: '2-digit', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); +}; + +export const ALL_EVENTS: Event[] = [ + { + id: '1', + title: 'Next-Gen AI Forum', + date: getFutureDate(3, 4), + + location: 'Innovation Lab', + category: 'TECHNICAL', + domain: 'Computer Science and Engineering', + club: 'Techspark', + pricingType: 'PAID', + coordinator: 'Dr. Ramesh Kumar', + image: 'https://images.unsplash.com/photo-1620712943543-bcc4628c9759?auto=format&fit=crop&q=80&w=800' + }, + { + id: '2', + title: 'Web Architecture 2025', + date: getFutureDate(1, 2), + location: 'Seminar Hall A', + category: 'TECHNICAL', + domain: 'Information Technology', + club: 'Infintus club', + pricingType: 'FREE', + coordinator: 'Prof. Anitha S.', + image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=800' + }, + { + id: '3', + title: 'Cultural Night Jam', + date: getFutureDate(15), + location: 'Main Auditorium', + category: 'NON-TECHNICAL', + domain: 'Arts & Culture', + club: 'Euphoria club', + pricingType: 'PAID', + coordinator: 'Dr. Sivakumar P.', + image: 'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&q=80&w=800' + }, + { + id: '4', + title: 'FullStack React Workshop', + date: getFutureDate(5, 1), + location: 'Computer Lab 3', + category: 'WORKSHOP', + domain: 'Software Engineering', + club: 'Fusion club', + pricingType: 'FREE', + coordinator: 'Prof. Rajesh B.', + image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=800' + }, + { + id: '5', + title: 'Entrepreneurship 101', + date: getFutureDate(12, 8), + location: 'Management Block', + category: 'NON-TECHNICAL', + domain: 'Management', + club: 'EDC club', + pricingType: 'FREE', + coordinator: 'Dr. Preethi M.', + image: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?auto=format&fit=crop&q=80&w=800' + }, + { + id: '6', + title: 'Robo-Wars 2025', + date: getFutureDate(20), + location: 'Mechanical Workshop', + category: 'TECHNICAL', + domain: 'Mechanical', + club: 'Steam', + pricingType: 'PAID', + coordinator: 'Prof. Karthikeyan J.', + image: 'https://images.unsplash.com/photo-1561144443-0559f2e4bd3c?auto=format&fit=crop&q=80&w=800' + } +]; + +export const CATEGORIES = [ + { name: 'Technical', id: 'TECHNICAL', image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=800' }, + { name: 'Non-Technical', id: 'NON-TECHNICAL', image: 'https://images.unsplash.com/photo-1523580494863-6f3031224c94?auto=format&fit=crop&q=80&w=800' }, + { name: 'Workshops', id: 'WORKSHOP', image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&q=80&w=800' }, + { name: 'Centre Activity', id: 'CENTRE-ACTIVITY', image: 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?auto=format&fit=crop&q=80&w=1200' } +]; + +export const CLUBS = [ + { name: 'Fusion club', image: 'https://github.com/Sachin-627/club/raw/main/Fusion%20club.jpeg' }, + { name: 'Helios club', image: 'https://github.com/Sachin-627/club/raw/main/Helios%20club.jpeg' }, + { name: 'Infintus club', image: 'https://github.com/Sachin-627/club/raw/main/Infintus%20club.jpeg' }, + { name: 'Mediastic', image: 'https://github.com/Sachin-627/club/raw/main/Mediastic.jpeg' }, + { name: 'Nippon Club', image: 'https://github.com/Sachin-627/club/raw/main/Nippon%20Club.jpeg' }, + { name: 'Pod x club', image: 'https://github.com/Sachin-627/club/raw/main/Pod%20x%20club.jpeg' }, + { name: 'Rotract', image: 'https://github.com/Sachin-627/club/raw/main/Rotract.jpeg' }, + { name: 'Unnat Bharath Abhiyan club', image: 'https://github.com/Sachin-627/club/raw/main/UBS.jpeg' }, + { name: 'Variti club', image: 'https://github.com/Sachin-627/club/raw/main/Variti%20club.jpeg' }, + { name: 'Wec club', image: 'https://github.com/Sachin-627/club/raw/main/Wec%20club.jpeg' }, + { name: 'Wistom club', image: 'https://github.com/Sachin-627/club/raw/main/Wistom%20club.jpeg' }, + { name: 'Yatra club', image: 'https://github.com/Sachin-627/club/raw/main/Yatra%20club.jpeg' }, + { name: 'Yuva club', image: 'https://github.com/Sachin-627/club/raw/main/Yuva%20club.jpeg' }, + { name: 'Artist League', image: 'https://github.com/Sachin-627/club/raw/main/artist%20league.jpeg' }, + { name: 'Classical club', image: 'https://github.com/Sachin-627/club/raw/main/classical%20club.jpeg' }, + { name: 'EDC club', image: 'https://github.com/Sachin-627/club/raw/main/edc%20club.jpeg' }, + { name: 'Euphoria club', image: 'https://github.com/Sachin-627/club/raw/main/euphoria%20club.jpeg' }, + { name: 'R Square', image: 'https://github.com/Sachin-627/club/raw/main/r%20square.jpeg' }, + { name: 'Raptology', image: 'https://github.com/Sachin-627/club/raw/main/raptology.jpeg' }, + { name: 'Steam', image: 'https://github.com/Sachin-627/club/raw/main/steam.jpeg' }, + { name: 'Techspark', image: 'https://github.com/Sachin-627/club/raw/main/techspark.jpeg' }, + { name: 'YRC club', image: 'https://github.com/Sachin-627/club/raw/main/yrc%20club.jpeg' }, + { name: 'NCC club', image: 'https://github.com/Sachin-627/club/raw/main/ncc%20club.jpeg' } +]; + +export const DOMAIN_MAP: Record = { + 'TECHNICAL': [ + { + id: 'CSE', + name: 'CSE', + image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'CSBS', + name: 'CSBS', + image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'AIML', + name: 'AIML', + image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'AIDS', + name: 'AIDS', + image: 'https://images.unsplash.com/photo-1509228627152-72ae9ae6848d?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'IT', + name: 'IT', + image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'ECE', + name: 'ECE', + image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'Mechanical', + name: 'Mechanical', + image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'Bio-tech', + name: 'Bio-tech', + image: 'https://images.unsplash.com/photo-1530210124550-912dc1381cb8?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'CCE', + name: 'CCE', + image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80&w=600' + }, + { + id: 'VLSI', + name: 'VLSI', + image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=600' + } + ], + 'NON-TECHNICAL': [ + { id: 'Management', name: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400' }, + { id: 'Arts & Culture', name: 'Arts & Culture', image: 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&q=80&w=800' }, + { id: 'Sports', name: 'Sports', image: 'https://images.unsplash.com/photo-1552674605-db6ffd4facb5?auto=format&fit=crop&q=80&w=400' }, + { id: 'Social Welfare', name: 'Social Welfare', image: 'https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?auto=format&fit=crop&q=80&w=400' } + ], + 'WORKSHOP': [ + { id: 'Software Dev', name: 'Software Dev', image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=400' }, + { id: 'Design', name: 'Design', image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?auto=format&fit=crop&q=80&w=800' }, + { id: 'Cloud/DevOps', name: 'Cloud/DevOps', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400' } + ], + 'CENTRE-ACTIVITY': [ + { id: 'Research Hub', name: 'Research Hub', image: 'https://images.unsplash.com/photo-1507679799987-c73779587ccf?auto=format&fit=crop&q=80&w=400' }, + { id: 'Innovation Cell', name: 'Innovation Cell', image: 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&q=80&w=400' }, + { id: 'Incubation Centre', name: 'Incubation Centre', image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&q=80&w=400' } + ], + 'ALL': [ + { id: 'CSE', name: 'CSE', image: 'https://images.unsplash.com/photo-1555255707-c07966488bc0?auto=format&fit=crop&q=80&w=400' }, + { id: 'IT', name: 'IT', image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=400' }, + { id: 'AIML', name: 'AIML', image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=400' }, + { id: 'AIDS', name: 'AIDS', image: 'https://images.unsplash.com/photo-1551288049-bbbda5366391?auto=format&fit=crop&q=80&w=400' }, + { id: 'Management', name: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400' }, + { id: 'Mechanical', name: 'Mechanical', image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=400' } + ] +}; + diff --git a/RIT-EMS-main/frontend/src/context/AuthContext.tsx b/RIT-EMS-main/frontend/src/context/AuthContext.tsx index 05eeb57..0ee2ce8 100644 --- a/RIT-EMS-main/frontend/src/context/AuthContext.tsx +++ b/RIT-EMS-main/frontend/src/context/AuthContext.tsx @@ -4,7 +4,7 @@ interface User { id: number; email: string; fullName: string; - role: 'FACULTY' | 'HOD' | 'PRINCIPAL' | 'ADMIN' | 'PLACEMENT'; + role: 'FACULTY' | 'HOD' | 'PRINCIPAL' | 'ADMIN' | 'PLACEMENT' | 'STUDENT'; department: string; isClubCoordinator?: boolean; isPlacementStaff?: boolean; @@ -14,6 +14,12 @@ interface User { inchargeSection?: string; classStrength?: number; assignedClubs?: string[]; + regNo?: string; + year?: string; + section?: string; + collegeName?: string; + gender?: string; + phone?: string; } interface AuthContextType { diff --git a/RIT-EMS-main/frontend/src/index.css b/RIT-EMS-main/frontend/src/index.css index 677fd23..d222744 100644 --- a/RIT-EMS-main/frontend/src/index.css +++ b/RIT-EMS-main/frontend/src/index.css @@ -69,3 +69,101 @@ ::-webkit-scrollbar-thumb { @apply bg-slate-300 rounded-full; } + +/* Site 2 Custom Animations and Classes */ +@keyframes sway { + 0% { transform: rotate(-1deg); } + 50% { transform: rotate(1deg); } + 100% { transform: rotate(-1deg); } +} + +@keyframes mist { + 0% { transform: translateX(-5%); opacity: 0.3; } + 50% { transform: translateX(5%); opacity: 0.5; } + 100% { transform: translateX(-5%); opacity: 0.3; } +} + +@keyframes breeze { + 0% { transform: translateY(0) scale(1); } + 50% { transform: translateY(-5px) scale(1.02); } + 100% { transform: translateY(0) scale(1); } +} + +@keyframes float { + 0% { transform: translateY(0px); } + 50% { transform: translateY(-20px); } + 100% { transform: translateY(0px); } +} + +.animate-sway { + animation: sway 8s ease-in-out infinite; +} + +.animate-mist { + animation: mist 15s ease-in-out infinite; +} + +.animate-breeze { + animation: breeze 10s ease-in-out infinite; +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.glass-navbar { + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.layered-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + overflow: hidden; +} + +.shape { + position: absolute; + border-radius: 50%; + filter: blur(80px); +} + +.shape-1 { + width: 600px; + height: 600px; + background: rgba(0, 0, 0, 0.05); + top: -200px; + left: -200px; +} + +.shape-2 { + width: 500px; + height: 500px; + background: rgba(0, 0, 0, 0.03); + bottom: -150px; + right: -150px; +} + +.hero-gradient { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +.text-stroke { + -webkit-text-stroke: 1px black; + color: transparent; +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} diff --git a/RIT-EMS-main/frontend/src/lib/firebaseBackend.ts b/RIT-EMS-main/frontend/src/lib/firebaseBackend.ts index 1fde65e..85012f2 100644 --- a/RIT-EMS-main/frontend/src/lib/firebaseBackend.ts +++ b/RIT-EMS-main/frontend/src/lib/firebaseBackend.ts @@ -769,6 +769,286 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro await deleteDoc(doc(db, 'ems_events', String(id))); return jsonResponse({ message: "Event deleted successfully" }); } + + // ---------------------------------------------------- + // STUDENT SIGNUP + // ---------------------------------------------------- + if (path === '/api/auth/signup' && method === 'POST') { + const payload = parseBody(init?.body); + const email = payload.email?.trim().toLowerCase(); + + const snap = await getDocs(collection(db, 'ems_users')); + if (snap.docs.some(d => d.data().email?.toLowerCase() === email)) { + return jsonResponse({ message: "A user with this email already exists" }, 400); + } + + const id = generateNumericId(); + const newUser = { + ...payload, + id, + email, + role: 'STUDENT', + department: payload.department || 'CSE', + year: payload.year || '1st Year', + section: payload.section || 'A', + regNo: payload.regNo || '', + phone: payload.phone || '', + gender: payload.gender || 'Male', + collegeName: payload.collegeName || 'Rajalakshmi Institute of Technology' + }; + + await setDoc(doc(db, 'ems_users', String(id)), newUser); + // Return user without password + const { password: _, ...userData } = newUser; + return jsonResponse(userData); + } + + // ---------------------------------------------------- + // REGISTRATIONS CONTROLLER + // ---------------------------------------------------- + if (path === '/api/registrations' && method === 'GET') { + const snap = await getDocs(collection(db, 'ems_registrations')); + let list = snap.docs.map(d => d.data()); + + // Filter by query parameters if any + const filterUserId = queryParams.get('userId') || queryParams.get('user_id'); + const filterEventId = queryParams.get('eventId') || queryParams.get('event_id'); + const filterTeamCode = queryParams.get('teamCode') || queryParams.get('team_code'); + + if (filterUserId) { + list = list.filter(r => String(r.userId || r.user_id) === String(filterUserId)); + } + if (filterEventId) { + list = list.filter(r => String(r.eventId || r.event_id) === String(filterEventId)); + } + if (filterTeamCode) { + list = list.filter(r => String(r.teamCode || r.team_code) === String(filterTeamCode)); + } + + return jsonResponse(list); + } + + if (path === '/api/registrations' && method === 'POST') { + const payload = parseBody(init?.body); + const userId = payload.userId || payload.user_id; + const eventId = payload.eventId || payload.event_id; + + if (!userId || !eventId) { + return jsonResponse({ message: "userId and eventId are required" }, 400); + } + + const regId = `${userId}_${eventId}`; + const regDocRef = doc(db, 'ems_registrations', regId); + const regSnap = await getDoc(regDocRef); + + if (regSnap.exists()) { + return jsonResponse({ message: "You are already registered for this event." }, 400); + } + + // Fetch event details + const eventSnap = await getDoc(doc(db, 'ems_events', String(eventId))); + if (!eventSnap.exists()) { + return jsonResponse({ message: "Event not found" }, 404); + } + const event = eventSnap.data(); + + // Check deadline + if (event.endDate) { + const deadline = new Date(event.registrationFee ? (event.registrationDeadline || event.endDate) : event.endDate); + if (new Date() > deadline) { + return jsonResponse({ message: "Registration Blocked: The deadline for this event has passed." }, 400); + } + } + + // Count current registrations for this event + const allRegsSnap = await getDocs(collection(db, 'ems_registrations')); + const eventRegs = allRegsSnap.docs.map(d => d.data()).filter(r => String(r.eventId || r.event_id) === String(eventId)); + const currentParticipants = eventRegs.length; + + // Validate overall capacity + if (event.maxParticipants && currentParticipants >= event.maxParticipants) { + return jsonResponse({ message: "Registration Blocked: The event has reached its capacity." }, 409); + } + + // Quota validation + const studentDept = payload.dept || payload.department || 'CSE'; + const studentSec = payload.section || 'A'; + + if (event.deptLimits && event.deptLimits[studentDept] !== undefined) { + const deptLimit = Number(event.deptLimits[studentDept]); + const deptRegs = eventRegs.filter(r => (r.dept || r.department) === studentDept); + + // Section limit check if present + if (event.deptSectionLimits && event.deptSectionLimits[studentDept]) { + const sectionLimits = event.deptSectionLimits[studentDept]; + const sectionLimit = Number(sectionLimits[studentSec] || 0); + + if (sectionLimit <= 0) { + return jsonResponse({ message: `Registration Blocked: Section ${studentSec} of ${studentDept} is not permitted to register.` }, 409); + } + + const secRegs = deptRegs.filter(r => r.section === studentSec); + if (secRegs.length >= sectionLimit) { + return jsonResponse({ message: `Registration Blocked: Quota for Section ${studentSec} of ${studentDept} is full.` }, 409); + } + } else { + // Standard department limit check + if (deptRegs.length >= deptLimit) { + return jsonResponse({ message: `Registration Blocked: Quota for department ${studentDept} is full.` }, 409); + } + } + } + + // Safe to create registration + const isFree = event.hasRegistrationFee === false || event.registrationFee === 0 || event.pricingType === 'FREE'; + const newReg = { + ...payload, + id: regId, + userId, + user_id: userId, + eventId, + event_id: eventId, + paymentStatus: isFree ? 'COMPLETED' : 'PENDING', + payment_status: isFree ? 'COMPLETED' : 'PENDING', + certificationStatus: 'NOT_SUBMITTED', + certification_status: 'NOT_SUBMITTED', + registeredAt: new Date().toISOString(), + registered_at: new Date().toISOString() + }; + + await setDoc(regDocRef, newReg); + return jsonResponse(newReg); + } + + if (path.startsWith('/api/registrations/') && method === 'PUT') { + const parts = path.split('/'); + const id = parts[parts.length - 1]; + const payload = parseBody(init?.body); + + const regRef = doc(db, 'ems_registrations', String(id)); + const snap = await getDoc(regRef); + + if (!snap.exists()) { + return jsonResponse({ message: "Registration not found" }, 404); + } + + // Map incoming keys to support both camelCase and snake_case + const updatedFields: any = { ...snap.data(), ...payload }; + if (payload.payment_status !== undefined) updatedFields.paymentStatus = payload.payment_status; + if (payload.paymentStatus !== undefined) updatedFields.payment_status = payload.paymentStatus; + + if (payload.team_code !== undefined) updatedFields.teamCode = payload.team_code; + if (payload.teamCode !== undefined) updatedFields.team_code = payload.teamCode; + + if (payload.team_name !== undefined) updatedFields.teamName = payload.team_name; + if (payload.teamName !== undefined) updatedFields.team_name = payload.teamName; + + if (payload.is_team_leader !== undefined) updatedFields.isTeamLeader = payload.is_team_leader; + if (payload.isTeamLeader !== undefined) updatedFields.is_team_leader = payload.isTeamLeader; + + if (payload.certification_url !== undefined) updatedFields.certificationUrl = payload.certification_url; + if (payload.certificationUrl !== undefined) updatedFields.certification_url = payload.certificationUrl; + + if (payload.certification_status !== undefined) updatedFields.certificationStatus = payload.certification_status; + if (payload.certificationStatus !== undefined) updatedFields.certification_status = payload.certificationStatus; + + if (payload.od_url !== undefined) updatedFields.odUrl = payload.od_url; + if (payload.odUrl !== undefined) updatedFields.od_url = payload.odUrl; + + if (payload.ticket_id !== undefined) updatedFields.ticketId = payload.ticket_id; + if (payload.ticketId !== undefined) updatedFields.ticket_id = payload.ticketId; + + if (payload.ticket_qrcode !== undefined) updatedFields.ticketQrcode = payload.ticket_qrcode; + if (payload.ticketQrcode !== undefined) updatedFields.ticket_qrcode = payload.ticketQrcode; + + await setDoc(regRef, updatedFields); + return jsonResponse({ message: "Registration updated successfully", data: updatedFields }); + } + + if (path.startsWith('/api/registrations/') && method === 'DELETE') { + const parts = path.split('/'); + const id = parts[parts.length - 1]; + await deleteDoc(doc(db, 'ems_registrations', String(id))); + return jsonResponse({ message: "Registration deleted successfully" }); + } + + // ---------------------------------------------------- + // ATTENDANCE CONTROLLER + // ---------------------------------------------------- + if (path === '/api/attendance' && method === 'GET') { + const snap = await getDocs(collection(db, 'ems_attendance')); + let list = snap.docs.map(d => d.data()); + + const regId = queryParams.get('registrationId') || queryParams.get('registration_id'); + const eventId = queryParams.get('eventId') || queryParams.get('event_id'); + + if (regId) { + list = list.filter(a => String(a.registrationId || a.registration_id) === String(regId)); + } + if (eventId) { + list = list.filter(a => String(a.eventId || a.event_id) === String(eventId)); + } + + return jsonResponse(list); + } + + if (path === '/api/attendance' && method === 'POST') { + const payload = parseBody(init?.body); + const records = Array.isArray(payload) ? payload : [payload]; + + for (const rec of records) { + const regId = rec.registrationId || rec.registration_id; + const day = rec.dayLabel || rec.day_idx || "Day 1"; + const slot = rec.batchLabel || rec.batch_idx || "Batch 1"; + const id = `${regId}_${day}_${slot}`; + + const docRef = doc(db, 'ems_attendance', id); + await setDoc(docRef, { + id, + registrationId: regId, + registration_id: regId, + eventId: rec.eventId || rec.event_id, + event_id: rec.eventId || rec.event_id, + dayLabel: day, + day_idx: day, + batchLabel: slot, + batch_idx: slot, + isPresent: !!rec.isPresent, + is_present: !!rec.isPresent, + date: rec.date || new Date().toLocaleDateString() + }); + } + return jsonResponse({ message: "Attendance updated successfully" }); + } + + // ---------------------------------------------------- + // ANNOUNCEMENTS CONTROLLER + // ---------------------------------------------------- + if (path === '/api/announcements' && method === 'GET') { + const snap = await getDocs(collection(db, 'ems_announcements')); + const list = snap.docs.map(d => d.data()); + list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + return jsonResponse(list); + } + + if (path === '/api/announcements' && method === 'POST') { + const payload = parseBody(init?.body); + const id = String(generateNumericId()); + const newAnn = { + ...payload, + id, + timestamp: new Date().toISOString() + }; + await setDoc(doc(db, 'ems_announcements', id), newAnn); + return jsonResponse(newAnn); + } + + if (path.startsWith('/api/announcements/') && method === 'DELETE') { + const parts = path.split('/'); + const id = parts[parts.length - 1]; + await deleteDoc(doc(db, 'ems_announcements', String(id))); + return jsonResponse({ message: "Announcement deleted successfully" }); + } } catch (err: any) { console.error(`Interceptor routing error:`, err); return jsonResponse({ message: "Internal server error: " + err.message }, 500); diff --git a/RIT-EMS-main/frontend/src/lib/pdfGenerator.ts b/RIT-EMS-main/frontend/src/lib/pdfGenerator.ts new file mode 100644 index 0000000..24d865b --- /dev/null +++ b/RIT-EMS-main/frontend/src/lib/pdfGenerator.ts @@ -0,0 +1,153 @@ +import { jsPDF } from 'jspdf'; + +interface CertificateData { + studentName: string; + eventName: string; + category: string; + date: string; + department: string; + collegeName: string; +} + +export function generateCertificatePdf(data: CertificateData) { + // Create a landscape PDF (A4) + const doc = new jsPDF({ + orientation: 'landscape', + unit: 'mm', + format: 'a4' + }); + + const width = doc.internal.pageSize.getWidth(); // ~297mm + const height = doc.internal.pageSize.getHeight(); // ~210mm + + // 1. Sleek Dark Navy Background Border + doc.setDrawColor(30, 41, 59); // Slate-800 + doc.setLineWidth(4); + doc.rect(8, 8, width - 16, height - 16); + + // 2. Innermost Gold Accent Border + doc.setDrawColor(249, 115, 22); // Orange-500/Amber gold + doc.setLineWidth(1.5); + doc.rect(12, 12, width - 24, height - 24); + + // 3. Corner Ornamental Accents + const corners = [ + { x: 12, y: 12 }, + { x: width - 12, y: 12 }, + { x: 12, y: height - 12 }, + { x: width - 12, y: height - 12 } + ]; + corners.forEach(c => { + doc.setFillColor(249, 115, 22); + doc.circle(c.x, c.y, 3, 'F'); + }); + + // 4. Header Section: College Branding + doc.setFont('Helvetica', 'bold'); + doc.setFontSize(22); + doc.setTextColor(30, 41, 59); // Slate-800 + doc.text('RAJALAKSHMI INSTITUTE OF TECHNOLOGY', width / 2, 35, { align: 'center' }); + + doc.setFont('Helvetica', 'normal'); + doc.setFontSize(9); + doc.setTextColor(100, 116, 139); // Slate-500 + doc.text('Accredited by NBA & NAAC, Approved by AICTE, Affiliated to Anna University', width / 2, 41, { align: 'center' }); + doc.text('Kuthambakkam, Chennai - 600124', width / 2, 46, { align: 'center' }); + + // 5. Divider Line + doc.setDrawColor(226, 232, 240); // Slate-200 + doc.setLineWidth(0.5); + doc.line(40, 52, width - 40, 52); + + // 6. Certificate Title + doc.setFont('Helvetica', 'bold'); + doc.setFontSize(24); + doc.setTextColor(249, 115, 22); // Brand Orange + doc.text('CERTIFICATE OF PARTICIPATION', width / 2, 68, { align: 'center' }); + + // 7. Statement Text + doc.setFont('Helvetica', 'normal'); + doc.setFontSize(14); + doc.setTextColor(71, 85, 105); // Slate-600 + doc.text('This is proudly presented to', width / 2, 85, { align: 'center' }); + + // 8. Student Name + doc.setFont('Helvetica', 'bold'); + doc.setFontSize(28); + doc.setTextColor(15, 23, 42); // Slate-900 + doc.text(data.studentName.toUpperCase(), width / 2, 102, { align: 'center' }); + + // Underline under the name + doc.setDrawColor(249, 115, 22); + doc.setLineWidth(1); + doc.line(width / 2 - 60, 106, width / 2 + 60, 106); + + // 9. Event Details + doc.setFont('Helvetica', 'normal'); + doc.setFontSize(13); + doc.setTextColor(71, 85, 105); + doc.text( + `of ${data.collegeName} for active participation in the event`, + width / 2, + 118, + { align: 'center' } + ); + + doc.setFont('Helvetica', 'bold'); + doc.setFontSize(18); + doc.setTextColor(30, 41, 59); + doc.text(`"${data.eventName.toUpperCase()}"`, width / 2, 130, { align: 'center' }); + + // 10. Metadata (Date & Category) + doc.setFont('Helvetica', 'normal'); + doc.setFontSize(12); + doc.setTextColor(100, 116, 139); + + const formattedDate = new Date(data.date).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + + doc.text( + `held on ${formattedDate} as a part of our ${data.category.toUpperCase()} activities`, + width / 2, + 140, + { align: 'center' } + ); + + doc.text( + `Organized by the Department / Club of ${data.department}`, + width / 2, + 148, + { align: 'center' } + ); + + // 11. Signatures + // Left Signature: Coordinator + doc.setFont('Helvetica', 'normal'); + doc.setFontSize(11); + doc.setTextColor(30, 41, 59); + doc.line(45, 180, 105, 180); + // Fake signature graphic text + doc.setFont('Courier', 'italic'); + doc.setTextColor(249, 115, 22); + doc.text('Event Coordinator', 75, 175, { align: 'center' }); + doc.setFont('Helvetica', 'bold'); + doc.setTextColor(30, 41, 59); + doc.text('CONVENOR', 75, 186, { align: 'center' }); + + // Right Signature: Principal + doc.setFont('Helvetica', 'normal'); + doc.line(width - 105, 180, width - 45, 180); + doc.setFont('Courier', 'italic'); + doc.setTextColor(249, 115, 22); + doc.text('Dr. College Principal', width - 75, 175, { align: 'center' }); + doc.setFont('Helvetica', 'bold'); + doc.setTextColor(30, 41, 59); + doc.text('PRINCIPAL, RIT', width - 75, 186, { align: 'center' }); + + // 12. Save PDF + const safeTitle = data.eventName.replace(/[^a-z0-9]/gi, '_').toLowerCase(); + doc.save(`certificate_${safeTitle}.pdf`); +} diff --git a/RIT-EMS-main/frontend/src/pages/LoginPage.tsx b/RIT-EMS-main/frontend/src/pages/LoginPage.tsx index cb37ba3..4e275b4 100644 --- a/RIT-EMS-main/frontend/src/pages/LoginPage.tsx +++ b/RIT-EMS-main/frontend/src/pages/LoginPage.tsx @@ -1,147 +1,503 @@ import { API_BASE_URL } from '../lib/config'; -import React, { useState } from 'react'; -import { motion } from 'framer-motion'; -import { User, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { useAuth } from '../context/AuthContext'; import { cn } from '../lib/utils'; import ritLogo from '../assets/images/college-logo.png'; -import ritBuilding from '../assets/images/ritchennai.jpg'; + +type UserRole = 'STUDENT' | 'COORDINATOR' | 'ADMIN'; + +const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S']; +const EXTERNAL_DEPARTMENTS = [ + ...DEPARTMENTS, + 'Information Technology (IT)', + 'Electrical & Electronics Engineering (EEE)', + 'Civil Engineering', + 'Biomedical Engineering', + 'Chemical Engineering', + 'Aeronautical / Aerospace Engineering', + 'Mechatronics Engineering', + 'Others' +]; +const SECTIONS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']; +const YEARS = ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year']; export const LoginPage: React.FC = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); const { login } = useAuth(); + const [selectedRole, setSelectedRole] = useState(null); - const handleLogin = async (e: React.FormEvent) => { + // Forms states + const [isSignUp, setIsSignUp] = useState(false); + const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null); + const [coordinatorLoginType, setCoordinatorLoginType] = useState<'Faculty' | 'HOD' | null>(null); + const [formData, setFormData] = useState({ + email: '', + phone: '', + regNo: '', + password: '', + confirmPassword: '', + name: '', + collegeName: 'Rajalakshmi Institute of Technology', + department: '', + section: '', + year: '', + gender: '', + collegeLocation: '', + }); + + const [showPassword, setShowPassword] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + // Auto-set internal for non-student roles + useEffect(() => { + if (selectedRole && selectedRole !== 'STUDENT') { + setSignUpType('INTERNAL'); + } else if (!isSignUp) { + setSignUpType(null); + } + }, [selectedRole, isSignUp]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setIsLoading(true); - setError(''); + setErrorMessage(null); + setIsSubmitting(true); try { - const response = await fetch(API_BASE_URL + '/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); + if (!isSignUp) { + // Login Logic + const response = await fetch(API_BASE_URL + '/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: formData.email, + password: formData.password + }) + }); - if (response.ok) { - const data = await response.json(); - login(data); + if (response.ok) { + const userData = await response.json(); + + // Role checks + if (selectedRole === 'STUDENT' && userData.role !== 'STUDENT') { + throw new Error("Access Denied: Please use the Staff Portal to log in."); + } + if (selectedRole === 'COORDINATOR') { + const isCoordRole = userData.role === 'FACULTY' || userData.role === 'HOD'; + if (!isCoordRole) { + throw new Error("Access Denied: You are not authorized for Coordinator Portal."); + } + if (coordinatorLoginType === 'HOD' && userData.role !== 'HOD') { + throw new Error("Access Denied: HOD access only."); + } + if (coordinatorLoginType === 'Faculty' && userData.role !== 'FACULTY') { + throw new Error("Access Denied: Faculty access only."); + } + } + if (selectedRole === 'ADMIN' && userData.role !== 'ADMIN' && userData.role !== 'PRINCIPAL') { + throw new Error("Access Denied: Admin or Principal access only."); + } + + login(userData); + } else { + const errData = await response.json(); + throw new Error(errData.message || "Invalid credentials."); + } } else { - const errData = await response.json(); - setError(errData.message || 'Login failed'); + // Sign-Up Logic (Only for Student Role) + if (formData.password !== formData.confirmPassword) { + throw new Error("Passwords do not match."); + } + + const signupPayload = { + email: formData.email, + password: formData.password, + fullName: formData.name, + regNo: formData.regNo, + phone: formData.phone, + gender: formData.gender, + collegeName: signUpType === 'EXTERNAL' ? formData.collegeName : 'Rajalakshmi Institute of Technology', + department: signUpType === 'EXTERNAL' ? formData.department : formData.department, + year: signUpType === 'EXTERNAL' ? 'N/A' : formData.year, + section: signUpType === 'EXTERNAL' ? 'N/A' : formData.section, + role: 'STUDENT' + }; + + const response = await fetch(API_BASE_URL + '/api/auth/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(signupPayload) + }); + + if (response.ok) { + const userData = await response.json(); + alert("Account registered successfully!"); + login(userData); + } else { + const errData = await response.json(); + throw new Error(errData.message || "Registration failed."); + } } - } catch (err) { - setError('Connection failed. Please ensure the backend is running.'); + } catch (err: any) { + setErrorMessage(err.message || "An error occurred."); } finally { - setIsLoading(false); + setIsSubmitting(false); } }; + const RIT_BLUE = 'bg-[#004a99]'; + const RIT_BLUE_TEXT = 'text-[#004a99]'; + const RIT_BLUE_HOVER = 'hover:bg-[#003366]'; + + const portals = [ + { + id: 'STUDENT' as UserRole, + title: 'Student Portal', + description: 'Access events, track registrations, and manage your academic profile.', + icon: 'fa-user-graduate', + color: 'bg-[#004a99]', + hoverColor: 'hover:border-[#004a99]', + textColor: 'text-[#004a99]' + }, + { + id: 'COORDINATOR' as UserRole, + title: 'Event Coordinator Portal', + description: 'Create, manage, and coordinate campus events and announcements.', + icon: 'fa-calendar-check', + color: 'bg-orange-500', + hoverColor: 'hover:border-orange-500', + textColor: 'text-orange-500' + }, + { + id: 'ADMIN' as UserRole, + title: 'Admin Portal', + description: 'System-wide oversight, user management, and high-level analytics.', + icon: 'fa-user-shield', + color: 'bg-[#004a99]', + hoverColor: 'hover:border-[#004a99]', + textColor: 'text-[#004a99]' + } + ]; + return ( -
-
- {/* Left Side: Login Form */} -
-
- RIT Logo -

- RAJALAKSHMI INSTITUTE OF TECHNOLOGY -

-

- RIT EMS -

-

- Event Management System -

-
- -
-
- -
- - setEmail(e.target.value)} - placeholder="Username or email" - className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-4 text-sm font-semibold focus:outline-none focus:bg-white focus:border-brand-indigo/30 transition-all" - required - /> -
-
- -
-
- - -
-
- - setPassword(e.target.value)} - placeholder="••••••••" - className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-12 text-sm font-semibold focus:outline-none focus:bg-white focus:border-brand-indigo/30 transition-all" - required - /> - -
-
- - {error && ( -

- {error} +

+ + {!selectedRole ? ( + // ---------------------------------------------------- + // WELCOME SCREEN GATEWAY + // ---------------------------------------------------- + +
+ RIT Logo +

+ Select your gateway to excellence. Connect, manage, and celebrate.

- )} - - - -
- - {/* Right Side: Building Image */} -
- College Building -
-
-
-

Rajalakshmi Institute of Technology

-

Nurturing Excellence since 2008

-
-
-
+ +
+ {portals.map((portal) => ( +
{ + setSelectedRole(portal.id); + setErrorMessage(null); + setIsSignUp(false); + setSignUpType(null); + setCoordinatorLoginType(null); + }} + className={cn( + "group relative bg-white border-2 border-slate-100 rounded-[3rem] p-10 cursor-pointer transition-all duration-500 hover:-translate-y-4 hover:shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)]", + portal.hoverColor + )} + > +
+ +
+ +

+ {portal.title} +

+ +

+ {portal.description} +

+ +
+ Enter Portal +
+
+ ))} +
+ +
+

+ © 2026 Rajalakshmi Institute of Technology • Academic Excellence +

+
+ + ) : ( + // ---------------------------------------------------- + // DUAL SLIDING LOGIN & SIGNUP FORMS (Site 2 Layout) + // ---------------------------------------------------- + + {/* Back Button */} + + + {/* Forms Section */} +
+ + {/* Sign Up (Left Side Panel) */} +
+ Logo + + {!signUpType && selectedRole === 'STUDENT' ? ( +
+

Choose Portal

+
+ + +
+
+ ) : ( +
+
+
+

+ {signUpType === 'INTERNAL' ? 'Internal' : 'External'} Sign Up +

+ +
+

Join the RIT Excellence Hub

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + + +
+ + {errorMessage &&

{errorMessage}

} + + +
+ )} +
+ + {/* Sign In (Right Side Panel) */} +
+ + {selectedRole === 'COORDINATOR' && !coordinatorLoginType ? ( +
+ Logo +

Choose Access Level

+
+ + +
+
+ ) : ( +
+
+ Logo +
+

Welcome Back

+ {selectedRole === 'COORDINATOR' && ( + + )} +
+

+ Access your {selectedRole === 'ADMIN' ? 'Admin / Principal' : (selectedRole === 'COORDINATOR' ? `${coordinatorLoginType} Event` : 'Student')} portal +

+
+ +
+
+ + +
+
+ + + +
+
+ +
+ +
+ + {errorMessage &&

{errorMessage}

} + + +
+ )} +
+
+ + {/* Sliding Overlay Panel (Matches RIT Navy theme) */} +
+ {/* Background patterns */} +
+
+
+ + {/* Login Overlay Message */} +
+

+ {selectedRole === 'STUDENT' ? 'New Here?' : 'Restricted Gateway'} +

+

+ {selectedRole === 'STUDENT' + ? 'Sign up and discover a world of possibilities at RIT Events Hub.' + : 'Coordinator and Admin profiles are pre-allocated. Please consult systems management for access credentials.'} +

+ {selectedRole === 'STUDENT' && ( + + )} +
+ + {/* Signup Overlay Message */} +
+

Welcome Back!

+

+ To keep connected with us please login with your personal credentials. +

+ +
+ +
+ +
+ )} +
); }; diff --git a/RIT-EMS-main/frontend/src/types.ts b/RIT-EMS-main/frontend/src/types.ts new file mode 100644 index 0000000..5fd0f99 --- /dev/null +++ b/RIT-EMS-main/frontend/src/types.ts @@ -0,0 +1,153 @@ + +export type AppState = 'WELCOME' | 'LOGIN' | 'DASHBOARD' | 'ADMIN_LANDING' | 'FACULTY_DASHBOARD' | 'VERIFY'; +export type UserRole = 'STUDENT' | 'COORDINATOR' | 'ADMIN'; +export type DashboardView = 'HOME' | 'EVENTS' | 'REGISTRATIONS' | 'PROFILE' | 'ABOUT' | 'CONTACT' | 'STATUS_TRACKER'; +export type FacultyView = 'OVERVIEW' | 'PARTICIPANTS' | 'ATTENDANCE' | 'NOTIFICATIONS' | 'PROFILE'; + +export interface ResourcePerson { + id?: string; + type: 'INTERNAL' | 'EXTERNAL'; + name: string; + dept?: string; + college_name?: string; + phone: string; + email: string; +} + +export interface Batch { + id: number; + startTime: string; + endTime: string; + date?: string; + resourcePerson?: ResourcePerson; +} + +export interface EventDay { + date: string; + batches: Batch[]; // If unified, this will have one batch at index 0 + startTime?: string; + endTime?: string; + resourcePerson?: ResourcePerson; +} + +export interface EventSchedule { + id: string; + event_id: string; + day_idx: number; + batch_idx: number; + date: string; + start_time: string; + end_time: string; + resource_person?: ResourcePerson; +} + +export interface Event { + id: string; + title: string; + date: string; + location: string; + category: 'TECHNICAL' | 'NON-TECHNICAL' | 'WORKSHOP' | 'CENTRE-ACTIVITY'; + domain: string; + club?: string; + image: string; + pricingType: 'PAID' | 'FREE'; + coordinator: string; + status?: 'Registered' | 'Event Ongoing' | 'Completed' | 'Scheduled' | 'APPROVED' | 'COMPLETED' | 'ONGOING' | 'REJECTED'; + hasRegistrationFee?: boolean; + registrationFee?: number; + registrationDeadline?: string; + maxParticipants?: number; + durationDays?: number; + schedule?: EventSchedule[]; + deptLimits?: Record; // Maps department codes to specific seat counts + deptSectionLimits?: Record>; // Maps department codes to their section capacities + currentParticipants?: number; + currentDeptCounts?: Record; + currentDeptSectionCounts?: Record>; // Maps department codes to their sections' current counts + event_summary?: string; + isTeamEvent?: boolean; + teamSizeLimit?: number; + teamComposition?: 'INTER_DEPT' | 'MIXED'; + created_by?: string; + participantType?: 'INTERNAL' | 'EXTERNAL' | 'BOTH'; + verificationStatus?: 'PENDING' | 'PENDING_HOD' | 'PENDING_ADMIN' | 'APPROVED' | 'REJECTED'; + refreshment_expense?: number; + transportation_expense?: number; + session_coverage_fee?: number; + total_expense?: number; + conducting_dept?: string; + request_by_faculty?: string; + request_by_HOD?: string; +} + +export interface Announcement { + id: string; + title: string; + message: string; + timestamp: any; + expiresAt?: any; + type: 'DELAY' | 'INFO' | 'URGENT' | 'ENDED' | 'ONGOING'; + eventId?: string; + eventTitle?: string; +} + +export interface Ticket { + ticketId: string; + qrCodeData: string; + eventId: string; + userId: string; + userEmail: string; + userName: string; + regNo?: string; + dept?: string; + section?: string; + status: 'ACTIVE' | 'USED' | 'CANCELLED'; + createdAt: any; +} + +export interface Participant { + id: string; + studentName: string; + regNo: string; + branch: string; + year: string; + eventName: string; + eventId: string; + location: string; + timings: string; + category: 'TECHNICAL' | 'NON-TECHNICAL' | 'WORKSHOP' | 'CENTRE-ACTIVITY'; +} + +export interface StudentRequest { + id: string; + studentName: string; + rollNo: string; + branch: string; + eventName: string; + eventId: string; + timestamp: string; + status: 'PENDING' | 'APPROVED' | 'REJECTED'; +} + + +export interface UserProfile { + name: string; + email: string; + phone?: string; + year?: string; + section?: string; + department?: string; + reg_no?: string; + id?: string; +} + +export interface SpecialEvent { + id: string; + created_at: string; + title: string; + description: string; + link: string; + created_by: string; + is_active: boolean; + verificationStatus?: 'PENDING' | 'PENDING_HOD' | 'PENDING_ADMIN' | 'APPROVED' | 'REJECTED'; +}