Merged two websites

This commit is contained in:
2026-06-18 17:56:04 +05:30
parent 684e947e78
commit 91842646f0
34 changed files with 6613 additions and 131 deletions

View File

@@ -5,6 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RIT EMS - Event Management System</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Pirata+One&family=Almendra:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<div id="root"></div>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<AnimatePresence mode="wait">
<motion.div
key="student-dashboard"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<StudentDashboard />
</motion.div>
</AnimatePresence>
);
}
const renderContent = () => {
const isDashboard = (
<Overview
@@ -137,6 +155,8 @@ const AppContent: React.FC = () => {
return (user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? <AutomationView /> : isDashboard;
case 'import-excel':
return <ExcelImport />;
case 'student-registrations':
return (user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? <StudentRegistrationsView /> : isDashboard;
case 'dashboard':
return isDashboard;
default:

View File

@@ -38,7 +38,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ 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 (

View File

@@ -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<DashboardView>('HOME');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [trackedEvent, setTrackedEvent] = useState<Event | null>(null);
// States
const [rawEvents, setRawEvents] = useState<any[]>([]);
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [allRegistrations, setAllRegistrations] = useState<any[]>([]);
const [specialEvents, setSpecialEvents] = useState<SpecialEvent[]>([]);
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<string, number> = {};
const deptSectionCounts: Record<string, Record<string, number>> = {};
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<string> => {
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 (
<>
<Hero events={events} />
<UpcomingEventsSlider events={events} />
<AboutHubSection />
<StatsSection events={events} />
<HomeDashboard
events={events}
announcements={announcements}
onNavigateToEvents={() => setActiveView('EVENTS')}
specialEvents={specialEvents}
/>
</>
);
case 'ABOUT':
return (
<div className="pt-16 min-h-screen bg-white">
<AboutSection />
</div>
);
case 'CONTACT':
return (
<div className="pt-16 min-h-screen bg-white">
<ContactSection />
</div>
);
case 'EVENTS':
return (
<EventList
events={events}
selectedCategory={selectedCategory}
bookedEventIds={bookedEventIds}
onToggleBooking={handleToggleBooking}
onSelectCategory={setSelectedCategory}
onTrackStatus={handleTrackStatus}
currentUserName={user?.fullName || ''}
userRole={user?.role}
userRegistrations={userRegistrations}
/>
);
case 'REGISTRATIONS':
return (
<RegistrationsView
events={events}
bookedEventIds={bookedEventIds}
userRegistrations={userRegistrations}
onToggleBooking={handleToggleBooking}
onTrackStatus={handleTrackStatus}
currentUserName={user?.fullName || ''}
userRole={user?.role}
/>
);
case 'PROFILE':
return <ProfileView onLogout={logout} />;
case 'STATUS_TRACKER':
const registration = userRegistrations.find(r => String(r.eventId || r.event_id) === String(trackedEvent?.id));
return trackedEvent ? (
<StatusTrackerView
event={trackedEvent}
registration={registration}
onBack={() => 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 (
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#f97316] mb-4"></div>
<p className="text-xs font-black uppercase tracking-widest text-slate-400">Syncing with Firestore Database...</p>
</div>
);
}
return (
<div className="min-h-screen bg-[#F3F4F6]">
<Navbar
activeView={activeView}
onViewChange={(view) => {
setActiveView(view);
setTrackedEvent(null);
}}
onLogout={logout}
/>
<main className="pb-20">
{renderContent()}
</main>
<Footer />
{/* Team Management Modals */}
{showCreateModal && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[3rem] w-full max-w-md p-10 md:p-12 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-orange-400 to-amber-400"></div>
<div className="w-16 h-16 flex items-center justify-center mx-auto mb-8">
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className="w-full h-full object-contain"
/>
</div>
<h3 className="text-2xl font-black text-slate-900 text-center uppercase mb-2 tracking-tight">Form a Team</h3>
<p className="text-[10px] text-gray-400 text-center font-bold uppercase tracking-widest mb-10 line-clamp-1 px-4">{showCreateModal.title}</p>
<div className="space-y-8">
<div>
<label className="block text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3 px-2">Team Identity</label>
<input
type="text"
placeholder="Enter team name..."
className="w-full bg-slate-50 border border-slate-200 rounded-[2rem] px-8 py-5 text-sm font-bold outline-none focus:ring-4 focus:ring-orange-100 focus:border-[#f97316] transition-all placeholder:text-slate-300"
value={teamName}
onChange={e => setTeamName(e.target.value)}
autoFocus
/>
</div>
<div className="flex flex-col gap-3">
<button
onClick={() => handleCreateTeam(showCreateModal.eventId)}
disabled={isProcessing}
className="w-full py-5 bg-[#f97316] text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl shadow-orange-200 hover:bg-[#ea580c] transition-all active:scale-95 disabled:opacity-50"
>
{isProcessing ? 'INITIALIZING...' : 'Establish Team & Code'}
</button>
<button onClick={() => setShowCreateModal(null)} className="w-full py-4 text-gray-400 font-black uppercase text-[9px] tracking-widest hover:text-slate-600 transition-colors">Dismiss</button>
</div>
</div>
</div>
</div>
</Portal>
)}
{showJoinModal && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[3rem] w-full max-w-md p-10 md:p-12 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-blue-400 to-indigo-400"></div>
<div className="w-16 h-16 flex items-center justify-center mx-auto mb-8">
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className="w-full h-full object-contain"
/>
</div>
<h3 className="text-2xl font-black text-slate-900 text-center uppercase mb-2 tracking-tight">Join Alliance</h3>
<p className="text-[10px] text-gray-400 text-center font-bold uppercase tracking-widest mb-10 line-clamp-1 px-4">{showJoinModal.title}</p>
<div className="space-y-8">
<div>
<label className="block text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3 px-2 text-center">Team Access Token</label>
<input
type="text"
placeholder="TOKEN"
className="w-full bg-slate-50 border border-slate-200 rounded-[2rem] px-5 py-6 text-center text-3xl font-black tracking-[0.4em] outline-none focus:ring-4 focus:ring-blue-100 focus:border-blue-500 uppercase transition-all placeholder:text-slate-200"
value={teamCode}
onChange={e => setTeamCode(e.target.value)}
maxLength={6}
autoFocus
/>
</div>
<div className="flex flex-col gap-3">
<button
onClick={() => handleJoinTeam(showJoinModal.eventId)}
disabled={isProcessing}
className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl shadow-gray-200 hover:bg-black transition-all active:scale-95 disabled:opacity-50"
>
{isProcessing ? 'AUTHENTICATING...' : 'Validate & Join'}
</button>
<button onClick={() => setShowJoinModal(null)} className="w-full py-4 text-gray-400 font-black uppercase text-[9px] tracking-widest hover:text-slate-600 transition-colors">Dismiss</button>
</div>
</div>
</div>
</div>
</Portal>
)}
</div>
);
};

View File

@@ -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<Event[]>([]);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [registrations, setRegistrations] = useState<Registration[]>([]);
const [attendanceLogs, setAttendanceLogs] = useState<any[]>([]);
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<Registration | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
// OD Upload states
const [odFileBase64, setOdFileBase64] = useState<string | null>(null);
// Attendance checking state (maps registrationId -> dayLabel -> isPresent)
const [attendanceGrid, setAttendanceGrid] = useState<Record<string, Record<string, boolean>>>({});
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<string, Record<string, boolean>> = {};
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<HTMLInputElement>) => {
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 (
<div className="space-y-8 animate-in fade-in duration-300">
{/* Header Info */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">Student Engagement Hub</h2>
<p className="text-text-muted font-medium text-sm">Review registration rosters, log session attendance checklists, release OD files, and audit certificate proofs.</p>
</div>
{/* Event Selector Dropdown */}
<div className="flex items-center gap-3">
<label className="text-xs font-black text-slate-400 uppercase tracking-widest">Selected Event:</label>
<select
value={selectedEvent?.id || ''}
onChange={(e) => {
const ev = events.find(ev => ev.id === Number(e.target.value));
if (ev) setSelectedEvent(ev);
}}
className="bg-white border border-slate-200 rounded-2xl py-3.5 px-5 text-xs font-black focus:outline-none focus:ring-2 focus:ring-brand-indigo/10 appearance-none cursor-pointer pr-10 shadow-sm"
>
{events.map(e => <option key={e.id} value={e.id}>{e.title}</option>)}
</select>
</div>
</div>
{selectedEvent ? (
<div className="space-y-8">
{/* Quick Metrics Panels */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Passes Secured</span>
<p className="text-2xl font-black text-slate-900">{registrations.length}</p>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Teamed Up</span>
<p className="text-2xl font-black text-slate-900">{registrations.filter(r => r.teamCode).length}</p>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">In Review Proofs</span>
<p className="text-2xl font-black text-amber-500">{registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').length}</p>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Verified Certificates</span>
<p className="text-2xl font-black text-emerald-500">{registrations.filter(r => r.certificationStatus === 'APPROVED').length}</p>
</div>
</div>
{/* Sub Navigation Tabs */}
<div className="flex bg-slate-100 p-1 rounded-2xl gap-1 max-w-md">
<button
onClick={() => setActiveSubTab('roster')}
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'roster' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
>
Roster & Fee
</button>
<button
onClick={() => setActiveSubTab('attendance')}
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'attendance' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
>
Attendance
</button>
<button
onClick={() => setActiveSubTab('od')}
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'od' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
>
On-Duty Release
</button>
<button
onClick={() => setActiveSubTab('certificates')}
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'certificates' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
>
Audits
</button>
</div>
{/* Sub Tab Contents */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 shadow-sm p-8">
{/* SUB TAB 1: ROSTER & MANUAL FEE */}
{activeSubTab === 'roster' && (
<div className="space-y-6">
<div className="flex items-center justify-between gap-6">
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Registration Roster</h3>
{/* Search Roster */}
<div className="relative max-w-xs w-full">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search name or regNo..."
className="w-full bg-slate-50 border border-slate-100 rounded-xl py-2.5 pl-10 pr-4 text-xs font-semibold focus:outline-none focus:bg-white focus:border-brand-indigo/20 transition-all"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs font-bold text-slate-500 uppercase tracking-widest">
<thead>
<tr className="border-b border-slate-100 text-slate-400">
<th className="py-4">Student Details</th>
<th className="py-4">Class</th>
<th className="py-4">College</th>
<th className="py-4">Alliance/Team</th>
<th className="py-4 text-center">Payment Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredRoster.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-slate-300 italic font-bold">No registered students matched the search.</td>
</tr>
) : (
filteredRoster.map((reg) => (
<tr key={reg.id} className="hover:bg-slate-50/50">
<td className="py-4">
<p className="text-sm font-black text-brand-navy">{reg.userName}</p>
<p className="text-[10px] text-slate-400 mt-0.5">{reg.regNo} | {reg.phone}</p>
</td>
<td className="py-4 text-slate-600">
{reg.year} Year / SEC {reg.section}
<p className="text-[9px] text-brand-indigo mt-0.5">{reg.dept}</p>
</td>
<td className="py-4 text-slate-600 truncate max-w-[150px]">{reg.college}</td>
<td className="py-4">
{reg.teamCode ? (
<div>
<p className="text-slate-800 font-black text-[11px]">{reg.teamName}</p>
<p className="text-[9px] text-slate-400">Code: {reg.teamCode}</p>
</div>
) : (
<span className="text-slate-300 font-normal">Individual</span>
)}
</td>
<td className="py-4 text-center">
<button
onClick={() => handleTogglePayment(reg)}
disabled={isProcessing}
className={cn(
"px-3 py-1.5 rounded-lg text-[9px] font-black tracking-widest border transition-all",
reg.paymentStatus === 'COMPLETED'
? "bg-emerald-50 border-emerald-100 text-emerald-600 hover:bg-emerald-100"
: "bg-red-50 border-red-100 text-red-600 hover:bg-red-100 animate-pulse"
)}
>
{reg.paymentStatus}
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* SUB TAB 2: SESSION ATTENDANCE GRID */}
{activeSubTab === 'attendance' && (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Mark Slot Presence</h3>
<button
onClick={handleSaveAttendance}
disabled={isProcessing}
className="px-6 py-2.5 bg-brand-navy text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:scale-[1.02] transition-all shadow-md"
>
{isProcessing ? 'Syncing...' : 'Save Attendance'}
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs font-bold text-slate-500 uppercase tracking-widest">
<thead>
<tr className="border-b border-slate-100 text-slate-400">
<th className="py-4">Student</th>
<th className="py-4">Register No</th>
{daysList.map(day => <th key={day} className="py-4 text-center">{day}</th>)}
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{registrations.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-slate-300 italic font-bold">No students registered yet.</td>
</tr>
) : (
registrations.map((reg) => (
<tr key={reg.id} className="hover:bg-slate-50/50">
<td className="py-4 font-black text-brand-navy">{reg.userName}</td>
<td className="py-4 text-slate-400">{reg.regNo}</td>
{daysList.map(day => (
<td key={day} className="py-4 text-center">
<input
type="checkbox"
checked={attendanceGrid[reg.id]?.[day] || false}
onChange={() => handleToggleAttendance(reg.id, day)}
className="w-4.5 h-4.5 rounded border-slate-300 text-brand-indigo focus:ring-brand-indigo cursor-pointer"
/>
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* SUB TAB 3: ON-DUTY UPLOADER */}
{activeSubTab === 'od' && (
<div className="space-y-6 max-w-xl">
<div>
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">On-Duty Official Release</h3>
<p className="text-xs text-slate-400 font-semibold mt-1">Upload the unified signed On-Duty letter for this event. It will be released to all registered students automatically.</p>
</div>
<div className="p-8 border-2 border-dashed border-slate-200 rounded-[2rem] text-center space-y-4">
<Upload className="w-10 h-10 text-slate-300 mx-auto mb-2" />
<div className="relative">
<input
type="file"
accept="application/pdf,image/*"
onChange={handleODFileChange}
className="hidden"
id="od-sheet-uploader"
/>
<label
htmlFor="od-sheet-uploader"
className="px-6 py-3 bg-slate-50 border border-slate-200 text-slate-600 rounded-xl font-black text-[10px] uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-all inline-block"
>
{odFileBase64 ? 'OD Document Loaded' : 'Choose OD Letter'}
</label>
</div>
{odFileBase64 && (
<p className="text-[10px] font-black text-emerald-500 uppercase tracking-wider">File captured ready to broadcast</p>
)}
</div>
<button
onClick={handleUploadODFile}
disabled={isProcessing || !odFileBase64}
className="w-full py-4 bg-brand-indigo text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-md shadow-brand-indigo/10 disabled:opacity-50"
>
{isProcessing ? 'Releasing ODs...' : 'Broadcast OD to All Students'}
</button>
</div>
)}
{/* SUB TAB 4: CERTIFICATE AUDITOR */}
{activeSubTab === 'certificates' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Audit Student Proofs</h3>
<p className="text-xs text-slate-400 font-semibold mt-1">Inspect uploaded completion screenshot proofs and approve them to unlock certificate downloads.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').length === 0 ? (
<div className="col-span-2 py-16 bg-slate-50 rounded-3xl border border-dashed border-slate-200 text-center text-slate-400 font-bold uppercase tracking-wider text-xs">
<FileCheck className="w-12 h-12 text-slate-300 mx-auto mb-4 opacity-50" />
All certificate proofs have been successfully audited!
</div>
) : (
registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').map((reg) => (
<div
key={reg.id}
className="bg-slate-50 p-6 rounded-3xl border border-slate-100 flex items-center justify-between hover:border-slate-200 transition-all"
>
<div>
<p className="text-sm font-black text-slate-800 uppercase">{reg.userName}</p>
<p className="text-[9px] text-slate-400 font-bold uppercase tracking-wider mt-0.5">{reg.regNo} | {reg.dept}</p>
</div>
<button
onClick={() => setInspectCertReg(reg)}
className="px-4 py-2.5 bg-brand-glow text-brand-indigo rounded-xl font-black text-[10px] uppercase tracking-widest flex items-center gap-1.5 shadow-sm border border-brand-indigo/5"
>
<Eye className="w-4 h-4" /> Inspect Proof
</button>
</div>
))
)}
</div>
</div>
)}
</div>
</div>
) : (
<div className="bg-white rounded-[2.5rem] p-20 text-center border border-slate-100 shadow-sm">
<Users className="w-16 h-16 text-slate-300 mx-auto mb-6 opacity-20" />
<h3 className="text-xl font-black text-slate-800 mb-2">No Live Events Available</h3>
<p className="text-slate-400 font-medium text-sm">Create/approve events in your main dashboard first to access student hub audits.</p>
</div>
)}
{/* INSPECT SCREENSHOT MODAL */}
{inspectCertReg && (
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<div className="bg-white rounded-[3rem] w-full max-w-md overflow-hidden shadow-2xl relative border border-slate-100 flex flex-col justify-between max-h-[85vh]">
<div className="p-6 bg-slate-900 text-white flex justify-between items-center">
<div>
<h3 className="text-lg font-black uppercase leading-none">Inspect Screenshot</h3>
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-1">{inspectCertReg.userName} | {inspectCertReg.regNo}</p>
</div>
<button
onClick={() => setInspectCertReg(null)}
className="p-1.5 hover:bg-white/10 rounded-lg text-white"
>
<XCircle className="w-5 h-5" />
</button>
</div>
{/* Proof Body */}
<div className="p-8 overflow-y-auto max-h-[50vh] flex flex-col items-center">
{inspectCertReg.certificationUrl ? (
<img
src={inspectCertReg.certificationUrl}
alt="Completion proof"
className="w-full object-contain rounded-2xl border border-slate-200 shadow-sm"
/>
) : (
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest">No proof uploaded</p>
)}
</div>
{/* Actions Bar */}
<div className="p-6 bg-slate-50 border-t border-slate-100 flex gap-4">
<button
onClick={() => handleAuditCertificate(inspectCertReg.id, 'NOT_SUBMITTED')}
disabled={isProcessing}
className="flex-1 py-3.5 bg-red-50 text-red-600 rounded-xl font-black text-[10px] uppercase tracking-widest border border-red-100 hover:bg-red-100"
>
Reject Proof
</button>
<button
onClick={() => handleAuditCertificate(inspectCertReg.id, 'APPROVED')}
disabled={isProcessing}
className="flex-1 py-3.5 bg-emerald-500 text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-md shadow-emerald-100 hover:bg-emerald-600"
>
Approve & Unlock Cert
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { motion } from 'framer-motion';
const AboutHubSection: React.FC = () => {
return (
<section className="w-full bg-[#F9FAFB] py-20 px-6 md:px-12 lg:px-20 overflow-hidden">
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
{/* Left Side: Image & Floating Card */}
<div className="relative w-full h-[500px] lg:h-[600px] group perspective-1000">
<motion.img
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
viewport={{ once: true }}
src="https://cache.careers360.mobi/media/presets/720X480/colleges/social-media/media-gallery/3425/2021/6/16/DSC08602.JPG"
alt="RIT Campus Collaboration"
className="w-full h-full object-cover rounded-3xl shadow-2xl transition-transform duration-700 group-hover:scale-[1.02]"
/>
{/* Floating Card */}
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.9 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
whileHover={{ y: -5 }}
transition={{ duration: 0.6, delay: 0.3, type: "spring", stiffness: 100 }}
viewport={{ once: true }}
className="absolute -bottom-6 -right-6 md:bottom-10 md:-right-10 bg-[#2D3748] text-[#f97316] p-6 md:p-8 rounded-xl shadow-xl max-w-[280px] z-10 border-l-4 border-[#f97316]"
>
<p className="text-xl md:text-2xl font-serif font-bold leading-tight tracking-wide">
INNOVATION
</p>
<p className="text-sm md:text-base text-gray-300 mt-2 font-medium">
Where creativity meets execution. Fueling the future of tech.
</p>
</motion.div>
</div>
{/* Right Side: Content */}
<div className="flex flex-col justify-center space-y-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="space-y-4"
>
<h4 className="text-[#f97316] font-serif tracking-widest text-sm font-bold uppercase">
Our Legacy
</h4>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif text-[#2D3748] leading-tight">
What is <span className="italic text-[#f97316]">RIT EVENTS HUB?</span>
</h2>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="text-[#2D3748] text-lg leading-relaxed max-w-xl"
>
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.
</motion.p>
</div>
</div>
</section>
);
};
export default AboutHubSection;

View File

@@ -0,0 +1,134 @@
import React from 'react';
const AboutSection: React.FC = () => {
return (
<section className="py-24 px-6 md:px-12 lg:px-24 bg-white">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h3 className="text-[#f97316] font-bold tracking-widest uppercase text-xl mb-2 relative inline-block">
ABOUT
<span className="absolute -bottom-2 left-0 w-full h-1 bg-[#f97316]"></span>
</h3>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-[#1e3a8a] mt-6 mb-8">
Rajalakshmi Institute of Technology (RIT)
</h2>
</div>
<div className="flex flex-col lg:flex-row gap-12 items-start">
<div className="lg:w-1/2 text-gray-700 leading-relaxed text-lg text-justify">
<p className="mb-4">
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.
</p>
<p className="mb-4">
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 <a href="#" className="text-blue-500 hover:underline">AICTE-approved colleges in Chennai</a> New Delhi, and also offers NBA-approved courses.
</p>
</div>
<div className="lg:w-1/2 w-full">
<img
src="https://cache.careers360.mobi/media/presets/720X480/colleges/social-media/media-gallery/3425/2021/6/16/DSC05418.JPG"
alt="Rajalakshmi Institute of Technology"
className="w-full h-auto object-cover shadow-lg"
referrerPolicy="no-referrer"
/>
</div>
</div>
{/* Graduate Programmes Section */}
<div className="mt-24">
<h2 className="text-3xl md:text-4xl font-bold text-[#1e3a8a] text-center mb-16">
Graduate Programmes offered
</h2>
<div className="flex flex-col lg:flex-row gap-12 items-start">
<div className="lg:w-1/2 w-full">
<img
src="https://ritchennai.org/img/image/slider-m-3.jpg"
alt="RIT Students"
className="w-full h-auto object-cover shadow-lg rounded-lg"
referrerPolicy="no-referrer"
/>
</div>
<div className="lg:w-1/2 w-full space-y-8">
<div>
<h3 className="text-xl font-bold text-[#1e3a8a] mb-4">UG Programmes</h3>
<ul className="space-y-2 text-gray-700 list-disc pl-5 marker:text-[#f97316]">
<li>B.E. Computer Science & Engineering</li>
<li>B.E. Computer Science & Engineering(AI&ML)</li>
<li>B.E. Computer & Communication Engineering</li>
<li>B.E. Electronics & Communication Engineering</li>
<li>B.E. Mechanical Engineering</li>
<li>B.E. Electronic Engineering (VLSI)</li>
<li>B.Tech. Artificial Intelligence & Data Science</li>
<li>B.Tech. Computer Science and Business Systems</li>
<li>B.Tech Bio Technology</li>
</ul>
</div>
<div>
<h3 className="text-xl font-bold text-[#1e3a8a] mb-4">PG Programmes</h3>
<ul className="space-y-2 text-gray-700 list-disc pl-5 marker:text-[#f97316]">
<li>M.E. Electronics and Communication Engineering (VLSI Design)</li>
</ul>
</div>
<div>
<h3 className="text-xl font-bold text-[#1e3a8a] mb-4">Anna University Approved Research Institute</h3>
<p className="text-gray-700">
Ph.D. Programmes are offered across all Engineering, Technology, Science & Humanities disciplines
</p>
</div>
</div>
</div>
</div>
{/* Campus Life & Events Section */}
<div className="mt-24">
<h2 className="text-3xl md:text-4xl font-bold text-[#1e3a8a] text-center mb-16">
Campus Life & Events
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column: YouTube Videos */}
<div className="flex flex-col gap-8">
<div className="w-full rounded-xl overflow-hidden shadow-lg aspect-video">
<iframe
className="w-full h-full"
src="https://www.youtube.com/embed/5_L5JMb-a5k"
title="RAJALAKSHMI INSTITUTE OF TECHNOLOGY"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
></iframe>
</div>
<div className="w-full rounded-xl overflow-hidden shadow-lg aspect-video">
<iframe
className="w-full h-full"
src="https://www.youtube.com/embed/RfMkWSZHw5o"
title="Inauguration of The Grover Centre for Quantum Computing"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
></iframe>
</div>
</div>
{/* Right Column: Instagram Post */}
<div className="w-full h-full min-h-[600px] rounded-xl overflow-hidden shadow-lg bg-white flex items-center justify-center">
<iframe
className="w-full h-full"
src="https://www.instagram.com/p/DCs0891p6HY/embed"
frameBorder="0"
scrolling="no"
allowTransparency={true}
></iframe>
</div>
</div>
</div>
</div>
</section>
);
};
export default AboutSection;

View File

@@ -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 (
<section className="py-20 bg-white/50 backdrop-blur-sm rounded-[2rem] shadow-xl border border-white/20 my-12 overflow-hidden relative">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 via-transparent to-orange-50/50 pointer-events-none"></div>
<div className="max-w-7xl mx-auto px-8 relative z-10">
<div className="text-center mb-12">
<h2 className="text-sm font-black text-[#f97316] uppercase tracking-[0.3em] mb-3">Our Strategic Partners</h2>
<h3 className="text-3xl md:text-4xl font-serif text-[#1e3a8a] font-bold">Global Accreditations & Collaborations</h3>
<div className="w-20 h-1 bg-gradient-to-r from-[#f97316] to-orange-400 mx-auto mt-6 rounded-full"></div>
</div>
<div className="flex justify-center items-center">
<div className="relative w-full max-w-5xl mx-auto px-4">
<img
src="https://raw.githubusercontent.com/Sachin-627/RIT-COLLABORATORS-PIC/main/Screenshot%202026-04-01%20120832.png"
alt="Global Accreditations & Collaborations"
className="w-full h-auto object-contain rounded-2xl shadow-lg border border-white/20 transition-transform duration-500 hover:scale-[1.01]"
referrerPolicy="no-referrer"
/>
</div>
</div>
</div>
</section>
);
};
export default AccreditationsSection;

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { CATEGORIES } from './constants';
interface CategoryGridProps {
onSelectCategory: (id: string) => void;
title?: string;
}
const CategoryGrid: React.FC<CategoryGridProps> = ({ onSelectCategory }) => {
const threeCategories = CATEGORIES.filter(c => c.id !== 'CENTRE-ACTIVITY');
const centreActivityCategory = CATEGORIES.find(c => c.id === 'CENTRE-ACTIVITY');
return (
<section className="py-12 px-6 md:px-12 lg:px-24 bg-white animate-in fade-in slide-in-from-bottom-4 duration-500">
<h2 className="text-3xl md:text-4xl font-black text-center text-[#1A202C] mb-12 tracking-tight uppercase">
Discover by <span className="text-[#f97316]">Category</span>
</h2>
{/* Three Standard Categories */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto mb-8">
{threeCategories.map((cat) => (
<div
key={cat.id}
onClick={() => 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 */}
<img
src={cat.image}
alt={cat.name}
className="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110"
/>
{/* Premium Multi-layer Overlay */}
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors duration-500"></div>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-90 transition-opacity duration-500"></div>
{/* Border Glow on Hover */}
<div className="absolute inset-0 border-[3px] border-white/0 group-hover:border-white/20 rounded-[3rem] transition-all duration-500 scale-95 group-hover:scale-100"></div>
{/* Rich Typography and Content */}
<div className="absolute bottom-10 left-10 right-10 flex items-end justify-between">
<div>
<span className="inline-block px-4 py-1.5 bg-white/10 backdrop-blur-md rounded-full text-white/70 text-[10px] uppercase font-black tracking-[0.2em] mb-3 opacity-0 group-hover:opacity-100 transition-all duration-500 translate-y-4 group-hover:translate-y-0">
Explore Events
</span>
<h3 className="text-3xl md:text-4xl font-black text-white tracking-tighter leading-none mb-1 shadow-black/20">
{cat.name}
</h3>
<div className="h-1 w-12 bg-[#f97316] rounded-full transition-all duration-500 group-hover:w-24"></div>
</div>
<div className="w-14 h-14 bg-white/10 backdrop-blur-md rounded-2xl flex items-center justify-center text-white text-xl transform translate-x-12 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-500 delay-100">
<i className="fas fa-arrow-right"></i>
</div>
</div>
</div>
))}
</div>
{/* Centre Activity Wide Card */}
{centreActivityCategory && (
<div className="max-w-7xl mx-auto">
<div
onClick={() => 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 */}
<img
src={centreActivityCategory.image}
alt={centreActivityCategory.name}
className="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-105"
/>
{/* Overlays */}
<div className="absolute inset-0 bg-black/45 group-hover:bg-black/35 transition-colors duration-500"></div>
<div className="absolute inset-0 bg-gradient-to-r from-black/95 via-black/60 to-transparent"></div>
{/* Rich Typography and Content */}
<div className="absolute inset-0 flex items-center justify-between px-10 md:px-20">
<div className="space-y-4 text-left">
<span className="inline-block px-5 py-2 bg-white/10 backdrop-blur-md rounded-full text-white/95 text-[10px] uppercase font-black tracking-[0.25em] opacity-0 group-hover:opacity-100 transition-all duration-500 translate-y-2 group-hover:translate-y-0">
Special Hub Activities
</span>
<h3 className="text-4xl md:text-6xl font-black text-white tracking-tighter leading-none shadow-black/20 uppercase">
{centreActivityCategory.name}
</h3>
<p className="text-white/80 font-medium tracking-wide text-xs md:text-sm max-w-2xl leading-relaxed">
Engage in multidisciplinary innovation, advanced research hubs, incubation cells, and collaborative special projects driving academic excellence.
</p>
<div className="h-1.5 w-20 bg-[#f97316] rounded-full transition-all duration-500 group-hover:w-40"></div>
</div>
<div className="w-16 h-16 md:w-20 md:h-20 bg-white/10 backdrop-blur-md rounded-3xl flex items-center justify-center text-white text-2xl md:text-3xl transform translate-x-12 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-500 delay-100 mr-4 shrink-0">
<i className="fas fa-arrow-right"></i>
</div>
</div>
</div>
</div>
)}
</section>
);
};
export default CategoryGrid;

View File

@@ -0,0 +1,114 @@
import React from 'react';
import { MapPin, Phone, Mail, Building } from 'lucide-react';
const ContactSection: React.FC = () => {
return (
<section className="py-24 px-6 md:px-12 lg:px-24 bg-white min-h-screen">
<div className="max-w-7xl mx-auto">
<div className="mb-16 text-center">
<h3 className="text-[#f97316] font-bold tracking-widest uppercase text-xl mb-2 relative inline-block">
CONTACT US
<span className="absolute -bottom-2 left-0 w-full h-1 bg-[#f97316]"></span>
</h3>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-[#1e3a8a] mt-6">
Get in Touch
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
{/* Contact Info */}
<div className="space-y-12">
{/* College Address */}
<div className="bg-gray-50 p-8 rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center text-[#1e3a8a]">
<Building size={24} />
</div>
<h3 className="text-2xl font-bold text-[#1e3a8a]">College</h3>
</div>
<div className="space-y-4 text-gray-700">
<div className="flex items-start gap-3">
<MapPin size={20} className="text-[#f97316] mt-1 shrink-0" />
<p className="leading-relaxed">
Rajalakshmi Institute of Technology,<br />
Poonamallee, Chennai - 600 124.
</p>
</div>
<div className="flex items-start gap-3">
<Phone size={20} className="text-[#f97316] mt-1 shrink-0" />
<p>
91 44 6718 1600 / 91 44 6718 1601 /<br />
8925977445
</p>
</div>
<div className="flex items-start gap-3">
<Mail size={20} className="text-[#f97316] mt-1 shrink-0" />
<a href="mailto:mail@ritchennai.edu.in" className="hover:text-[#f97316] transition-colors">
mail@ritchennai.edu.in
</a>
</div>
</div>
</div>
{/* Administrative Office */}
<div className="bg-gray-50 p-8 rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center text-[#f97316]">
<Building size={24} />
</div>
<h3 className="text-2xl font-bold text-[#1e3a8a]">Administrative Office</h3>
</div>
<div className="space-y-4 text-gray-700">
<div className="flex items-start gap-3">
<MapPin size={20} className="text-[#f97316] mt-1 shrink-0" />
<p className="leading-relaxed">
#69, New Avadi Road, Kilpauk,<br />
Chennai - 600 010.
</p>
</div>
<div className="flex items-start gap-3">
<Phone size={20} className="text-[#f97316] mt-1 shrink-0" />
<div className="space-y-1">
<p>Tel : 91 44 2644 2472</p>
<p>91 44 2646 1316</p>
<p>91 44 2646 0124</p>
</div>
</div>
<div className="flex items-start gap-3">
<Mail size={20} className="text-[#f97316] mt-1 shrink-0" />
<a href="mailto:mail@ritchennai.edu.in" className="hover:text-[#f97316] transition-colors">
mail@ritchennai.edu.in
</a>
</div>
</div>
</div>
</div>
{/* Map */}
<div className="h-full min-h-[500px] rounded-2xl overflow-hidden shadow-lg border border-gray-200">
<iframe
width="100%"
height="100%"
frameBorder="0"
scrolling="no"
marginHeight={0}
marginWidth={0}
src="https://maps.google.com/maps?q=Rajalakshmi%20Institute%20of%20Technology&t=&z=13&ie=UTF8&iwloc=&output=embed"
title="Rajalakshmi Institute of Technology Map"
className="w-full h-full"
></iframe>
</div>
</div>
</div>
</section>
);
};
export default ContactSection;

View File

@@ -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<DomainSelectionProps> = ({ category, onSelectDomain, onBack }) => {
const [domains, setDomains] = useState<any[]>([]);
useEffect(() => {
const mappedDomains = DOMAIN_MAP[category] || [];
setDomains([...mappedDomains].sort((a: any, b: any) => a.name.localeCompare(b.name)));
}, [category]);
return (
<section className="pt-40 pb-12 px-6 md:px-12 lg:px-24 bg-white animate-in fade-in slide-in-from-right-4 duration-500">
<button
onClick={onBack}
className="flex items-center gap-2 text-[#f97316] font-black uppercase tracking-widest mb-10 group hover:translate-x-[-5px] transition-transform"
>
<i className="fas fa-arrow-left"></i>
BACK TO CATEGORIES
</button>
<h2 className="text-3xl md:text-4xl font-black text-center text-[#1A202C] mb-12 tracking-tighter uppercase">
CHOOSE YOUR <span className="text-[#f97316]">DOMAIN</span>
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
{domains.map((domain) => (
<div
key={domain.id}
onClick={() => 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"
>
<img
src={domain.image}
alt={domain.name}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-90"></div>
<div className="absolute bottom-8 left-8 right-8 flex flex-col items-start">
<span className="bg-[#f97316] text-[10px] font-black text-white px-3 py-1 rounded-full uppercase tracking-[0.2em] mb-3">
Domain
</span>
<h3 className="text-2xl font-black text-white tracking-tight transform transition-transform duration-500 group-hover:translate-x-2">
{domain.name}
</h3>
</div>
<div className="absolute top-6 right-6 w-10 h-10 bg-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity border border-gray-200">
<i className="fas fa-chevron-right text-gray-800 text-xs"></i>
</div>
</div>
))}
</div>
</section>
);
};
export default DomainSelection;

View File

@@ -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<EventCardProps> = ({ 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<string>('');
const [userYear, setUserYear] = useState<string>('');
const [userSection, setUserSection] = useState<string>('');
const [now, setNow] = useState(new Date());
const [isDownloading, setIsDownloading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const ticketRef = useRef<HTMLDivElement>(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 (
<div className="bg-white rounded-[40px] overflow-hidden shadow-xl shadow-gray-900/5 transition-all duration-500 hover:-translate-y-2 flex flex-col h-full group border border-gray-200">
<div className="h-64 relative overflow-hidden">
<img src={event.image} alt={event.title} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110" />
<div className="absolute top-6 left-6 flex flex-col gap-3">
<div className="bg-white px-5 py-2 rounded-2xl text-[10px] font-black uppercase tracking-widest text-[#f97316] shadow-xl border border-gray-200">{event.category}</div>
{event.registrationDeadline && (
<div className={`px-5 py-2 rounded-2xl text-[9px] font-black uppercase tracking-widest text-white shadow-xl flex items-center gap-2 ${admissionStatus === 'DEADLINE_PASSED' ? 'bg-[#1A202C]' : 'bg-rose-600 animate-pulse'}`}>
<i className="fas fa-clock"></i>
{admissionStatus === 'DEADLINE_PASSED' ? 'Closed' : `Ends ${new Date(event.registrationDeadline).toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`}
</div>
)}
</div>
</div>
<div className="p-10 flex flex-col flex-1">
<div className="flex justify-between items-start mb-6">
<div className="flex-1">
{clubInfo && (
<div className="flex items-center gap-2 mb-2">
<img
src={clubInfo.image}
alt={clubInfo.name}
className="w-12 h-12 rounded-full object-cover border border-gray-100 shadow-sm"
referrerPolicy="no-referrer"
/>
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{clubInfo.name}</span>
</div>
)}
<h3 className="text-3xl font-black text-[#1A202C] line-clamp-2 uppercase tracking-tight group-hover:text-[#f97316] transition-colors">{event.title}</h3>
</div>
<div className={`px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest border ${event.pricingType === 'PAID' ? 'bg-rose-50 border-rose-100 text-rose-600' : 'bg-emerald-50 border-emerald-100 text-emerald-600'}`}>{event.pricingType}</div>
</div>
<div className="space-y-3 mb-8 text-gray-400 font-bold text-xs uppercase tracking-widest">
<div className="flex items-center gap-4">
<i className="far fa-calendar-alt w-4 text-[#f97316]"></i>
<span>{event.date}{event.schedule?.[0]?.start_time ? `${event.schedule[0].start_time}` : ''}</span>
</div>
{event.durationDays && event.durationDays > 1 && (
<div className="flex items-center gap-4">
<i className="fas fa-hourglass-half w-4 text-[#f97316]"></i>
<span>Duration: {event.durationDays} Days</span>
</div>
)}
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<i className="fas fa-location-dot w-4 text-[#f97316]"></i>
<span>{event.location}</span>
</div>
{event.event_summary && (
<button
onClick={() => setShowSummary(true)}
className="w-7 h-7 flex items-center justify-center rounded-xl bg-slate-50 border border-slate-200 text-slate-400 hover:bg-[#f97316] hover:text-white hover:border-[#f97316] transition-all shadow-sm group"
title="View summary"
>
<i className="fas fa-info text-[10px] group-hover:scale-110 transition-transform"></i>
</button>
)}
</div>
</div>
{event.schedule && event.schedule.length > 0 && (() => {
// Group schedule entries by day_idx
const dayMap = new Map<number, typeof event.schedule>();
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 (
<div className="mb-8 space-y-4">
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest block mb-2">Event Schedule</span>
<div className="space-y-4">
{sortedDays.map(([dayIdx, slots]) => (
<div key={dayIdx} className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-[10px] font-black text-[#1A202C] uppercase tracking-widest">Day {dayIdx}</span>
<span className="text-[9px] font-bold text-gray-400 uppercase">{slots![0]?.date}</span>
</div>
<div className="grid grid-cols-2 gap-2">
{slots!.map((slot) => (
<div key={slot.id} className="bg-gray-50 border border-gray-100 rounded-xl px-3 py-2 flex flex-col">
<span className="text-[8px] font-black text-[#f97316] uppercase tracking-tighter">Batch {slot.batch_idx}</span>
<span className="text-[10px] font-bold text-gray-600">{slot.start_time} - {slot.end_time}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
})()}
<div className="mb-8 p-6 bg-gray-50 rounded-3xl border border-gray-200">
<div className="flex justify-between items-center mb-3">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Global Seats</span>
<span className="text-xs font-black text-[#1A202C]">{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) : '∞')}</span>
</div>
{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 (
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Sec {userSection || 'N/A'} Allocation</span>
<span className={`text-xs font-black ${(admissionStatus === 'SECTION_FULL' || admissionStatus === 'SECTION_NOT_ALLOWED') ? 'text-rose-500' : 'text-[#1A202C]'}`}>
{limit > 0 ? `${current} / ${limit}` : 'RESTRICTED'}
</span>
</div>
);
}
return (
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Allocation</span>
<span className={`text-xs font-black ${admissionStatus === 'DEPT_FULL' ? 'text-rose-500' : 'text-[#1A202C]'}`}>
{event.currentDeptCounts?.[userDept] || 0} / {event.deptLimits[userDept]}
</span>
</div>
);
})()}
</div>
<div className="flex justify-between items-center mb-10 text-center">
{[ {l: 'D', v: timeLeft.d}, {l: 'H', v: timeLeft.h}, {l: 'M', v: timeLeft.m}, {l: 'S', v: timeLeft.s} ].map((t, i) => (
<div key={i} className="flex-1">
<span className={`block text-2xl font-black leading-none ${i === 3 ? 'text-[#f97316]' : 'text-[#1A202C]'}`}>{t.v}</span>
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest">{t.l}</span>
</div>
))}
</div>
<div className="mt-auto">
{userRole === 'ADMIN' || userRole === 'COORDINATOR' ? (
<button disabled className="w-full py-5 rounded-[2rem] bg-slate-100 border border-slate-200 text-slate-400 font-black uppercase tracking-[0.3em] text-xs cursor-not-allowed">
View Only ({userRole === 'ADMIN' ? 'Admin' : 'Coordinator'})
</button>
) : isBooked ? (
<div className="flex flex-col gap-4">
<button onClick={() => onTrackStatus?.(event)} className="w-full py-5 rounded-[2rem] bg-[#f97316] text-white font-black uppercase tracking-[0.3em] text-xs shadow-xl shadow-orange-100 flex items-center justify-center gap-3 active:scale-[0.98] transition-all">
Track Progress <i className="fas fa-arrow-right"></i>
</button>
{event.status !== 'Completed' && (
<button onClick={() => setShowTicket(true)} className="w-full py-5 rounded-[2rem] bg-[#1A202C] text-white font-black uppercase tracking-[0.3em] text-xs hover:bg-black transition-all active:scale-[0.98] shadow-xl shadow-gray-200 flex items-center justify-center gap-3">
View Ticket <i className="fas fa-ticket"></i>
</button>
)}
{event.status !== 'Event Ongoing' && event.status !== 'Completed' && (
<button onClick={() => setShowCancelConfirm(true)} className="w-full py-4 text-rose-500 font-black uppercase tracking-widest text-[9px] hover:bg-rose-50 rounded-2xl transition-all flex items-center justify-center gap-2 mt-2">
<i className="fas fa-times-circle"></i> Cancel Registration
</button>
)}
</div>
) : admissionStatus === 'OPEN' ? (
<button onClick={() => setShowConfirm(true)} className="w-full py-5 rounded-[2rem] bg-[#1A202C] text-white font-black uppercase tracking-[0.3em] text-xs hover:bg-black transition-all active:scale-95 shadow-xl shadow-gray-200">
Get Tickets
</button>
) : (
<button disabled className="w-full py-5 rounded-[2rem] bg-rose-50 border border-rose-100 text-rose-400 font-black uppercase tracking-[0.3em] text-xs cursor-not-allowed">
{admissionStatus === 'EVENT_ENDED'
? 'EVENT ENDED'
: admissionStatus === 'DEPT_FULL'
? 'THE SEATS ARE FULL'
: admissionStatus === 'DEADLINE_PASSED'
? 'Deadline Over'
: admissionStatus === 'SECTION_FULL'
? 'SEC SEATS FULL'
: admissionStatus === 'SECTION_NOT_ALLOWED'
? 'SEC RESTRICTED'
: 'Event Full'}
</button>
)}
</div>
</div>
{showConfirm && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 text-center shadow-2xl animate-in zoom-in-95">
<div className={`w-20 h-20 ${event.isTeamEvent ? 'bg-orange-50 text-orange-500' : 'bg-blue-50 text-blue-500'} rounded-full flex items-center justify-center text-3xl mx-auto mb-8`}>
<i className={`fas ${event.isTeamEvent ? 'fa-users' : 'fa-ticket'}`}></i>
</div>
<h3 className="text-2xl font-black text-[#1A202C] uppercase mb-4">
{event.isTeamEvent ? 'Team Registration' : 'Confirm Pass?'}
</h3>
{event.isTeamEvent ? (
<div className="space-y-6 mb-10">
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-6 text-left">
<p className="text-[10px] font-black text-orange-600 uppercase tracking-widest mb-3 flex items-center gap-2">
<i className="fas fa-exclamation-triangle"></i> Important Notice
</p>
<p className="text-xs text-orange-800 font-bold leading-relaxed lowercase first-letter:uppercase">
This is a <span className="underline">team-based event</span>. You are about to register as an individual, after which you must <span className="underline">create or join a team</span> in the registrations section to participate.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 border border-slate-100 rounded-xl p-3">
<span className="block text-[8px] font-black text-gray-400 uppercase tracking-widest mb-1">Max Size</span>
<span className="text-xs font-black text-slate-900">{event.teamSizeLimit || '∞'} Members</span>
</div>
<div className="bg-slate-50 border border-slate-100 rounded-xl p-3">
<span className="block text-[8px] font-black text-gray-400 uppercase tracking-widest mb-1">Type</span>
<span className="text-xs font-black text-slate-900">{event.teamComposition === 'INTER_DEPT' ? 'Inter-Dept' : 'Mixed'}</span>
</div>
</div>
<p className="text-gray-500 text-[10px] font-bold uppercase tracking-widest">
Proceed with registration for <span className="text-slate-900 underline">"{event.title}"</span>?
</p>
</div>
) : (
<p className="text-gray-500 text-sm mb-10 lowercase first-letter:uppercase">Register for <span className="font-bold">"{event.title}"</span>?</p>
)}
<div className="flex gap-4">
<button onClick={() => setShowConfirm(false)} className="flex-1 py-4 bg-gray-100 text-gray-500 rounded-2xl font-black uppercase tracking-widest text-[10px] hover:bg-gray-200 transition-all">Cancel</button>
<button onClick={() => { onToggle(); setShowConfirm(false); }} className={`flex-1 py-4 ${event.isTeamEvent ? 'bg-orange-500 shadow-orange-200' : 'bg-[#1A202C] shadow-gray-200'} text-white rounded-2xl font-black uppercase tracking-widest text-[10px] shadow-lg active:scale-95 transition-all`}>
{event.isTeamEvent ? 'I Understand' : 'Confirm'}
</button>
</div>
</div>
</div>
</Portal>
)}
{showCancelConfirm && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 text-center shadow-2xl animate-in zoom-in-95">
<div className="w-20 h-20 bg-rose-50 text-rose-500 rounded-full flex items-center justify-center text-3xl mx-auto mb-8"><i className="fas fa-calendar-xmark"></i></div>
<h3 className="text-2xl font-black text-[#1A202C] uppercase mb-4">Cancel Booking?</h3>
<p className="text-gray-500 text-sm mb-10">Are you sure you want to cancel your registration for <span className="font-bold">"{event.title}"</span>?</p>
<div className="flex flex-col gap-3">
<button onClick={() => { onToggle(); setShowCancelConfirm(false); }} className="w-full py-4 bg-rose-600 text-white rounded-2xl font-black uppercase tracking-widest text-[10px] shadow-lg shadow-rose-200">Yes, Cancel Registration</button>
<button onClick={() => setShowCancelConfirm(false)} className="w-full py-4 bg-gray-100 text-gray-500 rounded-2xl font-black uppercase tracking-widest text-[10px]">Keep My Booking</button>
</div>
</div>
</div>
</Portal>
)}
{showTicket && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 md:p-6 bg-slate-900/70 backdrop-blur-md animate-in fade-in duration-300">
<div ref={ticketRef} className="relative bg-white rounded-[3rem] w-full max-w-md overflow-hidden shadow-[0_50px_100px_rgba(0,0,0,0.3)] animate-in zoom-in-95 duration-500 border border-white/20">
{/* Header - Now White Theme as Requested */}
<div className="p-8 border-b border-gray-100 flex justify-between items-center bg-white">
<div className="flex items-center gap-4">
<div className="w-[3px] h-10 bg-[#f97316] rounded-full"></div>
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className="h-10 w-auto object-contain"
/>
<div className="flex flex-col leading-none">
<span className="text-[8px] font-black text-gray-400 uppercase tracking-[0.2em] mt-1">Secure Entry Pass</span>
</div>
</div>
<button onClick={() => setShowTicket(false)} className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-all"><i className="fas fa-times"></i></button>
</div>
<div className="p-10">
<div className="flex flex-col items-center mb-10">
<div className="w-48 h-48 bg-gray-50 rounded-[2.5rem] flex items-center justify-center mb-8 p-6 border border-gray-100 shadow-inner relative group">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(persistedTicket?.qr || verificationLink)}&color=1e293b`}
alt="QR"
className="w-full h-full object-contain"
/>
{persistedTicket?.id && (
<div className="absolute -bottom-3 bg-white px-4 py-1 rounded-full border border-gray-200 shadow-sm">
<span className="text-[8px] font-black text-gray-400 uppercase tracking-widest">{persistedTicket.id}</span>
</div>
)}
</div>
<h4 className="text-3xl font-black text-[#1A202C] text-center mb-2 tracking-tighter uppercase leading-none">{event.title}</h4>
<div className="w-12 h-1 bg-[#f97316] rounded-full"></div>
</div>
<div className="grid grid-cols-2 gap-y-8 gap-x-10 border-t border-dashed border-gray-200 pt-8 mb-10">
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Attendee</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{currentUserName}</p>
</div>
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Year / Section</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">
{registration?.year || userYear || 'N/A'} - {registration?.section || userSection || 'N/A'}
</p>
</div>
{event.isTeamEvent && registration?.team_name && (
<div className="col-span-2">
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Team Identity</span>
<p className="text-sm font-black text-[#f97316] uppercase tracking-tight">{registration.team_name}</p>
</div>
)}
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Event Date</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{event.date.split(',')[0]}</p>
</div>
<div>
<span className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Dept</span>
<p className="text-sm font-black text-[#1A202C] truncate uppercase">{registration?.dept || userDept || 'Student'}</p>
</div>
</div>
<div className="space-y-4">
<button
onClick={async () => {
setIsSaving(true);
await handleDownload();
// Handle bucket storage: since we transitioned to Firebase, we bypass the Supabase upload
if (ticketRef.current && registration?.id) {
try {
const dataUrl = await toPng(ticketRef.current, { quality: 1, backgroundColor: '#ffffff' });
const fileName = `Ticket_${registration.id}.png`;
} catch (err) {
console.error("Auto-archiving to bucket failed:", err);
}
}
setIsSaving(false);
}}
disabled={isDownloading || isSaving}
className="w-full py-5 bg-[#1A202C] text-white rounded-[2rem] font-black uppercase text-xs tracking-[0.3em] flex items-center justify-center gap-4 hover:bg-black transition-all shadow-xl shadow-gray-200 disabled:opacity-50"
>
{isDownloading || isSaving ? <><i className="fas fa-spinner fa-spin"></i> Processing...</> : <><i className="fas fa-download"></i> Save Pass</>}
</button>
<p className="text-[8px] text-center font-bold text-gray-400 uppercase tracking-widest">Digital Ticket ID: {persistedTicket?.id || 'GENERATING...'}</p>
</div>
</div>
<div className="bg-[#f97316] h-3 w-full"></div>
</div>
</div>
</Portal>
)}
{showSummary && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 sm:p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2rem] w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-500 max-h-[85vh] flex flex-col relative">
<button
onClick={() => setShowSummary(false)}
className="absolute top-6 right-6 w-8 h-8 rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-all flex items-center justify-center z-10"
>
<i className="fas fa-times text-sm"></i>
</button>
<div className="p-8 overflow-y-auto flex-1 custom-scrollbar">
<div className="flex items-center gap-4 mb-8 pb-6 border-b border-gray-100 pr-8">
{clubInfo && (
<img src={clubInfo.image} className="w-12 h-12 rounded-full object-cover border border-slate-200 shadow-sm" alt="" />
)}
<div>
<span className="block text-[10px] font-black text-[#f97316] uppercase tracking-[0.2em] mb-1">{event.club || 'Organized by'}</span>
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
<i className="fas fa-user text-[9px]"></i> {event.coordinator}
</span>
</div>
</div>
<div className="space-y-6 pb-6 mt-6 border-t border-gray-100 pt-6">
<div className="flex items-center gap-2 mb-2">
<div className="w-1 h-3 bg-[#f97316] rounded-full"></div>
<h4 className="text-[10px] font-black text-slate-800 uppercase tracking-[0.2em]">Summary</h4>
</div>
<div className="relative pt-2">
<i className="fas fa-quote-right absolute top-0 right-0 text-5xl text-slate-50 pointer-events-none -z-10"></i>
<p className="text-[14px] text-slate-600 leading-relaxed whitespace-pre-wrap font-medium">
{event.event_summary || "Details for this session will be provided soon."}
</p>
</div>
</div>
<div className="mt-4 shrink-0 border-t border-gray-100 pt-6">
<button
onClick={() => setShowSummary(false)}
className="w-full py-4 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200 focus:outline-none focus:ring-4 focus:ring-slate-100"
>
Close Summary
</button>
</div>
</div>
</div>
</div>
</Portal>
)}
</div>
);
};
export default EventCard;

View File

@@ -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<EventListProps> = ({
events,
selectedCategory,
bookedEventIds,
onToggleBooking,
onSelectCategory,
onTrackStatus,
currentUserName,
userRole,
userRegistrations = []
}) => {
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
const [selectedDomainName, setSelectedDomainName] = useState<string | null>(null);
useEffect(() => {
setSelectedDomain(null);
setSelectedDomainName(null);
}, [selectedCategory]);
if (!selectedCategory) {
const hasActiveEvents = events.some(e => e.status !== 'Completed');
return (
<div className={!hasActiveEvents ? 'pt-32' : ''}>
<EventsHeroSlider events={events} />
<CategoryGrid onSelectCategory={onSelectCategory} />
</div>
);
}
if (!selectedDomain) {
return (
<DomainSelection
category={selectedCategory}
onSelectDomain={(id, name) => {
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 (
<div className="pt-40 pb-16 px-6 md:px-12 lg:px-24 animate-in fade-in duration-500">
<button
onClick={() => setSelectedDomain(null)}
className="flex items-center gap-2 text-[#f97316] font-black uppercase tracking-widest mb-10 group hover:translate-x-[-5px] transition-transform"
>
<i className="fas fa-arrow-left"></i>
BACK TO DOMAINS
</button>
<div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6">
<div>
<h1 className="text-4xl md:text-5xl font-black text-[#1A202C] tracking-tight mb-2">
{domainName}
</h1>
<p className="text-[#f97316] font-bold text-sm tracking-[0.2em] uppercase">
Exploring {categoryName} Category
</p>
</div>
<p className="text-gray-400 font-medium">Showing {filteredEvents.length} events found</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
{filteredEvents.map(event => {
const reg = userRegistrations.find(r => String(r.event_id) === String(event.id));
return (
<EventCard
key={event.id}
event={event}
isBooked={bookedEventIds.includes(event.id)}
onToggle={() => onToggleBooking(event.id)}
onTrackStatus={onTrackStatus}
currentUserName={currentUserName}
userRole={userRole}
registration={reg}
/>
);
})}
</div>
{filteredEvents.length === 0 && (
<div className="py-24 text-center bg-white rounded-[40px] shadow-xl shadow-gray-900/10 border border-gray-200">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
<i className="fas fa-calendar-times text-gray-300 text-3xl"></i>
</div>
<p className="text-gray-400 text-xl font-medium">No events found in this domain yet.</p>
<button onClick={() => setSelectedDomain(null)} className="mt-6 px-6 py-2 bg-[#1A202C] text-white rounded-full text-xs font-bold uppercase tracking-widest hover:bg-black transition-colors">Try another domain</button>
</div>
)}
</div>
);
};
export default EventList;

View File

@@ -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<EventsHeroSliderProps> = ({ 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 (
<div className="relative w-full h-screen bg-[#1A202C] overflow-hidden group">
{/* Background Images & Content */}
{sortedEvents.map((event, idx) => {
const isActive = idx === currentIndex;
return (
<div
key={event.id}
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${isActive ? 'opacity-100 z-10' : 'opacity-0 z-0'}`}
>
{/* Background Image */}
<div className="absolute inset-0 bg-black">
<img
src={event.image}
alt={event.title}
className={`w-full h-full object-cover opacity-50 transition-transform duration-[5000ms] ${isActive ? 'scale-100' : 'scale-105'}`}
/>
</div>
{/* Top Right Location Button */}
<div className="absolute top-24 right-12 z-20">
<div className="bg-white px-6 py-3 rounded-full flex items-center gap-3 shadow-2xl shadow-gray-200/50">
<i className="fas fa-location-dot text-[#f97316]"></i>
<span className="text-[#1A202C] text-xs font-bold tracking-widest uppercase">{event.location}</span>
</div>
</div>
{/* Main Content */}
<div className="absolute inset-0 z-10 flex flex-col justify-center pb-[15vh] px-12 md:px-24">
<div className={`max-w-5xl transition-all duration-1000 transform ${isActive ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-[2px] bg-[#f97316]"></div>
<span className="text-white font-bold tracking-[0.3em] uppercase text-sm">
JOIN US <span className="text-[#f97316] ml-2">{event.date}</span>
</span>
</div>
<h1 className="text-white text-5xl md:text-[6rem] lg:text-[8rem] font-black leading-[0.9] tracking-tighter uppercase break-words drop-shadow-lg">
{event.title}
</h1>
</div>
</div>
</div>
);
})}
{/* Countdown Timer Block (Slanted Orange Design) */}
<div
className="absolute bottom-0 left-0 z-20 bg-[#f97316] h-[140px] md:h-[160px] flex items-center px-8 md:px-16"
style={{
clipPath: 'polygon(0 0, 85% 0, 100% 100%, 0% 100%)',
width: '100%',
maxWidth: '800px'
}}
>
<div className="flex items-center gap-6 md:gap-12 relative z-10 w-full pr-12 md:pr-24">
<div className="flex flex-col items-center flex-1">
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.d}</span>
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Days</span>
</div>
<div className="w-px h-16 bg-white/30"></div>
<div className="flex flex-col items-center flex-1">
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.h}</span>
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Hrs</span>
</div>
<div className="w-px h-16 bg-white/30"></div>
<div className="flex flex-col items-center flex-1">
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.m}</span>
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Min</span>
</div>
<div className="w-px h-16 bg-white/30"></div>
<div className="flex flex-col items-center flex-1">
<span className="text-5xl md:text-6xl font-black leading-none text-white tracking-tighter">{timeLeft.s}</span>
<span className="text-[10px] md:text-xs font-bold uppercase tracking-widest mt-2 text-white">Sec</span>
</div>
</div>
</div>
{/* Navigation Arrows */}
<button
onClick={prevSlide}
className="absolute left-8 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white/50 hover:text-white hover:bg-white/20 transition-all duration-300 z-20 opacity-0 group-hover:opacity-100 hover:scale-110 border border-white/10"
>
<i className="fas fa-chevron-left text-lg"></i>
</button>
<button
onClick={nextSlide}
className="absolute right-8 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white/50 hover:text-white hover:bg-white/20 transition-all duration-300 z-20 opacity-0 group-hover:opacity-100 hover:scale-110 border border-white/10"
>
<i className="fas fa-chevron-right text-lg"></i>
</button>
{/* Slider Dots */}
<div className="absolute bottom-12 right-12 z-20 flex gap-3">
{sortedEvents.map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentIndex(idx)}
className={`h-1 transition-all duration-500 ${currentIndex === idx ? 'w-12 bg-[#f97316]' : 'w-6 bg-gray-200 hover:bg-gray-400'}`}
></button>
))}
</div>
</div>
);
};
export default EventsHeroSlider;

View File

@@ -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 (
<footer className="bg-white pt-20 pb-10 border-t border-gray-100 font-sans text-gray-600">
<div className="max-w-7xl mx-auto px-6 md:px-12 lg:px-24">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-12 lg:gap-8 mb-16">
{/* Brand and Social */}
<div className="lg:col-span-4 space-y-8">
<div className="space-y-4">
<p className="text-sm leading-relaxed max-w-xs">
We are a leading Engineering college in India, Rajalakshmi Institutions. Imparting the Spirit of Excellence and Quality Technical Education that fosters Learning.
</p>
</div>
<div className="flex gap-4">
{socialLinks.map((social, index) => (
<a
key={index}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className={`w-10 h-10 rounded-full border border-gray-200 flex items-center justify-center transition-all duration-300 hover:border-transparent hover:shadow-lg ${social.color} group`}
>
<social.icon size={18} className="group-hover:scale-110 transition-transform" />
</a>
))}
</div>
<div className="pt-8">
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className="h-16 w-auto object-contain"
referrerPolicy="no-referrer"
/>
</div>
</div>
{/* Links Columns */}
<div className="lg:col-span-2 space-y-6 pt-2">
<ul className="space-y-4">
{footerLinks.col1.map((link) => (
<li key={link.name}>
<a href={link.href} className="text-sm font-medium hover:text-[#f97316] transition-colors leading-relaxed">{link.name}</a>
</li>
))}
</ul>
</div>
<div className="lg:col-span-2 space-y-6 pt-2">
<ul className="space-y-4">
{footerLinks.col2.map((link) => (
<li key={link.name}>
<a href={link.href} className="text-sm font-medium hover:text-[#f97316] transition-colors leading-relaxed">{link.name}</a>
</li>
))}
</ul>
</div>
<div className="lg:col-span-2 space-y-6 pt-2">
<div className="grid grid-cols-1 gap-4">
{footerLinks.col3.map((link) => (
<a key={link.name} href={link.href} className="text-sm font-medium hover:text-[#f97316] transition-colors leading-relaxed">{link.name}</a>
))}
</div>
</div>
{/* Contact Info */}
<div className="lg:col-span-2 space-y-8">
<div className="space-y-4">
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
ADDRESS
</h3>
<div className="space-y-4 text-[13px]">
<div>
<p className="font-bold text-gray-800 mb-1">Rajalakshmi Institutions Head Office</p>
<p># 69 New Avadi Road</p>
<p>Kilpauk</p>
<p>Chennai - 600 010.</p>
</div>
<div>
<p className="font-bold text-gray-800 mb-1">College Campus</p>
<p>Rajalakshmi Nagar</p>
<p>Thandalam</p>
<p>Tamil Nadu 602105</p>
</div>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
PHONE
</h3>
<p className="text-[13px]">44-26442472 / 44-26461316 / 44-26460124</p>
</div>
<div className="space-y-4">
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
EMAIL
</h3>
<a href="mailto:mail@ritchennai.edu.in" className="text-[13px] hover:text-[#f97316] transition-colors">
mail@ritchennai.edu.in
</a>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="pt-10 border-t border-gray-100 flex flex-col md:flex-row justify-between items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold text-gray-400 ml-2">© {currentYear} Rajalakshmi Institute Of Technology</span>
</div>
<div className="flex gap-8 text-[11px] font-bold uppercase tracking-widest">
{/* Links removed as per request */}
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,49 @@
import React from 'react';
interface HeroProps {
events?: any[];
}
const Hero: React.FC<HeroProps> = () => {
return (
<section className="relative w-full h-screen flex items-center justify-center overflow-hidden bg-[#F3F4F6] font-sans select-none">
{/* Optimized Video Background Container */}
<div className="absolute inset-0 z-0 overflow-hidden">
<video
autoPlay
muted
loop
playsInline
preload="auto"
className="w-full h-full object-cover scale-125 pointer-events-none"
style={{ imageRendering: 'auto' }}
>
<source src="https://mhvdpopbbtllhvzcpqkf.supabase.co/storage/v1/object/public/HERO_SECTION_VIDEO/VN20260331_201749.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
{/* Centered Content */}
<div className="relative z-20 text-center px-6 max-w-5xl">
<h1 className="text-white text-6xl md:text-[8rem] font-serif mb-8 tracking-tight leading-[0.9] animate-in fade-in slide-in-from-bottom-20 duration-1000 drop-shadow-lg">
The Future is <br /> Built Here
</h1>
<p className="text-white/90 text-sm md:text-xl font-sans uppercase tracking-[0.6em] animate-in fade-in slide-in-from-bottom-10 duration-1000 delay-500 drop-shadow-md">
Welcome to the RIT Events Hub
</p>
</div>
{/* Scroll Down Arrow */}
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 z-20 animate-bounce cursor-pointer opacity-40 hover:opacity-100 transition-opacity">
<div className="flex flex-col items-center gap-2">
<span className="text-[10px] text-white font-sans uppercase tracking-[0.3em] mb-2">Scroll Down</span>
<i className="fas fa-chevron-down text-white text-xl"></i>
</div>
</div>
</section>
);
};
export default Hero;

View File

@@ -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<HomeDashboardProps> = ({ 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 (
<div className="px-6 md:px-12 lg:px-24 py-12 bg-transparent animate-in fade-in duration-700">
<div
onClick={onNavigateToEvents}
className="relative w-full h-64 md:h-80 rounded-[2.5rem] overflow-hidden cursor-pointer group mb-12 shadow-xl shadow-gray-200/20 hover:shadow-2xl transition-all duration-500"
>
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/30 transition-colors duration-500 z-10"></div>
<img
src="https://img.freepik.com/premium-photo/outdoor-music-festival-evening-ambiance-with-festive-lights-crowd_114541-6739.jpg"
alt="Events"
className="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 z-20 flex flex-col justify-center px-12 md:px-20">
<div className="transform transition-transform duration-500 group-hover:translate-x-2 space-y-4">
<h2 className="text-4xl md:text-6xl font-serif text-white font-bold tracking-tight drop-shadow-lg">
Let's get started
</h2>
<div className="flex items-center gap-3 text-white/90 group-hover:text-[#f97316] transition-colors w-fit">
<span className="text-sm font-bold uppercase tracking-widest bg-black/20 backdrop-blur-sm px-4 py-2 rounded-full border border-white/20 group-hover:bg-white group-hover:text-[#f97316] transition-all duration-300">
Explore Events <i className="fas fa-arrow-right ml-2"></i>
</span>
</div>
</div>
</div>
</div>
{/* Notice Board Section */}
<div className="w-full bg-[#e8e4c9] p-8 md:p-12 rounded-[2rem] shadow-2xl border-[12px] border-[#8d6e63] relative overflow-hidden min-h-[600px]">
{/* Cork texture pattern */}
<div className="absolute inset-0 opacity-30 bg-[url('https://www.transparenttextures.com/patterns/cork-board.png')] pointer-events-none"></div>
<div className="relative z-10 mb-12 flex flex-col md:flex-row items-start md:items-center justify-between gap-6 border-b-2 border-[#8d6e63]/20 pb-6">
<div>
<h2 className="text-4xl md:text-5xl font-serif text-[#3e2723] tracking-tight font-bold drop-shadow-sm mb-2">
Campus Notice Board
</h2>
<p className="text-[#5d4037] font-medium opacity-80">Real-time updates from the administration</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-12 p-6">
<AnimatePresence mode="popLayout">
{filteredAnnouncements.length > 0 ? (
filteredAnnouncements.slice(0, 6).map((ann) => {
const style = getNoteStyle(ann.id);
return (
<motion.div
key={ann.id}
layout
initial={{ scale: 0.8, opacity: 0, y: 50, rotate: style.rotation + (Math.random() * 10 - 5) }}
animate={{ scale: 1, opacity: 1, y: 0, rotate: style.rotation }}
exit={{ scale: 0.8, opacity: 0, transition: { duration: 0.2 } }}
whileHover={{ scale: 1.05, rotate: 0, zIndex: 50, transition: { duration: 0.2 } }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className={`${style.color} p-8 shadow-[4px_4px_10px_rgba(0,0,0,0.2)] hover:shadow-[15px_15px_30px_rgba(0,0,0,0.3)] transition-shadow duration-300 relative group cursor-pointer min-h-[300px] flex flex-col transform-gpu`}
>
{/* Pin */}
<div className="absolute -top-3 left-1/2 -translate-x-1/2 w-5 h-5 rounded-full bg-[#d32f2f] shadow-[2px_2px_4px_rgba(0,0,0,0.3)] z-20 border border-[#b71c1c] after:content-[''] after:absolute after:top-1 after:left-1 after:w-1.5 after:h-1.5 after:bg-white/50 after:rounded-full"></div>
<div className="mb-6 pt-2">
<span className={`text-xs font-black uppercase tracking-widest px-3 py-1.5 rounded-sm ${
ann.type === 'URGENT' ? 'bg-red-500/20 text-red-800' :
ann.type === 'DELAY' ? 'bg-amber-500/20 text-amber-800' :
'bg-black/5 text-gray-800'
}`}>
{ann.type || 'NOTICE'}
</span>
</div>
<h3 className="font-serif font-bold text-2xl text-gray-900 mb-4 leading-tight">
{ann.title}
</h3>
<p className="font-sans text-gray-800 text-base flex-grow mb-6 leading-relaxed opacity-90">
{ann.message}
</p>
<div className="mt-auto pt-4 border-t border-black/10 flex flex-col gap-2 text-[11px] font-bold text-gray-700 uppercase tracking-wider">
<div className="flex justify-between items-center">
<span className="flex items-center gap-2">
<i className="far fa-calendar-alt"></i>
{new Date(ann.timestamp).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })}
</span>
<span className="flex items-center gap-2">
<i className="far fa-clock"></i>
{new Date(ann.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span>
</div>
{ann.expiresAt && <span className="text-red-700/70 text-right">Exp: {new Date(ann.expiresAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>}
</div>
</motion.div>
);
})
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="col-span-full flex flex-col items-center justify-center py-32 text-[#8d6e63]/50"
>
<i className="fas fa-thumbtack text-6xl mb-6 opacity-30 rotate-45"></i>
<p className="font-serif text-2xl font-bold">The board is empty right now.</p>
<p className="font-sans text-sm mt-2 uppercase tracking-widest">Check back later for updates</p>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<AccreditationsSection />
<SpecialEventsBanner specialEvents={specialEvents} />
</div>
);
};
export default HomeDashboard;

View File

@@ -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<NavbarProps> = ({ 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 (
<nav className={`fixed top-0 left-0 w-full z-[100] transition-all duration-500 px-8 py-5 flex items-center justify-between ${
isScrolled
? 'bg-white/90 backdrop-blur-2xl py-4 shadow-sm'
: 'bg-transparent py-6'
}`}>
{/* Brand Logo (Top Left) */}
<div
className="flex items-center cursor-pointer group transition-transform hover:scale-105"
onClick={() => onViewChange('HOME')}
>
<div className="transition-all duration-300">
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className={`h-12 w-auto object-contain transition-all duration-300`}
/>
</div>
</div>
{/* Nav Links & Logout (Right Side) */}
<div className="flex items-center gap-8 md:gap-12">
<div className="hidden md:flex items-center gap-8">
{onBackToCoordinatorHub && (
<button
onClick={onBackToCoordinatorHub}
className={`text-[10px] font-black tracking-[0.2em] px-4 py-1.5 rounded-lg transition-all border border-[#f97316] text-[#f97316] hover:bg-[#f97316] hover:text-white`}
>
COORDINATOR HUB
</button>
)}
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => onViewChange(item.id)}
className={`text-xs font-serif tracking-[0.2em] transition-all relative py-1 ${
activeView === item.id
? 'text-[#f97316]'
: isScrolled ? 'text-black hover:text-[#f97316]' : 'text-black hover:text-[#f97316]'
}`}
>
{item.label}
{activeView === item.id && (
<span className="absolute -bottom-1 left-0 w-full h-[1px] bg-[#f97316]"></span>
)}
</button>
))}
</div>
<button
onClick={onLogout}
className={`text-[10px] font-black tracking-[0.2em] px-6 py-2 rounded-full transition-all border bg-[#f97316] text-white border-[#f97316] hover:bg-[#ea580c] hover:border-[#ea580c]`}
>
LOG OUT
</button>
</div>
</nav>
);
};
export default Navbar;

View File

@@ -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<PortalAnimationProps> = ({ isVisible, onComplete, targetLink }) => {
const videoRef = useRef<HTMLVideoElement>(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 (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
// Using Fixed inset-0 and a massive z-index to cover EVERYTHING
className="fixed inset-0 z-[999999] bg-black flex items-center justify-center overflow-hidden touch-none pointer-events-auto"
>
<div className="relative w-full h-full">
{/* High-Quality Native Video - Absolute Full Screen */}
<video
ref={videoRef}
src="/vortex.mp4"
muted
playsInline
loop
className="w-full h-full object-cover"
style={{ width: '100vw', height: '100vh' }}
/>
{/* Premium Overlay Filter */}
<div className="absolute inset-0 bg-blue-900/20 mix-blend-overlay pointer-events-none"></div>
{/* Transition Status Text Overlay */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<div className="mb-4">
<div className="w-64 h-[2px] bg-white/20 relative overflow-hidden mx-auto">
<motion.div
initial={{ x: "-100%" }}
animate={{ x: "100%" }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
className="absolute inset-0 bg-orange-500 shadow-[0_0_10px_#f97316]"
/>
</div>
</div>
<span className="text-[10px] font-black uppercase tracking-[0.8em] text-white/70">
{statusText}
</span>
</motion.div>
</div>
{/* Subliminal Transition Glow (Pulses just before redirect) */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: [0, 0, 0.6, 0] }}
transition={{ duration: 5, times: [0, 0.85, 0.95, 1], ease: "easeInOut" }}
className="absolute inset-0 bg-white pointer-events-none"
/>
</div>
</motion.div>
</AnimatePresence>
);
};
export default PortalAnimation;

View File

@@ -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<string> => {
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<ProfileViewProps> = ({ 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<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [pastEvents, setPastEvents] = useState<any[]>([]);
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<HTMLInputElement>) => {
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 (
<div className="min-h-screen flex items-center justify-center bg-slate-50">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
</div>
);
}
const avatarUrl = editForm.profile_photo || (user as any)?.profile_photo;
return (
<div className="min-h-screen bg-gray-50 pt-32 pb-12 px-4 sm:px-6 lg:px-8 font-inter">
{showSuccess && createPortal(
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-6 bg-black/40 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-2xl w-full max-w-sm p-8 shadow-2xl flex flex-col items-center text-center transform transition-all scale-100">
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center text-2xl mb-6">
<i className="fas fa-check"></i>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Profile Updated</h3>
<p className="text-gray-500 mb-6">Your changes have been saved successfully.</p>
<button onClick={() => setShowSuccess(false)} className="w-full py-3 bg-gray-900 text-white rounded-xl font-semibold hover:bg-gray-800 transition-colors">
Continue
</button>
</div>
</div>, document.body
)}
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column: Profile Card */}
<div className="lg:col-span-1">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden sticky top-32">
<div className="h-32 bg-gradient-to-r from-orange-400 to-rose-500"></div>
<div className="px-6 pb-8">
<div className="relative -mt-16 mb-6 flex justify-center">
<div className="w-32 h-32 rounded-full border-4 border-white shadow-lg overflow-hidden bg-gray-100 relative group">
<img
src={avatarUrl || `https://api.dicebear.com/7.x/avataaars/svg?seed=${user?.fullName}`}
alt="Profile"
className="w-full h-full object-cover"
/>
{isEditing && (
<div
onClick={() => 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"
>
<i className="fas fa-camera text-white text-xl"></i>
</div>
)}
</div>
<input type="file" ref={fileInputRef} className="hidden" onChange={handlePhotoChange} accept="image/*" />
</div>
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-1">{user?.fullName}</h2>
<p className="text-sm font-medium text-orange-500 uppercase tracking-wider mb-4">{user?.role}</p>
<div className="flex items-center justify-center gap-2 text-gray-500 text-sm">
<i className="fas fa-envelope"></i>
<span>{user?.email}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4 border-t border-gray-100 pt-6 mb-6">
<div className="text-center">
<span className="block text-xl font-bold text-gray-900">{regCount}</span>
<span className="text-xs text-gray-500 uppercase tracking-wide">Registry</span>
</div>
<div className="text-center border-l border-gray-100">
<span className="block text-xl font-bold text-emerald-600">{pastEvents.length}</span>
<span className="text-xs text-gray-500 uppercase tracking-wide">Completed</span>
</div>
</div>
{!isEditing && (
<button
onClick={onLogout}
className="w-full py-2.5 border border-gray-200 text-gray-600 rounded-xl font-medium hover:bg-gray-50 hover:text-rose-600 transition-colors text-sm flex items-center justify-center gap-2"
>
<i className="fas fa-sign-out-alt"></i> Sign Out
</button>
)}
</div>
</div>
</div>
{/* Right Column: Details & Edit Form */}
<div className="lg:col-span-2 space-y-6">
{/* Personal Information Card */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="px-6 py-5 border-b border-gray-100 flex justify-between items-center">
<h3 className="text-lg font-bold text-gray-900">Personal Information</h3>
{!isEditing && user?.role === 'STUDENT' && (
<button
onClick={() => setIsEditing(true)}
className="text-sm font-medium text-orange-600 hover:text-orange-700 flex items-center gap-1"
>
<i className="fas fa-pen"></i> Edit
</button>
)}
</div>
<div className="p-6">
{isEditing ? (
<form onSubmit={handleUpdate} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Full Name</label>
<input
type="text"
value={editForm.name}
onChange={e => 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"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Phone Number</label>
<input
type="tel"
value={editForm.phone}
onChange={e => 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"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Year</label>
<select
value={editForm.year}
onChange={e => setEditForm({...editForm, year: 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"
>
<option value="">Select Year</option>
<option value="1st Year">1st Year</option>
<option value="2nd Year">2nd Year</option>
<option value="3rd Year">3rd Year</option>
<option value="4th Year">4th Year</option>
<option value="5th Year">5th Year</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Section</label>
<input
type="text"
value={editForm.section}
onChange={e => 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"
/>
</div>
</div>
{errorMessage && (
<div className="p-3 bg-rose-50 text-rose-600 text-sm rounded-lg flex items-center gap-2">
<i className="fas fa-exclamation-circle"></i>
{errorMessage}
</div>
)}
<div className="flex items-center gap-4 pt-4">
<button
type="submit"
disabled={isSaving}
className="px-6 py-2.5 bg-gray-900 text-white rounded-xl font-medium hover:bg-black transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isSaving ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-save"></i>}
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
<button
type="button"
onClick={() => { setIsEditing(false); setErrorMessage(null); }}
className="px-6 py-2.5 bg-white border border-gray-200 text-gray-700 rounded-xl font-medium hover:bg-gray-50 transition-colors"
>
Cancel
</button>
</div>
</form>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-8 gap-x-12">
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Full Name</label>
<p className="text-base font-semibold text-gray-900">{user?.fullName}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Email Address</label>
<p className="text-base font-semibold text-gray-900">{user?.email}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Phone Number</label>
<p className="text-base font-semibold text-gray-900">{user?.phone || 'Not provided'}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Role</label>
<p className="text-base font-semibold text-gray-900 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
{user?.role}
</p>
</div>
{user?.department && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Department</label>
<p className="text-base font-semibold text-gray-900">{user.department}</p>
</div>
)}
{user?.year && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Year</label>
<p className="text-base font-semibold text-gray-900">{user.year}</p>
</div>
)}
{user?.section && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Section</label>
<p className="text-base font-semibold text-gray-900">{user.section}</p>
</div>
)}
{user?.regNo && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Registration Number</label>
<p className="text-base font-semibold text-gray-900">{user.regNo}</p>
</div>
)}
</div>
)}
</div>
</div>
{/* Recent Activity Card */}
{!isEditing && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="px-6 py-5 border-b border-gray-100">
<h3 className="text-lg font-bold text-gray-900">Recent Activity</h3>
</div>
<div className="p-6">
{pastEvents.length > 0 ? (
<div className="space-y-6">
{pastEvents.map((pe, idx) => (
<div key={idx} className="flex items-start gap-5 p-4 rounded-2xl hover:bg-gray-50 transition-colors border border-transparent hover:border-gray-100 group">
<div className="w-12 h-12 rounded-2xl bg-orange-50 text-orange-500 flex items-center justify-center text-lg shrink-0 shadow-sm border border-orange-100 group-hover:scale-110 transition-transform">
<i className="fas fa-calendar-check"></i>
</div>
<div>
<p className="text-sm font-black text-gray-900 uppercase tracking-tight">{pe.events?.title || 'Unknown Event'}</p>
<p className="text-[10px] text-gray-500 mt-1 uppercase font-bold tracking-widest">{pe.events?.category} Completed on {new Date(pe.participation_date).toLocaleDateString()}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-gray-400">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-gray-100">
<i className="fas fa-history text-2xl opacity-20"></i>
</div>
<p className="text-xs font-black uppercase tracking-widest text-gray-400">No participation history yet</p>
<p className="text-[10px] text-gray-400 mt-2 font-medium">Events you participate in will appear here.</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default ProfileView;

View File

@@ -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<RegistrationsViewProps> = ({ 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<any[]>([]);
const [isProcessing, setIsProcessing] = React.useState(false);
const [copyingCode, setCopyingCode] = React.useState<string | null>(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 (
<div className="pt-40 pb-20 px-6 md:px-12 lg:px-24 animate-in fade-in duration-500 font-inter">
<h1 className="text-5xl font-black text-[#1A202C] mb-16 tracking-tight uppercase">My <span className="text-[#f97316]">Registrations</span></h1>
{bookedEvents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
{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 (
<div key={event.id} className="space-y-6">
<EventCard
event={event}
isBooked={true}
onToggle={() => onToggleBooking(event.id)}
onTrackStatus={onTrackStatus}
currentUserName={currentUserName}
userRole={userRole}
registration={reg}
/>
{event.isTeamEvent && (
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm relative overflow-hidden group">
<div className="absolute top-0 right-0 w-24 h-24 bg-orange-50/50 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl group-hover:bg-orange-100/50 transition-colors"></div>
{teamCodeVal ? (
<div className="flex items-center justify-between relative z-10">
<div>
<span className="block text-[8px] font-black text-emerald-500 uppercase tracking-widest mb-1 flex items-center gap-1">
<i className="fas fa-check-circle"></i> Status: Teamed Up
</span>
<h4 className="text-sm font-black text-slate-900 uppercase">{teamNameVal}</h4>
<p className="text-[10px] text-slate-400 font-bold mt-1 uppercase flex items-center gap-2">
CODE: <span className="text-slate-900 underline font-black">{teamCodeVal}</span>
<button
onClick={() => handleCopyCode(teamCodeVal)}
className="text-slate-400 hover:text-[#f97316] transition-colors"
title="Copy Code"
>
<i className={`fas ${copyingCode === teamCodeVal ? 'fa-check text-emerald-500' : 'fa-copy'}`}></i>
</button>
</p>
</div>
<div className="flex flex-col items-end gap-2">
<div className={`w-12 h-12 ${isLeaderVal ? 'bg-amber-100 text-amber-600' : 'bg-emerald-50 text-emerald-500'} rounded-2xl flex items-center justify-center text-lg shadow-sm border ${isLeaderVal ? 'border-amber-200' : 'border-emerald-100'}`}>
<i className={`fas ${isLeaderVal ? 'fa-crown animate-pulse' : 'fa-user-group'}`}></i>
</div>
<button
onClick={() => handleInspectTeam(event.id, teamCodeVal, event.title)}
className="text-[9px] font-black text-slate-400 uppercase tracking-widest hover:text-[#f97316] transition-colors flex items-center gap-1"
>
<i className="fas fa-search"></i> Inspect Team
</button>
</div>
</div>
) : (
<div className="space-y-5 relative z-10">
<div className="flex items-center gap-3">
<div className="w-1.5 h-4 bg-slate-200 rounded-full"></div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Team Required</span>
</div>
<p className="text-[9px] text-slate-400 font-bold uppercase leading-relaxed">Please use the "Track Progress" button to manage your team for this event.</p>
</div>
)}
</div>
)}
</div>
);
})}
</div>
) : (
<div className="flex flex-col items-center justify-center min-h-[40vh] bg-white rounded-[40px] border border-dashed border-gray-300 p-10 text-center">
<i className="fas fa-ticket-alt text-6xl text-gray-200 mb-6"></i>
<p className="text-gray-400 text-xl font-medium max-w-sm">You haven't booked any events yet, or your previously registered events have been removed.</p>
</div>
)}
{showInspectModal && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[3rem] w-full max-w-2xl p-10 shadow-2xl animate-in zoom-in-95 duration-500 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[#f97316] to-amber-500"></div>
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight">Team Roster</h3>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-1">Code: <span className="text-[#f97316]">{showInspectModal.teamCode}</span> | {showInspectModal.title}</p>
</div>
<button onClick={() => setShowInspectModal(null)} className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:text-slate-900 transition-colors">
<i className="fas fa-times"></i>
</button>
</div>
<div className="max-h-[50vh] overflow-y-auto no-scrollbar space-y-4 pr-2 mb-8">
{teamMembers.map((member, idx) => {
const isLeader = teamMembers.find(m => m.is_team_leader && m.name === currentUserName);
return (
<div key={idx} className="bg-slate-50 rounded-3xl p-6 flex items-center justify-between border border-slate-100 hover:border-slate-200 transition-all group">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-xl text-[#f97316] border border-slate-100 shadow-sm group-hover:scale-110 transition-transform">
<i className={`fas ${member.is_team_leader ? 'fa-crown' : 'fa-user'}`}></i>
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-black text-slate-900 uppercase">{member.name}</h4>
{member.is_team_leader && <span className="bg-amber-100 text-amber-600 text-[8px] font-black px-2 py-0.5 rounded-full uppercase tracking-tighter">Leader</span>}
</div>
<p className="text-[10px] text-slate-400 font-bold mt-0.5 uppercase tracking-widest">{member.reg_no}</p>
</div>
</div>
<div className="flex items-center gap-6 text-right">
<div>
<p className="text-[9px] font-black text-slate-900 uppercase">{member.department}</p>
<p className="text-[8px] text-slate-400 font-bold uppercase">{member.year} • SEC {member.section}</p>
</div>
{isLeader && !member.is_team_leader && (
<button
onClick={() => {
setShowRemoveConfirm({
member,
eventId: showInspectModal.eventId,
teamCode: showInspectModal.teamCode
});
}}
className="w-10 h-10 rounded-xl bg-white text-rose-500 border border-rose-100 flex items-center justify-center transition-all hover:bg-rose-500 hover:text-white shadow-sm"
title="Remove from team"
>
<i className="fas fa-user-minus"></i>
</button>
)}
</div>
</div>
);
})}
</div>
<div className="flex flex-col gap-3">
{teamMembers.find(m => m.is_team_leader && m.name === currentUserName) ? (
<button
onClick={() => setShowDisbandConfirm({ eventId: showInspectModal.eventId, teamCode: showInspectModal.teamCode })}
disabled={isProcessing}
className="w-full py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-[2rem] font-black uppercase text-[10px] tracking-widest hover:bg-rose-100 transition-all flex items-center justify-center gap-2"
>
<i className="fas fa-trash-can"></i> Disband Team
</button>
) : (
<button
onClick={() => setShowLeaveConfirm({ eventId: showInspectModal?.eventId || '' })}
disabled={isProcessing}
className="w-full py-4 bg-amber-50 text-amber-600 border border-amber-100 rounded-[2rem] font-black uppercase text-[10px] tracking-widest hover:bg-amber-100 transition-all flex items-center justify-center gap-2"
>
<i className="fas fa-right-from-bracket"></i> Leave Team
</button>
)}
<button
onClick={() => setShowInspectModal(null)}
className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[11px] tracking-[0.2em] shadow-xl hover:bg-black transition-all active:scale-95"
>
Close Inspection
</button>
</div>
{/* Confirmation Modals (Portaled to prevent clipping) */}
{showRemoveConfirm && (
<Portal>
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-8 text-center">
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-user-slash"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Remove Member?</h3>
<p className="text-sm text-slate-500 font-medium">Are you sure you want to remove <span className="font-black text-slate-900 uppercase">{showRemoveConfirm.member.name}</span> from the team?</p>
</div>
<div className="p-6 bg-slate-50 flex gap-4">
<button onClick={() => setShowRemoveConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button
onClick={async () => {
setIsProcessing(true);
try {
const registrationId = `${showRemoveConfirm.member.user_id}_${showRemoveConfirm.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 remove member");
setTeamMembers(prev => prev.filter(m => m.user_id !== showRemoveConfirm.member.user_id));
const removedName = showRemoveConfirm.member.name;
setShowRemoveConfirm(null);
setShowRemoveSuccess({ name: removedName });
} catch (err) {
console.error(err);
alert("Failed to remove member");
} finally {
setIsProcessing(false);
}
}}
disabled={isProcessing}
className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200"
>
{isProcessing ? 'Removing...' : 'Confirm'}
</button>
</div>
</div>
</div>
</Portal>
)}
{showRemoveSuccess && (
<Portal>
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-10 text-center">
<div className="w-16 h-16 bg-emerald-50 text-emerald-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-check-circle"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Member Removed</h3>
<p className="text-sm text-slate-500 font-medium"><span className="font-black text-slate-900 uppercase">{showRemoveSuccess.name}</span> has been removed.</p>
<button onClick={() => setShowRemoveSuccess(null)} className="mt-8 w-full py-4 bg-[#1A202C] text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200">Got it</button>
</div>
</div>
</div>
</Portal>
)}
{showLeaveConfirm && (
<Portal>
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-8 text-center">
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-sign-out-alt"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Leave Team?</h3>
<p className="text-sm text-slate-500 font-medium">Are you sure you want to leave this team?</p>
</div>
<div className="p-6 bg-slate-50 flex gap-4">
<button onClick={() => setShowLeaveConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button onClick={() => executeLeaveTeam(showLeaveConfirm.eventId)} disabled={isProcessing} className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200">{isProcessing ? 'Leaving...' : 'Confirm'}</button>
</div>
</div>
</div>
</Portal>
)}
{showDisbandConfirm && (
<Portal>
<div className="fixed inset-0 z-[13000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2.5rem] w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500">
<div className="p-8 text-center">
<div className="w-16 h-16 bg-rose-50 text-rose-500 rounded-2xl flex items-center justify-center text-2xl mx-auto mb-6">
<i className="fas fa-ban"></i>
</div>
<h3 className="text-xl font-black text-slate-900 uppercase mb-2">Disband Team?</h3>
<p className="text-sm text-slate-500 font-medium">Are you sure? This will disband the team for <span className="font-black text-slate-900">ALL</span> members.</p>
</div>
<div className="p-6 bg-slate-50 flex gap-4">
<button onClick={() => setShowDisbandConfirm(null)} className="flex-1 py-4 bg-white text-slate-400 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button onClick={() => executeDisbandTeam(showDisbandConfirm.eventId, showDisbandConfirm.teamCode)} disabled={isProcessing} className="flex-1 py-4 bg-rose-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-600 transition-all shadow-lg shadow-rose-200">{isProcessing ? 'Disbanding...' : 'Confirm'}</button>
</div>
</div>
</div>
</Portal>
)}
</div>
</div>
</Portal>
)}
</div>
);
};
export default RegistrationsView;

View File

@@ -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<SpecialEventsBannerProps> = ({ 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 (
<section className="py-12 relative overflow-hidden bg-transparent">
{/* Background glow (Softer and centered) */}
<div className="absolute inset-0 z-0 opacity-5 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] bg-amber-500/20 blur-[80px] rounded-full"></div>
</div>
<div className="max-w-2xl mx-auto px-6 relative z-10">
{/* New Header: Descriptive text above the scroll */}
<div className="text-center mb-12">
<h2 className="font-medieval text-3xl md:text-5xl text-[#3e2723] mb-4 opacity-90">
Special Events Registry
</h2>
<p className="font-parchment text-sm md:text-base text-[#5d4037]/70 italic tracking-wide">
Tap the ancient scroll to reveal the current decrees of RIT
</p>
<div className="w-16 h-[1px] bg-[#8d6e63]/20 mx-auto mt-6"></div>
</div>
<motion.div
initial="closed"
animate={isOpen ? "open" : "closed"}
className="relative flex flex-col items-center"
>
{/* Top Roller - Interactive */}
<motion.div
onClick={() => 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 */}
<div className="absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-[#1a0f0d] to-transparent opacity-80"></div>
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-[#1a0f0d] to-transparent opacity-80"></div>
<div className="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/20"></div>
<div className="absolute inset-x-4 inset-y-0 flex items-center justify-between pointer-events-none px-4">
<div className="w-4 h-4 rounded-full border border-white/5 bg-white/5 blur-[1px]"></div>
<div className="w-4 h-4 rounded-full border border-white/5 bg-white/5 blur-[1px]"></div>
</div>
<div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<span className="text-[10px] font-medieval text-white/60 uppercase tracking-[0.3em] bg-black/20 px-4 py-1 rounded-full backdrop-blur-sm">
{isOpen ? 'Tap to Close' : 'Tap to Open'}
</span>
</div>
</motion.div>
{/* Parchment Body */}
<motion.div
variants={{
closed: {
height: 0,
opacity: 0,
transition: { duration: 0.5, ease: "easeInOut" }
},
open: {
height: 'auto',
opacity: 1,
transition: { duration: 0.8, ease: "easeOut" }
}
}}
className="relative z-20 w-[94%] overflow-hidden origin-top"
style={{
backgroundImage: 'url(/parchment_texture.png)',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{/* Inner Shadow Shadow */}
<div className="absolute inset-0 bg-gradient-to-r from-black/5 via-transparent to-black/5 pointer-events-none shadow-inner"></div>
<div className="p-8 md:p-12 text-center space-y-8">
<motion.div
variants={{
closed: { opacity: 0, y: -20 },
open: { opacity: 1, y: 0 }
}}
>
<h2 className="font-medieval text-3xl md:text-5xl text-[#3e2723] mb-4 drop-shadow-sm">
{hasEvents ? defaultTitle : "The Void Chronicles"}
</h2>
<div className="w-24 h-[1px] bg-[#8d6e63]/20 mx-auto mb-6"></div>
<p className="font-parchment text-base md:text-xl text-[#5d4037] leading-relaxed max-w-lg mx-auto italic opacity-90">
{hasEvents ? defaultDescription : "The magic portal remains dormant. Check back when the moons align."}
</p>
</motion.div>
<div className="grid grid-cols-1 gap-6 pt-4">
{specialEvents.map((event, index) => (
<motion.div
key={event.id}
variants={{
closed: { opacity: 0, scale: 0.95 },
open: { opacity: 1, scale: 1 }
}}
transition={{ delay: isOpen ? 0.3 + (index * 0.1) : 0 }}
whileHover={{ scale: 1.02 }}
className="group cursor-pointer relative"
onClick={() => handleRedirect(event.link)}
>
<div className="border border-[#8d6e63]/15 bg-[#8d6e63]/5 p-5 rounded-lg backdrop-blur-[1px] transition-all duration-300 group-hover:bg-[#8d6e63]/10 group-hover:border-[#8d6e63]/30">
<h3 className="font-medieval text-xl md:text-2xl text-[#3e2723] mb-2 group-hover:text-[#795548] transition-colors">
{event.title}
</h3>
<p className="font-parchment text-xs md:text-sm text-[#5d4037] line-clamp-2 opacity-80">
{event.description}
</p>
<div className="mt-3 flex justify-end">
<span className="text-[9px] font-medieval uppercase tracking-[0.2em] text-[#8d6e63] group-hover:text-[#3e2723] flex items-center gap-2">
Behold <i className="fas fa-feather-pointed"></i>
</span>
</div>
</div>
</motion.div>
))}
</div>
{hasEvents && (
<div className="font-medieval text-[10px] text-[#8d6e63]/40 uppercase tracking-[0.4em] pt-8">
End of Scroll
</div>
)}
</div>
</motion.div>
{/* Bottom Roller - Interactive */}
<motion.div
onClick={() => 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 */}
<div className="absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-[#1a0f0d] to-transparent opacity-80"></div>
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-[#1a0f0d] to-transparent opacity-80"></div>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 via-transparent to-black/20"></div>
</motion.div>
</motion.div>
</div>
<style>{`
.font-medieval { font-family: 'Pirata One', cursive; }
.font-parchment { font-family: 'Almendra', serif; }
`}</style>
</section>
);
};
export default SpecialEventsBanner;

View File

@@ -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<StatsSectionProps> = ({ 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 (
<section className="w-full bg-[#F9FAFB] py-12 px-6 md:px-12 lg:px-20">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: stat.delay }}
viewport={{ once: true }}
className="bg-white p-8 rounded-2xl shadow-sm hover:shadow-md transition-shadow duration-300 border border-gray-100 relative overflow-hidden group"
>
{/* Corner accents similar to the image */}
<div className="absolute top-2 right-2 opacity-50 transform rotate-90">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 19V5C1 2.79086 2.79086 1 5 1H19" stroke="#f97316" strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
<div className="absolute bottom-2 left-2 opacity-50 transform -rotate-90">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 19V5C1 2.79086 2.79086 1 5 1H19" stroke="#f97316" strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
<div className="flex flex-col items-center justify-center text-center z-10 relative">
<h3 className="text-4xl md:text-5xl font-bold text-[#2D3748] mb-2 font-serif group-hover:text-[#f97316] transition-colors duration-300">
{stat.value}{stat.suffix}
</h3>
<p className="text-gray-500 font-medium text-sm uppercase tracking-wider">
{stat.label}
</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default StatsSection;

View File

@@ -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<string>;
onShowCreateTeam?: () => void;
onShowJoinTeam?: () => void;
}
const SuccessPopup: React.FC<{ onClose: () => void }> = ({ onClose }) => (
<div className="fixed inset-0 z-[10005] flex items-center justify-center p-6 bg-black/80 backdrop-blur-xl animate-in fade-in duration-300">
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
<div className="w-24 h-24 bg-emerald-100 text-emerald-500 rounded-full flex items-center justify-center text-4xl mb-8 shadow-inner">
<i className="fas fa-cloud-check"></i>
</div>
<h3 className="text-2xl font-black text-slate-900 mb-2 uppercase tracking-tight">File Captured</h3>
<p className="text-gray-500 font-bold text-[10px] uppercase tracking-[0.3em] mb-10">Proof uploaded successfully</p>
<button
onClick={onClose}
className="w-full py-5 bg-emerald-600 text-white rounded-2xl font-black uppercase tracking-widest text-[11px] hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-200 active:scale-95"
>
Track Verification
</button>
</div>
</div>
);
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return createPortal(children, document.body);
};
const StatusTrackerView: React.FC<StatusTrackerViewProps> = ({ event, registration, onBack, onShowCreateTeam, onShowJoinTeam }) => {
const { user } = useAuth();
const fileInputRef = React.useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [now, setNow] = useState(new Date());
const [attendanceRecords, setAttendanceRecords] = useState<any[]>([]);
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<HTMLInputElement>) => {
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 (
<div className="pt-40 pb-16 px-6 md:px-12 lg:px-24 bg-[#F3F4F6] min-h-screen animate-in fade-in duration-500 font-inter">
{showSuccess && <SuccessPopup onClose={() => { setShowSuccess(false); window.location.reload(); }} />}
<div className="max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-16">
<div className="flex flex-col items-start">
<button onClick={onBack} className="flex items-center gap-2 text-[#f97316] font-black uppercase tracking-[0.2em] mb-6 text-[10px] group">
<i className="fas fa-arrow-left transition-transform group-hover:-translate-x-1"></i> Return to Hub
</button>
<h1 className="text-6xl font-black text-[#1A202C] tracking-tighter mb-2 leading-none uppercase">REAL-TIME <span className="text-[#f97316]">PROGRESS</span></h1>
<p className="text-gray-400 font-bold uppercase tracking-[0.3em] text-xs">Tracking Node: {event.title}</p>
</div>
</div>
<div className="bg-white rounded-[3.5rem] p-8 md:p-16 shadow-xl shadow-gray-900/5 border border-gray-100 mb-12 relative overflow-hidden">
<div className="relative z-10 w-full overflow-x-auto no-scrollbar pb-6">
<div className="min-w-[800px] relative">
{/* Continuous Line Background */}
<div
className="absolute top-2.5 h-1.5 bg-gray-100 rounded-full z-0"
style={{ left: `${100 / (progressSteps.length * 2)}%`, right: `${100 / (progressSteps.length * 2)}%` }}
></div>
{/* Active Line Foreground */}
<div
className="absolute top-2.5 h-1.5 bg-gradient-to-r from-emerald-400 to-orange-400 rounded-full z-0 transition-all duration-1000"
style={{
left: `${100 / (progressSteps.length * 2)}%`,
width: `calc(${
(progressSteps.findIndex(s => 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}%))`
}}
></div>
{/* Steps Container */}
<div className="flex w-full justify-between items-start relative z-10">
{progressSteps.map((step, idx) => (
<div key={idx} className="flex flex-col items-center flex-1 relative group cursor-pointer">
{/* Dot */}
<div className={`w-6 h-6 rounded-full border-4 border-white shadow-sm mb-10 transition-all duration-500 group-hover:scale-125 ${
step.status === 'completed' ? 'bg-emerald-500' :
step.status === 'active' ? 'bg-orange-500 ring-4 ring-orange-100' :
'bg-gray-200'
}`}></div>
{/* Icon */}
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center transition-all duration-500 mb-6 group-hover:-translate-y-2 group-hover:shadow-lg ${
step.status === 'completed' ? 'bg-emerald-50 text-emerald-500 group-hover:bg-emerald-100' :
step.status === 'active' ? 'bg-white border-2 border-orange-500 text-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)] scale-110 group-hover:scale-125' :
'bg-gray-50 text-gray-300 group-hover:bg-gray-100 group-hover:text-gray-500'
}`}>
<i className={`fas ${step.icon} text-xl md:text-2xl transition-transform duration-300 group-hover:scale-110`}></i>
</div>
{/* Text */}
<div className="text-center transition-transform duration-300 group-hover:translate-y-1">
<span className={`block text-[10px] md:text-xs font-black uppercase tracking-[0.15em] mb-1.5 transition-colors duration-300 ${
step.status === 'active' ? 'text-orange-500' :
step.status === 'completed' ? 'text-gray-700 group-hover:text-emerald-600' :
'text-gray-400 group-hover:text-gray-600'
}`}>{step.label}</span>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest whitespace-nowrap">{step.detail}</p>
</div>
</div>
))}
</div>
{/* Attendance Tracker Redesign (Node-based) */}
<div className="mt-20 pt-12 border-t border-gray-100 flex flex-col items-center">
<div className="flex items-center gap-3 mb-10">
<div className="w-1.5 h-4 bg-[#f97316] rounded-full"></div>
<span className="text-[11px] font-black text-slate-800 uppercase tracking-[0.2em]">Attendance Milestones</span>
</div>
<div className="w-full max-w-4xl relative">
{(() => {
const allSessions: { label: string, status: string, day: string, batch: string }[] = [];
if (event.schedule && event.schedule.length > 0) {
const dayMap = new Map<number, typeof event.schedule>();
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 (
<div className="relative">
{/* Connecting Line Background */}
<div
className="absolute top-2.5 h-1.5 bg-gray-100 rounded-full z-0"
style={{ left: `${100 / (allSessions.length * 2)}%`, right: `${100 / (allSessions.length * 2)}%` }}
></div>
{/* Connecting Line Foreground */}
<div
className="absolute top-2.5 h-1.5 bg-emerald-500 rounded-full z-0 transition-all duration-1000"
style={{
left: `${100 / (allSessions.length * 2)}%`,
width: `calc(${
(allSessions.findIndex(s => 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}%))`
}}
></div>
{/* Nodes */}
<div className="flex w-full justify-between items-start relative z-10">
{allSessions.map((session, sIdx) => (
<div key={sIdx} className="flex flex-col items-center flex-1 relative group cursor-pointer">
{/* Dot on line */}
<div className={`w-6 h-6 rounded-full border-4 border-white shadow-sm mb-10 transition-all duration-500 group-hover:scale-125 ${
session.status === 'completed' ? 'bg-emerald-500' :
session.status === 'absent' ? 'bg-rose-500 animate-pulse' :
'bg-gray-200'
}`}></div>
{/* Large Circle Shaped Indicator */}
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center transition-all duration-500 mb-6 group-hover:-translate-y-2 group-hover:shadow-lg ${
session.status === 'completed' ? 'bg-emerald-50 text-emerald-500 group-hover:bg-emerald-100 border-2 border-emerald-500/20' :
session.status === 'absent' ? 'bg-rose-50 text-rose-500 group-hover:bg-rose-100 border-2 border-rose-500/20' :
'bg-gray-50 text-gray-300 group-hover:bg-gray-100 group-hover:text-gray-500 border-2 border-slate-100'
}`}>
<i className={`fas ${
session.status === 'completed' ? 'fa-calendar-check' :
session.status === 'absent' ? 'fa-calendar-times' :
'fa-calendar'
} text-xl md:text-2xl transition-transform duration-300 group-hover:scale-110`}></i>
</div>
{/* Text */}
<div className="text-center transition-transform duration-300 group-hover:translate-y-1">
<span className={`block text-[10px] md:text-xs font-black uppercase tracking-[0.15em] mb-1.5 transition-colors duration-300 ${
session.status === 'completed' ? 'text-[#1A202C]' :
session.status === 'absent' ? 'text-rose-500' :
'text-gray-400 group-hover:text-gray-600'
}`}>{session.label}</span>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest whitespace-nowrap">
{session.status === 'completed' ? 'Attended' :
session.status === 'absent' ? 'Absent' : 'Upcoming'}
</p>
</div>
</div>
))}
</div>
</div>
);
})()}
</div>
<div className="mt-12 bg-gray-50/50 px-8 py-3 rounded-2xl border border-gray-100">
<p className="text-[10px] font-black text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
<i className="fas fa-chart-line text-[#f97316]"></i>
{attendanceRecords.filter(r => r.isPresent || r.is_present).length} / {Math.max(1, event.schedule?.length || 1)} Sessions Attended
</p>
</div>
</div>
</div>
{/* Team Management Buttons */}
{event.isTeamEvent && !(registration?.teamCode || registration?.team_code) && (
<div className="mt-12 p-10 bg-white rounded-[3.5rem] shadow-xl shadow-gray-900/5 border border-gray-100 relative overflow-hidden group animate-in slide-in-from-bottom-5">
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl group-hover:bg-orange-100 transition-colors"></div>
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
<div className="text-center md:text-left">
<div className="flex justify-center md:justify-start items-center gap-3 mb-3">
<div className="w-2 h-5 bg-[#f97316] rounded-full"></div>
<span className="text-xs font-black text-slate-900 uppercase tracking-widest">Team Management</span>
</div>
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest max-w-md">This is a team event. Form your own team or join an existing alliance to participate in this competition.</p>
</div>
<div className="flex items-center gap-4 w-full md:w-auto">
<button
onClick={onShowCreateTeam}
className="flex-1 md:flex-none py-5 px-10 bg-white border-2 border-slate-100 text-slate-900 rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-sm hover:border-[#f97316] hover:text-[#f97316] transition-all active:scale-95 flex items-center justify-center gap-2"
>
<i className="fas fa-plus-circle"></i> Create Team
</button>
<button
onClick={onShowJoinTeam}
className="flex-1 md:flex-none py-5 px-10 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-xl shadow-gray-200 hover:bg-black transition-all active:scale-95 flex items-center justify-center gap-2"
>
<i className="fas fa-right-to-bracket"></i> Join Alliance
</button>
</div>
</div>
</div>
)}
</div>
</div>
{/* OD Document Download Section */}
{liveOdUrl && (
<div className="bg-white rounded-[3.5rem] p-8 md:p-12 shadow-xl shadow-gray-900/5 border border-gray-100 mb-12 flex flex-col md:flex-row items-center justify-between gap-8 animate-in slide-in-from-bottom-5">
<div className="flex items-center gap-6">
<div className="w-16 h-16 bg-teal-50 text-teal-500 rounded-3xl flex items-center justify-center text-2xl shadow-inner">
<i className="fas fa-file-signature"></i>
</div>
<div>
<h3 className="text-2xl font-black text-[#1A202C] tracking-tight uppercase">Official On-Duty Document</h3>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mt-1">Authorized by Faculty Coordinator</p>
</div>
</div>
<div className="flex gap-4 w-full md:w-auto">
<a href={liveOdUrl} target="_blank" rel="noreferrer" className="flex-1 md:w-auto px-8 py-4 bg-gray-50 text-gray-600 rounded-2xl font-black text-[10px] uppercase tracking-widest hover:bg-gray-100 transition-all border border-gray-200 text-center">
<i className="fas fa-eye mr-2"></i> Inspect
</a>
<button onClick={handleOdDownload} className="flex-1 md:w-auto px-8 py-4 bg-teal-600 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest hover:bg-teal-700 transition-all shadow-xl shadow-teal-600/20 text-center">
<i className="fas fa-download mr-2"></i> Download OD
</button>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 bg-white p-12 rounded-[3.5rem] shadow-xl shadow-gray-900/10 border border-gray-200 flex flex-col md:flex-row items-center gap-12">
<div className="flex-1 text-center md:text-left">
<h3 className="text-3xl font-black text-[#1A202C] mb-4 uppercase tracking-tight">Certification Portal</h3>
<p className="text-gray-500 font-medium mb-8 leading-relaxed max-w-sm">Provide valid proof of attendance or task completion to finalize your official event participation.</p>
<input type="file" ref={fileInputRef} className="hidden" onChange={handleFileChange} accept="image/*" />
{!isEventEnded ? (
<div className="flex flex-col items-start gap-3">
<button
disabled
className="px-12 py-5 rounded-2xl font-black text-xs uppercase tracking-[0.3em] bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200 flex items-center gap-4"
>
<i className="fas fa-lock"></i> UPLOAD LOCKED
</button>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
<i className="fas fa-hourglass-half text-[#f97316]"></i>
Upload unlocks once admin marks event as Ended
</p>
</div>
) : (
<>
<button
onClick={() => fileInputRef.current?.click()}
disabled={isUploading || liveCertStatus === 'APPROVED'}
className={`px-12 py-5 rounded-2xl font-black text-xs uppercase tracking-[0.3em] shadow-2xl transition-all active:scale-95 flex items-center gap-4 ${liveCertStatus === 'APPROVED' ? 'bg-emerald-50 text-emerald-500 cursor-not-allowed border border-emerald-100' :
isUploading ? 'bg-gray-200 text-gray-400 cursor-wait' : 'bg-[#1A202C] text-white hover:bg-black'
}`}
>
{isUploading ? <><i className="fas fa-spinner fa-spin"></i> SYNCING...</> :
liveCertStatus === 'APPROVED' ? <><i className="fas fa-check-double"></i> VERIFIED</> :
liveCertUrl ? <><i className="fas fa-clock"></i> RE-UPLOAD PROOF</> :
<><i className="fas fa-cloud-arrow-up"></i> UPLOAD PROOF</>}
</button>
{liveCertStatus === 'PENDING_APPROVAL' && (
<p className="mt-4 text-[10px] font-black text-[#f97316] uppercase tracking-widest flex items-center gap-2">
<i className="fas fa-info-circle"></i> Awaiting Faculty Review
</p>
)}
</>
)}
</div>
<div className="w-full md:w-64 aspect-square bg-gray-50 rounded-[2.5rem] border-2 border-dashed border-gray-200 flex items-center justify-center overflow-hidden group">
{liveCertUrl ? (
<img src={liveCertUrl} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" alt="Proof" />
) : (
<div className="text-center p-6">
<i className="fas fa-image text-gray-200 text-4xl mb-4"></i>
<p className="text-[10px] font-black text-gray-300 uppercase tracking-widest">Preview Area</p>
</div>
)}
</div>
</div>
<div className="bg-[#1A202C] p-12 rounded-[3.5rem] text-white shadow-2xl shadow-gray-900/20 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
<h3 className="text-2xl font-black mb-10 tracking-tight uppercase flex items-center gap-4">
Event Info
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
</h3>
<div className="space-y-8">
<div className="flex justify-between items-center border-b border-white/5 pb-4">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Conducting Club</span>
<span className="text-white font-black text-[11px] uppercase">{event.club || 'N/A'}</span>
</div>
<div className="flex justify-between items-center border-b border-white/5 pb-4">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Enrollment Status</span>
<span className="text-white font-black text-[11px] uppercase">{registration?.paymentStatus || registration?.payment_status || 'PENDING'}</span>
</div>
{event.durationDays && (
<div className="flex justify-between items-center border-b border-white/5 pb-4">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Duration</span>
<span className="text-white font-black text-[11px] uppercase">{event.durationDays} Days</span>
</div>
)}
{event.schedule && event.schedule.length > 0 && (
<div className="flex justify-between items-center border-b border-white/5 pb-4">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Sessions</span>
<span className="text-white font-black text-[11px] uppercase">
{event.schedule.length} Total
</span>
</div>
)}
<div className="flex justify-between items-center border-b border-white/5 pb-4">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Attendance Progress</span>
<span className="text-emerald-400 font-black text-[11px] uppercase">
{attendanceRecords.filter(r => r.isPresent || r.is_present).length} Marked
</span>
</div>
<div className="flex justify-between items-center border-b border-white/5 pb-4">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Certification</span>
<span className={`font-black text-[11px] uppercase ${liveCertStatus === 'APPROVED' ? 'text-emerald-400' : 'text-[#f97316]'}`}>
{liveCertStatus?.replace('_', ' ') || 'NOT SUBMITTED'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Verification Status</span>
<span className={`font-black text-[11px] uppercase ${liveCertStatus === 'APPROVED' ? 'text-emerald-400' : 'text-white/60'}`}>
{liveCertStatus === 'APPROVED' ? 'Finalized' : 'Pending'}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default StatusTrackerView;

View File

@@ -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<UpcomingEventsSliderProps> = ({ events }) => {
const [selectedEvent, setSelectedEvent] = useState<Event | null>(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 (
<div className="py-24 px-6 md:px-12 lg:px-24 bg-white overflow-hidden">
<style>
{`
@keyframes merryGoRound {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-merry-go-round {
animation: merryGoRound 30s linear infinite;
display: flex;
width: max-content;
}
.animate-merry-go-round:hover {
animation-play-state: paused;
}
`}
</style>
<div className="flex flex-col lg:flex-row items-start lg:items-end justify-between mb-16 gap-8">
<div>
<h3 className="text-4xl md:text-5xl font-serif text-[#1A202C] tracking-tight">Upcoming Experiences</h3>
</div>
</div>
<div className="relative overflow-hidden w-full">
{/* Gradient masks removed to eliminate fogginess */}
<div className="animate-merry-go-round gap-6 md:gap-8">
{loopEvents.map((event, idx) => (
<div
key={`${event.id}-${idx}`}
className="w-[280px] md:w-[350px] lg:w-[400px] flex-shrink-0 bg-white rounded-[2.5rem] p-5 group transition-all shadow-xl shadow-gray-900/5 hover:-translate-y-1 border border-gray-200"
>
<div className="h-48 md:h-56 w-full rounded-[2rem] overflow-hidden mb-6 md:mb-8 relative">
<img
src={event.image}
alt={event.title}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
/>
<div className="absolute top-4 right-4 bg-white px-4 py-2 rounded-full border border-gray-200">
<span className="text-[9px] font-black text-[#1A202C] uppercase tracking-widest">{event.category}</span>
</div>
</div>
<div className="px-2 pb-2 md:pb-4">
<h4 className="text-xl md:text-2xl font-serif text-[#1A202C] mb-4 md:mb-6 group-hover:text-[#f97316] transition-colors line-clamp-1">
{event.title}
</h4>
<div className="flex flex-col gap-3 text-gray-500 text-[9px] md:text-[10px] font-bold uppercase tracking-[0.2em]">
<div className="flex items-center gap-3">
<i className="far fa-calendar-alt text-[#f97316]"></i>
<span className="flex items-center gap-2">
{event.date}
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<span className="text-[#f97316]">{event.schedule?.[0]?.start_time || ''}</span>
</span>
</div>
<div className="flex items-center gap-3">
<i className="fas fa-location-dot text-[#f97316]"></i>
<span className="truncate">{event.location}</span>
</div>
</div>
<div className="mt-6 md:mt-8 pt-4 md:pt-6 border-t border-gray-200/30 flex justify-between items-center">
<span className="text-[8px] md:text-[9px] text-gray-400 font-black uppercase tracking-widest">Limited Access</span>
<button
onClick={() => setSelectedEvent(event)}
className="text-[#1A202C] text-[9px] md:text-[10px] font-black uppercase tracking-[0.3em] hover:text-[#f97316] transition-colors flex items-center gap-2"
>
Details <i className="fas fa-arrow-right text-[8px]"></i>
</button>
</div>
</div>
</div>
))}
</div>
</div>
{selectedEvent && (
<Portal>
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 sm:p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-[2rem] w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-500 max-h-[85vh] flex flex-col relative">
<button
onClick={() => setSelectedEvent(null)}
className="absolute top-6 right-6 w-8 h-8 rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-all flex items-center justify-center z-10"
>
<i className="fas fa-times text-sm"></i>
</button>
<div className="p-8 overflow-y-auto flex-1 custom-scrollbar">
<div className="flex items-center gap-4 mb-8 pb-6 border-b border-gray-100 pr-8">
{clubInfo && (
<img src={clubInfo.image} className="w-12 h-12 rounded-full object-cover border border-slate-200 shadow-sm" alt="" />
)}
<div>
<span className="block text-[10px] font-black text-[#f97316] uppercase tracking-[0.2em] mb-1">{selectedEvent.club || 'Organized by'}</span>
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
<i className="fas fa-user text-[9px]"></i> {selectedEvent.coordinator}
</span>
</div>
</div>
<div className="space-y-6 pb-6 mt-6 border-t border-gray-100 pt-6">
<div className="flex items-center gap-2 mb-2">
<div className="w-1 h-3 bg-[#f97316] rounded-full"></div>
<h4 className="text-[10px] font-black text-slate-800 uppercase tracking-[0.2em]">Summary</h4>
</div>
<div className="relative pt-2">
<i className="fas fa-quote-right absolute top-0 right-0 text-5xl text-slate-50 pointer-events-none -z-10"></i>
<p className="text-[14px] text-slate-600 leading-relaxed whitespace-pre-wrap font-medium">
{selectedEvent.event_summary || "Details for this session will be provided soon."}
</p>
</div>
</div>
<div className="mt-4 shrink-0 border-t border-gray-100 pt-6">
<button
onClick={() => setSelectedEvent(null)}
className="w-full py-4 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-black transition-all shadow-xl shadow-slate-200 focus:outline-none focus:ring-4 focus:ring-slate-100"
>
Close Summary
</button>
</div>
</div>
</div>
</div>
</Portal>
)}
</div>
);
};
export default UpcomingEventsSlider;

View File

@@ -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<string, { id: string, name: string, image: string }[]> = {
'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' }
]
};

View File

@@ -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 {

View File

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

View File

@@ -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);

View File

@@ -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`);
}

View File

@@ -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<UserRole | null>(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<string | null>(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<HTMLInputElement | HTMLSelectElement>) => {
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 (
<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
<div className="max-w-6xl w-full flex flex-col md:flex-row bg-white rounded-[2.5rem] overflow-hidden premium-shadow min-h-[700px]">
{/* Left Side: Login Form */}
<div className="w-full md:w-[45%] p-10 md:p-16 flex flex-col">
<div className="mb-12">
<img src={ritLogo} alt="RIT Logo" className="h-10 mb-8 object-contain" />
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-brand-indigo mb-2">
RAJALAKSHMI INSTITUTE OF TECHNOLOGY
</p>
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-2">
RIT EMS
</h1>
<p className="text-text-muted font-medium">
Event Management System
</p>
</div>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-[10px] font-black uppercase tracking-widest text-text-muted mb-2">
Institutional Email
</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
<input
type="email"
value={email}
onChange={(e) => 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
/>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="block text-[10px] font-black uppercase tracking-widest text-text-muted">
Passcode
</label>
<button type="button" className="text-[10px] font-black uppercase tracking-widest text-brand-indigo hover:opacity-70 transition-all">
Forgot?
</button>
</div>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => 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
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-text-muted hover:text-brand-indigo"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{error && (
<p className="text-status-danger text-xs font-bold bg-red-50 p-3 rounded-xl border border-red-100 animate-shake">
{error}
<div className="min-h-screen w-full bg-white flex items-center justify-center font-sans overflow-x-hidden">
<AnimatePresence mode="wait">
{!selectedRole ? (
// ----------------------------------------------------
// WELCOME SCREEN GATEWAY
// ----------------------------------------------------
<motion.div
key="welcome"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="w-full max-w-6xl px-6 py-12 text-center"
>
<div className="mb-16">
<img src={ritLogo} alt="RIT Logo" className="h-16 md:h-20 w-auto mx-auto mb-8 object-contain" />
<p className="text-slate-500 text-lg font-medium max-w-2xl mx-auto uppercase tracking-widest text-xs">
Select your gateway to excellence. Connect, manage, and celebrate.
</p>
)}
<button
type="submit"
disabled={isLoading}
className={cn(
"w-full bg-brand-navy text-white rounded-2xl py-4 flex items-center justify-center gap-3 font-black text-[11px] uppercase tracking-widest transition-all hover:scale-[1.01] active:scale-[0.99] premium-shadow",
isLoading && "opacity-70 cursor-not-allowed"
)}
>
{isLoading ? 'Processing...' : 'Enter Dashboard'}
{!isLoading && <ArrowRight className="w-4 h-4" />}
</button>
</form>
</div>
{/* Right Side: Building Image */}
<div className="hidden md:block flex-1 relative">
<img
src={ritBuilding}
alt="College Building"
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div className="absolute bottom-10 left-10 right-10">
<div className="bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-[2rem] inline-block">
<p className="text-white font-bold text-lg">Rajalakshmi Institute of Technology</p>
<p className="text-white/70 text-sm font-medium">Nurturing Excellence since 2008</p>
</div>
</div>
</div>
</div>
<div className="grid md:grid-cols-3 gap-8 text-left">
{portals.map((portal) => (
<div
key={portal.id}
onClick={() => {
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
)}
>
<div className={cn(
"w-20 h-20 rounded-3xl flex items-center justify-center mb-8 shadow-lg group-hover:scale-110 transition-transform duration-500",
portal.color
)}>
<i className={cn("fas text-3xl text-white", portal.icon)}></i>
</div>
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight mb-4 group-hover:text-orange-500 transition-colors">
{portal.title}
</h3>
<p className="text-slate-500 text-sm font-medium leading-relaxed mb-8">
{portal.description}
</p>
<div className={cn("flex items-center gap-3 text-xs font-black uppercase tracking-widest group-hover:gap-5 transition-all", portal.textColor)}>
Enter Portal <i className="fas fa-arrow-right"></i>
</div>
</div>
))}
</div>
<div className="mt-20">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.5em]">
© 2026 Rajalakshmi Institute of Technology Academic Excellence
</p>
</div>
</motion.div>
) : (
// ----------------------------------------------------
// DUAL SLIDING LOGIN & SIGNUP FORMS (Site 2 Layout)
// ----------------------------------------------------
<motion.div
key="forms"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="relative w-full max-w-5xl h-[700px] bg-white rounded-[3rem] shadow-[0_50px_100px_-20px_rgba(0,0,0,0.15)] overflow-hidden flex flex-col md:flex-row border border-slate-100"
>
{/* Back Button */}
<button
onClick={() => setSelectedRole(null)}
className="absolute top-8 left-8 z-[60] w-12 h-12 bg-white/10 md:bg-slate-100 hover:bg-slate-200 text-slate-500 rounded-full flex items-center justify-center transition-all border border-transparent shadow-sm"
type="button"
>
<i className="fas fa-arrow-left"></i>
</button>
{/* Forms Section */}
<div className="relative flex-1 flex">
{/* Sign Up (Left Side Panel) */}
<div className={cn(
"absolute inset-0 w-full md:w-1/2 h-full flex flex-col justify-center p-12 transition-all duration-700 ease-in-out z-10",
isSignUp ? "opacity-100 translate-x-0 visible" : "opacity-0 -translate-x-full invisible pointer-events-none"
)}>
<img src={ritLogo} alt="Logo" className="h-12 w-auto mb-6 object-contain self-start" />
{!signUpType && selectedRole === 'STUDENT' ? (
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className="text-orange-500">Portal</span></h2>
<div className="grid gap-4">
<button
onClick={() => setSignUpType('INTERNAL')}
type="button"
className={cn("w-full py-6 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95", RIT_BLUE)}
>
RIT Student (Internal)
</button>
<button
onClick={() => setSignUpType('EXTERNAL')}
type="button"
className="w-full py-6 bg-white border-2 border-slate-200 text-slate-700 rounded-2xl font-black uppercase text-xs tracking-[0.2em] transition-all hover:border-orange-500 hover:text-orange-500 hover:scale-[1.02] active:scale-95"
>
Other College (External)
</button>
</div>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4 overflow-y-auto no-scrollbar py-4 pr-1">
<div className="mb-6">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
{signUpType === 'INTERNAL' ? 'Internal' : 'External'} <span className="text-orange-500">Sign Up</span>
</h2>
<button type="button" onClick={() => setSignUpType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>
</div>
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
</div>
<div className="grid grid-cols-2 gap-4">
<input required name="name" placeholder="FULL NAME" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.name} onChange={handleInputChange} />
<input required type="email" name="email" placeholder="EMAIL" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.email} onChange={handleInputChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<input required name="regNo" placeholder="REG NO" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.regNo} onChange={handleInputChange} />
<input required name="phone" placeholder="PHONE NUMBER" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.phone} onChange={handleInputChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<input required type="password" name="password" placeholder="PASSWORD" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.password} onChange={handleInputChange} />
<input required type="password" name="confirmPassword" placeholder="CONFIRM" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.confirmPassword} onChange={handleInputChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<select required name="gender" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all cursor-pointer" value={formData.gender} onChange={handleInputChange}>
<option value="">SELECT GENDER</option>
<option value="Male">MALE</option>
<option value="Female">FEMALE</option>
<option value="Other">OTHER</option>
</select>
<input required name="collegeLocation" placeholder="COLLEGE LOCATION" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.collegeLocation} onChange={handleInputChange} />
</div>
<input
required
name="collegeName"
placeholder="COLLEGE NAME"
disabled={signUpType === 'INTERNAL'}
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all disabled:opacity-60"
value={formData.collegeName}
onChange={handleInputChange}
/>
<div className="grid grid-cols-3 gap-4">
<select required name="department" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none cursor-pointer" value={formData.department} onChange={handleInputChange}>
<option value="">DEPT</option>
{(signUpType === 'EXTERNAL' ? EXTERNAL_DEPARTMENTS : DEPARTMENTS).map(d => <option key={d} value={d}>{d}</option>)}
</select>
<select required name="year" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none cursor-pointer" value={formData.year} onChange={handleInputChange}>
<option value="">YEAR</option>
{YEARS.map(y => <option key={y} value={y}>{y}</option>)}
</select>
<select required name="section" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none cursor-pointer" value={formData.section} onChange={handleInputChange}>
<option value="">SEC</option>
{SECTIONS.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
{errorMessage && <p className="text-rose-500 text-[10px] font-bold uppercase">{errorMessage}</p>}
<button type="submit" disabled={isSubmitting} className="w-full py-5 bg-orange-500 text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl hover:bg-orange-600 transition-all shadow-lg shadow-orange-500/20 active:scale-95 disabled:opacity-55">
{isSubmitting ? 'Registering...' : 'Sign Up Now'}
</button>
</form>
)}
</div>
{/* Sign In (Right Side Panel) */}
<div className={cn(
"absolute inset-0 w-full md:w-1/2 h-full flex flex-col justify-center p-12 transition-all duration-700 ease-in-out z-10 ml-auto",
!isSignUp ? "opacity-100 translate-x-0 visible" : "opacity-0 translate-x-full invisible pointer-events-none"
)}>
{selectedRole === 'COORDINATOR' && !coordinatorLoginType ? (
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
<img src={ritLogo} alt="Logo" className="h-12 w-auto mb-6 object-contain mx-auto" />
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className="text-orange-500">Access Level</span></h2>
<div className="grid gap-4">
<button
onClick={() => setCoordinatorLoginType('Faculty')}
type="button"
className={cn("w-full py-6 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95", RIT_BLUE)}
>
Faculty Member
</button>
<button
onClick={() => setCoordinatorLoginType('HOD')}
type="button"
className="w-full py-6 bg-orange-500 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95"
>
Head of Department
</button>
</div>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="mb-8">
<img src={ritLogo} alt="Logo" className="h-12 w-auto mb-6 object-contain" />
<div className="flex items-center justify-between">
<h2 className="text-4xl font-black text-slate-900 uppercase tracking-tighter">Welcome <span className={RIT_BLUE_TEXT}>Back</span></h2>
{selectedRole === 'COORDINATOR' && (
<button type="button" onClick={() => setCoordinatorLoginType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>
)}
</div>
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-2">
Access your {selectedRole === 'ADMIN' ? 'Admin / Principal' : (selectedRole === 'COORDINATOR' ? `${coordinatorLoginType} Event` : 'Student')} portal
</p>
</div>
<div className="space-y-4">
<div className="relative">
<i className="fas fa-envelope absolute left-6 top-1/2 -translate-y-1/2 text-slate-300"></i>
<input required type="email" name="email" placeholder="EMAIL ADDRESS" className="w-full bg-slate-50 border-none rounded-2xl pl-14 pr-6 py-5 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.email} onChange={handleInputChange} />
</div>
<div className="relative">
<i className="fas fa-lock absolute left-6 top-1/2 -translate-y-1/2 text-slate-300"></i>
<input required type={showPassword ? 'text' : 'password'} name="password" placeholder="PASSWORD" className="w-full bg-slate-50 border-none rounded-2xl pl-14 pr-14 py-5 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.password} onChange={handleInputChange} />
<button type="button" onClick={() => setShowPassword(!showPassword)} className={cn("absolute right-6 top-1/2 -translate-y-1/2 text-slate-300 transition-colors", `hover:${RIT_BLUE_TEXT}`)}>
<i className={cn("fas", showPassword ? "fa-eye-slash" : "fa-eye")}></i>
</button>
</div>
</div>
<div className="flex justify-end">
<button type="button" className="text-[10px] font-black text-slate-400 uppercase tracking-widest hover:text-orange-500 transition-colors">Forgot Password?</button>
</div>
{errorMessage && <p className="text-rose-500 text-[10px] font-bold uppercase">{errorMessage}</p>}
<button type="submit" disabled={isSubmitting} className={cn("w-full py-5 text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl transition-all shadow-lg active:scale-95 disabled:opacity-55", RIT_BLUE, RIT_BLUE_HOVER)}>
{isSubmitting ? 'Verifying...' : 'Sign In Now'}
</button>
</form>
)}
</div>
</div>
{/* Sliding Overlay Panel (Matches RIT Navy theme) */}
<div className={cn(
"absolute top-0 left-0 w-full md:w-1/2 h-full z-50 transition-all duration-700 ease-[cubic-bezier(0.7,0,0.3,1)] flex flex-col items-center justify-center text-center p-12 overflow-hidden",
RIT_BLUE,
isSignUp ? "md:translate-x-full" : "translate-x-0"
)}>
{/* Background patterns */}
<div className="absolute inset-0 opacity-10 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_white_1px,_transparent_1px)] bg-[size:30px_30px]"></div>
</div>
{/* Login Overlay Message */}
<div className={cn(
"absolute inset-0 flex flex-col items-center justify-center p-12 transition-all duration-700 delay-100",
isSignUp ? "opacity-0 -translate-y-10 pointer-events-none invisible" : "opacity-100 translate-y-0 pointer-events-auto visible"
)}>
<h2 className="text-4xl font-black text-white uppercase tracking-tighter mb-4">
{selectedRole === 'STUDENT' ? 'New Here?' : 'Restricted Gateway'}
</h2>
<p className="text-white/80 text-sm font-medium leading-relaxed mb-10 max-w-xs mx-auto">
{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.'}
</p>
{selectedRole === 'STUDENT' && (
<button
onClick={() => setIsSignUp(true)}
className="px-12 py-4 border-2 border-white text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-full hover:bg-white hover:text-[#004a99] transition-all active:scale-95"
type="button"
>
Sign Up
</button>
)}
</div>
{/* Signup Overlay Message */}
<div className={cn(
"absolute inset-0 flex flex-col items-center justify-center p-12 transition-all duration-700 delay-100",
(!isSignUp || selectedRole !== 'STUDENT') ? "opacity-0 translate-y-10 pointer-events-none invisible" : "opacity-100 translate-y-0 pointer-events-auto visible"
)}>
<h2 className="text-4xl font-black text-white uppercase tracking-tighter mb-4">Welcome <span className="text-white/70">Back!</span></h2>
<p className="text-white/80 text-sm font-medium leading-relaxed mb-10 max-w-xs mx-auto">
To keep connected with us please login with your personal credentials.
</p>
<button
onClick={() => setIsSignUp(false)}
className="px-12 py-4 border-2 border-white text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-full hover:bg-white hover:text-[#004a99] transition-all active:scale-95"
type="button"
>
Sign In
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -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<string, number>; // Maps department codes to specific seat counts
deptSectionLimits?: Record<string, Record<string, number>>; // Maps department codes to their section capacities
currentParticipants?: number;
currentDeptCounts?: Record<string, number>;
currentDeptSectionCounts?: Record<string, Record<string, number>>; // 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';
}