Delete RIT-EMS-main directory

This commit is contained in:
2026-06-23 09:41:24 +05:30
committed by GitHub
parent 512c04bb6f
commit d0d77c0457
88 changed files with 0 additions and 24436 deletions

View File

@@ -1,407 +0,0 @@
# RIT Event Management System (EMS) - Comprehensive System Documentation
The **RIT Event Management System (EMS)** is a state-of-the-art web application developed for the Rajalakshmi Institute of Technology (RIT) to handle, automate, audit, and coordinate campus-wide events, venue requests, and academic schedules.
---
## 1. Executive Summary & Core Objective
In an active educational institution like RIT (and the sister school RSB), coordinating events such as academic workshops, guest lectures, sports events, student club activities, and high-priority placement drives presents significant scheduling challenges:
1. **Venue Booking Conflicts:** Overlapping bookings for major facilities (e.g., auditoriums, seminar halls, labs).
2. **Complex Approvals:** Manual routing of proposals from Faculty, through Heads of Departments (HOD), up to the Principal.
3. **Academic Semester Swapping:** Transitioning routine weekly/monthly schedules from one semester to the next.
4. **Data Overload:** Migrating bulk event schedules from legacy tools or Google Forms.
The EMS platform addresses these challenges by offering:
* **Interactive Venues at a Glance:** Live timeline visuals showing hourly and daily room utilization.
* **Role-Based Workflows:** Direct automated routing of event proposals based on standard institutional roles.
* **Smart Conflict Solver:** Automated, real-time database queries matching proposed locations, dates, and institutions.
* **Overriding Placement System:** Privileged scheduling options for Placement staff to secure facilities.
* **Semester Swap Simulation:** Dynamic tool mapping active calendars onto subsequent semesters with calendar shift simulations.
* **Fuzzy Excel Parser:** In-browser sheet processor mapping arbitrary data columns into structured event templates.
---
## 2. System Architecture & Tech Stack
The platform runs on a serverless, database-first architecture using Firebase. All backend endpoints are intercepted and resolved client-side in the browser.
```mermaid
graph LR
subgraph Frontend [React SPA Client]
React[React 19 / TS / Vite]
Router[Component & Context Routing]
Tailwind[Tailwind CSS & Glassmorphism UI]
Framer[Framer Motion Animations]
Interceptor[Fetch Interceptor firebaseBackend.ts]
end
subgraph Backend [Firebase Cloud Platform]
Firestore[(Cloud Firestore Database)]
Auth[Firebase Authentication]
end
React <--> Interceptor
Interceptor <-->|Firebase Client SDK| Firestore
Interceptor <-->|Firebase Client SDK| Auth
```
### Firebase Backend Specifications
* **Database Service:** Cloud Firestore for real-time document storage.
* **Data Access & Interception:** A client-side fetch interceptor implemented in `firebaseBackend.ts` intercepts all `/api/*` REST HTTP requests and handles the queries/transactions natively using the Firebase Client SDK.
* **Authentication:** Google OAuth 2.0 and email/passcode flows, integrated with Firebase Auth and sync'd to the `ems_users` Firestore collection.
* **Security & Roles:** Verified inside frontend routing handlers and secured using Firestore Security Rules.
### Frontend Specifications
* **Core Framework:** React 19, TypeScript, Vite
* **State Management:** React Context API (specifically `AuthContext` managing authenticated states).
* **Styling System:** Tailwind CSS with custom global CSS utilities for modern glassmorphism design layouts.
* **Animations:** Framer Motion powering tab transitions, modal slides, and responsive components.
* **Icon Suite:** Lucide React
* **Utility Libraries:**
* `date-fns`: Date formatting, calendar scheduling, and duration differences.
* `xlsx`: In-browser parsing of spreadsheet files (.xlsx, .csv).
---
## 3. Database Schema & Architecture
The database contains four primary collections managed in Cloud Firestore.
```mermaid
erDiagram
USERS {
Long id PK
String email UK
String password
String fullName
String role
String department
Boolean isClubCoordinator
Boolean isPlacementStaff
Boolean isClassIncharge
Integer classStrength
String inchargeClass
String inchargeBatch
String inchargeSection
}
EVENTS {
Long id PK
String title
Text description
LocalDateTime startDate
LocalDateTime endDate
String location
String category
String type
String institution
String department
String status
String guestName
String guestSocialProfile
Double budget
Boolean hasRegistrationFee
Double registrationFee
Text rejectionReason
String groupRequestId
Long proposer_id FK
}
CLASS_MAPPING {
Long id PK
String institution
String department
String academicYear
String status
}
INSTITUTIONAL_NOTE {
Long id PK
String content
LocalDate targetDate
String authorName
String authorEmail
LocalDateTime createdAt
}
USERS ||--o{ EVENTS : proposes
```
### A. User Entity (`users`)
Holds profiles, credentials, permissions, and roles.
* `id` (Long, Primary Key): Auto-incremented.
* `email` (String, Unique): Institutional email (`@rit.edu` or `@ritindia.edu`).
* `password` (String): BCrypt hashed password representation.
* `fullName` (String): User's display name.
* `role` (String): Enum-like values: `ADMIN`, `PRINCIPAL`, `HOD`, `FACULTY`, `PLACEMENT`.
* `department` (String): Department names (e.g., `CSE`, `AI&ML`, `ECE`, `H&S Dept`, etc.).
* `isClubCoordinator` (Boolean): Authorizes coordination of student club events.
* `isPlacementStaff` (Boolean): Grants authority to override schedule conflicts for placement events.
* `isClassIncharge` (Boolean): Authorizes dashboard widgets showing class sizes and updates.
* `classStrength` (Integer): Student count for the in-charge class.
* `inchargeClass` / `inchargeBatch` / `inchargeSection` (String): Specific details identifying the class in charge.
* `assignedClubs` (ElementCollection): List of student clubs managed by the user.
### B. Event Entity (`events`)
Details individual event proposals, resources, status, and audit workflows.
* `id` (Long, Primary Key): Auto-incremented.
* `title` (String, Not Null): The name of the event.
* `description` (Text): Detailed agenda or description.
* `startDate` / `endDate` (LocalDateTime): Timestamps for the event duration.
* `location` (String): Specific venue (e.g., `GB 4th floor auditorium`, `Wozniak Auditorium`, `Mini Seminar Hall`).
* `category` (String): Dictates the routing logic: `ACADEMIC`, `CLUB`, `PLACEMENT`, `SPORTS`, `INSTITUTIONAL`.
* `type` (String): Event type (e.g., `Workshop`, `Seminar`, `Guest Lecture`, `Competition`).
* `institution` (String): Targets `RIT` or `RSB`.
* `department` (String): Proposing department.
* `academicYears` (ElementCollection): Targeted cohorts (e.g., `1st Year`, `2nd Year`, `3rd Year`, `4th Year`).
* `targetedSections` (ElementCollection): Target sections (e.g., `A`, `B`, `C`).
* `status` (String): Current lifecycle stage:
* `REQUESTED`: Submitted by Faculty, pending HOD approval.
* `PENDING_PR`: Approved by HOD, pending Principal approval (or submitted directly by HOD/Placement, bypassing HOD stage).
* `APPROVED`: Fully approved, venue locked, and calendar entry created.
* `COMPLETED`: Event time has elapsed.
* `CANCELLED`: Cancelled manually or displaced by an overriding placement event.
* `HOD_REJECTED`: Rejected at the HOD stage.
* `PRINCIPAL_REJECTED`: Rejected at the Principal stage.
* `guestName` / `guestSocialProfile` (String): Guest details and links.
* `requirements` (ElementCollection): Resources required (e.g., `Mementos`, `Projector`, `Lunch`, `Wi-Fi`, `Mic`).
* `budget` (Double): Requested budget.
* `hasRegistrationFee` (Boolean) / `registrationFee` (Double): Entry fee details.
* `sponsors` (ElementCollection): Partner/sponsor list.
* `proposer` (ManyToOne -> User): Reference to user who created the event proposal.
* `rejectionReason` (Text): Comments input by HOD or Principal when rejecting.
* `groupRequestId` (String): Link identifier for recurring events.
### C. ClassMapping Entity (`classes`)
Used for maintaining lists of student cohorts and sections.
* `id` (Long, Primary Key)
* `institution` (String): `RIT` or `RSB`.
* `department` (String): Department name.
* `academicYear` (String): `1st Year`, `2nd Year`, etc.
* `sections` (ElementCollection): List of associated sections.
* `status` (String): Defaults to `Ready`.
### D. InstitutionalNote Entity (`institutional_notes`)
Pinned warnings, announcements, or notifications on the main calendar.
* `id` (Long, Primary Key)
* `content` (String, Not Null)
* `targetDate` (LocalDate): Date on which the note should appear on the calendar.
* `authorName` / `authorEmail` (String): Creator info.
* `createdAt` (LocalDateTime)
---
## 4. Role-Based Access Control & Navigation
The frontend app adjusts its views, menus, and dashboard components dynamically based on the logged-in user's role and authorization flags:
| Role / Flag | Menu Items Available | Special Features |
| :--- | :--- | :--- |
| **FACULTY** | Dashboard, Propose Event, All Events, Institutional Checklist, Event History | Can see personal proposal status, updates class strength if marked as Class Incharge. |
| **HOD** | Dashboard, Propose Event, Approvals, All Events, Institutional Checklist, Event History, Automation Console | Can approve/reject department proposals. Can access the Semester Swap simulator. |
| **PRINCIPAL** | Dashboard, Propose Event, Approvals, All Events, Institutional Checklist, Event History, Automation Console | Can approve/reject any pending events. Master checklist dashboard widget access. |
| **PLACEMENT** | Dashboard, Propose Event, All Events, Institutional Checklist, Event History | Proposes placement events that can optionally override existing bookings. |
| **ADMIN** | Dashboard, Propose Event, Approvals, Clubs Checklist, All Events, Institutional Checklist, Event History, Automation Console, Excel Import, Class Configurations, User Management | Full control over users, classes, excel file parses, database configurations, and approvals. |
| **isClubCoordinator = true** | Add *Clubs* Menu option | Grants access to the Club Checklist page to coordinate student club activities. |
---
## 5. Site Map & Route Specification
The system uses a Single-Page Application (SPA) shell in [App.tsx](file:///e:/RIT-EMS-main/frontend/src/App.tsx) driven by state routing.
```mermaid
graph TD
Login[LoginPage.tsx] -->|POST /api/auth/login success| Shell[App.tsx SPA Shell]
subgraph SPA Navigation & Components
Shell -->|dashboard| Overview[Overview.tsx]
Shell -->|propose| Proposal[EventProposalForm.tsx]
Shell -->|approvals| Approvals[ApprovalsView.tsx]
Shell -->|clubs| Clubs[ClubInstitutionalChecklist.tsx]
Shell -->|events| Events[AllEvents.tsx]
Shell -->|checklist| Checklist[InstitutionalChecklist.tsx]
Shell -->|history| History[EventHistory.tsx]
Shell -->|classes| ClassMgmt[ClassManagement.tsx]
Shell -->|automation| Automation[AutomationView.tsx]
Shell -->|import-excel| ExcelImport[ExcelImport.tsx]
Shell -->|user-management| UserMgmt[UserManagement.tsx]
end
subgraph Embedded Widgets in Overview
Overview --> Cal[InstitutionalCalendar.tsx]
Overview --> Ven[VenuesAtGlance.tsx]
Overview --> Time[VenueTimeline.tsx]
Overview --> Fac[FacultyEventManagement.tsx]
Overview --> Pri[PrincipalMasterChecklist.tsx]
end
```
### Route & Component Specification Details
#### 1. Login Gate
* **Component:** [LoginPage.tsx](file:///e:/RIT-EMS-main/frontend/src/pages/LoginPage.tsx)
* **Access:** Anonymous/Public.
* **Description:** Provides a modern UI with split-card layout, college logos, and forms for institutional email & password.
* **API Endpoint called:** `POST /api/auth/login`
#### 2. Overview Dashboard (`dashboard`)
* **Component:** [Overview.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/Overview.tsx)
* **Access:** All Authenticated Users.
* **Features:**
* Displays circular progress indicators representing events by category.
* Shows role-specific cards (e.g., Faculty see their proposals; Class Incharges see class stats; Principals see Master Checklist).
* **Embedded Sub-Components:**
* **[InstitutionalCalendar.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalCalendar.tsx):** Interactive calendar with day-based event popovers and notes.
* **[VenuesAtGlance.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/VenuesAtGlance.tsx):** Displays a grid of main campus venues and their immediate status.
* **[VenueTimeline.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/VenueTimeline.tsx):** A timeline mapping hour blocks for venue bookings.
* **[FacultyEventManagement.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/FacultyEventManagement.tsx):** Faculty-specific event table displaying submission stages.
* **[PrincipalMasterChecklist.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/PrincipalMasterChecklist.tsx):** Principal-specific checklist audit view.
* **API Endpoints called:** `GET /api/events`, `GET /api/notes`, `POST /api/notes`, `PUT /api/admin/users/{id}` (for class updates).
#### 3. Propose Event (`propose`)
* **Component:** [EventProposalForm.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx)
* **Access:** All Authenticated Users.
* **Features:**
* Handles standard scheduling, multi-select sections/target academic years, and venue requirements (Catering, Projector, Wi-Fi, etc.).
* Support for recurring group bookings.
* Placement role can toggle "Override Conflicts".
* **API Endpoints called:** `POST /api/events/propose`, `PUT /api/events/{id}`
#### 4. Approvals (`approvals`)
* **Component:** [ApprovalsView.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ApprovalsView.tsx)
* **Access:** HOD and PRINCIPAL.
* **Features:** Tabulated view listing pending events. HOD reviews `REQUESTED` events. Principal reviews `PENDING_PR` events. Rejections trigger a popover modal requesting comment.
* **API Endpoints called:** `GET /api/events`, `POST /api/events/{id}/approve`, `POST /api/events/{id}/reject`
#### 5. Clubs Checklist (`clubs`)
* **Component:** [ClubInstitutionalChecklist.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ClubInstitutionalChecklist.tsx)
* **Access:** Club Coordinators and Admin.
* **Features:** Filters events specifically marked under the `CLUB` category and presents a checklist layout.
* **API Endpoints called:** `GET /api/events`
#### 6. All Events (`events`)
* **Component:** [AllEvents.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/AllEvents.tsx)
* **Access:** All Authenticated Users.
* **Features:** Grid searching, text filters, date search, category chips, and delete/edit buttons for event owners or Admin.
* **API Endpoints called:** `GET /api/events`, `DELETE /api/events/{id}`
#### 7. Institutional Checklist (`checklist`)
* **Component:** [InstitutionalChecklist.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalChecklist.tsx)
* **Access:** All Authenticated Users.
* **Features:** Visual grid displaying requirements (Wi-Fi, catering, projector) for approved upcoming events, highlighting completeness.
* **API Endpoints called:** `GET /api/events`
#### 8. Event History (`history`)
* **Component:** [EventHistory.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/EventHistory.tsx)
* **Access:** All Authenticated Users.
* **Features:** Lists elapsed events with filters. Features summary stats showing total budget spent and event count.
* **API Endpoints called:** `GET /api/events`
#### 9. Automation Console (`automation`)
* **Component:** [AutomationView.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/AutomationView.tsx)
* **Access:** HOD, PRINCIPAL, and ADMIN.
* **Features:**
* **Semester Swap:** Selects a batch of events (e.g., CSE 3rd Year RIT) and maps them to a future date range (+/- 6 months) to replicate schedules.
* **Conflict Analysis:** In-memory check identifying overlap with existing entries or weekends, recommending alternatives.
* **Review Gate:** Verification modal showing proposed events before batch creation.
* **API Endpoints called:** `GET /api/events`, `POST /api/events/batch-create`
#### 10. Excel Import (`import-excel`)
* **Component:** [ExcelImport.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ExcelImport.tsx)
* **Access:** ADMIN.
* **Features:**
* File uploader utilizing `xlsx` to parse spreadsheet columns.
* Mapping panel to align sheet columns with event details.
* Proposer verification: Automatically registers unrecognized emails (`password` defaults to `Password@123`).
* Conflict resolver indicating venue/time overlaps and recommending shifts.
* **API Endpoints called:** `GET /api/events`, `GET /api/admin/users`, `POST /api/admin/users`, `POST /api/events/batch-create`
#### 11. Class Configurations (`classes`)
* **Component:** [ClassManagement.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx)
* **Access:** ADMIN.
* **Features:** Add classes, update sections, and perform the "Yearly Promotion" (e.g. promoting 1st Years -> 2nd Years, deleting graduating 4th Years).
* **API Endpoints called:** `GET /api/classes`, `POST /api/classes`, `DELETE /api/classes/{id}`, `POST /api/classes/promote`
#### 12. User Management (`user-management`)
* **Component:** [UserManagement.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/UserManagement.tsx)
* **Access:** ADMIN.
* **Features:** CRUD management for user accounts. Controls department assignments, club responsibilities, role changes, and passwords.
* **API Endpoints called:** `GET /api/admin/users`, `POST /api/admin/users`, `PUT /api/admin/users/{id}`, `DELETE /api/admin/users/{id}`
---
## 6. Key Workflows & Logic Specs
### A. Real-time Conflict Auditing Engine
During event creation, the application validates for timing and location conflicts directly against Firestore.
1. The frontend query helper scan the `ems_events` collection to identify overlapping bookings:
$$\text{Start}_A < \text{End}_B \quad \text{and} \quad \text{End}_A > \text{Start}_B$$
2. **Placement Override Case:**
If the proposer is a `PLACEMENT` coordinator and `cancelConflicting=true` is checked:
* The fetch interceptor updates all conflicting Firestore event documents' status to `CANCELLED`.
* It registers a rejection/displacement reason on the cancelled events.
* The placement event is saved in `PENDING_PR` (for Principal review).
3. **General Case:**
If conflicts exist and it is not an overridden placement, a conflict message is returned and flagged to the user.
### B. Event Approval Pipeline
The approval flow routes events dynamically depending on the proposer:
```
[ Faculty Proposes Event ]
[ HOD Desk Review ]
(Status: REQUESTED)
/ \
Approve Reject ──► [ Status: HOD_REJECTED ]
/
[ Principal Desk Review ]
(Status: PENDING_PR)
/ \
Approve Reject ──► [ Status: PRINCIPAL_REJECTED ]
/
[ Status: APPROVED ]
(Venue Locked / Calendar Pinned)
```
* **Bypass Rule 1:** If the proposer is the **Principal**, the event is auto-approved (`APPROVED`).
* **Bypass Rule 2:** If the proposer is an **HOD**, or the event category is **Club**, **Placement**, or **Institutional**, the proposal bypasses HOD review and goes directly to the Principal (`PENDING_PR`).
### C. Semester Swap Calendar Shift
This tool allows admins/HODs/Principals to duplicate schedules for new semesters:
1. Select source filters (e.g., Department = CSE, Academic Year = 3rd Year).
2. The UI fetches matching events, maps them to target dates (shifted by a user-specified offset, e.g., 180 days), and simulates their placement.
3. The UI checks each shifted event against the database for venue conflicts or weekends:
* If an event lands on a Saturday or Sunday, the UI suggests shifting it to the nearest Friday or Monday.
* If a venue conflict occurs, the UI flags it and suggests venue changes.
4. Clicking "Commit" submits a bulk payload to `POST /api/events/batch-create`.
---
## 7. Developer & Local Deployment Guide
### Prerequisites
* Node.js (v18+)
### 1. Launch Frontend Client
1. Navigate to the `frontend` folder.
2. Install dependencies:
```bash
npm install
```
3. Run the development server:
```bash
npm run dev
```
4. The frontend runs at `http://localhost:5173`.
### 2. Firebase Configurations
* The Firebase client config is hardcoded in `frontend/src/lib/firebaseBackend.ts` to connect directly to the Firestore backend.
* On first load, if the database collections (`ems_users` or `ems_classes`) are empty, `firebaseBackend.ts` automatically seeds default institutional accounts and mock classes.
### 3. Seed User Accounts & Logins
On startup, the system seeds accounts with their default passwords:
* **Faculty Member:** `faculty@rit.edu` / `faculty123`
* **HOD:** `hod@rit.edu` / `hod123`
* **Principal:** `principal@rit.edu` / `principal123`
* **Placement Officer:** `placement@rit.edu` / `placement123`
* **Administrator:** `admin@rit.edu` / `admin123`

View File

@@ -1,299 +0,0 @@
# RIT Event Management System (EMS) - Project Overview & Site Map
This document provides a comprehensive technical overview of the RIT Event Management System (EMS). It details the core goals, features, technology stack, database architecture, directory layout, and a functional site map.
This file is structured specifically to allow developers and agentic AI systems to decode and understand the system perfectly.
---
## 1. Project Goal & Context
The **RIT Event Management System (EMS)** is a unified platform developed for Rajalakshmi Institute of Technology (RIT) to manage, propose, audit, approve, and automate institutional events. Events range from academic workshops and guest lectures to placement drives, cultural events, and sports activities.
The system addresses critical administrative bottlenecks such as:
1. **Venue Conflicts:** Preventing overlapping bookings for key venues (e.g., auditoriums, seminar halls, computer labs).
2. **Multi-tier Approvals:** Routing proposals through HODs and the Principal based on roles, departments, and event categories.
3. **Semester Scheduling:** Transitioning calendar plans from one semester to the next with minimal manual effort.
4. **Data Aggregation:** Importing bulk event plans directly from external sources (such as Google Form spreadsheet responses).
---
## 2. Technology Stack
### Backend
* **Core Backend Service:** Firebase (Firestore Database, Firebase Authentication)
* **API Architecture:** Serverless client-side interceptor via `firebaseBackend.ts` which intercepts `/api/*` requests in the browser and processes them directly against Firestore collections.
* **Database:** Cloud Firestore (Collections: `ems_users`, `ems_events`, `ems_notes`, `ems_classes`)
* **Security:** Checked via frontend role verification and Firestore Security Rules.
### Frontend
* **Core Framework:** React 19, TypeScript, Vite
* **Routing & State:** Component/Context state driving dynamic view rendering in a single-page application (SPA) shell
* **Styling:** Tailwind CSS, custom Vanilla CSS tokens, modern glassmorphism aesthetic
* **Animations:** Framer Motion (for smooth layout and viewport transitions)
* **Icons:** Lucide React
* **Utility Libraries:**
* `date-fns` for robust date comparisons and timeline layouts
* `xlsx` for parsing spreadsheet files (.xlsx/.csv) in-browser
---
## 3. Database Schema & Architecture
The system contains four principal Firestore collections.
### A. User Entity (`users`)
Holds institutional account profiles, login credentials, permissions, and roles.
* `id` (Long, PK): Auto-incremented identifier.
* `email` (String, Unique): Institutional email (e.g., `@rit.edu` or `@ritindia.edu`).
* `password` (String): BCrypt hashed passcode.
* `fullName` (String): User's display name.
* `role` (String): Access levels: `ADMIN`, `PRINCIPAL`, `HOD`, `FACULTY`, `PLACEMENT`.
* `department` (String): E.g., `CSE`, `AI&ML`, `ECE`, `H&S Dept`, etc.
* `isClubCoordinator` (Boolean): Grants authorization to manage student club events.
* `isPlacementStaff` (Boolean): Grants authority to propose overriding placement activities.
* `isClassIncharge` (Boolean): Grants class representative dashboard view.
* `classStrength` (Integer): Total student count for incharge class.
* `inchargeClass` / `inchargeBatch` / `inchargeSection` (String): Targeted class details.
* `assignedClubs` (ElementCollection): List of student clubs the user is permitted to coordinate.
### B. Event Entity (`events`)
Models event proposals, scheduling, requirements, and life-cycles.
* `id` (Long, PK)
* `title` (String, Nullable=false)
* `description` (Text)
* `startDate` / `endDate` (LocalDateTime)
* `location` (String): Maps to standard venues (e.g., `GB 4th floor auditorium`, `Wozniak Auditorium`).
* `category` (String): Categories dictate processing logic: `ACADEMIC`, `CLUB`, `PLACEMENT`, `SPORTS`, `INSTITUTIONAL`.
* `type` (String): E.g., `Workshop`, `Seminar`, `Guest Lecture`, `Competition`.
* `institution` (String): `RIT` or `RSB`.
* `department` (String)
* `academicYears` (ElementCollection): List of targeted academic batches (e.g., `1st Year`, `2nd Year`).
* `targetedSections` (ElementCollection): Targeted sections (e.g., `A`, `B`).
* `status` (String): `REQUESTED` (Pending HoD review), `PENDING_PR` (Pending Principal approval), `APPROVED` (Active scheduled event), `COMPLETED` (Past event), `CANCELLED`, `HOD_REJECTED`, `PRINCIPAL_REJECTED`.
* `guestName` / `guestSocialProfile` (String)
* `requirements` (ElementCollection): Mandatory resources (e.g., `Mementos`, `Projector`, `Lunch`, `Wi-Fi`).
* `budget` (Double)
* `hasRegistrationFee` (Boolean) / `registrationFee` (Double)
* `sponsors` (ElementCollection)
* `proposer` (ManyToOne -> `User`)
* `rejectionReason` (Text)
* `groupRequestId` (String): Identifier tying grouped recurring requests.
### C. ClassMapping Entity (`classes`)
Used for maintaining class information and batch promotions.
* `id` (Long, PK)
* `institution` (String): E.g., `RIT`, `RSB`.
* `department` (String)
* `academicYear` (String): E.g., `1st Year`, `2nd Year`, etc.
* `sections` (ElementCollection): List of section names associated with this mapping.
* `status` (String): Defaults to `Ready`.
### D. InstitutionalNote Entity (`institutional_notes`)
Used for pinning warnings, reminders, or notifications on the main calendar.
* `id` (Long, PK)
* `content` (String, Nullable=false)
* `targetDate` (LocalDate)
* `authorName` / `authorEmail` (String)
* `createdAt` (LocalDateTime)
---
## 4. Key Workflows & Backend API Endpoints
### A. Authentication
1. **Login Flow:** User submits credentials to `POST /api/auth/login`.
2. **Matching:** The system performs database lookup and BCrypt check.
3. **Response:** A JSON representation of the logged-in User is returned (stored in `localStorage` as `ems_user`).
### B. Event Proposal & Approval Chain
1. **Proposal submission:** Submits to `POST /api/events/propose`.
2. **Real-time Conflict Audit:** The backend executes `findConflictingEvents()` matching `location`, `institution`, and date overlaps against `APPROVED` events.
* **Placement Overriding:** If the new proposal is a `PLACEMENT` event and `cancelConflicting` is `true`, the system sets conflicting events' status to `CANCELLED` and writes a displacement reason.
* **General Conflict:** If a conflict exists and it isn't an overridden placement, a `409 Conflict` status is returned with overlap logs.
3. **Workflow Routing:**
* Proposer is **Principal** -> Auto-approved (`APPROVED`).
* Proposer is **HoD**, or event is **Club/Placement/Institutional** -> Sent straight to Principal (`PENDING_PR`).
* Proposer is **Faculty** -> Sent to HoD (`REQUESTED`).
4. **HoD Action (`POST /api/events/{id}/approve`):** Sets status to `PENDING_PR` (Principal Desk).
5. **Principal Action (`POST /api/events/{id}/approve`):** Sets status to `APPROVED`.
6. **Rejections (`POST /api/events/{id}/reject`):** Updates status to `HOD_REJECTED` or `PRINCIPAL_REJECTED` and embeds a mandatory `reason`.
### C. Class Promotions
* `POST /api/classes/promote?institution=RIT`
* Promotes all class mappings for the specified institution:
* `1st Year` -> `2nd Year`
* `2nd Year` -> `3rd Year`
* `3rd Year` -> `4th Year`
* `4th Year` mappings are deleted (graduated).
---
## 5. Directory Layout & Architecture
### Frontend Layout
```text
frontend/src/
├── App.tsx # Main Router shell
├── main.tsx # React Entry mount
├── App.css # Framework and styling additions
├── index.css # Core Tailwind and glassmorphism styling definitions
├── assets/ # Logos, background images
├── context/
│ └── AuthContext.tsx # Session controller (localStorage wrapper)
├── lib/
│ ├── config.ts # API Base URL config (http://localhost:8081)
│ └── utils.ts # Class-name merging helper (cn)
├── pages/
│ └── LoginPage.tsx # Institutional Portal interface
└── components/
├── DashboardLayout.tsx # Frame containing sidebar, header, and content
├── Sidebar.tsx # Interactive sidebar navigation (role filtered)
└── dashboard/ # Workspace Tab Components:
├── Overview.tsx # Dashboard welcome, stats, and calendars
├── AllEvents.tsx # Grid view and edit trigger for all system events
├── EventProposalForm.x # Configurator for single and recurring proposals
├── ApprovalsView.tsx # HoD/Principal checklist for pending items
├── InstitutionalChecklist.tsx # Audit checklist of requirement fulfillments
├── InstitutionalCalendar.tsx # Dynamic React Calendar showing notes/events
├── VenueTimeline.tsx # Live visual scheduling chart of venue occupancies
├── VenuesAtGlance.tsx # Today's room availability checker
├── FacultyEventManagement.tsx # Faculty personal dashboard view
├── PrincipalMasterChecklist.x # Checklist of all active requirements
├── ClubInstitutionalChecklist.x # Special checklist for Student Clubs
├── AutomationView.tsx # Semester Swap automation dashboard
├── ExcelImport.tsx # Bulk spreadsheet spreadsheet import tool
├── UserManagement.tsx # Admin credentials manager
├── ClassManagement.tsx # Admin class config manager
├── EventDetailsModal.tsx # Detailed view overlays
└── Pagination.tsx # Table list page controllers
```
---
## 6. Functional Site Map (Page Router Decodes)
The following map defines frontend routes, navigation identifiers, role restrictions, and the underlying controllers/endpoints.
```mermaid
graph TD
Login[LoginPage.tsx] -->|Auth Success| Dashboard[App.tsx Dashboard Shell]
subgraph Dashboard Views
OverviewView[Overview.tsx <br> Dashboard Option]
ProposalView[EventProposalForm.tsx <br> Propose Option]
ApprovalsView[ApprovalsView.tsx <br> Approvals Option]
AllEventsView[AllEvents.tsx <br> All Events Option]
ChecklistView[InstitutionalChecklist.tsx <br> Checklist Option]
HistoryView[EventHistory.tsx <br> History Option]
ClubsView[ClubInstitutionalChecklist.tsx <br> Clubs Option]
AutomationView[AutomationView.tsx <br> Automation Option]
ExcelImportView[ExcelImport.tsx <br> Excel Import Option]
ClassesView[ClassManagement.tsx <br> Classes Option]
UsersView[UserManagement.tsx <br> Manage Users Option]
end
classDef roleAll fill:#e0f7fa,stroke:#00acc1,stroke-width:1px;
classDef roleStaff fill:#e8eaf6,stroke:#3f51b5,stroke-width:1px;
classDef roleApprover fill:#fff3e0,stroke:#ff9800,stroke-width:1px;
classDef roleAdmin fill:#fbe9e7,stroke:#ff5722,stroke-width:1px;
class OverviewView,ProposalView,AllEventsView,ChecklistView,HistoryView roleAll;
class ClubsView roleStaff;
class ApprovalsView,AutomationView roleApprover;
class ClassesView,UsersView,ExcelImportView roleAdmin;
```
### Route-by-Route Specification
#### 1. Login Gate
* **Component:** `LoginPage.tsx`
* **Route Logic:** Rendered if `isAuthenticated` is `false`.
* **Access:** Public.
* **Backend Interaction:** `POST /api/auth/login`
#### 2. Overview Dashboard (`dashboard`)
* **Component:** `Overview.tsx`
* **Access:** All Authenticated Users.
* **Contextual Features:**
* **Principal Desk:** Shows `PrincipalMasterChecklist` (auditing table).
* **Class Incharge Desk:** Shows current class details and a form to submit class strength updates via `PUT /api/admin/users/{id}`.
* **Faculty Desk:** Shows `FacultyEventManagement` (table of personal proposed events, status, edit buttons).
* **Live Analytics:** Renders circular progress counters matching categories (Academic, Cultural, Placement, Sports, Other).
* **Embedded Sub-Components:**
* `InstitutionalCalendar.tsx` (Grid of events, notes fetching from `GET /api/notes`, notes creation via `POST /api/notes`).
* `VenuesAtGlance.tsx` (Highlights venue reservation blocks).
* `VenueTimeline.tsx` (Hour-by-hour calendar lines for room allocations).
* **Backend Interaction:** `GET /api/events`, `GET /api/notes`, `POST /api/notes`
#### 3. Propose Event (`propose`)
* **Component:** `EventProposalForm.tsx`
* **Access:** `FACULTY`, `HOD`, `PRINCIPAL`, `PLACEMENT`, `ADMIN`.
* **Features:** Form requesting Title, Start/End DateTime, Location, Category (Academic, Club, Placement, Sports), Type (Workshop, Seminar, etc.), targeted Academic Years & Sections, Guest details, budget details, sponsors, and checklist requirements.
* **Modes:** Handles new proposals, recurring group proposals, and edit modes (loading existing values into form inputs).
* **Backend Interaction:** `POST /api/events/propose`, `PUT /api/events/{id}`
#### 4. Approvals (`approvals`)
* **Component:** `ApprovalsView.tsx`
* **Access:** `HOD`, `PRINCIPAL`.
* **Features:** Lists all events where status is:
* `REQUESTED` (If role is `HOD`)
* `PENDING_PR` (If role is `PRINCIPAL`)
* **Actions:** Approve triggers `POST /api/events/{id}/approve?userId={id}`. Reject opens a modal requesting description and triggers `POST /api/events/{id}/reject?userId={id}`.
* **Backend Interaction:** `GET /api/events`, `POST /api/events/{id}/approve`, `POST /api/events/{id}/reject`
#### 5. Clubs (`clubs`)
* **Component:** `ClubInstitutionalChecklist.tsx`
* **Access:** Users with `isClubCoordinator = true` or `role = ADMIN`.
* **Features:** Lists current club activity proposals and maps checklists for student societies.
* **Backend Interaction:** `GET /api/events`
#### 6. All Events (`events`)
* **Component:** `AllEvents.tsx`
* **Access:** All Authenticated Users.
* **Features:** Tabulated search dashboard filtering all system events (Upcoming vs. Finished). Allows editing events (via Propose Event view) if the user is the proposer or an administrator.
* **Backend Interaction:** `GET /api/events`
#### 7. Institutional Checklist (`checklist`)
* **Component:** `InstitutionalChecklist.tsx`
* **Access:** All Authenticated Users.
* **Features:** Generates a checklist report detailing logistics (Catering, venue prep, Wi-Fi configuration, etc.) for upcoming events.
* **Backend Interaction:** `GET /api/events`
#### 8. Event History (`history`)
* **Component:** `EventHistory.tsx`
* **Access:** All Authenticated Users.
* **Features:** Searchable catalog of past completed events with analytics summaries and export tools.
* **Backend Interaction:** `GET /api/events`
#### 9. Automation Console (`automation`)
* **Component:** `AutomationView.tsx`
* **Access:** `HOD`, `PRINCIPAL`, `ADMIN`.
* **Features:**
* **Semester Swap:** Selects source Institution/Dept/Batch (e.g. RIT CSE 3rd Year) and target Institution/Dept/Batch. Simulates calendar shifts (+/- 6 months).
* **Conflict Analysis:** Performs live in-memory evaluation for weekend events (recommends quick-shifting to Friday or Monday) and database overlaps.
* **Review Gate:** Displays simulated changes in a modal allowing the operator to Commit, Reschedule, or Skip. Committing makes a batch API request.
* **Backend Interaction:** `GET /api/events`, `POST /api/events/batch-create`
#### 10. Excel Import (`import-excel`)
* **Component:** `ExcelImport.tsx`
* **Access:** `ADMIN` (and accessible from Dashboard views).
* **Features:**
* **Fuzzy Parsing:** Converts excel columns (Form Responses) into structured Event payloads.
* **Auto-Registration:** Identifies if the listed proposer does not exist in the user database. If missing, registers a new account (`password` defaults to `Password@123`) using the `POST /api/admin/users` service.
* **Conflict Solver:** Identifies venue/timing conflicts. Suggests moving to alternative venues or shifting times.
* **Backend Interaction:** `GET /api/events`, `GET /api/admin/users`, `POST /api/admin/users`, `POST /api/events/batch-create`
#### 11. Class Configurations (`classes`)
* **Component:** `ClassManagement.tsx`
* **Access:** `ADMIN`.
* **Features:**
* Configures class metadata (Institution, Department, Academic Year, Sections).
* **Yearly Promotion:** Promotes student batches via a quick action (e.g. promoting 1st Years to 2nd Years, deleting graduating 4th Years).
* **Backend Interaction:** `GET /api/classes`, `POST /api/classes`, `DELETE /api/classes/{id}`, `POST /api/classes/promote`
#### 12. User Management (`user-management`)
* **Component:** `UserManagement.tsx`
* **Access:** `ADMIN`.
* **Features:** Add new users, edit existing user profile fields (including modifying student club assignments and Class Incharge details), delete accounts, or change passcodes.
* **Backend Interaction:** `GET /api/admin/users`, `POST /api/admin/users`, `PUT /api/admin/users/{id}`, `DELETE /api/admin/users/{id}`

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,73 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -1,15 +0,0 @@
Timestamp,Date of the event (start date),Date of the event (End date),starting Time,ending Time,Venue,Department,Requirements [Row 1],ending Time.1,Name of the event,Name of the faculty coordinator,Name of the Guest,Email Address,"Any Other Requirements, Please mention",Name of the faculty coordinator.1,Date of the event (End date).1,Status
2024-03-08 14:26:41.487,3/22/0024,,15:00:00,15:00:00,Wozniak Auditorium,RSB,"Memento, Projector, Mike, Refreshments",15:00:00,Biz-analytix guest lecture,,Mr Gokul Palani,badri.toppur@gmail.com,"pointer. cab for pickup and drop-off. Lunch for seven club members, and boquet.",,,
2024-07-05 12:03:24.523,1983-12-07 00:00:00,,15:00:00,15:00:00,Wozniak Auditorium,RIT Placement,"Memento, Projector, Mike, White Board, Refreshments, Lunch, Breakfast, Guest Transport (Transport Form to be submitted)",15:00:00,JP,,JP GANTHI,training@ritchennai.edu.in,,Pandithurai ,,
2024-01-23 00:00:00.000,2024-01-23 00:00:00,,11:00:00,11:00:00,Wozniak Auditorium,RSB,"2. Audio/Video, PA system, Uninterrupted power supply
3.LED Screen
4. Flower decoration
5. Computer lab with 65 systems with webcam and wifi connection.
6.Board Room for two days (stationery required)
7. Guest hospitality(Lunch & Refreshments for 8 people - Tea,Coffee, Juice, Cookies, Special Lunch)
8. Bouquets - 5
9. Transportation for Guest - (would intimate numbers once confirmed)",11:00:00,Mindsprint Campus Placement,,,,,,,
,2024-01-23 00:00:00,,15:00:00,15:00:00,Wozniak Auditorium,RSB,"1. Wozniak auditorium
2. Lunch - 4 (2 guests, Director and Faculty)
3. Memento - 2 guests",15:00:00,"guest lecture from CII, Logistics Center of Excellence, IIT Madras",,,,,,,
2024-01-22 12:33:45.130,2024-01-23 00:00:00,,15:00:00,15:00:00,Steve Jobs 1st Floor,RIT Mech,"Projector, White Board",15:00:00,Awareness session on Start - up and chatGpt,,"Internal faculties (Dr. V.M. Gopinath, Dr.M.Shaju and Dr.M.Dinesh Babu)",pragadish.n@ritchennai.edu.in,,,,
1 Timestamp Date of the event (start date) Date of the event (End date) starting Time ending Time Venue Department Requirements [Row 1] ending Time.1 Name of the event Name of the faculty coordinator Name of the Guest Email Address Any Other Requirements, Please mention Name of the faculty coordinator.1 Date of the event (End date).1 Status
2 2024-03-08 14:26:41.487 3/22/0024 15:00:00 15:00:00 Wozniak Auditorium RSB Memento, Projector, Mike, Refreshments 15:00:00 Biz-analytix guest lecture Mr Gokul Palani badri.toppur@gmail.com pointer. cab for pickup and drop-off. Lunch for seven club members, and boquet.
3 2024-07-05 12:03:24.523 1983-12-07 00:00:00 15:00:00 15:00:00 Wozniak Auditorium RIT Placement Memento, Projector, Mike, White Board, Refreshments, Lunch, Breakfast, Guest Transport (Transport Form to be submitted) 15:00:00 JP JP GANTHI training@ritchennai.edu.in Pandithurai
4 2024-01-23 00:00:00.000 2024-01-23 00:00:00 11:00:00 11:00:00 Wozniak Auditorium RSB 2. Audio/Video, PA system, Uninterrupted power supply 3.LED Screen 4. Flower decoration 5. Computer lab with 65 systems with webcam and wifi connection. 6.Board Room for two days (stationery required) 7. Guest hospitality(Lunch & Refreshments for 8 people - Tea,Coffee, Juice, Cookies, Special Lunch) 8. Bouquets - 5 9. Transportation for Guest - (would intimate numbers once confirmed) 11:00:00 Mindsprint Campus Placement
5 2024-01-23 00:00:00 15:00:00 15:00:00 Wozniak Auditorium RSB 1. Wozniak auditorium 2. Lunch - 4 (2 guests, Director and Faculty) 3. Memento - 2 guests 15:00:00 guest lecture from CII, Logistics Center of Excellence, IIT Madras
6 2024-01-22 12:33:45.130 2024-01-23 00:00:00 15:00:00 15:00:00 Steve Jobs 1st Floor RIT Mech Projector, White Board 15:00:00 Awareness session on Start - up and chatGpt Internal faculties (Dr. V.M. Gopinath, Dr.M.Shaju and Dr.M.Dinesh Babu) pragadish.n@ritchennai.edu.in

View File

@@ -1,36 +0,0 @@
import os
import re
api_import = "import { API_BASE_URL } from '../../lib/config';"
# Need to adjust depth based on file location
# But I can just use a relative path that works for most components
def fix_file(path):
with open(path, 'r') as f:
content = f.read()
# Calculate depth for import
rel_path = os.path.relpath('src/lib/config.ts', os.path.dirname(path))
rel_path = rel_path.replace('.ts', '').replace('\\', '/')
if not rel_path.startswith('.'):
rel_path = './' + rel_path
import_line = f"import {{ API_BASE_URL }} from '{rel_path}';"
# Replace the messy strings
new_content = content.replace("'http://' + window.location.hostname + ':8080", "API_BASE_URL")
new_content = new_content.replace("`http://' + window.location.hostname + ':8080", "`${API_BASE_URL}")
if new_content != content:
# Add import if not present
if 'API_BASE_URL' in new_content and 'import { API_BASE_URL }' not in new_content:
new_content = import_line + "\n" + new_content
with open(path, 'w') as f:
f.write(new_content)
print(f"Fixed {path}")
for root, dirs, files in os.walk('src'):
for file in files:
if file.endswith(('.ts', '.tsx')):
fix_file(os.path.join(root, file))

View File

@@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-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>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@types/uuid": "^10.0.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"firebase": "^12.15.0",
"framer-motion": "^12.38.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",
"uuid": "^14.0.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/postcss": "^4.2.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"postcss": "^8.5.10",
"tailwindcss": "^4.2.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.9"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 KiB

View File

@@ -1,187 +0,0 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
&>div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 25px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -1,203 +0,0 @@
import { API_BASE_URL } from './lib/config';
import React, { useState } from 'react';
import { DashboardLayout } from './components/DashboardLayout';
import { InstitutionalChecklist } from './components/dashboard/InstitutionalChecklist';
import { EventProposalForm } from './components/dashboard/EventProposalForm';
import { AllEvents } from './components/dashboard/AllEvents';
import { ApprovalsView } from './components/dashboard/ApprovalsView';
import { UserManagement } from './components/dashboard/UserManagement';
import { ClassManagement } from './components/dashboard/ClassManagement';
import { LoginPage } from './pages/LoginPage';
import { AuthProvider, useAuth } from './context/AuthContext';
import { DialogProvider } from './context/DialogContext';
import { motion, AnimatePresence } from 'framer-motion';
import { StatusTimeline, type Event } from './components/dashboard/EventStatusTimeline';
import { useEffect } from 'react';
import { InstitutionalCalendar } from './components/dashboard/InstitutionalCalendar';
import { VenueTimeline } from './components/dashboard/VenueTimeline';
import { EventHistory } from './components/dashboard/EventHistory';
import { ClubInstitutionalChecklist } from './components/dashboard/ClubInstitutionalChecklist';
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';
import { ManageNoticesView } from './components/dashboard/ManageNoticesView';
const AppContent: React.FC = () => {
const { isAuthenticated, user } = useAuth();
const isClubAuthorized = user?.isClubCoordinator || user?.role === 'ADMIN';
const [activeItem, setActiveItem] = useState('dashboard');
const [userEvents, setUserEvents] = useState<Event[]>([]);
const [preFillData, setPreFillData] = useState<any>(null);
const handleIncompleteClick = (data: any) => {
setPreFillData(data);
setActiveItem('propose');
};
const handleCalendarPropose = (date: string) => {
setPreFillData({ startDate: `${date}T09:00` });
setActiveItem('propose');
};
const handleClubEventClick = (data: any) => {
setPreFillData(data);
setActiveItem('propose');
};
const handleEditEvent = (event: any) => {
setPreFillData({
...event,
eventName: event.title,
venue: event.location,
socialProfile: event.guestSocialProfile,
isEditMode: true
});
setActiveItem('propose');
};
const handleSidebarClick = (item: string) => {
if (item === 'propose') setPreFillData(null);
setActiveItem(item);
};
useEffect(() => {
const handleNav = (e: any) => setActiveItem(e.detail);
window.addEventListener('navigate', handleNav);
return () => window.removeEventListener('navigate', handleNav);
}, []);
useEffect(() => {
if (isAuthenticated && activeItem === 'dashboard') {
fetch(API_BASE_URL + '/api/events')
.then(res => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
})
.then(data => {
if (Array.isArray(data)) {
let filtered = data;
if (user?.role === 'HOD') {
const userDepts = user?.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
filtered = data.filter((e: Event) => userDepts.includes(e.department?.trim().toLowerCase()));
} else if (user?.role === 'FACULTY') {
filtered = data.filter((e: Event) => e.proposer?.email === user?.email);
}
setUserEvents(filtered);
}
})
.catch(err => console.error("Failed to fetch dashboard events:", err));
}
}, [isAuthenticated, activeItem, user?.email, user?.role, user?.department]);
if (!isAuthenticated) {
return (
<AnimatePresence mode="wait">
<motion.div
key="login"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<LoginPage />
</motion.div>
</AnimatePresence>
);
}
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
userEvents={userEvents}
onNavigate={handleSidebarClick}
onIncompleteClick={handleIncompleteClick}
onCalendarPropose={handleCalendarPropose}
onClubEventClick={handleClubEventClick}
onEditEvent={handleEditEvent}
/>
);
switch (activeItem) {
case 'user-management':
return user?.role === 'ADMIN' ? <UserManagement /> : isDashboard;
case 'approvals':
return (user?.role === 'HOD' || user?.role === 'PRINCIPAL') ? <ApprovalsView /> : isDashboard;
case 'clubs':
return isClubAuthorized ? <ClubInstitutionalChecklist onEventClick={handleClubEventClick} /> : isDashboard;
case 'events':
return <AllEvents onEditEvent={handleEditEvent} />;
case 'propose':
return <EventProposalForm initialData={preFillData} />;
case 'checklist':
return <InstitutionalChecklist isGlobalView={true} onIncompleteClick={handleIncompleteClick} />;
case 'history':
return <EventHistory />;
case 'classes':
return user?.role === 'ADMIN' ? <ClassManagement /> : isDashboard;
case 'automation':
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 'manage-notices':
return user?.role === 'ADMIN' ? <ManageNoticesView /> : isDashboard;
case 'dashboard':
return isDashboard;
default:
return isDashboard;
}
};
return (
<DashboardLayout
activeItem={activeItem}
onItemClick={handleSidebarClick}
>
<AnimatePresence mode="wait">
<motion.div
key={activeItem}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{renderContent()}
</motion.div>
</AnimatePresence>
</DashboardLayout>
);
};
function App() {
return (
<DialogProvider>
<AuthProvider>
<AppContent />
</AuthProvider>
</DialogProvider>
);
}
export default App;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,214 +0,0 @@
import { Sidebar } from './Sidebar';
import { Search, Bell, Settings, LogOut, User as UserIcon, Plus, Calendar, ClipboardList, CheckCircle, Users, BookOpen, Zap, Clock, History as HistoryIcon, LayoutGrid, FileSpreadsheet } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { useState, useEffect, useRef } from 'react';
import { API_BASE_URL } from '../lib/config';
import { cn } from '../lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
interface DashboardLayoutProps {
children: React.ReactNode;
activeItem: string;
onItemClick: (id: string) => void;
}
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
children,
activeItem,
onItemClick
}) => {
const { user, logout } = useAuth();
const [searchQuery, setSearchQuery] = useState('');
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [events, setEvents] = useState<any[]>([]);
const searchRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setIsSearchOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
if (isSearchOpen && events.length === 0) {
fetch(API_BASE_URL + '/api/events')
.then(res => res.json())
.then(data => setEvents(Array.isArray(data) ? data : []))
.catch(err => console.error('Search fetch failed:', err));
}
}, [isSearchOpen]);
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutGrid, description: 'Main overview and stats' },
{ id: 'approvals', label: 'Approvals', icon: CheckCircle, description: 'Review pending requests', roles: ['HOD', 'PRINCIPAL', 'ADMIN'] },
{ id: 'events', label: 'All Events', icon: ClipboardList, description: 'Browse institution events' },
{ id: 'checklist', label: 'Checklist', icon: Calendar, description: 'Audit and compliance view' },
{ id: 'history', label: 'History', icon: HistoryIcon, description: 'Event ledger and audit trail' },
{ id: 'user-management', label: 'Manage Users', icon: Users, description: 'User roles and access', roles: ['ADMIN'] },
{ id: 'classes', label: 'Classes', icon: BookOpen, description: 'Academic structure', roles: ['ADMIN'] },
{ id: 'import-excel', label: 'Import Excel', icon: FileSpreadsheet, description: 'Bulk upload from response sheets', roles: ['ADMIN', 'PRINCIPAL'] },
{ id: 'automation', label: 'Automation', icon: Zap, description: 'Smart scheduling tools', roles: ['ADMIN', 'PRINCIPAL', 'HOD'] },
].filter(item => !item.roles || item.roles.includes(user?.role || ''));
const filteredNav = searchQuery.length > 1
? navItems.filter(item => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
: [];
const filteredEvents = searchQuery.length > 1
? events.filter(e =>
e.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
e.department?.toLowerCase().includes(searchQuery.toLowerCase())
).slice(0, 5)
: [];
const handleResultClick = (id: string, type: 'nav' | 'event') => {
if (type === 'nav') {
onItemClick(id);
} else {
// For events, we'll navigate to 'events' and we could potentially filter there
onItemClick('events');
}
setSearchQuery('');
setIsSearchOpen(false);
};
return (
<div className="min-h-screen bg-[#FDFDFE]">
<Sidebar
activeItem={activeItem}
onItemClick={onItemClick}
/>
<main className="ml-[18rem] min-h-screen transition-all duration-500 pr-8">
{/* Header */}
<header className="sticky top-0 z-40 bg-[#FDFDFE]/80 backdrop-blur-md py-6 flex items-center justify-between gap-8">
<div className="flex-1 max-w-xl relative group" ref={searchRef}>
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-slate-400 group-focus-within:text-brand-indigo transition-colors" />
<input
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setIsSearchOpen(true);
}}
onFocus={() => setIsSearchOpen(true)}
placeholder="Search events, pages, or tools..."
className="w-full bg-white border border-slate-200/60 rounded-[1.25rem] py-3 pl-12 pr-4 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-brand-indigo/10 focus:border-brand-indigo transition-all shadow-sm"
/>
<AnimatePresence>
{isSearchOpen && (filteredNav.length > 0 || filteredEvents.length > 0) && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute top-full left-0 right-0 mt-3 bg-white/95 backdrop-blur-xl border border-slate-100 rounded-[2rem] shadow-[0_20px_50px_rgba(0,0,0,0.1)] overflow-hidden z-50 p-2"
>
{filteredNav.length > 0 && (
<div className="mb-2">
<p className="px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-400">Navigation</p>
{filteredNav.map(item => (
<button
key={item.id}
onClick={() => handleResultClick(item.id, 'nav')}
className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 rounded-2xl transition-all text-left group"
>
<div className="w-10 h-10 rounded-xl bg-brand-glow flex items-center justify-center text-brand-indigo group-hover:bg-brand-indigo group-hover:text-white transition-all">
<item.icon className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-black text-brand-navy">{item.label}</p>
<p className="text-[10px] font-medium text-slate-400">{item.description}</p>
</div>
</button>
))}
</div>
)}
{filteredEvents.length > 0 && (
<div>
<p className="px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-400">Recent Events</p>
{filteredEvents.map(event => (
<button
key={event.id}
onClick={() => handleResultClick(event.id, 'event')}
className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 rounded-2xl transition-all text-left group"
>
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
event.status === 'APPROVED' ? "bg-emerald-50 text-emerald-600" :
event.status === 'PENDING' || event.status === 'REQUESTED' ? "bg-amber-50 text-amber-600" : "bg-slate-50 text-slate-400"
)}>
<ClipboardList className="w-5 h-5" />
</div>
<div className="flex-1">
<p className="text-sm font-black text-brand-navy truncate max-w-[250px]">{event.title}</p>
<div className="flex items-center gap-2">
<span className="text-[9px] font-bold uppercase tracking-wider text-slate-400">{event.department}</span>
<span className={cn(
"text-[8px] font-black uppercase px-1.5 py-0.5 rounded",
event.status === 'APPROVED' ? "bg-emerald-50 text-emerald-600" :
event.status === 'PENDING' || event.status === 'REQUESTED' ? "bg-amber-50 text-amber-600" : "bg-slate-100 text-slate-400"
)}>{event.status}</span>
</div>
</div>
<Clock className="w-3.5 h-3.5 text-slate-300" />
</button>
))}
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => onItemClick('propose')}
className="flex items-center gap-2 px-5 py-2.5 bg-brand-indigo text-white rounded-[1.25rem] text-sm font-black transition-all hover:shadow-lg hover:shadow-brand-indigo/20 hover:scale-[1.02]"
>
<Plus className="w-4 h-4" />
New Event
</button>
<div className="flex items-center gap-1 bg-white p-1 rounded-2xl border border-slate-100 shadow-sm">
<button className="p-2.5 rounded-xl hover:bg-slate-50 transition-all text-slate-500 relative">
<Bell className="w-5 h-5" />
<span className="absolute top-2.5 right-2.5 w-2 h-2 bg-red-500 rounded-full border-2 border-white" />
</button>
</div>
<div className="flex items-center gap-3 bg-white pl-1 pr-4 py-1 rounded-[1.5rem] shadow-sm border border-slate-100">
<div className="w-10 h-10 rounded-full bg-brand-glow flex items-center justify-center overflow-hidden border border-brand-indigo/10">
<UserIcon className="w-5 h-5 text-brand-indigo" />
</div>
<div className="flex flex-col">
<span className="text-xs font-black text-brand-navy leading-none mb-0.5">
{user?.fullName}
</span>
<span className="text-[10px] font-bold text-brand-indigo/60 uppercase tracking-wider">
{user?.role}
</span>
</div>
</div>
<button
onClick={logout}
className="p-3 rounded-2xl bg-white border border-slate-100 text-slate-400 hover:text-red-500 hover:border-red-100 hover:bg-red-50 transition-all shadow-sm"
title="Sign Out"
>
<LogOut className="w-5 h-5" />
</button>
</div>
</header>
<div className="pb-10">
{children}
</div>
</main>
</div>
);
};

View File

@@ -1,107 +0,0 @@
import React, { useState } from 'react';
import {
LayoutGrid,
ClipboardList,
CheckSquare,
History as HistoryIcon,
Zap,
BookOpen,
PlusCircle,
ChevronRight,
ShieldCheck,
Users,
Calendar,
HelpCircle,
ExternalLink,
CheckCircle,
Bell
} from 'lucide-react';
import { cn } from '../lib/utils';
import { motion } from 'framer-motion';
import ritLogo from '../assets/images/college-logo.png';
import { useAuth } from '../context/AuthContext';
interface SidebarProps {
activeItem: string;
onItemClick: (id: string) => void;
}
export const Sidebar: React.FC<SidebarProps> = ({ activeItem, onItemClick }) => {
const { user } = useAuth();
const isClubAuthorized = user?.isClubCoordinator || user?.role === 'ADMIN';
const menuItems = [
{ icon: LayoutGrid, label: 'Dashboard', id: 'dashboard' },
...((user?.role === 'HOD' || user?.role === 'PRINCIPAL') ? [{ icon: CheckCircle, label: 'Approvals', id: 'approvals' }] : []),
...(isClubAuthorized ? [{ icon: ShieldCheck, label: 'Clubs', id: 'clubs' }] : []),
{ icon: ClipboardList, label: 'All Events', id: 'events' },
{ icon: CheckSquare, label: 'Checklist', id: 'checklist' },
{ 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: Bell, label: 'Manage Notices', id: 'manage-notices' }] : []),
...((user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? [{ icon: Users, label: 'Student Hub', id: 'student-registrations' }] : [])
];
return (
<aside className="floating-sidebar">
{/* Logo */}
<div className="flex items-center gap-3 mb-8 px-2">
<img src={ritLogo} alt="College Logo" className="h-14 w-auto object-contain" />
</div>
{/* Navigation */}
<nav className="flex-1 space-y-1.5 overflow-y-auto custom-scrollbar pr-1">
<p className="px-4 text-[10px] font-black uppercase tracking-widest text-slate-400 mb-4">Main Menu</p>
{menuItems.map((item) => (
<motion.div
key={item.id}
whileHover={{ x: 4 }}
whileTap={{ scale: 0.98 }}
onClick={() => onItemClick(item.id)}
className={cn(
"sidebar-item group relative",
activeItem === item.id && "sidebar-item-active"
)}
>
<div className={cn(
"p-2 rounded-xl transition-all",
activeItem === item.id ? "bg-white shadow-sm" : "bg-transparent group-hover:bg-white/50"
)}>
<item.icon className="w-4.5 h-4.5" strokeWidth={2.5} />
</div>
<span className="flex-1 text-sm">{item.label}</span>
{activeItem === item.id && (
<div className="w-1.5 h-1.5 rounded-full bg-brand-indigo" />
)}
</motion.div>
))}
</nav>
{/* Footer Build Info */}
<div className="mt-auto px-4 pt-6 border-t border-slate-50">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-1.5 px-3 py-1 bg-brand-glow text-brand-indigo rounded-full border border-brand-indigo/10 backdrop-blur-sm group/beta hover:bg-brand-indigo hover:text-white transition-all duration-300 cursor-default">
<div className="w-1 h-1 bg-brand-indigo rounded-full group-hover/beta:bg-white animate-pulse" />
<span className="text-[9px] font-black uppercase tracking-[0.2em]">Beta</span>
</div>
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-300">
v0.1.5
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-emerald-500/70">
Live Systems
</span>
</div>
</div>
</div>
</aside>
);
};

View File

@@ -1,234 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import {
Calendar,
MapPin,
Building2,
Clock,
ChevronRight,
Filter,
Search,
MoreVertical,
FileSpreadsheet
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { useAuth } from '../../context/AuthContext';
import { StatusTimeline, type Event } from './EventStatusTimeline';
import { Pagination } from './Pagination';
interface AllEventsProps {
onEditEvent?: (event: any) => void;
}
export const AllEvents: React.FC<AllEventsProps> = ({ onEditEvent }) => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchEvents();
}, []);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
};
const getStatusColor = (status: Event['status']) => {
switch (status) {
case 'REQUESTED': return 'bg-brand-glow text-brand-indigo border-brand-indigo/20';
case 'PENDING_PR': return 'bg-amber-50 text-amber-600 border-amber-100';
case 'APPROVED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
case 'COMPLETED': return 'bg-slate-100 text-slate-600 border-slate-200';
case 'CANCELLED': return 'bg-red-50 text-red-600 border-red-100';
default: return 'bg-slate-50 text-slate-500 border-slate-100';
}
};
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const filteredEvents = events
.filter(event =>
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
event.department.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => {
const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime();
const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime();
return dateB - dateA;
});
const totalPages = Math.ceil(filteredEvents.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const currentEvents = filteredEvents.slice(startIndex, startIndex + itemsPerPage);
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
return (
<div className="space-y-8">
{/* Header & Controls */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">All Events</h2>
<p className="text-text-muted font-medium">Overview of all institutional activities and their status.</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
placeholder="Filter events..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white border border-slate-200 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-brand-indigo/10 focus:border-brand-indigo transition-all premium-shadow-sm"
/>
</div>
{(user?.role === 'ADMIN' || user?.role === 'PRINCIPAL') && (
<button
onClick={() => window.dispatchEvent(new CustomEvent('navigate', { detail: 'import-excel' }))}
className="flex items-center gap-2 px-4 py-2 bg-brand-glow text-brand-indigo rounded-xl hover:bg-brand-indigo hover:text-white transition-all premium-shadow-sm border border-brand-indigo/10 text-sm font-black uppercase tracking-widest"
>
<FileSpreadsheet className="w-4 h-4" />
Import
</button>
)}
<button className="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-all premium-shadow-sm">
<Filter className="w-5 h-5 text-text-secondary" />
</button>
</div>
</div>
{/* Events List */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden flex flex-col">
{isLoading ? (
<div className="p-20 text-center text-text-muted italic">Loading events...</div>
) : filteredEvents.length === 0 ? (
<div className="p-20 text-center">
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<Calendar className="w-8 h-8 text-slate-300" />
</div>
<p className="text-text-muted font-medium italic">No events found matching your search.</p>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-50">
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Event Details</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Date & Venue</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Department</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Status</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{currentEvents.map((event, idx) => (
<motion.tr
key={event.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className="group hover:bg-slate-50/50 transition-colors"
>
<td className="px-8 py-6">
<div className="flex flex-col">
<span className="font-bold text-text-dark text-sm group-hover:text-brand-indigo transition-colors">{event.title}</span>
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted mt-1">{event.type} {event.institution}</span>
</div>
</td>
<td className="px-8 py-6">
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-xs font-semibold text-text-secondary">
<Clock className="w-3.5 h-3.5 text-brand-indigo opacity-60" />
{parseDate(event.startDate) ? parseDate(event.startDate)!.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }) : 'N/A'}
</div>
<div className="flex items-center gap-2 text-[10px] font-bold text-text-muted uppercase tracking-wider">
<MapPin className="w-3.5 h-3.5 text-brand-indigo opacity-60" />
{event.location}
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-brand-glow flex items-center justify-center text-[10px] font-black text-brand-indigo">
{event.department.substring(0, 2)}
</div>
<span className="text-xs font-bold text-text-secondary">{event.department}</span>
</div>
</td>
<td className="px-8 py-6">
{user?.role === 'FACULTY' && event.proposer?.email === user?.email ? (
<StatusTimeline event={event} compact />
) : (
<span className={cn(
"inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest border",
getStatusColor(event.status)
)}>
{event.status === 'PENDING_PR' ? 'PENDING PRINCIPAL' : event.status}
</span>
)}
</td>
<td className="px-8 py-6 text-right">
<div className="flex items-center justify-end gap-2">
{(user?.role === 'ADMIN' || (event.status === 'REQUESTED' && event.proposer?.email === user?.email)) && onEditEvent && (
<button
onClick={() => onEditEvent(event)}
className="px-4 py-2 bg-slate-50 text-brand-indigo hover:bg-brand-indigo hover:text-white rounded-xl text-[9px] font-black uppercase tracking-widest transition-all border border-brand-indigo/10"
>
Edit
</button>
)}
<button className="p-2 rounded-lg hover:bg-white hover:premium-shadow transition-all text-slate-300 hover:text-brand-indigo">
<MoreVertical className="w-5 h-5" />
</button>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredEvents.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</>
)}
</div>
</div>
);
};

View File

@@ -1,418 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
CheckCircle,
XCircle,
Clock,
User,
Building2,
ChevronRight,
AlertCircle,
ShieldCheck,
MapPin,
Wallet,
Users,
Layers,
Ticket,
AlertTriangle,
FileText,
Heart,
Calendar
} from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { cn } from '../../lib/utils';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
type: string;
institution: string;
department: string;
academicYears: string[];
status: string;
location: string;
proposer?: {
fullName: string;
department: string;
};
budget: number;
hasRegistrationFee: boolean;
registrationFee: number;
category: string;
targetedSections: string[];
conflictMessage?: string;
description?: string;
sponsors?: string[];
rejectionReason?: string;
}
export const ApprovalsView: React.FC = () => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [processingId, setProcessingId] = useState<number | null>(null);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
// Filter based on role
if (user?.role === 'HOD') {
const userDepts = user.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
setEvents(data.filter((e: Event) => e.status === 'REQUESTED' && userDepts.includes(e.department?.trim().toLowerCase())));
} else if (user?.role === 'PRINCIPAL') {
setEvents(data.filter((e: Event) => e.status === 'PENDING_PR'));
}
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
};
const handleAction = async (id: number, action: 'approve' | 'reject') => {
let reason = '';
if (action === 'reject') {
reason = window.prompt('Please enter a rejection reason:') || '';
if (!reason) return;
}
setProcessingId(id);
try {
const response = await fetch(`${API_BASE_URL}/api/events/${id}/${action}?userId=${user?.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: action === 'reject' ? JSON.stringify({ reason }) : undefined
});
if (response.ok) {
setEvents(events.filter(e => e.id !== id));
setSelectedEvent(null);
} else {
const err = await response.json();
alert(err.message || 'Operation failed');
}
} catch (error) {
console.error(`Failed to ${action} event:`, error);
} finally {
setProcessingId(null);
}
};
const sortedEvents = useMemo(() => {
return [...events].sort((a: any, b: any) => {
const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime();
const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime();
return dateB - dateA;
});
}, [events]);
const currentEvents = sortedEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">Pending Approvals</h2>
<p className="text-text-muted font-medium">Review and take action on event proposals from your {user?.role === 'HOD' ? 'department' : 'institution'}.</p>
</div>
<div className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-indigo"></div>
</div>
) : events.length === 0 ? (
<div className="bg-white rounded-[2.5rem] p-20 text-center border border-slate-100 premium-shadow">
<CheckCircle className="w-16 h-16 text-emerald-500 mx-auto mb-6 opacity-20" />
<h3 className="text-xl font-black text-text-dark mb-2">Queue is Empty</h3>
<p className="text-text-muted italic">All proposals have been processed. Great work!</p>
</div>
) : (
<AnimatePresence mode="popLayout">
{currentEvents.map((event) => (
<motion.div
key={event.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
onClick={() => setSelectedEvent(event)}
className={cn(
"bg-white rounded-[2rem] p-6 border transition-all flex flex-col md:flex-row items-center justify-between gap-6 cursor-pointer group",
event.conflictMessage ? "border-red-200 bg-red-50/30" : "border-slate-100 hover:premium-shadow"
)}
>
<div className="flex items-center gap-6 w-full md:w-auto">
<div className={cn(
"w-16 h-16 rounded-2xl flex items-center justify-center font-black text-xl shadow-lg",
event.conflictMessage ? "bg-red-500 text-white" : "bg-brand-glow text-brand-indigo"
)}>
{event.title[0]}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
{event.category === 'CLUB' && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded-md text-[8px] font-black uppercase tracking-widest border border-emerald-100">
Institutional Club
</span>
)}
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">
{event.type}
</span>
</div>
<h3 className="text-xl font-black text-text-dark leading-tight mb-2 group-hover:text-brand-indigo transition-colors">{event.title}</h3>
{event.conflictMessage ? (
<div className="flex items-center gap-2 text-red-500 text-[10px] font-black uppercase tracking-widest mt-1">
<AlertTriangle className="w-3.5 h-3.5" />
{event.conflictMessage}
</div>
) : (
<div className="flex items-center justify-center md:justify-start gap-4 text-xs font-bold text-text-muted">
<div className="flex items-center gap-1.5">
<User className="w-3.5 h-3.5" />
{event.proposer?.fullName || 'Faculty Member'}
</div>
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{new Date(event.startDate).toLocaleDateString()}
</div>
</div>
)}
</div>
</div>
<div className="flex items-center gap-3 w-full md:w-auto" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleAction(event.id, 'reject')}
disabled={processingId === event.id}
className="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 transition-all font-black text-[10px] uppercase tracking-widest border border-red-100/50"
>
<XCircle className="w-4 h-4" />
Reject
</button>
<button
onClick={() => handleAction(event.id, 'approve')}
disabled={processingId === event.id || !!event.conflictMessage}
className={cn(
"flex-1 md:flex-none flex items-center justify-center gap-2 px-8 py-3 rounded-xl transition-all font-black text-[10px] uppercase tracking-widest premium-shadow",
event.conflictMessage
? "bg-slate-200 text-slate-400 cursor-not-allowed"
: "bg-brand-navy text-white hover:scale-105"
)}
>
{processingId === event.id ? 'Processing...' : (
<>
<CheckCircle className="w-4 h-4" />
Approve
</>
)}
</button>
</div>
</motion.div>
))}
</AnimatePresence>
)}
</div>
{events.length > 0 && (
<Pagination
currentPage={currentPage}
totalItems={events.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 15, 20]}
/>
)}
{/* Detail Modal */}
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden max-h-[90vh] overflow-y-auto"
>
<div className={cn(
"p-8 text-white flex justify-between items-start",
selectedEvent.conflictMessage ? "bg-red-600" : "bg-brand-navy"
)}>
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">Proposal Details</span>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">{selectedEvent.institution}</span>
{selectedEvent.category === 'CLUB' && (
<>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-emerald-400">Institutional Club Event</span>
</>
)}
</div>
<h3 className="text-2xl font-black tracking-tight">{selectedEvent.title}</h3>
</div>
<button
onClick={() => setSelectedEvent(null)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-8">
{/* Description Section */}
{selectedEvent.description && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<FileText className="w-3.5 h-3.5" />
Event Description
</div>
<p className="text-sm font-medium text-text-dark leading-relaxed bg-slate-50 p-4 rounded-2xl border border-slate-100">
{selectedEvent.description}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Calendar className="w-3.5 h-3.5" />
Schedule
</div>
<p className="text-sm font-bold text-text-dark">
{new Date(selectedEvent.startDate).toLocaleString()}
</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<MapPin className="w-3.5 h-3.5" />
Venue
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.location}</p>
</div>
</div>
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<User className="w-3.5 h-3.5" />
Proposed By
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.proposer?.fullName || 'Faculty'}</p>
<p className="text-[10px] font-black text-brand-indigo uppercase">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Users className="w-3.5 h-3.5" />
Target Audience
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears.join(', ')} Batches</p>
</div>
</div>
</div>
{/* Sponsors Section */}
{selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Heart className="w-3.5 h-3.5 text-brand-indigo" />
Event Sponsors
</div>
<div className="flex flex-wrap gap-2">
{selectedEvent.sponsors.map((sponsor, idx) => (
<span key={idx} className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-lg text-[10px] font-black uppercase tracking-widest border border-brand-indigo/10">
{sponsor}
</span>
))}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-slate-50">
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Wallet className="w-3.5 h-3.5" />
Estimated Budget
</div>
<p className="text-lg font-black text-text-dark">{selectedEvent.budget?.toLocaleString() || '0'}</p>
</div>
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Ticket className="w-3.5 h-3.5" />
Registration
</div>
<p className="text-lg font-black text-text-dark">
{selectedEvent.hasRegistrationFee ? `${selectedEvent.registrationFee}` : 'FREE'}
</p>
</div>
</div>
{selectedEvent.conflictMessage && (
<div className="p-4 bg-red-50 border border-red-100 rounded-2xl flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<div>
<p className="text-[10px] font-black uppercase text-red-500">Critical Conflict Detected</p>
<p className="text-xs font-bold text-red-700">{selectedEvent.conflictMessage}</p>
</div>
</div>
)}
<div className="flex items-center gap-4 pt-4">
<button
onClick={() => handleAction(selectedEvent.id, 'reject')}
disabled={processingId === selectedEvent.id}
className="flex-1 bg-red-50 text-red-600 py-4 rounded-2xl font-black text-[11px] uppercase tracking-widest hover:bg-red-100 transition-all"
>
Reject Proposal
</button>
<button
onClick={() => handleAction(selectedEvent.id, 'approve')}
disabled={processingId === selectedEvent.id || !!selectedEvent.conflictMessage}
className={cn(
"flex-1 py-4 rounded-2xl font-black text-[11px] uppercase tracking-widest transition-all premium-shadow",
selectedEvent.conflictMessage ? "bg-slate-200 text-slate-400" : "bg-brand-navy text-white hover:scale-[1.02]"
)}
>
{processingId === selectedEvent.id ? 'Processing...' : 'Approve Event'}
</button>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -1,787 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import {
Zap,
ArrowRightLeft,
ChevronLeft,
ChevronRight,
ShieldAlert,
Play,
Building2,
Users,
Layers,
Calendar,
ChevronDown,
Loader2,
CheckCircle2,
X,
AlertTriangle,
Clock,
ChevronUp
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import {
format,
addMonths,
subMonths,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
isSameMonth,
isSameDay
} from 'date-fns';
const DEPARTMENTS = [
"AI&DS", "AI&ML", "CSE", "CCE", "CSBS", "ECE",
"MECH", "EE(VLSI)", "BIOTECH", "Placement Department",
"H&S Dept", "Club", "Centre"
];
const BATCHES = ['1st Year', '2nd Year', '3rd Year', '4th Year'];
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
department: string;
academicYears: string[];
institution?: string;
type?: string;
category?: string;
location?: string;
proposer?: any;
}
const MiniCalendar: React.FC<{
title: string;
subtitle: string;
date: Date;
onDateChange: (d: Date) => void;
events: Event[];
accentColor: string;
onEventClick?: (e: Event) => void;
}> = ({ title, subtitle, date, onDateChange, events, accentColor, onEventClick }) => {
const days = eachDayOfInterval({
start: startOfWeek(startOfMonth(date)),
end: endOfWeek(endOfMonth(date))
});
return (
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden flex-1">
<div className="p-6 border-b border-slate-50 flex items-center justify-between">
<div>
<h4 className="text-sm font-black text-text-dark tracking-tight">{title}</h4>
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mt-0.5">{subtitle}</p>
</div>
<div className="flex gap-1">
<button onClick={() => onDateChange(subMonths(date, 1))} className="p-1.5 hover:bg-slate-50 rounded-lg transition-all"><ChevronLeft className="w-4 h-4 text-text-muted" /></button>
<button onClick={() => onDateChange(addMonths(date, 1))} className="p-1.5 hover:bg-slate-50 rounded-lg transition-all"><ChevronRight className="w-4 h-4 text-text-muted" /></button>
</div>
</div>
<div className="p-6">
<div className="text-center mb-4">
<span className="text-lg font-black text-text-dark">{format(date, 'MMMM yyyy')}</span>
</div>
<div className="grid grid-cols-7 gap-px bg-slate-50 border border-slate-50 rounded-2xl overflow-hidden">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => (
<div key={`${d}-${i}`} className="bg-white py-2 text-center text-[8px] font-black text-text-muted">{d}</div>
))}
{days.map(day => {
const isCurrentMonth = isSameMonth(day, date);
const dayStr = format(day, 'yyyy-MM-dd');
const dayEvents = events.filter(e => {
const d = parseDate(e.startDate);
return d && format(d, 'yyyy-MM-dd') === dayStr;
});
return (
<div key={day.toString()} className={cn(
"min-h-[8rem] bg-white p-2 border-t border-slate-50 relative",
!isCurrentMonth && "opacity-20"
)}>
<span className="text-xs font-black text-text-muted mb-2 block">{format(day, 'd')}</span>
<div className="space-y-1.5 overflow-y-auto max-h-[6rem] custom-scrollbar">
{dayEvents.map(e => (
<div
key={e.id}
onClick={() => onEventClick?.(e)}
className="bg-brand-glow border border-brand-indigo/10 rounded-lg p-2 transition-all hover:scale-[1.02] cursor-pointer"
>
<h5 className="text-[9px] font-black text-brand-indigo leading-tight truncate">{e.title}</h5>
<p className="text-[7px] font-bold text-brand-indigo/60 uppercase tracking-wider">{e.department}</p>
</div>
))}
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export const AutomationView: React.FC = () => {
const [sourceDate, setSourceDate] = useState(new Date());
const [targetDate, setTargetDate] = useState(addMonths(new Date(), 6));
const [events, setEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isExecuting, setIsExecuting] = useState(false);
const [status, setStatus] = useState<'idle' | 'success'>('idle');
const [sourceConfig, setSourceConfig] = useState({
institution: 'RIT',
department: 'AI&DS',
batch: '1st Year'
});
const [targetConfig, setTargetConfig] = useState({
institution: 'RIT',
department: 'AI&DS',
batch: '1st Year'
});
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
}
const filteredSourceEvents = events.filter(e =>
e.department?.trim() === sourceConfig.department.trim() &&
(e.academicYears || []).map((y: string) => y.trim()).includes(sourceConfig.batch.trim())
);
const filteredTargetEvents = events.filter(e =>
e.department?.trim() === targetConfig.department.trim() &&
(e.academicYears || []).map((y: string) => y.trim()).includes(targetConfig.batch.trim())
);
const [proposedChanges, setProposedChanges] = useState<any[]>([]);
const [showReviewModal, setShowReviewModal] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<any>(null);
const prepareSwap = () => {
const changes = filteredSourceEvents.map(event => {
const originalDate = parseDate(event.startDate);
if (!originalDate) return { ...event, resolution: 'skip' };
const isOddSem = originalDate.getMonth() >= 6;
const targetBaseDate = isOddSem ? subMonths(originalDate, 6) : addMonths(originalDate, 6);
// Preserving time
const targetDateStr = format(targetBaseDate, 'yyyy-MM-dd');
const timeStr = format(originalDate, 'HH:mm:ss');
const finalDateTimeStr = `${targetDateStr}T${timeStr}`;
const dayOfWeek = targetBaseDate.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
// Conflict Detection (Any event on target date)
const targetConflicts = filteredTargetEvents.filter(te =>
format(parseDate(te.startDate)!, 'yyyy-MM-dd') === targetDateStr
);
const hasTimeConflict = targetConflicts.some(tc => {
const tcDate = parseDate(tc.startDate);
return tcDate && format(tcDate, 'HH:mm') === format(originalDate, 'HH:mm');
});
const isAlreadySwapped = targetConflicts.some(tc =>
tc.title.trim().toLowerCase() === event.title.trim().toLowerCase()
);
return {
...event,
originalDate: event.startDate,
targetDate: finalDateTimeStr, // Now includes time
targetDepartment: targetConfig.department,
targetBatch: targetConfig.batch,
isWeekend,
isAlreadySwapped,
targetConflicts,
hasTimeConflict,
resolution: isAlreadySwapped ? 'skip' : (targetConflicts.length > 0 ? 'review' : 'create'),
adjustment: isWeekend ? 'monday' : 'none'
};
});
setProposedChanges(changes);
setShowReviewModal(true);
};
const handleExecuteSwap = async () => {
setIsExecuting(true);
const finalizedChanges = proposedChanges
.filter(c => c.resolution !== 'skip')
.map(change => {
let finalDate = new Date(change.targetDate);
// Handle Weekend Adjustments
if (change.resolution !== 'reschedule' && change.isWeekend) {
if (change.adjustment === 'friday') {
const day = finalDate.getDay();
const diff = day === 6 ? 1 : 2;
finalDate.setDate(finalDate.getDate() - diff);
} else if (change.adjustment === 'monday') {
const day = finalDate.getDay();
const diff = day === 6 ? 2 : 1;
finalDate.setDate(finalDate.getDate() + diff);
}
}
return {
...change,
finalDate: format(finalDate, "yyyy-MM-dd'T'HH:mm:ss")
};
});
console.log('Finalized Swap Payload:', finalizedChanges);
try {
const response = await fetch(API_BASE_URL + '/api/events/batch-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(finalizedChanges.map(c => ({
...c,
targetDepartment: c.targetDepartment,
targetBatch: c.targetBatch,
startDate: c.finalDate
})))
});
if (response.ok) {
setStatus('success');
fetchEvents();
} else {
const errData = await response.json();
alert('Failed to execute automation swap: ' + (errData.message || 'Unknown error'));
}
} catch (error) {
console.error('Automation swap failed:', error);
alert('Network error during automation swap');
} finally {
setIsExecuting(false);
setShowReviewModal(false);
setTimeout(() => setStatus('idle'), 3000);
}
};
return (
<div className="space-y-8 pb-20">
<div className="flex justify-between items-start">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">Event Automation</h2>
<p className="text-text-muted font-medium">Configure and manage automated department event workflows.</p>
</div>
<div className="w-12 h-12 bg-amber-50 rounded-2xl flex items-center justify-center text-amber-500 premium-shadow">
<Zap className="w-6 h-6 fill-current" />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white rounded-[2.5rem] p-8 border border-slate-100 premium-shadow relative overflow-hidden">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-text-muted">
<Building2 className="w-5 h-5" />
</div>
<div>
<h3 className="font-black text-text-dark">Source Selection</h3>
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted">Select branch to clone events from</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Institution</label>
<div className="bg-slate-50 rounded-xl p-3 flex justify-between items-center cursor-pointer">
<span className="text-xs font-bold text-text-dark">RIT</span>
<ChevronDown className="w-3 h-3 text-text-muted" />
</div>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Department</label>
<select
value={sourceConfig.department}
onChange={(e) => setSourceConfig({...sourceConfig, department: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Batch</label>
<select
value={sourceConfig.batch}
onChange={(e) => setSourceConfig({...sourceConfig, batch: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{BATCHES.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
</div>
</div>
<div className="bg-white rounded-[2.5rem] p-8 border border-slate-100 premium-shadow relative overflow-hidden">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
<ArrowRightLeft className="w-5 h-5" />
</div>
<div>
<h3 className="font-black text-text-dark">Target Workspace</h3>
<p className="text-[9px] font-black uppercase tracking-widest text-brand-indigo">Automation output destination</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Target Inst</label>
<div className="bg-slate-50 rounded-xl p-3 flex justify-between items-center cursor-pointer">
<span className="text-xs font-bold text-text-dark">RIT</span>
<ChevronDown className="w-3 h-3 text-text-muted" />
</div>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Target Dept</label>
<select
value={targetConfig.department}
onChange={(e) => setTargetConfig({...targetConfig, department: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Target Batch</label>
<select
value={targetConfig.batch}
onChange={(e) => setTargetConfig({...targetConfig, batch: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{BATCHES.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
</div>
</div>
</div>
<div className="bg-brand-navy rounded-3xl p-6 flex items-center justify-between text-white premium-shadow">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-white/10 flex items-center justify-center text-amber-400">
<ShieldAlert className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-black tracking-tight">Automation Safety Protocols Active</h4>
<p className="text-[9px] font-medium text-white/50">Cloning institutional events shifts schedules by +/- 6 months (Semester Swap).</p>
</div>
</div>
<div className="flex items-center gap-6">
<button className="text-[9px] font-black uppercase tracking-widest text-white/40 hover:text-white transition-colors">Clear Workspace</button>
<button
onClick={prepareSwap}
disabled={isExecuting || filteredSourceEvents.length === 0}
className={cn(
"flex items-center gap-3 px-8 py-4 rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] transition-all premium-shadow",
status === 'success' ? "bg-emerald-500 text-white" :
filteredSourceEvents.length === 0 ? "bg-slate-200 text-slate-400 cursor-not-allowed" :
"bg-brand-indigo text-white hover:scale-105"
)}
>
{isExecuting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : status === 'success' ? (
<>
<CheckCircle2 className="w-4 h-4" />
Swap Completed
</>
) : filteredSourceEvents.length === 0 ? (
<>
<ShieldAlert className="w-4 h-4" />
No Events Found to Swap
</>
) : (
<>
<ArrowRightLeft className="w-4 h-4" />
Execute Semester Swap
</>
)}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<MiniCalendar
title="Primary Schedule (Source)"
subtitle={`${sourceConfig.department}${sourceConfig.batch}`}
date={sourceDate}
onDateChange={setSourceDate}
events={filteredSourceEvents}
accentColor="bg-brand-indigo"
onEventClick={setSelectedEvent}
/>
<MiniCalendar
title="Target Workspace Events"
subtitle={`${targetConfig.department}${targetConfig.batch}`}
date={targetDate}
onDateChange={setTargetDate}
events={filteredTargetEvents}
accentColor="bg-emerald-400"
onEventClick={setSelectedEvent}
/>
</div>
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden"
>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<span className="px-3 py-1 bg-white/10 rounded-full text-[8px] font-black uppercase tracking-widest text-brand-indigo border border-white/10">
{selectedEvent.category || 'Academic'} Event
</span>
<h3 className="text-2xl font-black tracking-tight mt-3">{selectedEvent.title}</h3>
</div>
<button onClick={() => setSelectedEvent(null)} className="p-2 hover:bg-white/10 rounded-xl transition-all">
<X className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-6">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Start Date</span>
<p className="text-xs font-bold text-text-dark">
{parseDate(selectedEvent.startDate) ? format(parseDate(selectedEvent.startDate)!, 'PPP') : 'N/A'}
</p>
</div>
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Venue</span>
<p className="text-xs font-bold text-text-dark">{selectedEvent.location || 'Seminar Hall'}</p>
</div>
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Department</span>
<p className="text-xs font-bold text-text-dark">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Academic Year</span>
<p className="text-xs font-bold text-text-dark">{selectedEvent.academicYears?.join(', ') || 'N/A'}</p>
</div>
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted block mb-2">Proposer Information</span>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white rounded-lg border border-slate-200 flex items-center justify-center text-[10px] font-black text-brand-indigo">
{selectedEvent.proposer?.fullName?.charAt(0) || 'F'}
</div>
<div>
<p className="text-[10px] font-black text-text-dark leading-none">{selectedEvent.proposer?.fullName || 'Faculty Member'}</p>
<p className="text-[8px] font-bold text-text-muted mt-1">{selectedEvent.proposer?.email || 'faculty@rit.edu'}</p>
</div>
</div>
</div>
</div>
<div className="p-8 border-t border-slate-100 flex justify-end">
<button
onClick={() => setSelectedEvent(null)}
className="px-10 py-4 bg-brand-navy text-white rounded-2xl font-black text-xs uppercase tracking-widest premium-shadow hover:scale-105 active:scale-95 transition-all"
>
Close Insights
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<AnimatePresence>
{showReviewModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowReviewModal(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-4xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[80vh]"
>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<h3 className="text-2xl font-black tracking-tight">Review Automated Changes</h3>
<p className="text-white/60 text-xs font-medium mt-1">Audit and adjust planned semester-swap dates.</p>
</div>
<button onClick={() => setShowReviewModal(false)} className="p-2 hover:bg-white/10 rounded-xl transition-all">
<ChevronDown className="w-6 h-6 rotate-180" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
<div className="space-y-4">
{proposedChanges.map((change, idx) => (
<div key={change.id} className={cn(
"p-6 rounded-[2.5rem] border transition-all space-y-6",
change.resolution === 'skip' ? "bg-slate-50 border-slate-100 opacity-60" :
change.targetConflicts?.length > 0 ? "bg-red-50/30 border-red-100" :
change.isWeekend ? "bg-amber-50 border-amber-100" : "bg-white border-slate-100"
)}>
<div className="flex items-center justify-between gap-8">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Event</span>
<h4 className="text-sm font-black text-text-dark">{change.title}</h4>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-100 rounded-lg">
<Calendar className="w-3 h-3 text-text-muted" />
<span className="text-[10px] font-bold text-text-dark">
{parseDate(change.originalDate) ? format(parseDate(change.originalDate)!, 'MMM d') : 'N/A'}
</span>
<ArrowRightLeft className="w-3 h-3 text-brand-indigo" />
<span className="text-[10px] font-black text-brand-indigo">
{parseDate(change.targetDate) ? format(parseDate(change.targetDate)!, 'MMM d, yyyy') : 'N/A'}
</span>
</div>
{change.isAlreadySwapped && (
<span className="flex items-center gap-1.5 px-3 py-1 bg-emerald-100 text-emerald-700 rounded-full text-[8px] font-black uppercase tracking-widest">
<CheckCircle2 className="w-3 h-3" /> Already Swapped
</span>
)}
</div>
</div>
<div className="flex items-center gap-3">
{change.targetConflicts?.length > 0 && change.resolution !== 'skip' && (
<div className="flex items-center gap-2 px-4 py-2 bg-red-100 text-red-700 rounded-2xl border border-red-200">
<AlertTriangle className="w-4 h-4" />
<div className="flex flex-col">
<span className="text-[9px] font-black uppercase">Conflict Detected</span>
<span className="text-[8px] font-bold opacity-70">
{change.targetConflicts.length} event(s) already on this date
{change.hasTimeConflict && " (Time Overlap!)"}
</span>
</div>
</div>
)}
<div className="flex bg-white rounded-2xl border border-slate-100 p-1.5 premium-shadow-sm">
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].resolution = 'create';
setProposedChanges(updated);
}}
className={cn(
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all",
change.resolution === 'create' || change.resolution === 'review' ? "bg-brand-indigo text-white shadow-lg shadow-brand-indigo/20" : "text-text-muted hover:text-text-dark"
)}
>
Commit
</button>
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].resolution = 'reschedule';
setProposedChanges(updated);
}}
className={cn(
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all",
change.resolution === 'reschedule' ? "bg-amber-500 text-white shadow-lg shadow-amber-500/20" : "text-text-muted hover:text-text-dark"
)}
>
Reschedule
</button>
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].resolution = 'skip';
setProposedChanges(updated);
}}
className={cn(
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all",
change.resolution === 'skip' ? "bg-slate-200 text-slate-600" : "text-text-muted hover:text-text-dark"
)}
>
Skip
</button>
</div>
{/* Weekend Quick-Shift */}
{change.isWeekend && change.resolution !== 'skip' && change.resolution !== 'reschedule' && (
<div className="flex bg-amber-100/50 p-1 rounded-xl border border-amber-200">
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].adjustment = 'friday';
setProposedChanges(updated);
}}
title="Shift to Friday"
className={cn(
"px-3 py-1.5 rounded-lg text-[8px] font-black uppercase tracking-widest transition-all",
change.adjustment === 'friday' ? "bg-amber-600 text-white shadow-sm" : "text-amber-700 hover:bg-amber-100"
)}
>
Fri
</button>
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].adjustment = 'monday';
setProposedChanges(updated);
}}
title="Shift to Monday"
className={cn(
"px-3 py-1.5 rounded-lg text-[8px] font-black uppercase tracking-widest transition-all",
change.adjustment === 'monday' ? "bg-amber-600 text-white shadow-sm" : "text-amber-700 hover:bg-amber-100"
)}
>
Mon
</button>
</div>
)}
</div>
</div>
{/* Expanded Conflict / Reschedule Details */}
{(change.resolution === 'reschedule' || (change.targetConflicts?.length > 0 && change.resolution !== 'skip')) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="pt-6 border-t border-slate-100 space-y-4"
>
{change.targetConflicts?.length > 0 && (
<div className="space-y-2">
<span className="text-[8px] font-black uppercase tracking-widest text-red-500">Conflicts at Destination</span>
{change.targetConflicts.map((conf: any) => (
<div key={conf.id} className="flex items-center justify-between p-3 bg-red-50/50 rounded-xl border border-red-100">
<div className="flex items-center gap-3">
<div className="w-6 h-6 bg-white rounded-lg flex items-center justify-center text-[8px] font-black text-red-500 border border-red-100">
{conf.department.substring(0, 2)}
</div>
<span className="text-[11px] font-bold text-text-dark">{conf.title}</span>
</div>
<div className="flex items-center gap-2 text-[9px] font-black text-red-600/60 uppercase">
<Clock className="w-3 h-3" />
{format(new Date(conf.startDate), 'hh:mm a')}
</div>
</div>
))}
</div>
)}
{change.resolution === 'reschedule' && (
<div className="flex items-end gap-4 bg-amber-50/50 p-6 rounded-[2rem] border border-amber-100">
<div className="flex-1">
<label className="text-[8px] font-black uppercase tracking-widest text-amber-600 mb-2 block">New Target Date</label>
<input
type="date"
value={change.targetDate}
onChange={(e) => {
const updated = [...proposedChanges];
updated[idx].targetDate = e.target.value;
setProposedChanges(updated);
}}
className="w-full bg-white border border-amber-200 rounded-xl p-3 text-xs font-bold text-text-dark focus:outline-none focus:ring-2 focus:ring-amber-500/20"
/>
</div>
<div className="flex flex-col gap-2">
<button
onClick={() => {
const d = new Date(change.targetDate);
d.setDate(d.getDate() + 1);
const updated = [...proposedChanges];
updated[idx].targetDate = format(d, 'yyyy-MM-dd');
setProposedChanges(updated);
}}
className="p-2 bg-white border border-amber-200 rounded-lg hover:bg-amber-100 transition-all"
>
<ChevronUp className="w-4 h-4 text-amber-600" />
</button>
<button
onClick={() => {
const d = new Date(change.targetDate);
d.setDate(d.getDate() - 1);
const updated = [...proposedChanges];
updated[idx].targetDate = format(d, 'yyyy-MM-dd');
setProposedChanges(updated);
}}
className="p-2 bg-white border border-amber-200 rounded-lg hover:bg-amber-100 transition-all"
>
<ChevronDown className="w-4 h-4 text-amber-600" />
</button>
</div>
<div className="flex flex-col gap-1 px-4 py-2 bg-white rounded-2xl border border-amber-100">
<span className="text-[7px] font-black uppercase text-amber-400">Shift Result</span>
<span className="text-[10px] font-black text-amber-700">{format(new Date(change.targetDate), 'EEEE')}</span>
</div>
</div>
)}
</motion.div>
)}
</div>
))}
</div>
</div>
<div className="p-8 border-t border-slate-100 bg-slate-50/50 flex justify-end gap-4">
<button
onClick={() => setShowReviewModal(false)}
className="px-8 py-4 text-xs font-black uppercase tracking-widest text-text-muted hover:text-text-dark transition-colors"
>
Cancel
</button>
<button
onClick={handleExecuteSwap}
className="px-10 py-4 bg-brand-navy text-white rounded-2xl font-black text-xs uppercase tracking-widest premium-shadow hover:scale-105 active:scale-95 transition-all"
>
Commit Automation Swap
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -1,643 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Plus,
Search,
Trash2,
Layers,
RefreshCw,
AlertTriangle,
Building2,
GraduationCap,
ChevronRight,
X,
CheckCircle2
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { Pagination } from './Pagination';
import { useDialog } from '../../context/DialogContext';
interface ClassMapping {
id: number;
institution: string;
department: string;
academicYear: string;
sections: string[];
status: string;
}
const ritDepartments = ['AI&DS', 'AI&ML', 'CSE', 'CCE', 'CSBS', 'ECE', 'MECH', 'EE(VLSI)', 'BIOTECH', 'Placement Department', 'H&S Dept', 'Club', 'Centre'];
const rsbDepartments = ['PGDM'];
const years = ['1st Year', '2nd Year', '3rd Year', '4th Year'];
import rsbLogo from '../../assets/images/rsb_logo.png';
import ritLogo from '../../assets/images/college-logo.png';
export const ClassManagement: React.FC = () => {
const { showAlert, showConfirm } = useDialog();
const [classes, setClasses] = useState<ClassMapping[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [newSection, setNewSection] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [formData, setFormData] = useState({
institution: 'RIT',
department: 'AI&DS',
academicYear: '1st Year',
sections: [] as string[]
});
const [batches, setBatches] = useState<any[]>([]);
const [newBatchName, setNewBatchName] = useState('');
const [selectedBatchDept, setSelectedBatchDept] = useState('AI&DS');
const [tempBatches, setTempBatches] = useState<any[]>([]);
useEffect(() => {
const deptBatches = batches.filter(b => b.department === selectedBatchDept);
setTempBatches(JSON.parse(JSON.stringify(deptBatches)));
}, [batches, selectedBatchDept]);
const currentDepartments = formData.institution === 'RIT' ? ritDepartments : rsbDepartments;
useEffect(() => {
if (formData.institution === 'RSB' && formData.department !== 'PGDM') {
setFormData(prev => ({ ...prev, department: 'PGDM', sections: [] }));
}
}, [formData.institution]);
useEffect(() => {
fetchClasses();
fetchBatches();
}, []);
const fetchBatches = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/batches');
if (response.ok) {
const data = await response.json();
setBatches(data);
}
} catch (error) {
console.error('Failed to fetch batches:', error);
}
};
const fetchClasses = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/classes');
if (response.ok) {
const data = await response.json();
setClasses(data);
}
} catch (error) {
console.error('Failed to fetch classes:', error);
} finally {
setIsLoading(false);
}
};
const handleAddSection = (e: React.FormEvent) => {
e.preventDefault();
if (newSection && !formData.sections.includes(newSection.toUpperCase())) {
setFormData({
...formData,
sections: [...formData.sections, newSection.toUpperCase()]
});
setNewSection('');
}
};
const removeSection = (section: string) => {
setFormData({
...formData,
sections: formData.sections.filter(s => s !== section)
});
};
const handleSave = async () => {
if (formData.sections.length === 0) {
showAlert('Required', 'Please add at least one section', 'error');
return;
}
try {
const response = await fetch(API_BASE_URL + '/api/classes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
fetchClasses();
setFormData({ ...formData, sections: [] });
showAlert('Success', 'Class mapping saved successfully.', 'success');
}
} catch (error) {
console.error('Failed to save class mapping:', error);
showAlert('Error', 'Failed to save class mapping.', 'error');
}
};
const handleDelete = async (id: number) => {
showConfirm(
'Delete Mapping',
'Are you sure you want to delete this mapping?',
async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/classes/${id}`, {
method: 'DELETE'
});
if (response.ok) {
setClasses(prev => prev.filter(c => c.id !== id));
showAlert('Success', 'Mapping deleted successfully.', 'success');
}
} catch (error) {
console.error('Delete failed:', error);
showAlert('Error', 'Failed to delete mapping.', 'error');
}
}
);
};
const handlePromote = async () => {
showConfirm(
'Academic Promotion',
`WARNING: This action will shift all academic structures for ${formData.institution} forward:\n• 1st Year → 2nd Year\n• 2nd Year → 3rd Year\n• 3rd Year → 4th Year\n• 4th Year mappings will be PERMANENTLY DELETED.\n\nDo you wish to proceed?`,
async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/classes/promote?institution=${formData.institution}`, {
method: 'POST'
});
if (response.ok) {
fetchClasses();
showAlert('Success', `${formData.institution} academic year promotion completed successfully!`, 'success');
}
} catch (error) {
console.error('Promotion failed:', error);
showAlert('Error', 'Promotion failed.', 'error');
}
}
);
};
const handleCreateTempBatch = () => {
if (!newBatchName.trim()) {
showAlert('Required', 'Please enter a batch name', 'error');
return;
}
const newBatch = {
id: Date.now() + Math.floor(Math.random() * 1000),
name: newBatchName.trim(),
department: selectedBatchDept,
classes: []
};
setTempBatches([...tempBatches, newBatch]);
setNewBatchName('');
};
const handleDeleteTempBatch = (batchId: number) => {
setTempBatches(tempBatches.filter(b => b.id !== batchId));
};
const handleToggleClassInTempBatch = (batchId: number, classStr: string) => {
setTempBatches(tempBatches.map(b => {
if (b.id !== batchId) return b;
const isAssigned = b.classes?.includes(classStr);
return {
...b,
classes: isAssigned
? b.classes.filter((c: string) => c !== classStr)
: [...(b.classes || []), classStr]
};
}));
};
const handleSaveChanges = async () => {
try {
const originalDeptBatches = batches.filter(b => b.department === selectedBatchDept);
// 1. Identify deleted batches (in original but not in temp)
const deletedBatches = originalDeptBatches.filter(ob => !tempBatches.some(tb => String(tb.id) === String(ob.id)));
// 2. Perform deletes
for (const db of deletedBatches) {
await fetch(`${API_BASE_URL}/api/batches/${db.id}`, { method: 'DELETE' });
}
// 3. Save / Update current batches in tempBatches
for (const tb of tempBatches) {
await fetch(API_BASE_URL + '/api/batches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: tb.id,
name: tb.name,
department: tb.department,
classes: tb.classes
})
});
}
// 4. Refresh state
await fetchBatches();
showAlert('Success', 'Batch configurations saved successfully.', 'success');
} catch (error) {
console.error('Failed to save batch changes:', error);
showAlert('Error', 'Failed to save changes.', 'error');
}
};
const filteredClasses = classes
.filter(c =>
c.institution === formData.institution && (
c.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.academicYear.toLowerCase().includes(searchTerm.toLowerCase())
)
)
.sort((a, b) => b.id - a.id);
const currentClasses = filteredClasses.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const selectedDeptClasses = classes.filter(c => c.institution === formData.institution && c.department === formData.department);
const totalClassesCount = selectedDeptClasses.reduce((sum, c) => sum + c.sections.length, 0);
const hasMoreThanFourClasses = totalClassesCount > 4;
const deptClassesList = selectedDeptClasses.flatMap(c => c.sections.map(sec => `${c.academicYear} - ${sec}`));
const selectedBatchDeptClasses = classes.filter(c => c.department === selectedBatchDept);
const batchDeptClassesList = selectedBatchDeptClasses.flatMap(c => c.sections.map(sec => `${c.academicYear} - ${sec}`));
return (
<div className="space-y-10">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-1">Class Management</h1>
<p className="text-text-muted font-black uppercase tracking-widest text-[10px]">Define institutional academic structures & sections</p>
</div>
<div className="flex items-center gap-4">
<button
onClick={handlePromote}
className="flex items-center gap-3 bg-[#FFF9ED] text-[#D97706] border border-[#FDE68A] px-6 py-3 rounded-2xl font-black text-[11px] uppercase tracking-widest hover:scale-105 transition-all"
>
<RefreshCw className="w-4 h-4" />
Promote Academic Year
<AlertTriangle className="w-4 h-4" />
</button>
<div className="w-12 h-12 bg-brand-glow rounded-2xl flex items-center justify-center text-brand-indigo premium-shadow">
<Building2 className="w-6 h-6" />
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
{/* Left Column - Form */}
<div className="lg:col-span-4">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-8 space-y-8 sticky top-10"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
<Plus className="w-6 h-6" />
</div>
<h2 className="text-xl font-black text-text-dark tracking-tight">Add New Mapping</h2>
</div>
<div className="space-y-6">
{/* Institution */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Select Institution</label>
<div className="flex gap-4">
{[
{ id: 'RIT', logo: ritLogo, label: 'RIT' },
{ id: 'RSB', logo: rsbLogo, label: 'RSB' }
].map(inst => (
<button
key={inst.id}
type="button"
onClick={() => setFormData({...formData, institution: inst.id})}
className={cn(
"flex-1 h-20 rounded-2xl transition-all border-2 flex items-center justify-center relative overflow-hidden group",
formData.institution === inst.id
? "bg-white border-brand-indigo premium-shadow"
: "bg-slate-50 border-transparent grayscale opacity-50 hover:opacity-100 hover:grayscale-0 hover:bg-white hover:border-slate-200"
)}
>
<img
src={inst.logo}
alt={inst.label}
className={cn(
"transition-all",
inst.id === 'RIT' ? "h-10" : "h-7"
)}
/>
</button>
))}
</div>
</div>
{/* Department */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Department</label>
<select
value={formData.department}
onChange={e => setFormData({...formData, department: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-2xl py-4 px-6 text-sm font-bold focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
{currentDepartments.map(dept => <option key={dept} value={dept}>{dept}</option>)}
</select>
</div>
{/* Academic Year */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Academic Year</label>
<select
value={formData.academicYear}
onChange={e => setFormData({...formData, academicYear: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-2xl py-4 px-6 text-sm font-bold focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
{years.map(year => <option key={year} value={year}>{year}</option>)}
</select>
</div>
{/* Sections */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Sections</label>
<div className="bg-slate-50 rounded-2xl p-4 space-y-4">
<div className="flex flex-wrap gap-2">
<AnimatePresence>
{formData.sections.map(section => (
<motion.span
key={section}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
className="px-3 py-1.5 bg-white rounded-lg text-xs font-black text-brand-indigo border border-brand-indigo/10 flex items-center gap-2 group"
>
{section}
<button onClick={() => removeSection(section)} className="hover:text-status-danger transition-colors">
<X className="w-3 h-3" />
</button>
</motion.span>
))}
</AnimatePresence>
</div>
<form onSubmit={handleAddSection} className="flex gap-2">
<input
type="text"
placeholder="Type section (eg. A)..."
value={newSection}
onChange={e => setNewSection(e.target.value)}
className="flex-1 bg-transparent border-none text-xs font-bold focus:ring-0 p-0"
/>
<button type="submit" className="w-8 h-8 bg-brand-glow rounded-lg flex items-center justify-center text-brand-indigo hover:bg-brand-indigo hover:text-white transition-all">
<Plus className="w-4 h-4" />
</button>
</form>
</div>
</div>
<button
onClick={handleSave}
className="w-full bg-brand-indigo text-white rounded-2xl py-5 font-black text-[12px] uppercase tracking-[0.2em] premium-shadow hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-3"
>
<Layers className="w-5 h-5" />
Save Configuration
</button>
</div>
</motion.div>
</div>
{/* Right Column - Table */}
<div className="lg:col-span-8">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden"
>
<div className="p-8 border-b border-slate-50 flex items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="w-2 h-8 bg-brand-indigo rounded-full" />
<h3 className="text-xl font-black text-text-dark tracking-tight">Academic Structure</h3>
</div>
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
placeholder="Search structures..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-full bg-slate-50 border-transparent rounded-2xl py-3 pl-12 pr-4 text-sm font-bold focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50/50">
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted">Institution</th>
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted">Department \ Year</th>
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted">Configured Sections</th>
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{isLoading ? (
<tr><td colSpan={4} className="p-20 text-center text-text-muted italic">Loading structure...</td></tr>
) : filteredClasses.length === 0 ? (
<tr><td colSpan={4} className="p-20 text-center text-text-muted italic">No academic structures defined.</td></tr>
) : (
currentClasses.map((item, idx) => (
<motion.tr
key={item.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className="group hover:bg-slate-50/30 transition-colors"
>
<td className="px-8 py-6">
<div className="w-16 h-10 bg-slate-50 rounded-xl border border-slate-100 flex items-center justify-center p-1.5">
<img
src={item.institution === 'RIT' ? ritLogo : rsbLogo}
alt={item.institution}
className="h-full object-contain"
/>
</div>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-5">
<div className="w-12 h-12 rounded-2xl bg-slate-50 border border-slate-100 flex flex-col items-center justify-center">
<span className="text-[9px] font-black text-text-muted uppercase">{item.department.substring(0, 3)}</span>
<div className="w-4 h-0.5 bg-brand-indigo/30 rounded-full mt-0.5" />
</div>
<div>
<h4 className="text-sm font-black text-text-dark">{item.department}</h4>
<div className="flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-brand-indigo mt-0.5">
<GraduationCap className="w-3.5 h-3.5" />
{item.academicYear}
</div>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
{item.sections.map(section => (
<span key={section} className="w-7 h-7 rounded-lg bg-white border border-slate-200 flex items-center justify-center text-[10px] font-black text-text-muted group-hover:border-brand-indigo group-hover:text-brand-indigo transition-all">
{section}
</span>
))}
<div className="ml-4 flex items-center gap-1.5 px-3 py-1 bg-emerald-50 text-status-success rounded-lg border border-emerald-100/50">
<CheckCircle2 className="w-3.5 h-3.5" />
<span className="text-[10px] font-black uppercase tracking-widest">Ready</span>
</div>
</div>
</td>
<td className="px-8 py-6 text-right">
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-all">
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:text-brand-indigo hover:border-brand-indigo premium-shadow-sm transition-all">
<Layers className="w-4.5 h-4.5" />
</button>
<button onClick={() => handleDelete(item.id)} className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:text-status-danger hover:border-status-danger premium-shadow-sm transition-all">
<Trash2 className="w-4.5 h-4.5" />
</button>
</div>
</td>
</motion.tr>
))
)}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredClasses.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</motion.div>
</div>
</div>
{/* Batches Section - Always Visible */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-8 space-y-6 mt-10">
<div className="flex flex-col lg:flex-row lg:items-center justify-between border-b border-slate-50 pb-6 gap-6">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
<Layers className="w-5 h-5" />
</div>
<div>
<h3 className="text-xl font-black text-text-dark tracking-tight">Department Batches</h3>
<p className="text-xs text-text-muted font-bold">Configure custom student batches for auditing & event planning.</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-4">
{/* Select Department */}
<div className="flex items-center gap-2">
<span className="text-[9px] font-black text-text-muted uppercase tracking-wider">Department:</span>
<select
value={selectedBatchDept}
onChange={e => setSelectedBatchDept(e.target.value)}
className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-3 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800 cursor-pointer"
>
{ritDepartments.filter(d => d !== 'Club' && d !== 'Centre' && d !== 'Placement Department').map(dept => (
<option key={dept} value={dept}>{dept}</option>
))}
</select>
</div>
{/* Create Batch Form */}
<div className="flex items-center gap-2">
<input
type="text"
placeholder="New Batch Name (e.g. Batch 1)"
value={newBatchName}
onChange={e => setNewBatchName(e.target.value)}
className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800"
/>
<button
onClick={handleCreateTempBatch}
className="px-4 py-2 bg-brand-indigo text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all"
>
Create
</button>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{tempBatches.map(batch => (
<div key={batch.id} className="bg-slate-50 p-6 rounded-3xl border border-slate-100 space-y-4">
<div className="flex items-center justify-between border-b border-slate-200 pb-2">
<span className="text-sm font-black text-brand-navy uppercase">{batch.name}</span>
<button
onClick={() => handleDeleteTempBatch(batch.id)}
className="text-[10px] font-black text-rose-500 hover:text-rose-700 uppercase tracking-widest transition-all"
>
Delete
</button>
</div>
<div className="space-y-3">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest">Assign Classes to this Batch:</span>
{batchDeptClassesList.length > 0 ? (
<div className="grid grid-cols-2 gap-3">
{batchDeptClassesList.map(cls => {
const isChecked = batch.classes?.includes(cls);
return (
<label key={cls} className="flex items-center gap-2 bg-white p-3 rounded-xl border border-slate-200 hover:border-brand-indigo/30 transition-all cursor-pointer">
<input
type="checkbox"
checked={isChecked}
onChange={() => handleToggleClassInTempBatch(batch.id, cls)}
className="w-4 h-4 rounded border-slate-300 text-brand-indigo focus:ring-brand-indigo cursor-pointer"
/>
<span className="text-[10px] font-black text-slate-600 uppercase tracking-tight">{cls}</span>
</label>
);
})}
</div>
) : (
<span className="block text-[10px] font-bold text-text-muted italic">No classes configured for {selectedBatchDept}.</span>
)}
</div>
</div>
))}
{tempBatches.length === 0 && (
<div className="col-span-2 py-12 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2rem]">
No batches created yet for {selectedBatchDept}. Use the form above to add your first batch.
</div>
)}
</div>
{tempBatches.length > 0 && (
<div className="flex items-center justify-end border-t border-slate-100 pt-6 mt-6">
<button
onClick={handleSaveChanges}
className="px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center gap-2 shadow-md shadow-emerald-500/10"
>
<CheckCircle2 className="w-4 h-4" />
Save Changes
</button>
</div>
)}
</div>
</div>
);
};

View File

@@ -1,121 +0,0 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Calendar, CheckCircle2, Circle } from 'lucide-react';
import { cn } from '../../lib/utils';
import { INSTITUTIONAL_EVENTS } from '../../constants/institutionalEvents';
import { getCurrentAcademicYear } from '../../lib/dateUtils';
import { API_BASE_URL } from '../../lib/config';
interface ClubEvent {
id: string;
name: string;
month: string;
semester: 'ODD' | 'EVEN';
}
interface Event {
id: number;
title: string;
status: string;
academicYears: string[];
}
interface ClubInstitutionalChecklistProps {
onEventClick: (data: { eventName: string, isClubEvent: boolean }) => void;
}
export const ClubInstitutionalChecklist: React.FC<ClubInstitutionalChecklistProps> = ({ onEventClick }) => {
const [events, setEvents] = useState<Event[]>([]);
const currentAcademicYear = getCurrentAcademicYear();
useEffect(() => {
fetch(API_BASE_URL + '/api/events')
.then(res => res.json())
.then(data => setEvents(data))
.catch(err => console.error('Failed to fetch events:', err));
}, []);
const getEventStatus = (eventName: string) => {
const matching = events.filter(e =>
e.title.toLowerCase() === eventName.toLowerCase() &&
e.academicYears?.includes(currentAcademicYear) &&
(e.status?.toUpperCase() === 'APPROVED' || e.status?.toUpperCase() === 'COMPLETED')
);
return matching.length > 0;
};
const oddSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'ODD');
const evenSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'EVEN');
const SemesterCard = ({ title, subtitle, events, gradient }: { title: string, subtitle: string, events: ClubEvent[], gradient: string }) => (
<div className="flex-1 min-w-[400px] bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden">
<div className={cn("p-8 text-white flex justify-between items-start", gradient)}>
<div>
<h3 className="text-2xl font-black tracking-tight">{title}</h3>
<p className="text-[10px] font-black uppercase tracking-widest opacity-80 mt-1">{subtitle}</p>
</div>
<Calendar className="w-6 h-6 opacity-60" />
</div>
<div className="p-8 space-y-6 max-h-[600px] overflow-y-auto custom-scrollbar">
{events.map((event, i) => {
const isScheduled = getEventStatus(event.name);
return (
<motion.button
key={event.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
onClick={() => onEventClick({ eventName: event.name, isClubEvent: true })}
className="w-full flex items-center gap-6 group text-left"
>
<div className="relative">
<Circle className={cn("w-8 h-8 fill-white", isScheduled ? "text-emerald-500" : "text-slate-100")} />
<div className="absolute inset-0 flex items-center justify-center">
{isScheduled ? (
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
) : (
<div className="w-4 h-4 rounded-full border-2 border-slate-200 group-hover:border-brand-indigo transition-colors" />
)}
</div>
</div>
<div>
<p className={cn("text-sm font-black transition-colors", isScheduled ? "text-emerald-600" : "text-text-dark group-hover:text-brand-indigo")}>{event.name}</p>
<p className="text-[9px] font-bold text-text-muted uppercase tracking-widest mt-0.5">
{isScheduled ? 'Scheduled & Approved' : `Target: ${event.month}`}
</p>
</div>
</motion.button>
);
})}
</div>
</div>
);
return (
<div className="space-y-10">
<div className="flex items-center gap-4">
<div className="w-1.5 h-10 bg-brand-indigo rounded-full" />
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tighter">Institutional Checklists</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted mt-1">Track Institutional Club Requirements</p>
</div>
</div>
<div className="flex flex-col xl:flex-row gap-8">
<SemesterCard
title="ODD Semester"
subtitle="Club & Institutional Events"
events={oddSemesterEvents}
gradient="bg-gradient-to-r from-blue-600 to-indigo-600"
/>
<SemesterCard
title="Even Semester"
subtitle="Club & Institutional Events"
events={evenSemesterEvents}
gradient="bg-gradient-to-r from-indigo-600 to-purple-600"
/>
</div>
</div>
);
};

View File

@@ -1,196 +0,0 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
X,
Calendar as CalendarIcon,
MapPin,
Clock,
Building2,
Users,
Tag,
Target
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
interface EventDetailsModalProps {
isOpen: boolean;
onClose: () => void;
event: any;
}
export const EventDetailsModal: React.FC<EventDetailsModalProps> = ({ isOpen, onClose, event }) => {
if (!isOpen || !event) return null;
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const startDate = parseDate(event.startDate);
const endDate = parseDate(event.endDate);
return (
<AnimatePresence>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
>
{/* Header */}
<div className="p-8 bg-brand-navy text-white flex justify-between items-start relative overflow-hidden">
<div className="relative z-10 pr-12">
<div className="flex items-center gap-3 mb-3">
<span className="px-3 py-1 bg-white/10 rounded-full text-[10px] font-black uppercase tracking-widest border border-white/10">
{event.department}
</span>
<span className="px-3 py-1 bg-brand-indigo/30 rounded-full text-[10px] font-black uppercase tracking-widest text-brand-glow border border-brand-indigo/50">
{event.type}
</span>
</div>
<h2 className="text-2xl sm:text-3xl font-black tracking-tight leading-tight">
{event.title}
</h2>
</div>
<button
onClick={onClose}
className="absolute top-8 right-8 p-2 bg-white/10 hover:bg-white/20 rounded-xl transition-colors z-10"
>
<X className="w-5 h-5" />
</button>
{/* Background Decoration */}
<div className="absolute -right-10 -bottom-10 w-40 h-40 bg-brand-indigo/20 rounded-full blur-3xl" />
</div>
{/* Content */}
<div className="p-8 overflow-y-auto custom-scrollbar space-y-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="flex items-start gap-4 p-4 rounded-2xl bg-slate-50 border border-slate-100">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-sm text-brand-indigo shrink-0">
<CalendarIcon className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">Date & Time</p>
<p className="text-sm font-bold text-text-dark">
{startDate ? format(startDate, 'MMMM d, yyyy') : 'TBD'}
</p>
<p className="text-xs font-bold text-text-muted mt-0.5">
{startDate ? format(startDate, 'hh:mm a') : 'TBD'} - {endDate ? format(endDate, 'hh:mm a') : 'TBD'}
</p>
</div>
</div>
<div className="flex items-start gap-4 p-4 rounded-2xl bg-slate-50 border border-slate-100">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-sm text-emerald-500 shrink-0">
<MapPin className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">Venue</p>
<p className="text-sm font-bold text-text-dark">
{event.location || 'TBD'}
</p>
<p className="text-xs font-bold text-text-muted mt-0.5">
{event.institution || 'RIT'}
</p>
</div>
</div>
</div>
{event.description && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3">Event Description</h3>
<div className="p-5 rounded-2xl bg-slate-50 border border-slate-100">
<p className="text-sm font-medium text-text-muted leading-relaxed whitespace-pre-wrap">
{event.description}
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{event.academicYears && event.academicYears.length > 0 && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3 flex items-center gap-2">
<Target className="w-4 h-4 text-brand-indigo" />
Target Audience
</h3>
<div className="flex flex-wrap gap-2">
{(event as any).targetedBatch ? (
<span className="px-3 py-1.5 bg-brand-indigo text-white text-xs font-bold rounded-xl shadow-sm">
{(event as any).targetedBatch}
</span>
) : (
event.academicYears.map((year: string) => (
<span key={year} className="px-3 py-1.5 bg-brand-glow text-brand-indigo text-xs font-bold rounded-xl border border-brand-indigo/10">
{year}
</span>
))
)}
</div>
</div>
)}
{event.targetedSections && event.targetedSections.length > 0 && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3 flex items-center gap-2">
<Users className="w-4 h-4 text-emerald-500" />
Sections
</h3>
<div className="flex flex-wrap gap-2">
{event.targetedSections.map((sec: string) => (
<span key={sec} className="px-3 py-1.5 bg-emerald-50 text-emerald-600 text-xs font-bold rounded-xl border border-emerald-100">
Section {sec}
</span>
))}
</div>
</div>
)}
</div>
{event.guestName && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3">Special Guest</h3>
<div className="p-4 rounded-2xl bg-slate-50 border border-slate-100 flex items-center justify-between">
<div>
<p className="text-sm font-bold text-text-dark">{event.guestName}</p>
{event.guestSocialProfile && (
<a href={event.guestSocialProfile} target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-brand-indigo hover:underline mt-0.5 inline-block">
View Profile
</a>
)}
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-slate-100 bg-slate-50/50 flex justify-end">
<button
onClick={onClose}
className="px-6 py-3 bg-white border border-slate-200 text-text-dark rounded-xl text-sm font-black hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm"
>
Close
</button>
</div>
</motion.div>
</div>
</AnimatePresence>
);
};

View File

@@ -1,387 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
History,
Search,
Calendar,
MapPin,
CheckCircle2,
XCircle,
Clock,
Download,
Filter,
AlertTriangle,
ChevronRight,
User,
Heart,
FileText,
X,
Building2,
Users,
Wallet,
Ticket
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
type: string;
status: string;
department: string;
location: string;
budget: number;
conflictMessage?: string;
rejectionReason?: string;
institution: string;
category: string;
academicYears: string[];
proposer?: {
fullName: string;
};
description?: string;
sponsors?: string[];
hasRegistrationFee: boolean;
registrationFee: number;
}
export const EventHistory: React.FC = () => {
const [events, setEvents] = useState<Event[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('ALL');
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
useEffect(() => {
fetch(API_BASE_URL + '/api/events')
.then(res => res.json())
.then(data => setEvents(data))
.catch(err => console.error('History fetch failed:', err));
}, []);
const getStatusStyle = (status: string, hasConflict: boolean) => {
if (hasConflict && status !== 'APPROVED') return {
bg: 'bg-red-50',
text: 'text-red-600',
border: 'border-red-100',
icon: AlertTriangle,
label: 'CONFLICT'
};
switch (status) {
case 'APPROVED': return { bg: 'bg-emerald-50', text: 'text-emerald-600', border: 'border-emerald-100', icon: CheckCircle2, label: 'APPROVED' };
case 'REQUESTED': return { bg: 'bg-blue-50', text: 'text-blue-600', border: 'border-blue-100', icon: Clock, label: 'REQUESTED' };
case 'PENDING_PR': return { bg: 'bg-purple-50', text: 'text-purple-600', border: 'border-purple-100', icon: ShieldCheck, label: 'PENDING PRINCIPAL' };
case 'HOD_REJECTED':
case 'PRINCIPAL_REJECTED': return { bg: 'bg-red-50', text: 'text-red-600', border: 'border-red-100', icon: XCircle, label: 'REJECTED' };
default: return { bg: 'bg-slate-50', text: 'text-slate-600', border: 'border-slate-100', icon: Clock, label: status };
}
};
const filteredEvents = events
.filter(e => {
const matchesSearch = e.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
e.department.toLowerCase().includes(searchQuery.toLowerCase());
if (statusFilter === 'ALL') return matchesSearch;
if (statusFilter === 'PENDING') return matchesSearch && (e.status === 'REQUESTED' || e.status === 'PENDING_PR');
if (statusFilter === 'REJECTED') return matchesSearch && e.status.includes('REJECTED');
return matchesSearch && e.status === statusFilter;
})
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
const currentEvents = filteredEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8 pb-10">
{/* Header Section */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-center gap-5">
<div className="w-12 h-12 bg-brand-navy rounded-2xl flex items-center justify-center text-white premium-shadow">
<History className="w-6 h-6" />
</div>
<div>
<h2 className="text-2xl font-black text-text-dark tracking-tight">Institutional Event History</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted">Audit-ready comprehensive event ledger</p>
</div>
</div>
<div className="flex items-center gap-3">
<button className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-100 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-muted hover:bg-slate-50 transition-all">
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
</div>
{/* Filters Bar */}
<div className="flex flex-col lg:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300" />
<input
type="text"
placeholder="Search by event title or department..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-16 pl-16 pr-8 bg-white border border-slate-100 rounded-3xl text-sm font-bold text-text-dark focus:outline-none focus:ring-4 focus:ring-brand-indigo/5 focus:border-brand-indigo transition-all"
/>
</div>
<div className="flex items-center gap-2 bg-white p-2 rounded-3xl border border-slate-100">
{['ALL', 'APPROVED', 'PENDING', 'REJECTED'].map(status => (
<button
key={status}
onClick={() => setStatusFilter(status)}
className={cn(
"px-6 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all",
statusFilter === status
? "bg-brand-indigo text-white premium-shadow-sm"
: "text-text-muted hover:bg-slate-50"
)}
>
{status}
</button>
))}
</div>
</div>
{/* Events Ledger */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b border-slate-50">
<th className="px-8 py-6 text-left text-[10px] font-black uppercase tracking-widest text-text-muted">Event Details</th>
<th className="px-8 py-6 text-left text-[10px] font-black uppercase tracking-widest text-text-muted">Logistics</th>
<th className="px-8 py-6 text-left text-[10px] font-black uppercase tracking-widest text-text-muted">Status</th>
<th className="px-8 py-6 text-right text-[10px] font-black uppercase tracking-widest text-text-muted">Budget</th>
<th className="px-8 py-6 text-right text-[10px] font-black uppercase tracking-widest text-text-muted"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
<AnimatePresence mode="popLayout">
{currentEvents.map((event, idx) => {
const statusStyle = getStatusStyle(event.status, !!event.conflictMessage);
const StatusIcon = statusStyle.icon;
return (
<motion.tr
key={event.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.03 }}
onClick={() => setSelectedEvent(event)}
className="group hover:bg-slate-50/50 transition-colors cursor-pointer"
>
<td className="px-8 py-6">
<div className="flex items-center gap-4">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center font-black text-[10px]",
event.conflictMessage ? "bg-red-50 text-red-500" : "bg-brand-indigo/5 text-brand-indigo"
)}>
{event.department?.slice(0, 3).toUpperCase()}
</div>
<div>
<p className="text-sm font-black text-text-dark group-hover:text-brand-indigo transition-colors">{event.title}</p>
<p className="text-[10px] font-bold text-text-muted uppercase tracking-widest mt-0.5">{event.type}</p>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="space-y-2">
<div className="flex items-center gap-2 text-text-muted">
<Calendar className="w-3.5 h-3.5" />
<span className="text-[10px] font-bold">{format(new Date(event.startDate), 'MMM d, yyyy')}</span>
</div>
<div className="flex items-center gap-2 text-text-muted">
<MapPin className="w-3.5 h-3.5" />
<span className="text-[10px] font-bold">{event.location}</span>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className={cn(
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full border text-[9px] font-black uppercase tracking-widest",
statusStyle.bg, statusStyle.text, statusStyle.border
)}>
<StatusIcon className="w-3.5 h-3.5" />
{statusStyle.label}
</div>
</td>
<td className="px-8 py-6 text-right">
<span className="text-sm font-black text-text-dark">{event.budget?.toLocaleString() || '0'}</span>
</td>
<td className="px-8 py-6 text-right">
<button className="p-2 hover:bg-white hover:premium-shadow-sm rounded-lg transition-all text-slate-300 group-hover:text-brand-indigo">
<ChevronRight className="w-5 h-5" />
</button>
</td>
</motion.tr>
);
})}
</AnimatePresence>
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredEvents.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</div>
{/* Detail Modal */}
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden max-h-[90vh] overflow-y-auto"
>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">Event History</span>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">{selectedEvent.institution}</span>
</div>
<h3 className="text-2xl font-black tracking-tight">{selectedEvent.title}</h3>
</div>
<button
onClick={() => setSelectedEvent(null)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-8">
{/* Description */}
{selectedEvent.description && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<FileText className="w-3.5 h-3.5" />
Description
</div>
<p className="text-sm font-medium text-text-dark leading-relaxed bg-slate-50 p-4 rounded-2xl border border-slate-100">
{selectedEvent.description}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Calendar className="w-3.5 h-3.5" />
Date & Time
</div>
<p className="text-sm font-bold text-text-dark">{new Date(selectedEvent.startDate).toLocaleString()}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<MapPin className="w-3.5 h-3.5" />
Location
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.location}</p>
</div>
</div>
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Building2 className="w-3.5 h-3.5" />
Department
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Users className="w-3.5 h-3.5" />
Target Batches
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears?.join(', ') || 'N/A'}</p>
</div>
</div>
</div>
{/* Sponsors */}
{selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Heart className="w-3.5 h-3.5 text-brand-indigo" />
Event Sponsors
</div>
<div className="flex flex-wrap gap-2">
{selectedEvent.sponsors.map((sponsor, idx) => (
<span key={idx} className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-lg text-[10px] font-black uppercase tracking-widest border border-brand-indigo/10">
{sponsor}
</span>
))}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-slate-50">
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Wallet className="w-3.5 h-3.5" />
Budget
</div>
<p className="text-lg font-black text-text-dark">{selectedEvent.budget?.toLocaleString() || '0'}</p>
</div>
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Ticket className="w-3.5 h-3.5" />
Status
</div>
<p className="text-xs font-black text-brand-indigo uppercase">{selectedEvent.status}</p>
</div>
</div>
{selectedEvent.rejectionReason && (
<div className="p-4 bg-red-50 border border-red-100 rounded-2xl">
<p className="text-[10px] font-black uppercase text-red-500 mb-1">Rejection Reason</p>
<p className="text-xs font-bold text-red-700">{selectedEvent.rejectionReason}</p>
</div>
)}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};
const ShieldCheck = (props: any) => (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="m9 12 2 2 4-4" />
</svg>
);

View File

@@ -1,146 +0,0 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle2, Circle, AlertCircle, Clock, XCircle, ShieldCheck, Flag } from 'lucide-react';
import { cn } from '../../lib/utils';
export type Event = {
id: number;
title: string;
startDate: string;
endDate: string;
location: string;
category: string;
type: string;
institution: string;
department: string;
status: 'REQUESTED' | 'APPROVED' | 'COMPLETED' | 'CANCELLED' | 'PENDING_PR' | 'HOD_REJECTED' | 'PRINCIPAL_REJECTED';
rejectionReason?: string;
guestName?: string;
proposer?: {
email: string;
fullName: string;
};
createdAt?: string;
updatedAt?: string;
}
export const StatusTimeline: React.FC<{ event: Event; compact?: boolean }> = ({ event, compact }) => {
const stages = [
{ id: 'REQUESTED', label: 'Proposal Raised', icon: Clock },
{ id: 'PENDING_PR', label: 'HoD Review', icon: ShieldCheck },
{ id: 'APPROVED', label: 'Principal Approval', icon: CheckCircle2 },
{ id: 'COMPLETED', label: 'Event Success', icon: Flag }
];
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const now = new Date();
const endDate = parseDate(event.endDate);
const isAutoCompleted = event.status === 'APPROVED' && endDate && now > endDate;
const isManuallyCompleted = event.status === 'COMPLETED';
const effectiveStatus = (isAutoCompleted || isManuallyCompleted) ? 'COMPLETED' : event.status;
const isHODRejected = effectiveStatus === 'HOD_REJECTED';
const isPRRejected = effectiveStatus === 'PRINCIPAL_REJECTED';
const isCancelled = effectiveStatus === 'CANCELLED';
const isRejected = isHODRejected || isPRRejected || isCancelled;
let currentStageIdx = stages.findIndex(s => s.id === effectiveStatus);
if (currentStageIdx === -1) {
if (isHODRejected) currentStageIdx = 1;
else if (isPRRejected) currentStageIdx = 2;
else if (isCancelled) currentStageIdx = 0;
else currentStageIdx = 0;
}
return (
<div className={cn("w-full space-y-8", compact ? "py-2" : "py-6")}>
<div className="relative flex items-center justify-between w-full px-4">
<div className="absolute left-10 right-10 h-[2px] bg-slate-100 top-[18px]" />
{!isRejected && (
<motion.div
initial={{ width: 0 }}
animate={{ width: `calc(${(currentStageIdx / (stages.length - 1)) * 100}%)` }}
className="absolute left-10 h-[2px] bg-emerald-500 top-[18px] origin-left transition-all duration-1000"
/>
)}
{stages.map((stage, i) => {
const isPast = i < currentStageIdx;
const isCurrent = i === currentStageIdx;
const isFuture = i > currentStageIdx;
const stageRejected = isRejected && isCurrent;
const stageCompleted = isPast || (isCurrent && !isRejected);
return (
<div key={stage.id} className="relative z-10 flex flex-col items-center">
<motion.div
initial={false}
animate={{
scale: isCurrent ? 1.1 : 1,
backgroundColor: stageRejected ? "#ef4444" : stageCompleted ? "#10b981" : "#f1f5f9",
borderColor: stageRejected ? "#fee2e2" : stageCompleted ? "#d1fae5" : "#fff"
}}
className={cn(
"w-9 h-9 rounded-full flex items-center justify-center border-4 shadow-sm transition-all duration-500",
stageRejected ? "text-white" : stageCompleted ? "text-white" : "text-slate-300"
)}
>
{stageRejected ? <XCircle className="w-5 h-5" /> :
stageCompleted ? <CheckCircle2 className="w-5 h-5" /> :
<stage.icon className="w-4 h-4" />}
</motion.div>
<div className="absolute top-12 flex flex-col items-center min-w-[100px]">
<span className={cn(
"text-[8px] font-black uppercase tracking-[0.2em] whitespace-nowrap",
stageRejected ? "text-red-500" : stageCompleted ? "text-emerald-600" : "text-slate-400"
)}>
{stageRejected ? (isHODRejected ? 'HoD Rejected' : isPRRejected ? 'PR Rejected' : 'Cancelled') : stage.label}
</span>
{isCurrent && !isRejected && (
<motion.span
animate={{ opacity: [0.4, 1, 0.4] }}
transition={{ repeat: Infinity, duration: 2 }}
className="text-[6px] font-bold text-brand-indigo mt-1 uppercase tracking-tighter"
>
Current Phase
</motion.span>
)}
</div>
</div>
);
})}
</div>
<AnimatePresence>
{isRejected && event.rejectionReason && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-16 p-6 bg-red-50/50 border border-red-100 rounded-[2rem] flex gap-4 items-start"
>
<div className="w-10 h-10 bg-red-500 rounded-xl flex items-center justify-center shrink-0 shadow-lg shadow-red-200">
<AlertCircle className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-red-500 mb-1">Feedback Message</p>
<p className="text-sm font-bold text-red-700 leading-relaxed italic">"{event.rejectionReason}"</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,252 +0,0 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Clock,
CheckCircle2,
AlertCircle,
Edit3,
Calendar,
MapPin,
ChevronRight,
ShieldCheck,
XCircle,
Timer,
ChevronLeft,
ChevronDown,
Users
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
interface Event {
id: number;
title: string;
startDate: any;
endDate: any;
location: string;
status: 'REQUESTED' | 'APPROVED' | 'COMPLETED' | 'CANCELLED' | 'PENDING_PR' | 'HOD_REJECTED' | 'PRINCIPAL_REJECTED';
rejectionReason?: string;
category?: string;
currentParticipants?: number;
}
interface FacultyEventManagementProps {
events: Event[];
onEditEvent: (event: Event) => void;
}
export const FacultyEventManagement: React.FC<FacultyEventManagementProps> = ({ events, onEditEvent }) => {
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(3);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const sortedEvents = useMemo(() => {
return [...events].sort((a, b) => {
const dateA = parseDate(a.startDate)?.getTime() || 0;
const dateB = parseDate(b.startDate)?.getTime() || 0;
return dateB - dateA;
});
}, [events]);
const totalPages = Math.ceil(sortedEvents.length / itemsPerPage);
const currentEvents = sortedEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const rangeStart = events.length === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const rangeEnd = Math.min(currentPage * itemsPerPage, events.length);
const getStatusConfig = (status: string) => {
switch (status) {
case 'APPROVED':
return { color: 'text-emerald-500', bg: 'bg-emerald-50', border: 'border-emerald-100', icon: CheckCircle2, label: 'Approved' };
case 'PENDING_PR':
return { color: 'text-brand-indigo', bg: 'bg-brand-glow', border: 'border-brand-indigo/10', icon: ShieldCheck, label: 'At Principal Desk' };
case 'REQUESTED':
return { color: 'text-amber-500', bg: 'bg-amber-50', border: 'border-amber-100', icon: Timer, label: 'At HoD Desk' };
case 'HOD_REJECTED':
case 'PRINCIPAL_REJECTED':
return { color: 'text-red-500', bg: 'bg-red-50', border: 'border-red-100', icon: XCircle, label: 'Rejected' };
case 'CANCELLED':
return { color: 'text-slate-400', bg: 'bg-slate-50', border: 'border-slate-200', icon: XCircle, label: 'Cancelled' };
default:
return { color: 'text-slate-500', bg: 'bg-slate-50', border: 'border-slate-100', icon: Clock, label: status };
}
};
return (
<div className="card-widget h-full flex flex-col">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-black text-brand-navy tracking-tight flex items-center gap-2">
<Calendar className="w-5 h-5 text-brand-indigo" />
My Event Proposals
</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">
Track and manage your submitted requests
</p>
</div>
<div className="flex items-center gap-3">
<div className="bg-slate-50 px-3 py-1.5 rounded-xl border border-slate-100">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
{events.length} Total
</span>
</div>
</div>
</div>
<div className="flex-1 space-y-4 overflow-hidden">
<AnimatePresence mode="wait">
{events.length === 0 ? (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center justify-center py-12 text-center"
>
<div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mb-4">
<Calendar className="w-8 h-8 text-slate-200" />
</div>
<p className="text-sm font-bold text-slate-400 italic">No event proposals yet.</p>
</motion.div>
) : (
<motion.div
key={currentPage + '-' + itemsPerPage}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-4"
>
{currentEvents.map((event, idx) => {
const config = getStatusConfig(event.status);
const eventDate = parseDate(event.startDate);
const canEdit = ['REQUESTED', 'PENDING_PR'].includes(event.status);
return (
<div
key={event.id}
className="group relative bg-white border border-slate-100 rounded-2xl p-4 hover:border-brand-indigo/30 hover:shadow-md transition-all"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className={cn("px-2 py-0.5 rounded-md text-[8px] font-black uppercase tracking-widest border", config.bg, config.color, config.border)}>
{config.label}
</div>
<span className="text-[9px] font-black text-slate-300 uppercase tracking-widest"></span>
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">ID: #{event.id}</span>
</div>
<h4 className="text-sm font-black text-brand-navy truncate group-hover:text-brand-indigo transition-colors mb-2">
{event.title}
</h4>
<div className="flex flex-wrap gap-x-4 gap-y-2">
<div className="flex items-center gap-1.5 text-slate-500">
<Clock className="w-3.5 h-3.5 text-brand-indigo/60" />
<span className="text-[10px] font-bold">
{eventDate ? format(eventDate, 'MMM d, h:mm a') : 'TBD'}
</span>
</div>
<div className="flex items-center gap-1.5 text-slate-500">
<MapPin className="w-3.5 h-3.5 text-brand-indigo/60" />
<span className="text-[10px] font-bold truncate max-w-[120px]">
{event.location}
</span>
</div>
<div className="flex items-center gap-1.5 text-slate-500">
<Users className="w-3.5 h-3.5 text-brand-indigo/60" />
<span className="text-[10px] font-bold">
{event.currentParticipants || 0} Registered
</span>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
{canEdit ? (
<button
onClick={() => onEditEvent(event)}
className="p-2.5 bg-brand-glow text-brand-indigo rounded-xl hover:bg-brand-indigo hover:text-white transition-all shadow-sm group/btn"
title="Edit Proposal"
>
<Edit3 className="w-4 h-4 transition-transform group-hover/btn:scale-110" />
</button>
) : (
<div className="p-2.5 bg-slate-50 text-slate-300 rounded-xl cursor-not-allowed" title="Cannot edit once in review">
<Edit3 className="w-4 h-4" />
</div>
)}
<button className="p-2.5 bg-white border border-slate-100 text-slate-400 rounded-xl hover:bg-slate-50 hover:text-brand-indigo transition-all shadow-sm">
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
{event.rejectionReason && (
<div className="mt-3 p-3 bg-red-50/50 border border-red-100 rounded-xl flex items-start gap-2">
<AlertCircle className="w-3.5 h-3.5 text-red-500 shrink-0 mt-0.5" />
<p className="text-[10px] font-medium text-red-700 italic">
"{event.rejectionReason}"
</p>
</div>
)}
</div>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
<div className="mt-6 flex items-center justify-end gap-8 pt-4 border-t border-slate-50">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Rows per page</span>
<div className="relative group">
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[11px] font-black text-brand-navy appearance-none pr-10 cursor-pointer hover:border-brand-indigo/30 transition-all focus:outline-none"
>
{[3, 5, 10, 20].map(val => (
<option key={val} value={val}>{val}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400 pointer-events-none" />
</div>
</div>
<div className="text-[11px] font-black text-brand-navy tracking-tight min-w-[80px] text-center">
{rangeStart}-{rangeEnd} <span className="text-slate-300 mx-1">of</span> {events.length}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || events.length === 0}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
};

View File

@@ -1,488 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import {
Calendar as CalendarIcon,
ChevronLeft,
ChevronRight,
Plus,
MessageSquare,
Loader2,
X,
FilePlus,
Clock
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
isSameMonth,
isSameDay,
addMonths,
subMonths,
isWeekend
} from 'date-fns';
import { useAuth } from '../../context/AuthContext';
interface Event {
id: number;
title: string;
type: string;
status: string;
department: string;
startDate: string;
endDate: string;
}
interface Note {
id: number;
content: string;
targetDate: string;
authorName: string;
}
interface InstitutionalCalendarProps {
onProposeEvent?: (date: string) => void;
compact?: boolean;
}
export const InstitutionalCalendar: React.FC<InstitutionalCalendarProps> = ({ onProposeEvent, compact }) => {
const { user } = useAuth();
const [currentDate, setCurrentDate] = useState(new Date());
const [events, setEvents] = useState<Event[]>([]);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState(true);
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [newNote, setNewNote] = useState('');
const [isSubmittingNote, setIsSubmittingNote] = useState(false);
// Advanced Filters
const [filters, setFilters] = useState({
department: '',
batch: '',
section: ''
});
// Rescheduling State
const [reschedulingEvent, setReschedulingEvent] = useState<Event | null>(null);
const [newTargetDate, setNewTargetDate] = useState('');
const [startTime, setStartTime] = useState('10:00');
const [endTime, setEndTime] = useState('12:00');
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
// Handle Spring Boot LocalDateTime array: [yyyy, mm, dd, hh, mm, ss]
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const fetchData = async () => {
try {
const [eventsRes, notesRes] = await Promise.all([
fetch(API_BASE_URL + '/api/events'),
fetch(API_BASE_URL + '/api/notes')
]);
if (eventsRes.ok && notesRes.ok) {
const eventsData = await eventsRes.json();
const notesData = await notesRes.json();
// Robust filtering and status check
const filteredEvents = eventsData.filter((e: any) => {
const status = e.status?.toUpperCase();
return status === 'APPROVED' || status === 'COMPLETED';
});
setEvents(filteredEvents);
setNotes(notesData);
}
} catch (error) {
console.error('Failed to fetch calendar data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleAddNote = async () => {
if (!newNote.trim() || !selectedDay) return;
setIsSubmittingNote(true);
try {
const response = await fetch(API_BASE_URL + '/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: newNote,
targetDate: format(selectedDay, 'yyyy-MM-dd'),
authorName: user?.fullName || 'Anonymous',
authorEmail: user?.email
})
});
if (response.ok) {
setNewNote('');
fetchData();
}
} catch (error) {
console.error('Failed to add note:', error);
} finally {
setIsSubmittingNote(false);
}
};
const handleReschedule = async () => {
if (!reschedulingEvent || !newTargetDate) return;
try {
const response = await fetch(`${API_BASE_URL}/api/events/${reschedulingEvent.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
startDate: `${newTargetDate}T${startTime}:00`,
endDate: `${newTargetDate}T${endTime}:00`
})
});
if (response.ok) {
setReschedulingEvent(null);
setNewTargetDate('');
setSelectedDay(null);
fetchData();
}
} catch (error) {
console.error('Failed to reschedule event:', error);
}
};
const filteredEvents = events.filter(e => {
const matchesDept = !filters.department || e.department === filters.department;
// We'd need to cast to any for targetedSections if not in interface
const matchesBatch = !filters.batch || (e as any).academicYears?.includes(filters.batch);
const matchesSection = !filters.section || (e as any).targetedSections?.includes(filters.section);
return matchesDept && matchesBatch && matchesSection;
});
const days = eachDayOfInterval({
start: startOfWeek(startOfMonth(currentDate)),
end: endOfWeek(endOfMonth(currentDate))
});
if (loading) {
return (
<div className="flex flex-col items-center justify-center p-20 gap-4">
<Loader2 className="w-10 h-10 text-brand-indigo animate-spin" />
<p className="text-sm font-black text-brand-indigo uppercase tracking-widest">Loading Institutional Schedule...</p>
</div>
);
}
return (
<div className={cn("space-y-8", compact && "space-y-4")}>
{/* Header & Advanced Filters */}
{!compact && (
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-brand-indigo rounded-2xl flex items-center justify-center text-white premium-shadow">
<CalendarIcon className="w-6 h-6" />
</div>
<div>
<h2 className="text-2xl font-black text-text-dark tracking-tight">Event & Notes Manager</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted">Master schedule audit & granular management</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-3">
<select
value={filters.department}
onChange={(e) => setFilters({...filters, department: e.target.value})}
className="bg-white border border-slate-100 px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-dark outline-none premium-shadow-sm"
>
<option value="">All Departments</option>
{Array.from(new Set(events.map(e => e.department))).sort().map(d => (
<option key={d} value={d}>{d}</option>
))}
</select>
<select
value={filters.batch}
onChange={(e) => setFilters({...filters, batch: e.target.value})}
className="bg-white border border-slate-100 px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-dark outline-none premium-shadow-sm"
>
<option value="">All Batches</option>
{['1st Year', '2nd Year', '3rd Year', '4th Year'].map(b => (
<option key={b} value={b}>{b}</option>
))}
</select>
<select
value={filters.section}
onChange={(e) => setFilters({...filters, section: e.target.value})}
className="bg-white border border-slate-100 px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-dark outline-none premium-shadow-sm"
>
<option value="">All Sections</option>
{['A', 'B', 'C', 'D'].map(s => (
<option key={s} value={s}>Section {s}</option>
))}
</select>
</div>
</div>
)}
<div className={cn(
"bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden",
compact && "rounded-none shadow-none border-none"
)}>
{/* Calendar Navigation */}
<div className={cn("flex items-center justify-between border-b border-slate-50", compact ? "p-4" : "p-8")}>
<h3 className={cn("font-black text-text-dark tracking-tighter", compact ? "text-xl" : "text-3xl")}>
{format(currentDate, 'MMMM yyyy')}
</h3>
<div className="flex gap-2">
<button
onClick={() => setCurrentDate(subMonths(currentDate, 1))}
className={cn("hover:bg-slate-50 rounded-2xl transition-all border border-slate-100", compact ? "p-2" : "p-3")}
>
<ChevronLeft className={cn("text-text-muted", compact ? "w-4 h-4" : "w-5 h-5")} />
</button>
<button
onClick={() => setCurrentDate(addMonths(currentDate, 1))}
className={cn("hover:bg-slate-50 rounded-2xl transition-all border border-slate-100", compact ? "p-2" : "p-3")}
>
<ChevronRight className={cn("text-text-muted", compact ? "w-4 h-4" : "w-5 h-5")} />
</button>
</div>
</div>
{/* Calendar Grid */}
<div className={cn(compact ? "p-4" : "p-8")}>
<div className="grid grid-cols-7 gap-px bg-slate-100 border border-slate-100 rounded-3xl overflow-hidden">
{['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'].map((day, i) => (
<div key={`${day}-${i}`} className="bg-white py-4 text-center text-[10px] font-black text-text-muted tracking-widest border-b border-slate-50">
{day}
</div>
))}
{days.map((day, idx) => {
const dayStr = format(day, 'yyyy-MM-dd');
const dayEvents = filteredEvents.filter(e => {
const date = parseDate(e.startDate);
return date && format(date, 'yyyy-MM-dd') === dayStr;
});
const dayNotes = notes.filter(n => {
const date = parseDate(n.targetDate);
return date && format(date, 'yyyy-MM-dd') === dayStr;
});
const isToday = isSameDay(day, new Date());
const isCurrentMonth = isSameMonth(day, currentDate);
const weekend = isWeekend(day);
return (
<div
key={day.toString()}
onClick={() => setSelectedDay(day)}
className={cn(
compact ? "min-h-[80px] p-2" : "min-h-[140px] p-4",
"bg-white hover:bg-slate-50/50 transition-all cursor-pointer group relative",
!isCurrentMonth && "opacity-30 grayscale pointer-events-none",
isToday && "bg-brand-glow/10"
)}
>
<div className="flex justify-between items-start mb-2">
<span className={cn(
"text-sm font-black",
isToday ? "text-brand-indigo" : "text-text-dark",
weekend && !isToday && "text-red-400"
)}>
{format(day, 'd')}
</span>
{weekend && isCurrentMonth && (
<span className="text-[8px] font-black text-red-300 uppercase tracking-widest">Holiday</span>
)}
</div>
{/* Day Content */}
<div className="space-y-1.5 overflow-y-auto max-h-[100px] custom-scrollbar pr-1">
{dayEvents.map(event => (
<div key={event.id} className="p-2.5 bg-brand-glow/20 border border-brand-indigo/10 rounded-xl transition-all hover:bg-brand-glow/40">
<p className="text-[10px] font-black text-brand-indigo leading-tight truncate">{event.title}</p>
<p className="text-[8px] font-black text-brand-indigo/60 uppercase tracking-widest mt-0.5">
{event.department === 'AI&DS' ? 'AI&DS' : event.department?.split(' ')[0]}
</p>
</div>
))}
{dayNotes.map(note => (
<div key={note.id} className="p-2 bg-amber-50 border border-amber-200 rounded-lg flex gap-2 items-start">
<MessageSquare className="w-2.5 h-2.5 text-amber-500 mt-0.5 shrink-0" />
<p className="text-[9px] font-bold text-amber-700 leading-tight">{note.content}</p>
</div>
))}
</div>
{/* Hover Action Indicator */}
<div className="absolute inset-0 border-2 border-brand-indigo/0 group-hover:border-brand-indigo/10 rounded-xl transition-all pointer-events-none" />
</div>
);
})}
</div>
</div>
</div>
<AnimatePresence>
{selectedDay && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 bg-slate-900/40 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white w-full max-w-md rounded-[2.5rem] premium-shadow-2xl overflow-hidden p-10 space-y-8"
>
{/* Modal Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-2xl font-black text-text-dark tracking-tight">Add Event Info</h3>
<p className="text-sm font-bold text-text-muted mt-1">Details for {format(selectedDay, 'yyyy-MM-dd')}</p>
</div>
<button onClick={() => setSelectedDay(null)} className="p-2 hover:bg-slate-50 rounded-xl transition-all">
<X className="w-6 h-6 text-slate-300" />
</button>
</div>
{/* Event List in Modal */}
<div className="space-y-4 max-h-[300px] overflow-y-auto custom-scrollbar pr-2">
{filteredEvents.filter(e => {
const date = parseDate(e.startDate);
return date && format(date, 'yyyy-MM-dd') === format(selectedDay, 'yyyy-MM-dd');
}).map(event => (
<div key={event.id} className="flex flex-col gap-4 p-5 bg-slate-50 border border-slate-100 rounded-[2rem] transition-all hover:bg-white hover:premium-shadow group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-1.5 h-8 bg-brand-indigo rounded-full" />
<div>
<p className="text-sm font-black text-text-dark">{event.title}</p>
<p className="text-[8px] font-black uppercase tracking-widest text-text-muted mt-0.5">{event.department} {event.type}</p>
</div>
</div>
<button
onClick={() => {
setReschedulingEvent(event);
setNewTargetDate(format(selectedDay, 'yyyy-MM-dd'));
const d = parseDate(event.startDate);
if (d) {
setStartTime(format(d, 'HH:mm'));
}
const ed = parseDate(event.endDate);
if (ed) {
setEndTime(format(ed, 'HH:mm'));
}
}}
className="p-2.5 bg-white rounded-xl border border-slate-100 text-brand-indigo hover:bg-brand-indigo hover:text-white transition-all premium-shadow-sm"
>
<Clock className="w-4 h-4" />
</button>
</div>
{reschedulingEvent?.id === event.id && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="pt-4 border-t border-slate-100 space-y-4"
>
<div className="space-y-4">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Reschedule Date</label>
<input
type="date"
value={newTargetDate}
onChange={(e) => setNewTargetDate(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-[10px] font-black text-text-dark focus:outline-none focus:ring-2 focus:ring-brand-indigo/10"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Start Time</label>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-[10px] font-black text-text-dark focus:outline-none focus:ring-2 focus:ring-brand-indigo/10"
/>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">End Time</label>
<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-[10px] font-black text-text-dark focus:outline-none focus:ring-2 focus:ring-brand-indigo/10"
/>
</div>
</div>
<button
onClick={handleReschedule}
className="w-full py-4 bg-brand-indigo text-white rounded-xl text-[9px] font-black uppercase tracking-widest premium-shadow active:scale-95 transition-all mt-2"
>
Apply Schedule Change
</button>
</div>
</motion.div>
)}
</div>
))}
</div>
{/* Note Input */}
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted">New Note</label>
<input
type="text"
value={newNote}
onChange={(e) => setNewNote(e.target.value)}
placeholder="e.g. AI Workshop"
className="w-full h-16 px-6 bg-white border-2 border-brand-indigo/30 rounded-2xl text-sm font-bold text-text-dark focus:outline-none focus:border-brand-indigo transition-all"
/>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-end gap-6 pt-4">
<button
onClick={() => setSelectedDay(null)}
className="text-sm font-black text-text-dark/60 hover:text-text-dark transition-colors"
>
Cancel
</button>
<button
onClick={handleAddNote}
disabled={!newNote.trim() || isSubmittingNote}
className="px-8 py-4 bg-brand-indigo text-white rounded-3xl font-black text-sm premium-shadow-sm hover:translate-y-[-2px] active:translate-y-[0px] transition-all disabled:opacity-50"
>
{isSubmittingNote ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Save Note'}
</button>
</div>
{/* Propose Action Fallback */}
<div className="pt-6 border-t border-slate-50 text-center">
<button
onClick={() => {
if (onProposeEvent) onProposeEvent(format(selectedDay, 'yyyy-MM-dd'));
setSelectedDay(null);
}}
className="text-[10px] font-black uppercase tracking-widest text-brand-indigo hover:underline"
>
+ Propose New Event for this date
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -1,408 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState, useMemo } from 'react';
import {
CheckCircle2,
Circle,
Loader2,
Users,
AlertTriangle,
PlusCircle,
ChevronRight,
ShieldCheck,
Split,
Globe,
Layers,
ArrowUpRight,
Calendar,
ChevronDown
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '../../context/AuthContext';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
type: string;
status: string;
department: string;
institution: string;
academicYears: string[];
targetedSections: string[];
category: string;
createdAt?: string;
updatedAt?: string;
targetedBatch?: string;
}
interface ClassMapping {
institution: string;
department: string;
academicYear: string;
sections: string[];
}
const CATEGORIES = [
{ id: 'GUEST_LECTURE', label: 'Guest Lectures', matchTypes: ['Guest Lecture', 'Workshop', 'Seminar'] },
{ id: 'ALUMNI_LECTURE', label: 'Alumni Lectures', matchTypes: ['Alumni Lecture'] },
{ id: 'CONFERENCE', label: 'Conference', matchTypes: ['Conference'] },
{ id: 'INDUSTRIAL_VISIT', label: 'Industrial Visit', matchTypes: ['Industrial Visit'] },
{ id: 'TECHFEST', label: 'Techfest', matchTypes: ['Techfest'] },
{ id: 'PROF_SOCIETY', label: 'Prof. Society Activities', matchTypes: ['Club', 'Professional Society'] },
{ id: 'INSTITUTIONAL', label: 'Institutional Events', matchTypes: ['Institutional'] },
];
const ALL_DEPARTMENTS = [
'AI&DS', 'AI&ML', 'CSE', 'CCE', 'CSBS', 'ECE', 'MECH', 'EE(VLSI)', 'BIOTECH', 'Placement Department', 'H&S Dept'
];
const DepartmentCard: React.FC<{
deptName: string;
events: Event[];
selectedBatch: string;
index: number;
isApplicable: boolean;
sections: string[];
batches: any[];
onIncompleteClick?: (data: any) => void;
isCompact?: boolean;
}> = ({ deptName, events, selectedBatch, index, isApplicable, sections, batches, onIncompleteClick, isCompact }) => {
const activeBatchesForYear = useMemo(() => {
return batches.filter(b =>
b.department === deptName &&
b.classes?.some((cls: string) => cls.startsWith(selectedBatch))
);
}, [batches, deptName, selectedBatch]);
const hasCustomBatches = activeBatchesForYear.length > 0;
const departmentEvents = events.filter(e => {
const status = e.status?.toUpperCase();
const isDeptMatch = e.department?.trim().toLowerCase() === deptName.trim().toLowerCase();
const isInstitutional = e.category?.toUpperCase() === 'INSTITUTIONAL';
return (isDeptMatch || isInstitutional) &&
(status === 'APPROVED' || status === 'COMPLETED' || status === 'PENDING_PR');
});
// Base requirement is 1 event per category per batch, or 1 if no custom batches exist
const getRequiredCount = (catId: string) => {
return 1;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05, duration: 0.5 }}
className={cn(
"bg-white rounded-[2rem] flex flex-col h-full relative group transition-all",
isCompact ? "border-none shadow-none p-0" : "border border-slate-100 overflow-hidden premium-shadow p-0"
)}
>
<div className={cn("p-6 border-b border-slate-50", isCompact ? "bg-white p-0 pb-4 border-none" : "bg-slate-50/30 p-8")}>
<div className="flex items-center justify-between gap-4 mb-2">
<h3 className={cn("font-black text-text-dark group-hover:text-brand-indigo transition-colors", isCompact ? "text-lg" : "text-2xl tracking-tight")}>{deptName}</h3>
<div className="flex items-center gap-2">
{!isApplicable ? (
<span className="px-3 py-1 bg-amber-50 text-amber-600 rounded-full border border-amber-100 text-[8px] font-black uppercase tracking-widest">Inert Batch</span>
) : hasCustomBatches ? (
<span className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-full border border-brand-indigo/10 text-[8px] font-black uppercase tracking-widest flex items-center gap-1.5">
<Layers className="w-2.5 h-2.5" />
{activeBatchesForYear.length} Batches
</span>
) : (
<span className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-full border border-brand-indigo/10 text-[8px] font-black uppercase tracking-widest flex items-center gap-1.5">
<Users className="w-2.5 h-2.5" />
{sections.length} Sections
</span>
)}
</div>
</div>
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">
Academic Year Profile: <span className="text-brand-indigo">{selectedBatch}</span>
</p>
</div>
<div className={cn("space-y-6 flex-1 bg-white", isCompact ? "p-0" : "p-8")}>
{CATEGORIES.map((cat) => {
const matchingEvents = departmentEvents.filter(event => {
const matchesType = cat.matchTypes.some(mt => mt.toLowerCase() === event.type?.toLowerCase());
const eventBatches = event.academicYears?.map(y => y.trim().toLowerCase()) || [];
const isInstitutional = event.category?.toUpperCase() === 'INSTITUTIONAL';
const batchMatches = eventBatches.length === 0 || eventBatches.includes(selectedBatch.trim().toLowerCase());
return matchesType && (isInstitutional ? batchMatches : eventBatches.includes(selectedBatch.trim().toLowerCase()));
});
const currentCount = matchingEvents.length;
let coveredSections = new Set<string>();
matchingEvents.forEach(e => {
if (e.category === 'INSTITUTIONAL' || !e.targetedSections || e.targetedSections.length === 0) {
sections.forEach(s => coveredSections.add(s));
} else {
e.targetedSections.forEach(s => coveredSections.add(s));
}
});
const eventsRequired = hasCustomBatches ? activeBatchesForYear.length : getRequiredCount(cat.id);
const missingSections = sections.filter(s => !coveredSections.has(s));
const coveredBatches = activeBatchesForYear.filter(b =>
matchingEvents.some(e => e.category === 'INSTITUTIONAL' || !e.targetedBatch || e.targetedBatch === b.name)
);
const missingBatches = activeBatchesForYear.filter(b => !coveredBatches.includes(b));
const isFullyCovered = hasCustomBatches
? (missingBatches.length === 0 && matchingEvents.length > 0)
: (missingSections.length === 0 && currentCount > 0) || (currentCount >= eventsRequired);
return (
<div
key={cat.id}
className="group/item relative"
onClick={() => !isFullyCovered && onIncompleteClick?.({
department: deptName,
eventType: cat.matchTypes[0],
academicYears: [selectedBatch]
})}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-4">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all border shadow-sm",
isFullyCovered ? "bg-emerald-500 border-emerald-400 text-white" : currentCount > 0 ? "bg-brand-indigo border-brand-indigo text-white" : "bg-white border-slate-100 text-slate-300"
)}>
{isFullyCovered ? <ShieldCheck className="w-5 h-5" /> : currentCount > 0 ? <Layers className="w-5 h-5" /> : <Circle className="w-5 h-5" />}
</div>
<div>
<span className={cn(
"text-sm font-black tracking-tight",
isFullyCovered ? "text-text-dark" : currentCount > 0 ? "text-brand-indigo" : "text-text-muted"
)}>
{cat.label}
</span>
<div className="flex items-center gap-2 mt-0.5">
<span className={cn(
"text-[8px] font-black uppercase tracking-[0.1em]",
isFullyCovered ? "text-emerald-500" : currentCount > 0 ? "text-brand-indigo" : "text-slate-400"
)}>
{isFullyCovered ? 'Complete Coverage' : missingSections.length > 0 && currentCount > 0 ? 'Partial Coverage' : 'Awaiting Proposals'}
</span>
</div>
</div>
</div>
<div className="flex flex-col items-end">
<div className={cn(
"px-3 py-1.5 rounded-xl text-[10px] font-black tracking-widest border transition-all",
isFullyCovered ? "bg-emerald-50 text-emerald-600 border-emerald-100" : currentCount > 0 ? "bg-brand-glow text-brand-indigo border-brand-indigo/10" : "bg-slate-50 text-text-muted border-slate-100"
)}>
{currentCount}/{eventsRequired}
</div>
</div>
</div>
<div className="h-1.5 bg-slate-100 rounded-full overflow-hidden mb-3">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${Math.min((currentCount / eventsRequired) * 100, 100)}%` }}
className={cn(
"h-full rounded-full transition-all duration-1000",
isFullyCovered ? "bg-emerald-500" : "bg-brand-indigo"
)}
/>
</div>
{hasCustomBatches && missingBatches.length > 0 && currentCount > 0 && (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="flex items-start gap-2 p-3 bg-red-50 rounded-xl border border-red-100 mt-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-500 shrink-0" />
<div>
<p className="text-[9px] font-black text-red-600 uppercase tracking-widest">Batches Left Out</p>
<p className="text-[10px] font-bold text-red-700">{missingBatches.map(b => b.name).join(', ')}</p>
</div>
</motion.div>
)}
{!hasCustomBatches && missingSections.length > 0 && currentCount > 0 && (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="flex items-start gap-2 p-3 bg-red-50 rounded-xl border border-red-100 mt-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-500 shrink-0" />
<div>
<p className="text-[9px] font-black text-red-600 uppercase tracking-widest">Sections Left Out</p>
<p className="text-[10px] font-bold text-red-700">{missingSections.join(', ')}</p>
</div>
</motion.div>
)}
</div>
);
})}
</div>
</motion.div>
);
};
export const InstitutionalChecklist: React.FC<{
isCompact?: boolean;
isGlobalView?: boolean;
onIncompleteClick?: (data: any) => void;
}> = ({ isCompact, isGlobalView, onIncompleteClick }) => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [classes, setClasses] = useState<ClassMapping[]>([]);
const [loading, setLoading] = useState(true);
const [selectedBatch, setSelectedBatch] = useState<string>('');
const [batches, setBatches] = useState<any[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(6);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const [eventsRes, classesRes, batchesRes] = await Promise.all([
fetch(API_BASE_URL + '/api/events'),
fetch(API_BASE_URL + '/api/classes'),
fetch(API_BASE_URL + '/api/batches')
]);
if (eventsRes.ok && classesRes.ok) {
const [eventsData, classesData] = await Promise.all([
eventsRes.json(),
classesRes.json()
]);
const batchesData = batchesRes.ok ? await batchesRes.json() : [];
setEvents(eventsData);
setClasses(classesData);
setBatches(batchesData);
const yearBatches = Array.from(new Set(classesData.map((c: any) => c.academicYear.trim()))).sort().reverse();
if (yearBatches.length > 0 && !selectedBatch) {
setSelectedBatch(yearBatches[0] as string);
}
}
} catch (error) {
console.error('Failed to fetch checklist data:', error);
} finally {
setLoading(false);
}
};
const allBatches = useMemo(() => {
return Array.from(new Set(classes.map(c => c.academicYear.trim()))).sort().reverse();
}, [classes]);
const filteredDepartments = useMemo(() => {
let depts = ALL_DEPARTMENTS;
if (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN') depts = ALL_DEPARTMENTS;
else if (isGlobalView && user?.role === 'HOD') depts = ALL_DEPARTMENTS;
else {
const userDepts = user?.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
depts = ALL_DEPARTMENTS.filter(d => userDepts.includes(d.toLowerCase()));
}
// Recent items at top logic: Sort departments by the date of their latest event
return [...depts].sort((a, b) => {
const latestA = events.filter(e => e.department === a || e.category === 'INSTITUTIONAL').reduce((max, e) => Math.max(max, new Date(e.updatedAt || e.createdAt || 0).getTime()), 0);
const latestB = events.filter(e => e.department === b || e.category === 'INSTITUTIONAL').reduce((max, e) => Math.max(max, new Date(e.updatedAt || e.createdAt || 0).getTime()), 0);
return latestB - latestA;
});
}, [user, isGlobalView, events]);
const currentDepts = filteredDepartments.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
</div>
);
}
return (
<div className={cn("space-y-8", isCompact ? "space-y-4" : "space-y-8")}>
{!isCompact && (
<div className="bg-white p-8 rounded-[3rem] border border-slate-100 premium-shadow flex flex-col md:flex-row md:items-center justify-between gap-8 relative overflow-hidden">
{/* Adjusted Badge Position to prevent overlapping with dropdown */}
<div className="absolute top-4 left-4">
<div className="px-3 py-1 bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100 flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse" />
<span className="text-[8px] font-black uppercase tracking-widest whitespace-nowrap">Audit Engine Live</span>
</div>
</div>
<div className="pt-6 md:pt-0">
<h2 className="text-3xl font-black text-text-dark tracking-tight mb-2">Academic Audit</h2>
<p className="text-xs font-bold text-text-muted flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-emerald-500" />
Monitoring department-wide compliance and session coverage.
</p>
</div>
<div className="flex items-center gap-4 bg-slate-50 p-4 rounded-3xl group relative min-w-[240px]">
<div className="flex flex-col flex-1">
<span className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1 ml-2">Audit Year</span>
<div className="relative">
<select
value={selectedBatch}
onChange={(e) => setSelectedBatch(e.target.value)}
className="w-full bg-transparent text-brand-indigo font-black text-lg outline-none cursor-pointer appearance-none relative z-10 pr-10"
>
{allBatches.map(batch => <option key={batch} value={batch}>{batch}</option>)}
</select>
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-5 h-5 text-brand-indigo pointer-events-none transition-transform group-hover:translate-y-[-40%]" />
</div>
</div>
<div className="w-12 h-12 bg-brand-indigo text-white rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-all shrink-0">
<Calendar className="w-6 h-6" />
</div>
</div>
</div>
)}
<div className={cn(
"grid",
isCompact
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10"
)}>
{currentDepts.map((deptName, idx) => {
const deptClasses = classes.filter(c => c.department === deptName && c.academicYear.trim() === selectedBatch.trim());
const sections = deptClasses.reduce((acc, c) => [...acc, ...c.sections], [] as string[]);
return (
<DepartmentCard
key={`${deptName}-${selectedBatch}`}
deptName={deptName}
events={events}
selectedBatch={selectedBatch}
index={idx}
isApplicable={deptClasses.length > 0}
sections={sections}
batches={batches}
onIncompleteClick={onIncompleteClick}
isCompact={isCompact}
/>
);
})}
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredDepartments.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[3, 6, 9, 12]}
/>
</div>
);
};

View File

@@ -1,798 +0,0 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Bell,
Trash2,
Plus,
Layers,
Settings,
Upload,
Calendar,
X,
CheckCircle2,
AlertTriangle,
Globe,
Loader2,
FileImage
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { API_BASE_URL } from '../../lib/config';
import { useDialog } from '../../context/DialogContext';
interface Notice {
id: string;
title: string;
message: string;
timestamp: string;
type: 'INFO' | 'URGENT' | 'DELAY';
image?: string;
expiresAt?: string;
}
interface SpecialEvent {
id: string;
title: string;
description: string;
link: string;
created_at: string;
}
export const ManageNoticesView: React.FC = () => {
const { showAlert, showConfirm } = useDialog();
// Navigation Tabs
const [activeTab, setActiveTab] = useState<'notices' | 'scroll' | 'special-events'>('notices');
// Data States
const [notices, setNotices] = useState<Notice[]>([]);
const [specialEvents, setSpecialEvents] = useState<SpecialEvent[]>([]);
const [scrollConfig, setScrollConfig] = useState({
isActive: false,
title: '',
message: ''
});
const [loading, setLoading] = useState(true);
// Modal States
const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false);
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
// Form States (Notice)
const [noticeTitle, setNoticeTitle] = useState('');
const [noticeMessage, setNoticeMessage] = useState('');
const [noticeType, setNoticeType] = useState<'INFO' | 'URGENT' | 'DELAY'>('INFO');
const [noticeImage, setNoticeImage] = useState<string | null>(null);
// Form States (Special Event)
const [eventTitle, setEventTitle] = useState('');
const [eventDesc, setEventDesc] = useState('');
const [eventLink, setEventLink] = useState('');
const [eventImage, setEventImage] = useState<string | null>(null);
const handleEventImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 1.5 * 1024 * 1024) {
showAlert('File Too Large', 'Please upload an image smaller than 1.5MB.', 'error');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setEventImage(reader.result as string);
};
reader.readAsDataURL(file);
}
};
useEffect(() => {
fetchInitialData();
}, []);
const fetchInitialData = async () => {
try {
setLoading(true);
const [noticesRes, eventsRes, settingsRes] = await Promise.all([
fetch(`${API_BASE_URL}/api/announcements`),
fetch(`${API_BASE_URL}/api/special-events`),
fetch(`${API_BASE_URL}/api/settings`)
]);
if (noticesRes.ok) {
const noticesData = await noticesRes.json();
setNotices(noticesData);
}
if (eventsRes.ok) {
const eventsData = await eventsRes.json();
setSpecialEvents(eventsData);
}
if (settingsRes.ok) {
const settingsData = await settingsRes.json();
if (settingsData.scroll_notification) {
setScrollConfig(settingsData.scroll_notification);
}
}
} catch (err) {
console.error('Failed to load notice configuration data:', err);
} finally {
setLoading(false);
}
};
// Sticky notice limits check
const handleOpenNoticeModal = () => {
if (notices.length >= 3) {
showAlert(
'Limit Reached',
'A maximum of 3 notices can be active on the campus notice board at any time. Please delete an existing notice first.',
'info'
);
return;
}
setNoticeTitle('');
setNoticeMessage('');
setNoticeType('INFO');
setNoticeImage(null);
setIsNoticeModalOpen(true);
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 1.5 * 1024 * 1024) {
showAlert('File Too Large', 'Please upload an image smaller than 1.5MB.', 'error');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setNoticeImage(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const handleAddNotice = async (e: React.FormEvent) => {
e.preventDefault();
if (!noticeTitle.trim() || !noticeMessage.trim()) {
showAlert('Required Fields', 'Please fill in both title and message.', 'error');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/announcements`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: noticeTitle.trim(),
message: noticeMessage.trim(),
type: noticeType,
image: noticeImage
})
});
if (response.ok) {
setIsNoticeModalOpen(false);
fetchInitialData();
showAlert('Success', 'Notice posted successfully to Campus Notice Board.', 'success');
}
} catch (err) {
console.error('Failed to post notice:', err);
showAlert('Error', 'Failed to save notice. Please try again.', 'error');
}
};
const handleDeleteNotice = async (id: string) => {
showConfirm(
'Remove Notice',
'Are you sure you want to remove this notice from the board?',
async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/announcements/${id}`, {
method: 'DELETE'
});
if (response.ok) {
fetchInitialData();
showAlert('Success', 'Notice removed successfully.', 'success');
}
} catch (err) {
console.error('Delete failed:', err);
showAlert('Error', 'Failed to delete notice.', 'error');
}
}
);
};
// Scroll Notification Updates
const handleSaveScrollConfig = async (e: React.FormEvent) => {
e.preventDefault();
if (scrollConfig.isActive && (!scrollConfig.title.trim() || !scrollConfig.message.trim())) {
showAlert('Required fields', 'Please enter a title and message for the scroll notification.', 'error');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: 'scroll_notification',
value: scrollConfig
})
});
if (response.ok) {
fetchInitialData();
showAlert('Success', 'Ancient scroll notification settings saved successfully.', 'success');
}
} catch (err) {
console.error('Save failed:', err);
showAlert('Error', 'Failed to update scroll settings.', 'error');
}
};
// Special Events Updates
const handleAddSpecialEvent = async (e: React.FormEvent) => {
e.preventDefault();
if (!eventTitle.trim() || !eventDesc.trim() || !eventLink.trim()) {
showAlert('Required Fields', 'Please fill in all special event details.', 'error');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/special-events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: eventTitle.trim(),
description: eventDesc.trim(),
link: eventLink.trim(),
created_by: 'ADMIN',
image: eventImage
})
});
if (response.ok) {
setIsEventModalOpen(false);
setEventTitle('');
setEventDesc('');
setEventLink('');
setEventImage(null);
fetchInitialData();
showAlert('Success', 'Special event added to the registry successfully.', 'success');
}
} catch (err) {
console.error('Post failed:', err);
showAlert('Error', 'Failed to create special event.', 'error');
}
};
const handleDeleteSpecialEvent = async (id: string) => {
showConfirm(
'Delete Special Event',
'Are you sure you want to remove this event from the registry?',
async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/special-events/${id}`, {
method: 'DELETE'
});
if (response.ok) {
fetchInitialData();
showAlert('Success', 'Special event deleted successfully.', 'success');
}
} catch (err) {
console.error('Delete failed:', err);
showAlert('Error', 'Failed to delete event.', 'error');
}
}
);
};
if (loading) {
return (
<div className="flex items-center justify-center p-24">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
</div>
);
}
return (
<div className="space-y-10 font-sans">
{/* Title */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-1">Notices & Banner Settings</h1>
<p className="text-text-muted font-black uppercase tracking-widest text-[10px]">Configure notice boards, scroll announcements, & special events</p>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-slate-100 gap-6">
<button
onClick={() => setActiveTab('notices')}
className={cn(
"pb-4 text-xs font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2",
activeTab === 'notices'
? "border-brand-indigo text-brand-indigo"
: "border-transparent text-text-muted hover:text-brand-indigo/70"
)}
>
<Bell className="w-4 h-4" />
Notice Board ({notices.length}/3)
</button>
<button
onClick={() => setActiveTab('scroll')}
className={cn(
"pb-4 text-xs font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2",
activeTab === 'scroll'
? "border-brand-indigo text-brand-indigo"
: "border-transparent text-text-muted hover:text-brand-indigo/70"
)}
>
<Settings className="w-4 h-4" />
Scroll Notification
</button>
<button
onClick={() => setActiveTab('special-events')}
className={cn(
"pb-4 text-xs font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2",
activeTab === 'special-events'
? "border-brand-indigo text-brand-indigo"
: "border-transparent text-text-muted hover:text-brand-indigo/70"
)}
>
<Layers className="w-4 h-4" />
Special Events
</button>
</div>
{/* Notices Tab */}
{activeTab === 'notices' && (
<div className="space-y-6">
<div className="flex justify-between items-center bg-slate-50 p-6 rounded-3xl border border-slate-100">
<div>
<h2 className="text-lg font-black text-text-dark">Active Campus Notices</h2>
<p className="text-xs text-text-muted font-bold">These cards are pinned to the student Campus Notice Board (Maximum 3 notices).</p>
</div>
<button
onClick={handleOpenNoticeModal}
className="flex items-center gap-2 bg-brand-indigo text-white px-5 py-3 rounded-2xl font-black text-xs uppercase tracking-widest hover:scale-[1.02] transition-all shadow-md shadow-brand-indigo/15"
>
<Plus className="w-4 h-4" />
Post Notice
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{notices.map(notice => (
<div key={notice.id} className="bg-white border border-slate-100 p-6 rounded-[2rem] premium-shadow flex flex-col justify-between min-h-[250px] relative">
<div>
<div className="flex items-center justify-between mb-4">
<span className={cn(
"px-2.5 py-1 text-[8px] font-black uppercase tracking-wider rounded",
notice.type === 'URGENT' ? 'bg-red-550/10 text-rose-600' :
notice.type === 'DELAY' ? 'bg-amber-500/10 text-amber-600' :
'bg-slate-100 text-slate-600'
)}>
{notice.type}
</span>
<button
onClick={() => handleDeleteNotice(notice.id)}
className="p-2 hover:bg-rose-50 text-slate-400 hover:text-rose-500 rounded-xl transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{notice.image && (
<img
src={notice.image}
alt="Notice Attachment"
className="w-full h-36 object-cover rounded-2xl mb-4 border border-slate-100"
/>
)}
<h3 className="text-base font-black text-text-dark tracking-tight mb-2 leading-tight">{notice.title}</h3>
<p className="text-xs text-text-muted font-medium leading-relaxed mb-4">{notice.message}</p>
</div>
<div className="text-[10px] font-bold text-slate-400 border-t border-slate-50 pt-3 mt-auto">
{new Date(notice.timestamp).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' })}
</div>
</div>
))}
{notices.length === 0 && (
<div className="col-span-full py-16 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2.5rem]">
No active announcements on the notice board.
</div>
)}
</div>
</div>
)}
{/* Scroll Notifications Tab */}
{activeTab === 'scroll' && (
<div className="max-w-xl bg-white border border-slate-100 premium-shadow rounded-[2rem] p-8 space-y-6">
<div>
<h2 className="text-lg font-black text-text-dark">Ancient Scroll Notifications</h2>
<p className="text-xs text-text-muted font-bold">Configure a custom styled scroll overlay alert visible on the student home page.</p>
</div>
<form onSubmit={handleSaveScrollConfig} className="space-y-5">
{/* Toggle Status */}
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-100">
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Display Notification Scroll</p>
<p className="text-[9px] font-bold text-text-muted">Turns the scroll visibility on/off for students</p>
</div>
<button
type="button"
onClick={() => setScrollConfig({...scrollConfig, isActive: !scrollConfig.isActive})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
scrollConfig.isActive ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
scrollConfig.isActive ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
{/* Title */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Scroll Title</label>
<input
type="text"
placeholder="E.g., The Grand Decrees of RIT"
value={scrollConfig.title}
onChange={e => setScrollConfig({...scrollConfig, title: e.target.value})}
className="w-full bg-slate-50 border border-slate-200 rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800"
required={scrollConfig.isActive}
/>
</div>
{/* Message */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Scroll Decree Message</label>
<textarea
rows={4}
placeholder="Enter message to display on the parchment scroll..."
value={scrollConfig.message}
onChange={e => setScrollConfig({...scrollConfig, message: e.target.value})}
className="w-full bg-slate-50 border border-slate-200 rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800"
required={scrollConfig.isActive}
/>
</div>
<button
type="submit"
className="w-full py-4 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center justify-center gap-2"
>
<CheckCircle2 className="w-4 h-4" />
Save Configuration
</button>
</form>
</div>
)}
{/* Special Events Tab */}
{activeTab === 'special-events' && (
<div className="space-y-6">
<div className="flex justify-between items-center bg-slate-50 p-6 rounded-3xl border border-slate-100">
<div>
<h2 className="text-lg font-black text-text-dark">Special Events Registry</h2>
<p className="text-xs text-text-muted font-bold">These events are rendered in the modern slanted registry section of the student dashboard.</p>
</div>
<button
onClick={() => setIsEventModalOpen(true)}
className="flex items-center gap-2 bg-brand-indigo text-white px-5 py-3 rounded-2xl font-black text-xs uppercase tracking-widest hover:scale-[1.02] transition-all shadow-md shadow-brand-indigo/15"
>
<Plus className="w-4 h-4" />
Add Special Event
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{specialEvents.map(event => (
<div key={event.id} className="bg-white border border-slate-100 p-6 rounded-[2rem] premium-shadow flex flex-col justify-between min-h-[220px]">
<div>
<div className="flex items-center justify-between mb-4">
<span className="px-2.5 py-1 bg-cyan-50 text-cyan-600 border border-cyan-100 text-[8px] font-black uppercase tracking-wider rounded">
Special Event
</span>
<button
onClick={() => handleDeleteSpecialEvent(event.id)}
className="p-2 hover:bg-rose-50 text-slate-400 hover:text-rose-500 rounded-xl transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<h3 className="text-base font-black text-text-dark tracking-tight mb-2 leading-tight">{event.title}</h3>
<p className="text-xs text-text-muted font-medium leading-relaxed mb-4 line-clamp-3">{event.description}</p>
</div>
<div className="border-t border-slate-50 pt-3 mt-auto flex items-center justify-between">
<a
href={event.link}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-black text-brand-indigo uppercase tracking-wider hover:underline flex items-center gap-1.5"
>
View Registry Link
<Globe className="w-3.5 h-3.5" />
</a>
</div>
</div>
))}
{specialEvents.length === 0 && (
<div className="col-span-full py-16 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2.5rem]">
No special events currently registered.
</div>
)}
</div>
</div>
)}
{/* Notice Dialog Modal */}
<AnimatePresence>
{isNoticeModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsNoticeModalOpen(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-lg bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[90vh]"
>
<form onSubmit={handleAddNotice} className="flex flex-col overflow-hidden">
{/* Header */}
<div className="p-8 bg-brand-navy text-white flex justify-between items-start shrink-0">
<div>
<h3 className="text-2xl font-black tracking-tight">Post New Notice</h3>
<p className="text-white/60 text-xs font-medium mt-1">Configure campus notice board bulletin.</p>
</div>
<button
type="button"
onClick={() => setIsNoticeModalOpen(false)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<div className="p-8 space-y-5 overflow-y-auto custom-scrollbar flex-1 max-h-[60vh]">
{/* Title */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Notice Title</label>
<input
type="text"
required
value={noticeTitle}
onChange={e => setNoticeTitle(e.target.value)}
placeholder="Enter short, descriptive title"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
/>
</div>
{/* Message */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Notice Message</label>
<textarea
rows={3}
required
value={noticeMessage}
onChange={e => setNoticeMessage(e.target.value)}
placeholder="Write announcement body..."
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
/>
</div>
{/* Alert Type */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Alert Casing</label>
<select
value={noticeType}
onChange={e => setNoticeType(e.target.value as any)}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all appearance-none"
>
<option value="INFO">Information (INFO)</option>
<option value="URGENT">Urgent Announcement (URGENT)</option>
<option value="DELAY">Schedule Delay / Change (DELAY)</option>
</select>
</div>
{/* Image Dropzone */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Notice Banner Image</label>
<div className="border-2 border-dashed border-slate-200 hover:border-brand-indigo/40 rounded-2xl p-6 bg-slate-50/50 flex flex-col items-center justify-center text-center relative group transition-colors">
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full z-10"
/>
{noticeImage ? (
<div className="space-y-3 z-20">
<img
src={noticeImage}
alt="Notice preview"
className="max-h-28 object-cover rounded-xl border border-slate-200"
/>
<button
type="button"
onClick={() => setNoticeImage(null)}
className="text-[10px] font-black text-rose-500 hover:text-rose-700 uppercase tracking-widest block mx-auto transition-all"
>
Remove Banner
</button>
</div>
) : (
<div className="space-y-2 pointer-events-none">
<FileImage className="w-8 h-8 text-slate-300 mx-auto group-hover:text-brand-indigo/60 transition-colors" />
<div>
<span className="text-[10px] font-black text-brand-indigo uppercase tracking-wider">Upload banner image</span>
<p className="text-[9px] text-slate-400 font-bold mt-0.5">JPEG, PNG up to 1.5MB</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Footer */}
<div className="shrink-0 w-full">
<button
type="submit"
className="w-full bg-brand-navy hover:bg-[#0c365c] text-white py-5 font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center"
>
Post Notice Bulletin
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
{/* Special Event Dialog Modal */}
<AnimatePresence>
{isEventModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsEventModalOpen(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-lg bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[90vh]"
>
<form onSubmit={handleAddSpecialEvent} className="flex flex-col overflow-hidden">
{/* Header */}
<div className="p-8 bg-brand-navy text-white flex justify-between items-start shrink-0">
<div>
<h3 className="text-2xl font-black tracking-tight">Add Special Event</h3>
<p className="text-white/60 text-xs font-medium mt-1">Configure special event registry entry.</p>
</div>
<button
type="button"
onClick={() => setIsEventModalOpen(false)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<div className="p-8 space-y-5 overflow-y-auto custom-scrollbar flex-1 max-h-[60vh]">
{/* Title */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Event Title</label>
<input
type="text"
required
value={eventTitle}
onChange={e => setEventTitle(e.target.value)}
placeholder="Hackathon, techfest, etc."
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
/>
</div>
{/* Description */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Event Description</label>
<textarea
rows={3}
required
value={eventDesc}
onChange={e => setEventDesc(e.target.value)}
placeholder="Write brief description..."
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
/>
</div>
{/* Link */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Registry External Link</label>
<input
type="url"
required
value={eventLink}
onChange={e => setEventLink(e.target.value)}
placeholder="https://example.com/registration"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all"
/>
</div>
{/* Image Dropzone */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Event Cover Image (Optional)</label>
<div className="border-2 border-dashed border-slate-200 hover:border-brand-indigo/40 rounded-2xl p-6 bg-slate-50/50 flex flex-col items-center justify-center text-center relative group transition-colors">
<input
type="file"
accept="image/*"
onChange={handleEventImageUpload}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full z-10"
/>
{eventImage ? (
<div className="space-y-3 z-20">
<img
src={eventImage}
alt="Event preview"
className="max-h-28 object-cover rounded-xl border border-slate-200"
/>
<button
type="button"
onClick={() => setEventImage(null)}
className="text-[10px] font-black text-rose-500 hover:text-rose-700 uppercase tracking-widest block mx-auto transition-all"
>
Remove Cover Image
</button>
</div>
) : (
<div className="space-y-2 pointer-events-none">
<FileImage className="w-8 h-8 text-slate-300 mx-auto group-hover:text-brand-indigo/60 transition-colors" />
<div>
<span className="text-[10px] font-black text-brand-indigo uppercase tracking-wider">Upload cover image</span>
<p className="text-[9px] text-slate-400 font-bold mt-0.5">JPEG, PNG up to 1.5MB</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Footer */}
<div className="shrink-0 w-full">
<button
type="submit"
className="w-full bg-brand-navy hover:bg-[#0c365c] text-white py-5 font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center"
>
Register Special Event
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -1,433 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Calendar as CalendarIcon,
Clock,
CheckCircle2,
AlertCircle,
TrendingUp,
Users,
MapPin,
ChevronRight,
Plus,
ShieldCheck,
Zap,
ArrowUpRight,
Target,
BarChart3,
LayoutDashboard,
Bell,
GraduationCap,
Save,
Check
} from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { InstitutionalChecklist } from './InstitutionalChecklist';
import { StatusTimeline, type Event } from './EventStatusTimeline';
import { InstitutionalCalendar } from './InstitutionalCalendar';
import { VenueTimeline } from './VenueTimeline';
import { PrincipalMasterChecklist } from './PrincipalMasterChecklist';
import { VenuesAtGlance } from './VenuesAtGlance';
import { FacultyEventManagement } from './FacultyEventManagement';
import { cn } from '../../lib/utils';
import { format, formatDistanceToNow } from 'date-fns';
interface OverviewProps {
userEvents: any[];
onNavigate: (id: string) => void;
onIncompleteClick: (data: any) => void;
onCalendarPropose: (date: string) => void;
onClubEventClick: (data: any) => void;
onEditEvent?: (event: any) => void;
}
export const Overview: React.FC<OverviewProps> = ({
userEvents,
onNavigate,
onIncompleteClick,
onCalendarPropose,
onClubEventClick,
onEditEvent
}) => {
const { user } = useAuth();
const [stats, setStats] = useState({
upcoming: 0,
pending: 0,
completed: 0,
budget: '₹0'
});
const [alerts, setAlerts] = useState<Array<{type: string, msg: string, time: string}>>([]);
const { login } = useAuth();
const [strengthInput, setStrengthInput] = useState<number>(user?.classStrength || 0);
const [isSavingStrength, setIsSavingStrength] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const handleSaveStrength = async () => {
if (!user) return;
setIsSavingStrength(true);
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/${user.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...user, classStrength: strengthInput })
});
if (response.ok) {
const updatedUser = await response.json();
login(updatedUser);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
}
} catch (error) {
console.error('Failed to update class strength:', error);
} finally {
setIsSavingStrength(false);
}
};
useEffect(() => {
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const calculateStats = (data: any[]) => {
if (!Array.isArray(data)) return;
const now = new Date();
const upcoming = data.filter(e => {
const d = parseDate(e.startDate);
return e.status === 'APPROVED' && d && d > now;
}).length;
const pending = data.filter(e => e.status === 'REQUESTED' || e.status === 'PENDING_PR').length;
const completed = data.filter(e => {
const d = parseDate(e.endDate);
return e.status === 'APPROVED' && d && d < now;
}).length;
const totalBudget = data
.filter(e => e.status === 'APPROVED')
.reduce((acc, e) => acc + (e.budget || 0), 0);
let budgetDisplay = `${totalBudget.toLocaleString()}`;
if (totalBudget >= 100000) {
budgetDisplay = `${(totalBudget / 100000).toFixed(1)}L`;
} else if (totalBudget >= 1000) {
budgetDisplay = `${(totalBudget / 1000).toFixed(1)}K`;
}
setStats({
upcoming,
pending,
completed,
budget: budgetDisplay
});
const generatedAlerts: any[] = [];
const sortedEvents = [...data].sort((a, b) => {
return new Date(b.updatedAt || b.createdAt || 0).getTime() - new Date(a.updatedAt || a.createdAt || 0).getTime();
});
for (const event of sortedEvents) {
if (generatedAlerts.length >= 4) break;
const dateToUse = event.updatedAt || event.createdAt;
if (!dateToUse) continue;
let timeStr = formatDistanceToNow(new Date(dateToUse), { addSuffix: true })
.replace('about ', '')
.replace(' minutes', 'm')
.replace(' minute', 'm')
.replace(' hours', 'h')
.replace(' hour', 'h')
.replace(' days', 'd')
.replace(' day', 'd');
if (event.conflictMessage) {
generatedAlerts.push({ type: 'warning', msg: `Conflict: ${event.title}`, time: timeStr });
} else if (event.status === 'REJECTED') {
generatedAlerts.push({ type: 'error', msg: `Rejected: ${event.title}`, time: timeStr });
} else if (event.status === 'APPROVED') {
generatedAlerts.push({ type: 'success', msg: `Approved: ${event.title}`, time: timeStr });
} else if (event.status === 'REQUESTED' || event.status === 'PENDING_PR') {
generatedAlerts.push({ type: 'info', msg: `New proposal: ${event.title}`, time: timeStr });
}
}
if (generatedAlerts.length === 0) {
generatedAlerts.push({ type: 'info', msg: 'No recent activity', time: 'Just now' });
}
setAlerts(generatedAlerts);
};
if (userEvents && userEvents.length > 0) {
calculateStats(userEvents);
} else {
fetch(API_BASE_URL + '/api/events')
.then(res => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
})
.then(data => {
if (Array.isArray(data)) {
const statsData = (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN')
? data
: data.filter(e => {
if (e.proposer?.email === user?.email) return true;
const userDepts = user?.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
return userDepts.includes(e.department?.trim().toLowerCase());
});
calculateStats(statsData);
}
})
.catch(err => console.error("Stats fetch error:", err));
}
}, [userEvents, user]);
return (
<div className="space-y-8 animate-in fade-in duration-700">
{/* Welcome Bar - Subtle */}
<div className="flex items-center justify-between mb-2">
<div>
<h2 className="text-2xl font-black text-brand-navy tracking-tight">
Welcome back, <span className="text-brand-indigo">{user?.fullName?.split(' ')[0]}</span>
</h2>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">
Institutional Dashboard {format(new Date(), 'EEEE, MMMM do')}
</p>
</div>
</div>
{/* Principal Master Checklist - Only for Principal Desk */}
{user?.role === 'PRINCIPAL' && (
<PrincipalMasterChecklist />
)}
{user?.role === 'FACULTY' && user?.isClassIncharge && (
<div className="bg-brand-indigo/5 border border-brand-indigo/10 rounded-[1.5rem] p-6 mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-brand-indigo text-white flex items-center justify-center shrink-0 shadow-sm">
<GraduationCap className="w-6 h-6" />
</div>
<div>
<h3 className="text-lg font-black text-brand-navy tracking-tight">Class Incharge Dashboard</h3>
<p className="text-xs font-bold text-slate-500 mt-1">
Assigned Class: <span className="text-brand-indigo uppercase tracking-wider">{user.inchargeClass} {user.inchargeBatch} Sec {user.inchargeSection}</span>
</p>
</div>
</div>
<div className="flex items-center gap-4 bg-white p-2 pr-4 rounded-xl shadow-sm border border-slate-100">
<div className="flex flex-col pl-3">
<label className="text-[9px] font-black uppercase tracking-widest text-slate-400">Class Strength</label>
<input
type="number"
min="1"
value={strengthInput || ''}
onChange={(e) => setStrengthInput(parseInt(e.target.value) || 0)}
className="w-20 text-lg font-black text-brand-navy focus:outline-none placeholder:text-slate-200"
placeholder="0"
/>
</div>
<button
onClick={handleSaveStrength}
disabled={isSavingStrength || strengthInput === user.classStrength}
className={cn(
"p-2.5 rounded-lg transition-all flex items-center justify-center text-white min-w-[40px]",
saveSuccess ? "bg-emerald-500" :
strengthInput !== user.classStrength ? "bg-brand-indigo hover:shadow-md hover:scale-105 active:scale-95" :
"bg-slate-200 text-slate-400"
)}
>
{isSavingStrength ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> :
saveSuccess ? <Check className="w-4 h-4" /> :
<Save className="w-4 h-4" />}
</button>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Row 1: Live Status and Analytics */}
<div className="lg:col-span-12 flex flex-col gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="w-full">
<div className="card-widget">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Live Status
</h3>
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[10px] font-black text-emerald-500 uppercase tracking-widest">Active</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Upcoming', value: stats.upcoming, icon: CalendarIcon, color: 'text-indigo-600', bg: 'bg-indigo-50' },
{ label: 'Pending', value: stats.pending, icon: Bell, color: 'text-amber-600', bg: 'bg-amber-50' },
{ label: 'Finished', value: stats.completed, icon: CheckCircle2, color: 'text-emerald-600', bg: 'bg-emerald-50' },
{ label: 'Budget', value: stats.budget, icon: TrendingUp, color: 'text-slate-600', bg: 'bg-slate-50' },
].map((stat) => (
<div key={stat.label} className={cn("p-4 rounded-2xl border border-slate-100/50", stat.bg)}>
<div className="flex items-center gap-3 mb-2">
<stat.icon className={cn("w-4 h-4", stat.color)} />
<span className="text-[9px] font-black uppercase tracking-widest text-slate-400">{stat.label}</span>
</div>
<p className="text-xl font-black text-brand-navy">{stat.value}</p>
</div>
))}
</div>
<div className="mt-6 p-4 bg-brand-glow rounded-2xl border border-brand-indigo/10">
<div className="flex items-center gap-3 mb-2">
<Zap className="w-4 h-4 text-brand-indigo" />
<span className="text-[10px] font-black uppercase tracking-widest text-brand-indigo">Quick Insight</span>
</div>
<p className="text-[11px] font-bold text-brand-indigo/70 leading-relaxed">
{stats.pending > 0
? `You have ${stats.pending} proposals waiting for your review. Processing them now ensures timely venue booking.`
: "All systems clear. Your departmental compliance is currently at 100% for the current cycle."}
</p>
</div>
</div>
</div>
<div className="w-full">
<div className="card-widget">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Analytics
</h3>
<BarChart3 className="w-4 h-4 text-slate-300" />
</div>
<div className="flex flex-col items-center justify-center py-4">
<div className="relative w-32 h-32 flex items-center justify-center">
<svg className="w-full h-full -rotate-90">
<circle cx="64" cy="64" r="58" fill="transparent" stroke="#f1f5f9" strokeWidth="12" />
<circle cx="64" cy="64" r="58" fill="transparent" stroke="#4f46e5" strokeWidth="12" strokeDasharray="364.4" strokeDashoffset={364.4 * (1 - 0.65)} strokeLinecap="round" />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-black text-brand-navy">{userEvents.length}</span>
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Events</span>
</div>
</div>
<div className="grid grid-cols-2 gap-x-8 gap-y-3 mt-8 w-full">
{[
{ label: 'Technical', color: 'bg-brand-indigo' },
{ label: 'Cultural', color: 'bg-emerald-500' },
{ label: 'Sports', color: 'bg-amber-500' },
{ label: 'Other', color: 'bg-slate-400' },
].map(cat => (
<div key={cat.label} className="flex items-center gap-2">
<div className={cn("w-2 h-2 rounded-full", cat.color)} />
<span className="text-[10px] font-bold text-slate-500">{cat.label}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Today's Tasks / My Proposals */}
<div className="lg:col-span-12">
{(user?.role === 'PRINCIPAL' || user?.role === 'ADMIN') ? (
<div className="card-widget flex flex-col">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Today's Tasks
<span className="bg-slate-100 text-slate-500 text-[10px] px-2 py-0.5 rounded-full">
{stats.pending}
</span>
</h3>
<button
onClick={() => onNavigate('checklist')}
className="text-[10px] font-black text-brand-indigo uppercase tracking-widest hover:underline"
>
See All
</button>
</div>
<div className="w-full">
<InstitutionalChecklist isCompact onIncompleteClick={onIncompleteClick} />
</div>
</div>
) : (
<FacultyEventManagement
events={userEvents}
onEditEvent={onEditEvent || (() => {})}
/>
)}
</div>
<div className="lg:col-span-12">
<VenuesAtGlance events={userEvents} />
</div>
{/* Row 2: Upcoming Events & Alerts */}
<div className="lg:col-span-8">
<div className="card-widget">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-black text-brand-navy tracking-tight">Institutional Schedule</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">Unified view of all approved events & academic notes</p>
</div>
<div className="flex gap-2">
<div className="px-3 py-1.5 bg-slate-50 rounded-xl border border-slate-100">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Master Feed</span>
</div>
</div>
</div>
<div className="rounded-[1.5rem] overflow-hidden border border-slate-50">
<InstitutionalCalendar compact onProposeEvent={onCalendarPropose} />
</div>
</div>
</div>
<div className="lg:col-span-4">
<div className="card-widget h-full">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Recent Alerts
</h3>
<button className="text-[10px] font-black text-brand-indigo uppercase tracking-widest hover:underline">
Clear All
</button>
</div>
<div className="space-y-4">
{alerts.map((alert, i) => (
<div key={i} className="flex gap-4 group">
<div className={cn(
"w-1 h-8 rounded-full mt-1 shrink-0",
alert.type === 'success' ? 'bg-emerald-500' :
alert.type === 'warning' ? 'bg-amber-500' :
alert.type === 'error' ? 'bg-red-500' : 'bg-brand-indigo'
)} />
<div>
<p className="text-xs font-bold text-brand-navy leading-snug group-hover:text-brand-indigo transition-colors">{alert.msg}</p>
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">{alert.time}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,68 +0,0 @@
import React from 'react';
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
import { cn } from '../../lib/utils';
interface PaginationProps {
currentPage: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (page: number) => void;
onItemsPerPageChange: (items: number) => void;
itemsPerPageOptions?: number[];
}
export const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalItems,
itemsPerPage,
onPageChange,
onItemsPerPageChange,
itemsPerPageOptions = [5, 10, 20, 50]
}) => {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const rangeStart = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const rangeEnd = Math.min(currentPage * itemsPerPage, totalItems);
if (totalItems === 0) return null;
return (
<div className="flex items-center justify-end gap-8 py-4 px-8 border-t border-slate-50 bg-slate-50/10">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Rows per page</span>
<div className="relative group">
<select
value={itemsPerPage}
onChange={(e) => onItemsPerPageChange(Number(e.target.value))}
className="bg-white border border-slate-200 rounded-xl px-4 py-2 text-[11px] font-black text-brand-navy appearance-none pr-10 cursor-pointer hover:border-brand-indigo/30 transition-all focus:outline-none premium-shadow-sm"
>
{itemsPerPageOptions.map(val => (
<option key={val} value={val}>{val}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400 pointer-events-none" />
</div>
</div>
<div className="text-[11px] font-black text-brand-navy tracking-tight min-w-[80px] text-center">
{rangeStart}-{rangeEnd} <span className="text-slate-300 mx-1">of</span> {totalItems}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
);
};

View File

@@ -1,152 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import { motion } from 'framer-motion';
import {
Calendar,
CheckCircle2,
Circle,
Clock,
AlertCircle,
ShieldCheck,
ChevronRight,
Loader2
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { API_BASE_URL } from '../../lib/config';
import { INSTITUTIONAL_EVENTS } from '../../constants/institutionalEvents';
import { getCurrentAcademicYear } from '../../lib/dateUtils';
interface Event {
id: number;
title: string;
status: string;
academicYears: string[];
startDate: string;
}
export const PrincipalMasterChecklist: React.FC = () => {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const currentAcademicYear = getCurrentAcademicYear();
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (err) {
console.error('Failed to fetch events:', err);
} finally {
setLoading(false);
}
};
const getEventStatus = (eventName: string) => {
const currentYear = new Date().getFullYear();
const matchingEvents = events.filter(e => {
const matchesName = e.title.toLowerCase().includes(eventName.toLowerCase());
const eventDate = new Date(e.startDate);
// Check if event is within the current academic cycle (roughly current year +/- 1)
const matchesCycle = eventDate.getFullYear() === currentYear ||
(eventDate.getFullYear() === currentYear - 1 && eventDate.getMonth() >= 5) ||
(eventDate.getFullYear() === currentYear + 1 && eventDate.getMonth() < 5);
return matchesName && matchesCycle;
});
if (matchingEvents.some(e => e.status?.toUpperCase() === 'COMPLETED')) return 'COMPLETED';
if (matchingEvents.some(e => e.status?.toUpperCase() === 'APPROVED')) return 'APPROVED';
if (matchingEvents.some(e => e.status?.toUpperCase() === 'PENDING_PR' || e.status?.toUpperCase() === 'REQUESTED')) return 'PENDING';
return 'NOT_SCHEDULED';
};
const oddSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'ODD');
const evenSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'EVEN');
const SemesterCard = ({ title, events }: { title: string, events: typeof INSTITUTIONAL_EVENTS }) => (
<div className="flex-1 bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden flex flex-col">
<div className="p-8 border-b border-slate-50 bg-slate-50/30">
<h3 className="text-xl font-black text-brand-navy tracking-tight">{title}</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">Institutional Milestones</p>
</div>
<div className="p-8 space-y-4 flex-1 overflow-y-auto max-h-[500px] custom-scrollbar">
{events.map((event, i) => {
const status = getEventStatus(event.name);
return (
<motion.div
key={event.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="flex items-center justify-between group p-4 hover:bg-slate-50 rounded-2xl transition-all"
>
<div className="flex items-center gap-4">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
status === 'COMPLETED' ? "bg-emerald-500 text-white" :
status === 'APPROVED' ? "bg-brand-indigo text-white" :
status === 'PENDING' ? "bg-amber-500 text-white" : "bg-slate-100 text-slate-300"
)}>
{status === 'COMPLETED' ? <CheckCircle2 className="w-5 h-5" /> :
status === 'APPROVED' ? <Calendar className="w-5 h-5" /> :
status === 'PENDING' ? <Clock className="w-5 h-5" /> : <Circle className="w-5 h-5" />}
</div>
<div>
<p className="text-sm font-black text-brand-navy group-hover:text-brand-indigo transition-colors">{event.name}</p>
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">TARGET: {event.month}</p>
</div>
</div>
<div className={cn(
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
status === 'COMPLETED' ? "bg-emerald-50 text-emerald-600 border-emerald-100" :
status === 'APPROVED' ? "bg-brand-glow text-brand-indigo border-brand-indigo/10" :
status === 'PENDING' ? "bg-amber-50 text-amber-600 border-amber-100" : "bg-slate-50 text-slate-400 border-slate-100"
)}>
{status.replace('_', ' ')}
</div>
</motion.div>
);
})}
</div>
</div>
);
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
</div>
);
}
return (
<div className="space-y-8">
<div className="bg-brand-navy p-10 rounded-[3rem] text-white flex flex-col md:flex-row md:items-center justify-between gap-8 relative overflow-hidden">
<div className="relative z-10">
<h2 className="text-3xl font-black tracking-tight mb-2">Institutional Master Checklist</h2>
<p className="text-sm font-medium text-white/60 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-emerald-400" />
Strategic event tracking for Academic Year {currentAcademicYear}
</p>
</div>
<div className="relative z-10 flex items-center gap-4">
<div className="px-5 py-3 bg-white/10 rounded-2xl border border-white/10">
<span className="text-[10px] font-black uppercase tracking-[0.2em]">Principal Desk Control</span>
</div>
</div>
<div className="absolute top-0 right-0 w-64 h-64 bg-brand-indigo/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<SemesterCard title="ODD Semester Milestones" events={oddSemesterEvents} />
<SemesterCard title="EVEN Semester Milestones" events={evenSemesterEvents} />
</div>
</div>
);
};

View File

@@ -1,585 +0,0 @@
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 { useDialog } from '../../context/DialogContext';
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();
const { showAlert } = useDialog();
// 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 [settings, setSettings] = useState<Record<string, any>>({});
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;
}
}
});
const mapDepartmentToDomain = (dept: string): string => {
if (!dept) return '';
const d = dept.trim().toUpperCase();
if (d === 'AI&DS' || d === 'AIDS' || d === 'AI & DS') return 'AIDS';
if (d === 'AI&ML' || d === 'AIML' || d === 'AI & ML') return 'AIML';
if (d === 'MECH' || d === 'MECHANICAL') return 'Mechanical';
if (d === 'BIOTECH' || d === 'BIO-TECH') return 'Bio-tech';
if (d === 'EE(VLSI)' || d === 'VLSI') return 'VLSI';
return dept.trim();
};
const mapCategoryToStudentCategory = (dbCat: string, dbType: string): 'TECHNICAL' | 'NON-TECHNICAL' | 'WORKSHOP' | 'CENTRE-ACTIVITY' => {
const cat = (dbCat || '').toUpperCase();
if (cat === 'TECHNICAL' || cat === 'NON-TECHNICAL' || cat === 'WORKSHOP' || cat === 'CENTRE-ACTIVITY') {
return cat as any;
}
if (cat === 'CENTRE') {
return 'CENTRE-ACTIVITY';
}
if (cat === 'ACADEMIC') {
return 'TECHNICAL';
}
const type = (dbType || '').toLowerCase();
if (type === 'workshop') {
return 'WORKSHOP';
}
if (type === 'cultural event' || type === 'cultural') {
return 'NON-TECHNICAL';
}
return 'TECHNICAL';
};
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: mapCategoryToStudentCategory(dbEvent.category, dbEvent.eventType || dbEvent.type || ''),
domain: mapDepartmentToDomain(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, setRes] = await Promise.all([
fetch(`${API_BASE_URL}/api/special-events`),
fetch(`${API_BASE_URL}/api/settings`)
]);
if (seRes.ok) {
const seData = await seRes.json();
setSpecialEvents(seData);
}
if (setRes.ok) {
const setData = await setRes.json();
setSettings(setData);
}
} catch (err) {
console.error("Failed to fetch special events/settings:", err);
setSpecialEvents([]);
setSettings({});
}
} 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, customDetails?: any) => {
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 {
showAlert("Error", "Failed to cancel registration.", "error");
}
} 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));
showAlert("Registration Blocked", `You must upload your certificate and obtain OD approval for your completed event "${ev?.title || 'Past Event'}" before registering for another event.`, "error");
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: customDetails?.email || user.email,
userName: customDetails?.userName || user.fullName,
regNo: customDetails?.regNo || user.regNo,
phone: customDetails?.phone || user.phone,
gender: user.gender,
dept: customDetails?.dept || user.department,
section: customDetails?.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();
showAlert("Error", err.message || "Registration failed.", "error");
}
}
} 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()) {
showAlert("Required", "Enter team name", "error");
return;
}
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);
showAlert("Error", "Failed to form team", "error");
} finally {
setIsProcessing(false);
}
};
const handleJoinTeam = async (eventId: string) => {
if (!teamCode.trim()) {
showAlert("Required", "Enter team code", "error");
return;
}
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) {
showAlert("Invalid Team", "Invalid team code for this event.", "error");
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) {
showAlert("Team Full", "Team is already full.", "error");
setIsProcessing(false);
return;
}
if (eventVal?.teamComposition === 'INTER_DEPT' && leader && user?.department !== (leader.dept || leader.department)) {
showAlert("Inter-Department Only", `This event requires INTER-DEPARTMENT teams. You must join a team from department ${(leader.dept || leader.department)}`, "error");
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);
showAlert("Error", "Failed to join team", "error");
} 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}
bookedEventIds={bookedEventIds}
onToggleBooking={handleToggleBooking}
userRole={user?.role}
/>
<AboutHubSection />
<StatsSection events={events} />
<HomeDashboard
events={events}
announcements={announcements}
onNavigateToEvents={() => setActiveView('EVENTS')}
specialEvents={specialEvents}
settings={settings}
/>
</>
);
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]"></div>
</div>
);
}
return (
<div className="min-h-screen bg-white">
<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

@@ -1,745 +0,0 @@
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 { useDialog } from '../../context/DialogContext';
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 { showAlert, showConfirm } = useDialog();
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
const [batches, setBatches] = useState<any[]>([]);
useEffect(() => {
fetchEvents();
fetchBatches();
}, []);
const fetchBatches = async () => {
try {
const res = await fetch(API_BASE_URL + '/api/batches');
if (res.ok) {
const data = await res.json();
setBatches(data);
}
} catch (err) {
console.error(err);
}
};
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') {
const userDepts = user.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
filtered = filtered.filter((e: Event) => userDepts.includes(e.department?.trim().toLowerCase()));
}
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);
}
};
// Dismiss Student from event
const handleDismissStudent = async (regId: string, studentName: string) => {
showConfirm(
"Dismiss Student",
`Are you sure you want to dismiss ${studentName} from this event? This will completely remove their registration.`,
async () => {
setIsProcessing(true);
try {
const res = await fetch(`${API_BASE_URL}/api/registrations/${regId}`, {
method: 'DELETE'
});
if (res.ok) {
showAlert("Success", `${studentName} has been successfully dismissed.`, "success");
await fetchRegistrations(selectedEvent!.id);
} else {
showAlert("Error", "Failed to dismiss student.", "error");
}
} catch (err) {
console.error(err);
showAlert("Error", "An error occurred while dismissing the student.", "error");
} 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) {
showAlert("Success", "Attendance records synchronized successfully!", "success");
await fetchRegistrations(selectedEvent!.id);
} else {
showAlert("Error", "Failed to save attendance.", "error");
}
} 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) {
showAlert("Error", "Please select an On-Duty file first.", "error");
return;
}
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 })
});
}
showAlert("Success", "OD sheet broadcasted to all registered students successfully!", "success");
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);
showAlert("Certificate Audited", status === 'APPROVED' ? "Certificate approved!" : "Certificate rejected.", status === 'APPROVED' ? "success" : "info");
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())
);
const getStudentBatchName = (reg: any) => {
const classStr = `${reg.year} - ${reg.section}`;
const match = batches.find(b => b.department === reg.dept && b.classes?.includes(classStr));
return match ? match.name : '';
};
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>
<th className="py-4 text-center">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredRoster.length === 0 ? (
<tr>
<td colSpan={6} 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}
{getStudentBatchName(reg) && (
<span className="ml-1.5 px-1.5 py-0.5 bg-brand-indigo/10 text-brand-indigo text-[8px] font-black rounded">
{getStudentBatchName(reg)}
</span>
)}
<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>
<td className="py-4 text-center">
<button
onClick={() => handleDismissStudent(reg.id, reg.userName)}
disabled={isProcessing}
className="px-3 py-1.5 bg-rose-50 border border-rose-100 text-rose-600 rounded-lg text-[9px] font-black tracking-widest uppercase hover:bg-rose-100 hover:text-rose-700 transition-all"
>
Dismiss
</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 ? 'Saving...' : '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>)}
<th className="py-4 text-center">
<div className="flex flex-col items-center gap-1">
<span className="text-[10px]">Mark All</span>
<input
type="checkbox"
checked={
registrations.length > 0 &&
registrations.every(reg =>
daysList.every(day => attendanceGrid[reg.id]?.[day] || false)
)
}
onChange={(e) => {
const checked = e.target.checked;
setAttendanceGrid(prev => {
const next = { ...prev };
registrations.forEach(reg => {
next[reg.id] = next[reg.id] || {};
daysList.forEach(day => {
next[reg.id][day] = checked;
});
});
return next;
});
}}
className="w-4 h-4 rounded border-slate-300 text-brand-indigo focus:ring-brand-indigo cursor-pointer"
title="Toggle all days for all students"
/>
</div>
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{registrations.length === 0 ? (
<tr>
<td colSpan={6} 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}
{getStudentBatchName(reg) && (
<span className="ml-1.5 px-1 py-0.5 bg-brand-indigo/10 text-brand-indigo text-[7px] font-black rounded block w-max mt-0.5">
{getStudentBatchName(reg)}
</span>
)}
</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>
))}
<td className="py-4 text-center">
<input
type="checkbox"
checked={daysList.every(day => attendanceGrid[reg.id]?.[day] || false)}
onChange={(e) => {
const checked = e.target.checked;
setAttendanceGrid(prev => ({
...prev,
[reg.id]: daysList.reduce((acc, day) => {
acc[day] = checked;
return acc;
}, {} as Record<string, boolean>)
}));
}}
className="w-4.5 h-4.5 rounded border-slate-300 text-brand-indigo focus:ring-brand-indigo cursor-pointer"
title="Toggle all days for this student"
/>
</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

@@ -1,645 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Users,
UserPlus,
Edit2,
Trash2,
X,
Check,
Search,
Shield,
Mail,
Building2,
Lock,
Zap,
GraduationCap
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { CLUBS } from '../../constants/clubs';
import { Pagination } from './Pagination';
interface User {
id: number;
fullName: string;
email: string;
role: string;
department: string;
password?: string;
isClubCoordinator?: boolean;
isPlacementStaff?: boolean;
isClassIncharge?: boolean;
inchargeClass?: string;
inchargeBatch?: string;
inchargeSection?: string;
classStrength?: number;
assignedClubs?: string[];
}
export const UserManagement: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [classes, setClasses] = useState<any[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [formData, setFormData] = useState({
fullName: '',
email: '',
role: 'FACULTY',
department: '',
password: '',
isClubCoordinator: false,
isPlacementStaff: false,
isClassIncharge: false,
inchargeClass: '',
inchargeBatch: '',
inchargeSection: '',
classStrength: 0,
assignedClubs: [] as string[]
});
useEffect(() => {
fetchUsers();
fetchClasses();
}, []);
const fetchClasses = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/classes');
if (response.ok) {
setClasses(await response.json());
}
} catch (error) {
console.error('Failed to fetch classes:', error);
}
};
const fetchUsers = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/admin/users');
if (response.ok) {
const data = await response.json();
setUsers(data);
}
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setIsLoading(false);
}
};
const handleOpenModal = (user: User | null = null) => {
if (user) {
setEditingUser(user);
setFormData({
fullName: user.fullName,
email: user.email,
role: user.role,
department: user.department || '',
password: '',
isClubCoordinator: user.isClubCoordinator || false,
isPlacementStaff: user.isPlacementStaff || false,
isClassIncharge: user.isClassIncharge || false,
inchargeClass: user.inchargeClass || '',
inchargeBatch: user.inchargeBatch || '',
inchargeSection: user.inchargeSection || '',
classStrength: user.classStrength || 0,
assignedClubs: user.assignedClubs || []
});
} else {
setEditingUser(null);
setFormData({
fullName: '',
email: '',
role: 'FACULTY',
department: '',
password: '',
isClubCoordinator: false,
isPlacementStaff: false,
isClassIncharge: false,
inchargeClass: '',
inchargeBatch: '',
inchargeSection: '',
classStrength: 0,
assignedClubs: []
});
}
setIsModalOpen(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if ((formData.role === 'HOD' || formData.role === 'FACULTY') && !formData.department.trim()) {
alert("Please select at least one department.");
return;
}
const url = editingUser
? `${API_BASE_URL}/api/admin/users/${editingUser.id}`
: API_BASE_URL + '/api/admin/users';
const method = editingUser ? 'PUT' : 'POST';
try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
fetchUsers();
setIsModalOpen(false);
}
} catch (error) {
console.error('Operation failed:', error);
}
};
const handleDelete = async (id: number) => {
if (window.confirm('Are you sure you want to delete this user?')) {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/${id}`, {
method: 'DELETE'
});
if (response.ok) {
setUsers(prev => prev.filter(u => u.id !== id));
}
} catch (error) {
console.error('Delete failed:', error);
}
}
};
const filteredUsers = users
.filter(u =>
u.fullName.toLowerCase().includes(searchTerm.toLowerCase()) ||
u.email.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => b.id - a.id);
const currentUsers = filteredUsers.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">User Management</h2>
<p className="text-text-muted font-medium">Add and manage institutional roles and permissions.</p>
</div>
<button
onClick={() => handleOpenModal()}
className="flex items-center gap-2 bg-brand-navy text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase tracking-widest premium-shadow hover:scale-105 transition-all"
>
<UserPlus className="w-4 h-4" />
Add New User
</button>
</div>
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden">
<div className="p-6 border-b border-slate-50 flex items-center justify-between gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
placeholder="Search users by name or email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-50 border-transparent rounded-xl py-2 pl-10 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
<div className="text-[10px] font-black uppercase tracking-widest text-text-muted">
Total Users: {users.length}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50/50">
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">User Info</th>
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Role</th>
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Department</th>
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{currentUsers.map((user) => (
<tr key={user.id} className="group hover:bg-slate-50/50 transition-colors">
<td className="px-8 py-5">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-brand-glow flex items-center justify-center text-brand-indigo font-black text-xs">
{user.fullName.split(' ').map(n => n[0]).join('').substring(0, 2)}
</div>
<div className="flex flex-col">
<span className="font-bold text-text-dark text-sm">{user.fullName}</span>
<span className="text-xs text-text-muted">{user.email}</span>
</div>
</div>
</td>
<td className="px-8 py-5">
<div className="flex flex-col gap-2 items-start">
<span className={cn(
"px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border",
user.role === 'ADMIN' ? 'bg-purple-50 text-purple-600 border-purple-100' :
user.role === 'HOD' ? 'bg-amber-50 text-amber-600 border-amber-100' :
user.role === 'PRINCIPAL' ? 'bg-emerald-50 text-emerald-600 border-emerald-100' :
'bg-blue-50 text-blue-600 border-blue-100'
)}>
{user.role}
</span>
{user.isClassIncharge && (
<span className="px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border bg-indigo-50 text-indigo-600 border-indigo-100 flex items-center gap-1">
<GraduationCap className="w-3 h-3" />
Class Incharge ({user.inchargeClass} {user.inchargeBatch} - Sec {user.inchargeSection})
</span>
)}
</div>
</td>
<td className="px-8 py-5 text-sm font-bold text-text-secondary">
{user.department || 'N/A'}
</td>
<td className="px-8 py-5">
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleOpenModal(user)}
className="p-2 hover:bg-white rounded-lg hover:premium-shadow-sm transition-all text-slate-400 hover:text-brand-indigo"
>
<Edit2 className="w-4 h-4" />
</button>
{user.role !== 'ADMIN' && (
<button
onClick={() => handleDelete(user.id)}
className="p-2 hover:bg-white rounded-lg hover:premium-shadow-sm transition-all text-slate-400 hover:text-status-danger"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredUsers.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</div>
{/* User Modal */}
<AnimatePresence>
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-lg bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[90vh]"
>
<form onSubmit={handleSubmit} className="flex flex-col overflow-hidden">
<div className="p-8 bg-brand-navy text-white flex justify-between items-start shrink-0">
<div>
<h3 className="text-2xl font-black tracking-tight">
{editingUser ? 'Edit User' : 'Add New User'}
</h3>
<p className="text-white/60 text-xs font-medium mt-1">Configure institutional access credentials.</p>
</div>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-5 overflow-y-auto custom-scrollbar flex-1 max-h-[65vh]">
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Full Name</label>
<div className="relative">
<Shield className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
required
value={formData.fullName}
onChange={(e) => setFormData({...formData, fullName: e.target.value})}
placeholder="E.g. Dr. Jane Smith"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 pl-12 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Email Address</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
placeholder="jane.smith@rit.edu"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 pl-12 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Role</label>
<select
value={formData.role}
onChange={(e) => {
const newRole = e.target.value;
setFormData({
...formData,
role: newRole,
department: (newRole === 'HOD' || newRole === 'FACULTY') ? formData.department : ''
});
}}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="FACULTY">Faculty</option>
<option value="HOD">HoD</option>
<option value="PRINCIPAL">Principal</option>
<option value="PLACEMENT">Placement</option>
<option value="ADMIN">Admin</option>
</select>
</div>
{(formData.role === 'HOD' || formData.role === 'FACULTY') && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="space-y-3"
>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Department(s)</label>
<div className="flex flex-wrap gap-2 p-4 bg-slate-50/50 rounded-2xl border border-slate-100 min-h-[60px]">
{[
"AI&DS", "AI&ML", "CSE", "CCE", "CSBS", "ECE",
"MECH", "EE(VLSI)", "BIOTECH", "Placement Department",
"H&S Dept", "Club", "Centre"
].map(dept => {
const selectedDepts = formData.department ? formData.department.split(',').map(d => d.trim()).filter(Boolean) : [];
const isSelected = selectedDepts.includes(dept);
return (
<button
key={dept}
type="button"
onClick={() => {
let updated: string[];
if (isSelected) {
updated = selectedDepts.filter(d => d !== dept);
} else {
updated = [...selectedDepts, dept];
}
setFormData({...formData, department: updated.join(', ')});
}}
className={cn(
"px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all border",
isSelected
? "bg-brand-navy text-white border-brand-navy premium-shadow-sm"
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
)}
>
{dept}
</button>
);
})}
</div>
</motion.div>
)}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">
{editingUser ? 'New Password (Optional)' : 'Password'}
</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="password"
required={!editingUser}
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
placeholder="••••••••"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 pl-12 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div className="pt-2 grid grid-cols-2 gap-4">
<div className="flex items-center justify-between p-4 bg-slate-50/50 rounded-2xl border border-slate-100">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
formData.isClubCoordinator ? "bg-brand-indigo text-white" : "bg-slate-200 text-text-muted"
)}>
<Shield className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Club Coordinator</p>
<p className="text-[9px] font-bold text-text-muted">Proposals Permission</p>
</div>
</div>
<button
type="button"
onClick={() => setFormData({...formData, isClubCoordinator: !formData.isClubCoordinator})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
formData.isClubCoordinator ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
formData.isClubCoordinator ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50/50 rounded-2xl border border-slate-100">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
formData.isPlacementStaff ? "bg-brand-indigo text-white" : "bg-slate-200 text-text-muted"
)}>
<Zap className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Placement Coordinator</p>
<p className="text-[9px] font-bold text-text-muted">High Priority Access</p>
</div>
</div>
<button
type="button"
onClick={() => setFormData({...formData, isPlacementStaff: !formData.isPlacementStaff})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
formData.isPlacementStaff ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
formData.isPlacementStaff ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
</div>
{formData.role === 'FACULTY' && (
<div className="pt-2">
<div className="flex items-center justify-between p-4 bg-slate-50/50 rounded-2xl border border-slate-100">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
formData.isClassIncharge ? "bg-brand-indigo text-white" : "bg-slate-200 text-text-muted"
)}>
<GraduationCap className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Class Incharge</p>
<p className="text-[9px] font-bold text-text-muted">Manage Class Operations</p>
</div>
</div>
<button
type="button"
onClick={() => setFormData({...formData, isClassIncharge: !formData.isClassIncharge})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
formData.isClassIncharge ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
formData.isClassIncharge ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
{formData.isClassIncharge && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="mt-4 space-y-4"
>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Class / Department</label>
<select
required={formData.isClassIncharge}
value={formData.inchargeClass}
onChange={(e) => setFormData({...formData, inchargeClass: e.target.value, inchargeBatch: '', inchargeSection: ''})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Class</option>
{Array.from(new Set(classes.map(c => c.department))).map(dept => (
<option key={dept as string} value={dept as string}>{dept as string}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Batch / Year</label>
<select
required={formData.isClassIncharge}
value={formData.inchargeBatch}
onChange={(e) => setFormData({...formData, inchargeBatch: e.target.value, inchargeSection: ''})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Batch</option>
{Array.from(new Set(classes.filter(c => c.department === formData.inchargeClass).map(c => c.academicYear))).map(year => (
<option key={year as string} value={year as string}>{year as string}</option>
))}
</select>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Section</label>
<select
required={formData.isClassIncharge}
value={formData.inchargeSection}
onChange={(e) => setFormData({...formData, inchargeSection: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Section</option>
{(classes.find(c => c.department === formData.inchargeClass && c.academicYear === formData.inchargeBatch)?.sections || []).map((sec: string) => (
<option key={sec} value={sec}>{sec}</option>
))}
</select>
</div>
</div>
</motion.div>
)}
</div>
)}
{formData.isClubCoordinator && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="mt-4 space-y-3"
>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Assigned Clubs</label>
<div className="flex flex-wrap gap-2 p-4 bg-slate-50/50 rounded-2xl border border-slate-100 min-h-[60px]">
{CLUBS.map(club => (
<button
key={club}
type="button"
onClick={() => {
const updated = formData.assignedClubs.includes(club)
? formData.assignedClubs.filter(c => c !== club)
: [...formData.assignedClubs, club];
setFormData({...formData, assignedClubs: updated});
}}
className={cn(
"px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all border",
formData.assignedClubs.includes(club)
? "bg-brand-navy text-white border-brand-navy premium-shadow-sm"
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
)}
>
{club}
</button>
))}
</div>
</motion.div>
)}
</div>
<div className="shrink-0 w-full">
<button
type="submit"
className="w-full bg-brand-navy hover:bg-[#0c365c] text-white py-5 font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center"
>
{editingUser ? 'Update User Credentials' : 'Create System User'}
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -1,289 +0,0 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import { Calendar as CalendarIcon, MapPin, Clock, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
import { startOfMonth } from 'date-fns';
import { endOfMonth } from 'date-fns';
import { startOfWeek } from 'date-fns';
import { endOfWeek } from 'date-fns';
import { eachDayOfInterval } from 'date-fns';
import { isSameMonth } from 'date-fns';
import { isSameDay } from 'date-fns';
import { addMonths } from 'date-fns';
import { subMonths } from 'date-fns';
interface Event {
id: number;
title: string;
type: string;
status: string;
department: string;
startDate: string;
endDate: string;
location: string;
academicYears: string[];
}
const CORE_VENUES = [
'GB 4th floor auditorium',
'Wozniak Auditorium',
'C6-02 Indoor Theatre',
'H Block Guest Lecture Theatre',
'Steve Jobs Computer Centre 1',
'Steve Jobs Computer Centre 2'
];
export const VenueTimeline: React.FC<{ compact?: boolean }> = ({ compact }) => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [currentMonth, setCurrentMonth] = useState(new Date());
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data.filter((e: any) =>
e.status === 'APPROVED' || e.status === 'COMPLETED'
));
}
} catch (error) {
console.error('Failed to fetch events for timeline:', error);
} finally {
setLoading(false);
}
};
fetchEvents();
}, []);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const eventsOnSelectedDate = events.filter(event => {
const eventDate = parseDate(event.startDate);
if (!eventDate) return false;
return format(eventDate, 'yyyy-MM-dd') === format(selectedDate, 'yyyy-MM-dd');
});
// Dynamically discover all unique venues from events on the selected date and combine with CORE_VENUES
const allVenues = (() => {
const dynamicVenues = eventsOnSelectedDate
.map(e => e.location?.trim())
.filter(l => l && l !== 'N/A' && l !== 'Others.') as string[];
const finalVenues = [...CORE_VENUES];
dynamicVenues.forEach(dv => {
if (!finalVenues.some(cv => cv.toLowerCase() === dv.toLowerCase())) {
finalVenues.push(dv);
}
});
return finalVenues;
})();
const calendarDays = (() => {
const start = startOfWeek(startOfMonth(currentMonth));
const end = endOfWeek(endOfMonth(currentMonth));
return eachDayOfInterval({ start, end });
})();
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
<span className="ml-3 text-sm font-black text-brand-indigo uppercase tracking-widest tracking-widest">Initialising Timeline Audit...</span>
</div>
);
}
return (
<div className={cn(
"flex gap-8 items-start",
compact ? "flex-col" : "flex-col lg:flex-row min-h-[600px]"
)}>
{/* Sidebar Filter - STICKY */}
{!compact && (
<div className="w-full lg:w-80 space-y-6 sticky top-24 self-start">
<div className="bg-white p-8 rounded-[2.5rem] border border-slate-100 premium-shadow">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 bg-brand-indigo rounded-2xl flex items-center justify-center text-white premium-shadow-sm">
<CalendarIcon className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted">Timeline Filter</p>
<h3 className="text-lg font-black text-text-dark tracking-tight">View Date</h3>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between px-2">
<h4 className="text-sm font-black text-text-dark">{format(currentMonth, 'MMMM yyyy')}</h4>
<div className="flex gap-2">
<button onClick={() => setCurrentMonth(subMonths(currentMonth, 1))} className="p-1 hover:bg-slate-50 rounded-lg transition-colors">
<ChevronLeft className="w-4 h-4 text-text-muted" />
</button>
<button onClick={() => setCurrentMonth(addMonths(currentMonth, 1))} className="p-1 hover:bg-slate-50 rounded-lg transition-colors">
<ChevronRight className="w-4 h-4 text-text-muted" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-1">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
<div key={`${day}-${i}`} className="text-[10px] font-black text-text-muted text-center py-2">{day}</div>
))}
{calendarDays.map(day => {
const hasEvents = events.some(e =>
e.startDate && format(new Date(e.startDate), 'yyyy-MM-dd') === format(day, 'yyyy-MM-dd')
);
const isSelected = isSameDay(day, selectedDate);
return (
<button
key={day.toISOString()}
onClick={() => setSelectedDate(day)}
className={cn(
"aspect-square flex items-center justify-center text-xs font-bold rounded-xl transition-all relative group",
!isSameMonth(day, currentMonth) && "text-slate-200",
isSelected ? "bg-brand-indigo text-white premium-shadow-sm" : "hover:bg-slate-50 text-text-dark",
isSameDay(day, new Date()) && !isSelected && "text-brand-indigo underline decoration-2 underline-offset-4"
)}
>
{format(day, 'd')}
{hasEvents && (
<div className={cn(
"absolute bottom-1 w-1 h-1 rounded-full",
isSelected ? "bg-white/40" : "bg-brand-indigo/30"
)} />
)}
</button>
);
})}
</div>
</div>
<div className="mt-8 p-4 bg-brand-glow/30 rounded-2xl border border-brand-indigo/10">
<p className="text-[9px] font-bold text-brand-indigo/80 leading-relaxed italic">
Tip: This calendar specifically filters the venue bookings timeline below.
</p>
</div>
</div>
</div>
)}
<div className="flex-1 space-y-6">
{!compact && (
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tighter">Institutional Timeline</h2>
<p className="text-sm font-bold text-text-muted mt-1">
Confirmed slots for <span className="text-brand-indigo">{format(selectedDate, 'MMMM d, yyyy')}</span>
</p>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest">Approved Only</span>
</div>
</div>
)}
<div className="space-y-6">
{allVenues.map((venue, idx) => {
const venueEvents = eventsOnSelectedDate.filter(e =>
e.location?.trim().toLowerCase() === venue.trim().toLowerCase()
);
return (
<motion.div
key={venue}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
className={cn(
"bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden",
compact && "rounded-[1.5rem] premium-shadow-sm"
)}
>
<div className={cn(
"p-8 border-b border-slate-50 bg-slate-50/30 flex items-center gap-4",
compact && "p-4"
)}>
<div className={cn(
"w-12 h-12 bg-white rounded-2xl flex items-center justify-center premium-shadow-sm border border-slate-50",
compact && "w-8 h-8 rounded-xl"
)}>
<MapPin className={cn("text-slate-300", compact ? "w-4 h-4" : "w-6 h-6")} />
</div>
<h3 className={cn("font-black text-text-dark tracking-tight", compact ? "text-sm" : "text-xl")}>{venue}</h3>
</div>
<div className={cn("p-8", compact && "p-4")}>
{venueEvents.length > 0 ? (
<div className="space-y-4">
{venueEvents.map(event => (
<div key={event.id} className={cn(
"flex flex-col md:flex-row md:items-center justify-between gap-6 bg-slate-50/50 border border-slate-100 rounded-3xl hover:bg-white hover:premium-shadow transition-all group",
compact ? "p-4" : "p-6"
)}>
<div className="flex items-center gap-4">
<div className={cn(
"bg-white rounded-2xl flex items-center justify-center text-[10px] font-black text-brand-indigo border border-brand-indigo/10",
compact ? "w-8 h-8 rounded-xl text-[8px]" : "w-12 h-12"
)}>
{event.department === 'AI&DS' ? 'AIDS' : event.department?.slice(0, 4).toUpperCase()}
</div>
<div>
<h4 className={cn("font-black text-text-dark group-hover:text-brand-indigo transition-colors", compact ? "text-xs" : "text-lg")}>{event.title}</h4>
{!compact && (
<div className="flex items-center gap-3 mt-1">
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted">{event.department}</span>
<span className="text-[10px] font-black uppercase tracking-widest text-slate-300"></span>
<span className="text-[10px] font-black uppercase tracking-widest text-brand-indigo/60">{event.type}</span>
</div>
)}
</div>
</div>
<div className={cn(
"flex items-center gap-4 bg-white rounded-2xl border border-slate-100 premium-shadow-sm",
compact ? "px-3 py-2" : "px-6 py-4"
)}>
<Clock className={cn("text-brand-indigo", compact ? "w-3 h-3" : "w-4 h-4")} />
<div className="flex flex-col">
<p className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-0.5">Slot</p>
<p className={cn("font-black text-text-dark", compact ? "text-[10px]" : "text-sm")}>
{parseDate(event.startDate) ? format(parseDate(event.startDate)!, 'hh:mm a') : 'TBD'}
</p>
</div>
</div>
</div>
))}
</div>
) : (
<div className={cn(
"flex flex-col items-center justify-center border-2 border-dashed border-slate-50 rounded-3xl",
compact ? "py-4" : "py-12"
)}>
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-200">
{compact ? 'Available' : 'Available for Booking'}
</p>
</div>
)}
</div>
</motion.div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -1,166 +0,0 @@
import React, { useState, useMemo } from 'react';
import { motion } from 'framer-motion';
import { MapPin, Clock, Calendar as CalendarIcon, ArrowRight, Building2 } from 'lucide-react';
import { cn } from '../../lib/utils';
import { format, isSameDay } from 'date-fns';
import { EventDetailsModal } from './EventDetailsModal';
const CORE_VENUES = [
'GB 4th floor auditorium',
'Wozniak Auditorium',
'C6-02 Indoor Theatre',
'H Block Guest Lecture Theatre',
'Steve Jobs Computer Centre 1',
'Steve Jobs Computer Centre 2'
];
interface VenuesAtGlanceProps {
events: any[];
}
export const VenuesAtGlance: React.FC<VenuesAtGlanceProps> = ({ events }) => {
const [selectedEvent, setSelectedEvent] = useState<any | null>(null);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const today = new Date();
// Combine core venues with any dynamically used venues in events today
const allVenues = useMemo(() => {
const todayEvents = events.filter(e => {
if (e.status !== 'APPROVED' && e.status !== 'COMPLETED') return false;
const eventDate = parseDate(e.startDate);
if (!eventDate) return false;
return isSameDay(eventDate, today);
});
const dynamicVenues = todayEvents
.filter(e => e.location && e.location !== 'N/A' && e.location !== 'Others.')
.map(e => e.location.trim());
const finalVenues = [...CORE_VENUES];
dynamicVenues.forEach(dv => {
if (!finalVenues.some(cv => cv.toLowerCase() === dv.toLowerCase())) {
finalVenues.push(dv);
}
});
return finalVenues;
}, [events]);
return (
<div className="card-widget">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-black text-brand-navy tracking-tight flex items-center gap-2">
<MapPin className="w-5 h-5 text-brand-indigo" />
Venues At A Glance
</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">
Today's Scheduled Events • {format(today, 'MMMM d, yyyy')}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{allVenues.map((venue, idx) => {
// Find if there is an approved/completed event for this venue today
const todayEvents = events.filter(e => {
if (e.status !== 'APPROVED' && e.status !== 'COMPLETED') return false;
const eventDate = parseDate(e.startDate);
if (!eventDate) return false;
return isSameDay(eventDate, today) && e.location?.trim().toLowerCase() === venue.trim().toLowerCase();
});
const isOccupied = todayEvents.length > 0;
return (
<motion.div
key={venue}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className={cn(
"rounded-[1.5rem] p-5 border transition-all relative overflow-hidden",
isOccupied
? "bg-slate-50 border-slate-200 hover:border-brand-indigo/30 premium-shadow-sm group cursor-pointer"
: "bg-white border-slate-100 hover:bg-slate-50 opacity-70 hover:opacity-100"
)}
onClick={() => isOccupied && setSelectedEvent(todayEvents[0])}
>
{isOccupied && (
<div className="absolute top-0 right-0 w-16 h-16 bg-brand-indigo/5 rounded-bl-[100%] transition-transform group-hover:scale-110" />
)}
<div className="flex items-start justify-between gap-4 mb-4 relative z-10">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center shrink-0 transition-colors",
isOccupied ? "bg-white text-brand-indigo shadow-sm" : "bg-slate-50 text-slate-400"
)}>
<Building2 className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-black text-brand-navy tracking-tight line-clamp-1" title={venue}>
{venue}
</h4>
<span className={cn(
"text-[9px] font-black uppercase tracking-widest",
isOccupied ? "text-emerald-500" : "text-slate-400"
)}>
{isOccupied ? 'Occupied' : 'Available'}
</span>
</div>
</div>
</div>
{isOccupied ? (
<div className="space-y-3 relative z-10">
<div className="bg-white rounded-xl p-3 border border-slate-100 shadow-sm transition-transform group-hover:-translate-y-1">
<p className="text-xs font-bold text-brand-navy line-clamp-1 mb-2">
{todayEvents[0].title}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-text-muted">
<Clock className="w-3.5 h-3.5 text-brand-indigo" />
<span className="text-[10px] font-black uppercase tracking-widest">
{format(parseDate(todayEvents[0].startDate)!, 'hh:mm a')}
</span>
</div>
<div className="w-6 h-6 rounded-lg bg-brand-glow flex items-center justify-center text-brand-indigo group-hover:bg-brand-indigo group-hover:text-white transition-colors">
<ArrowRight className="w-3 h-3" />
</div>
</div>
</div>
{todayEvents.length > 1 && (
<p className="text-[9px] font-bold text-brand-indigo/60 text-right">
+ {todayEvents.length - 1} more today
</p>
)}
</div>
) : (
<div className="h-16 flex items-center justify-center border border-dashed border-slate-200 rounded-xl">
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-300">
No Events
</p>
</div>
)}
</motion.div>
);
})}
</div>
<EventDetailsModal
isOpen={!!selectedEvent}
onClose={() => setSelectedEvent(null)}
event={selectedEvent}
/>
</div>
);
};

View File

@@ -1,73 +0,0 @@
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

@@ -1,134 +0,0 @@
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

@@ -1,34 +0,0 @@
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

@@ -1,107 +0,0 @@
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

@@ -1,114 +0,0 @@
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

@@ -1,65 +0,0 @@
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

@@ -1,689 +0,0 @@
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: (customDetails?: any) => 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);
// Form states for registration details
const [formName, setFormName] = useState('');
const [formDept, setFormDept] = useState('');
const [formSection, setFormSection] = useState('');
const [formRegNo, setFormRegNo] = useState('');
const [formPhone, setFormPhone] = useState('');
const [formEmail, setFormEmail] = useState('');
const { user } = useAuth();
const [batches, setBatches] = useState<any[]>([]);
useEffect(() => {
const fetchBatches = async () => {
try {
const res = await fetch(`${API_BASE_URL}/api/batches`);
if (res.ok) {
const data = await res.json();
setBatches(data);
}
} catch (err) {
console.error(err);
}
};
fetchBatches();
}, []);
const studentBatchName = useMemo(() => {
const userDeptVal = registration?.dept || userDept || (user?.department || '');
const userYearVal = registration?.year || userYear || (user?.year || '');
const userSecVal = registration?.section || userSection || (user?.section || '');
if (!userDeptVal || !userYearVal || !userSecVal) return null;
const classStr = `${userYearVal} - ${userSecVal}`;
const match = batches.find(b => b.department === userDeptVal && b.classes?.includes(classStr));
return match ? match.name : null;
}, [batches, registration, userDept, userYear, userSection, user]);
useEffect(() => {
if (showConfirm && user) {
setFormName(user.fullName || '');
setFormDept(user.department || '');
setFormSection(user.section || 'A');
setFormRegNo(user.regNo || '');
setFormPhone(user.phone || '');
setFormEmail(user.email || '');
}
}, [showConfirm, user]);
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]);
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: 2, backgroundColor: '#1e293b' });
const { jsPDF } = await import('jspdf');
const width = ticketRef.current.offsetWidth || 380;
const height = ticketRef.current.offsetHeight || 580;
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'px',
format: [width, height]
});
pdf.addImage(dataUrl, 'PNG', 0, 0, width, height);
pdf.save(`Pass_${event.title.replace(/\s+/g, '_')}.pdf`);
} 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-lg p-8 shadow-2xl animate-in zoom-in-95 flex flex-col max-h-[90vh]">
<div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-6">
<div className={`w-16 h-16 ${event.isTeamEvent ? 'bg-orange-50 text-orange-500' : 'bg-blue-50 text-blue-500'} rounded-full flex items-center justify-center text-2xl mx-auto mb-2`}>
<i className={`fas ${event.isTeamEvent ? 'fa-users' : 'fa-ticket'}`}></i>
</div>
<h3 className="text-xl font-black text-[#1A202C] uppercase text-center mb-1">
{event.isTeamEvent ? 'Team Registration' : 'Confirm Pass?'}
</h3>
{event.isTeamEvent && (
<div className="bg-orange-50 border border-orange-100 rounded-2xl p-4 text-left">
<p className="text-[9px] font-black text-orange-600 uppercase tracking-widest mb-1 flex items-center gap-1.5">
<i className="fas fa-exclamation-triangle"></i> Important Notice
</p>
<p className="text-xs text-orange-800 font-bold leading-normal">
This is a <span className="underline">team-based event</span>. You must create or join a team in the registrations section after registering.
</p>
</div>
)}
{/* Paid Event Links Box */}
{event.paymentLink && (
<div className="bg-indigo-50 border border-brand-indigo/10 rounded-2xl p-4 text-left">
<span className="block text-[8px] font-black text-brand-indigo uppercase tracking-widest mb-2 flex items-center gap-1">
<i className="fas fa-link"></i> Required Payment & Form Link
</span>
<a
href={event.paymentLink}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-black text-blue-600 hover:underline flex items-center gap-1.5 break-all"
>
{event.paymentLink} <i className="fas fa-external-link-alt text-[9px]"></i>
</a>
</div>
)}
{/* Registration Details Form */}
<div className="space-y-4 text-left">
<span className="block text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 pb-1">Review Registration Details</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-[8px] font-black text-slate-400 uppercase tracking-wider mb-1">Name</label>
<input
type="text"
value={formName}
onChange={e => setFormName(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold outline-none focus:ring-2 focus:ring-brand-indigo/20 text-slate-800"
required
/>
</div>
<div>
<label className="block text-[8px] font-black text-slate-400 uppercase tracking-wider mb-1">College Email ID</label>
<input
type="email"
value={formEmail}
onChange={e => setFormEmail(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold outline-none focus:ring-2 focus:ring-brand-indigo/20 text-slate-800"
required
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-[8px] font-black text-slate-400 uppercase tracking-wider mb-1">Dept</label>
<input
type="text"
value={formDept}
onChange={e => setFormDept(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold outline-none focus:ring-2 focus:ring-brand-indigo/20 text-slate-800"
required
/>
</div>
<div>
<label className="block text-[8px] font-black text-slate-400 uppercase tracking-wider mb-1">Section</label>
<input
type="text"
value={formSection}
onChange={e => setFormSection(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold outline-none focus:ring-2 focus:ring-brand-indigo/20 text-slate-800"
required
/>
</div>
<div>
<label className="block text-[8px] font-black text-slate-400 uppercase tracking-wider mb-1">Roll / Reg No</label>
<input
type="text"
value={formRegNo}
onChange={e => setFormRegNo(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold outline-none focus:ring-2 focus:ring-brand-indigo/20 text-slate-800"
required
/>
</div>
</div>
<div>
<label className="block text-[8px] font-black text-slate-400 uppercase tracking-wider mb-1">Phone Number</label>
<input
type="text"
value={formPhone}
onChange={e => setFormPhone(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold outline-none focus:ring-2 focus:ring-brand-indigo/20 text-slate-800"
required
/>
</div>
</div>
</div>
<div className="flex gap-4 border-t border-slate-100 pt-4 mt-6 shrink-0">
<button onClick={() => setShowConfirm(false)} className="flex-1 py-3.5 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({
userName: formName,
email: formEmail,
dept: formDept,
section: formSection,
regNo: formRegNo,
phone: formPhone
});
setShowConfirm(false);
}}
className={`flex-1 py-3.5 ${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`}
>
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 flex items-center gap-1.5">
{registration?.year || userYear || 'N/A'} - {registration?.section || userSection || 'N/A'}
{studentBatchName && <span className="px-1.5 py-0.5 bg-[#f97316]/10 text-[#f97316] text-[8px] font-black rounded">{studentBatchName}</span>}
</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

@@ -1,126 +0,0 @@
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

@@ -1,180 +0,0 @@
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

@@ -1,186 +0,0 @@
import React from 'react';
import { Youtube, Instagram, Facebook, Linkedin, MapPin, Phone, Mail, ExternalLink } from 'lucide-react';
const XLogo = ({ size = 24, className = "" }: { size?: number, className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
className={className}
>
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</svg>
);
const Footer: React.FC = () => {
const currentYear = new Date().getFullYear();
const footerLinks = {
col1: [
{ name: 'About The College', href: 'https://ritchennai.org/about-chairman-message.php' },
{ name: 'News Room', href: 'https://ritchennai.org/rit-news-room.php' },
{ name: 'Governing Council', href: 'https://ritchennai.org/about-governing-council.php' },
{ name: 'Career @ RIT', href: 'https://ritchennai.org/career.php' },
{ name: 'Blog', href: 'https://ritchennai.org/blog' },
{ name: 'In alliance with Rajalakshmi Eduverse (www.myeduverse.net)', href: 'https://eduver.se/' },
{ name: 'Cancellation & Refund Policy', href: 'https://ritchennai.org/downloads/C&R-Policy.pdf' },
],
col2: [
{ name: 'Courses', href: 'https://ritchennai.org/admission-courses-offered.php' },
{ name: 'Admissions', href: 'https://apply.ritchennai.org/' },
{ name: 'Eligibility', href: 'https://ritchennai.org/admission-eligibility.php' },
{ name: 'Code of Conduct - Students', href: 'https://ritchennai.org/downloads/CODE%20OF%20CONDUCT%20FOR%20STUDENTS.pdf' },
{ name: 'Online Fees Payment', href: 'https://epayments.in.worldline.com/rajalakshmi?swith=rollnumber' },
{ name: 'Terms & Conditions', href: 'https://ritchennai.org/downloads/T&C.pdf' },
{ name: 'Privacy Policy', href: 'https://ritchennai.org/downloads/Privacy-Policy.pdf' },
],
col3: [
{ name: 'Library', href: 'https://ritchennai.org/facilities-library.php' },
{ name: 'Hostel', href: 'https://ritchennai.org/facilities-hostel.php' },
{ name: 'Sports', href: 'https://ritchennai.org/facilities-sports.php' },
{ name: 'Transport', href: 'https://ritchennai.org/facilities-transport.php' },
{ name: 'HR Manual', href: 'https://ritchennai.org/downloads/HR%20manual.pdf' },
{ name: 'Patent', href: 'https://ritchennai.org/downloads/document%207.01.2022.pdf' },
{ name: 'Audited Statements', href: 'https://ritchennai.org/audited-statements.php' },
{ name: 'Shopping and Delivery Conditions', href: 'https://ritchennai.org/downloads/S&D-Conditions.pdf' },
],
};
const socialLinks = [
{ icon: Facebook, href: 'https://www.facebook.com/ritchennai/', color: 'hover:text-blue-600' },
{ icon: XLogo, href: 'https://x.com/rit_chennai', color: 'hover:text-gray-900' },
{ 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-6 pr-4 lg:pr-8">
<div className="pb-2">
<img
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
alt="RIT Logo"
className="h-16 w-auto object-contain mb-6"
referrerPolicy="no-referrer"
/>
<p className="text-sm leading-relaxed text-gray-600 text-justify">
Established in 2008, Rajalakshmi Institute of Technology (An Autonomous Institution) is a premier engineering college in Chennai. Accredited with an A++ grade by NAAC, affiliated to Anna University, and approved by AICTE & NBA, RIT is dedicated to providing excellence in higher education.
</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 bg-gray-50 border border-gray-200 flex items-center justify-center transition-all duration-300 hover:border-transparent hover:shadow-md ${social.color} group`}
>
<social.icon size={18} className="group-hover:scale-110 transition-transform" />
</a>
))}
</div>
</div>
{/* Links Columns */}
<div className="lg:col-span-2 space-y-6 pt-2">
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
DISCOVER RIT
</h3>
<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">
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
ADMISSIONS
</h3>
<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">
<h3 className="text-xs font-black text-[#1e3a8a] uppercase tracking-widest border-b-2 border-[#f97316] pb-2 w-fit">
FACILITIES
</h3>
<ul className="space-y-4">
{footerLinks.col3.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>
{/* 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 INSTITUTE OF TECHNOLOGY</p>
<p>BANGALORE HIGHWAY ROAD, KUTHAMBAKKAM</p>
<p>CHENNAI TAMIL NADU - 600124,</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>
<div className="text-[13px] space-y-1">
<p>Mobile : +91 8925977445</p>
<p>Ph : 91 44 6718 1601</p>
</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">
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

@@ -1,49 +0,0 @@
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-slate-950 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

@@ -1,183 +0,0 @@
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';
import ScrollNotification from './ScrollNotification';
interface HomeDashboardProps {
events: Event[];
announcements: Announcement[];
onNavigateToEvents: () => void;
specialEvents: SpecialEvent[];
settings?: Record<string, any>;
}
const HomeDashboard: React.FC<HomeDashboardProps> = ({
events = [],
announcements = [],
onNavigateToEvents,
specialEvents = [],
settings = {}
}) => {
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>
{/* Scroll Notification - displayed only when enabled by admin */}
<ScrollNotification settings={settings} />
{/* 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] mt-12">
{/* 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, 3).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>
{(ann as any).image && (
<img
src={(ann as any).image}
alt={ann.title}
className="w-full h-40 object-cover rounded-xl mb-4 shrink-0 shadow-sm border border-black/10"
/>
)}
<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

@@ -1,93 +0,0 @@
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

@@ -1,109 +0,0 @@
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

@@ -1,426 +0,0 @@
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

@@ -1,413 +0,0 @@
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

@@ -1,128 +0,0 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
interface ScrollNotificationProps {
settings?: Record<string, any>;
}
export const ScrollNotification: React.FC<ScrollNotificationProps> = ({ settings = {} }) => {
const [isOpen, setIsOpen] = useState(false);
const scrollData = settings.scroll_notification;
if (!scrollData || !scrollData.isActive) return null;
const title = scrollData.title || "The Grand Decrees of RIT";
const message = scrollData.message || "No decrees active at this time.";
return (
<section className="py-8 relative overflow-hidden bg-transparent">
<div className="max-w-2xl mx-auto px-6 relative z-10">
<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'
}}
>
<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 Decree' : 'Tap to Open Decree'}
</span>
</div>
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span className="text-[11px] font-medieval text-white uppercase tracking-[0.25em] drop-shadow-md">
{title}
</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'
}}
>
<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-6">
<motion.div
variants={{
closed: { opacity: 0, y: -20 },
open: { opacity: 1, y: 0 }
}}
>
<h2 className="font-medieval text-2xl md:text-3xl text-[#3e2723] mb-4 drop-shadow-sm">
{title}
</h2>
<div className="w-24 h-[1px] bg-[#8d6e63]/20 mx-auto mb-6"></div>
<p className="font-parchment text-sm md:text-lg text-[#5d4037] leading-relaxed max-w-lg mx-auto italic opacity-95 whitespace-pre-wrap">
{message}
</p>
</motion.div>
</div>
</motion.div>
{/* Bottom Roller - Interactive */}
<motion.div
onClick={() => setIsOpen(!isOpen)}
variants={{
closed: { y: -64 },
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'
}}
>
<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>{`
@import url('https://fonts.googleapis.com/css2?family=Almendra:ital,wght@0,400;0,700;1,400&family=Pirata+One&display=swap');
.font-medieval { font-family: 'Pirata One', cursive; }
.font-parchment { font-family: 'Almendra', serif; }
`}</style>
</section>
);
};
export default ScrollNotification;

View File

@@ -1,255 +0,0 @@
import React from 'react';
import { motion } from 'framer-motion';
import type { SpecialEvent } from '../../types';
import { Sparkles, ArrowUpRight, Zap } from 'lucide-react';
interface SpecialEventsBannerProps {
specialEvents: SpecialEvent[];
}
const SpecialEventsBanner: React.FC<SpecialEventsBannerProps> = ({ specialEvents }) => {
const hasEvents = specialEvents && specialEvents.length > 0;
const handleRedirect = (link: string) => {
if (link) {
window.open(link, '_blank', 'noopener,noreferrer');
}
};
if (!hasEvents) return null;
const marqueeText = Array(8).fill("⚡ SPECIAL EVENT ALERT ⚡ REGISTRATION LIVE ⚡").join(" /// ") + " /// ";
// Animation variants for container
const containerVariants: any = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2
}
}
};
// Animation variants for cards
const cardVariants: any = {
hidden: { opacity: 0, y: 40, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: 'spring',
stiffness: 100,
damping: 15
}
}
};
// Header animation variants
const headerVariants: any = {
hidden: { opacity: 0, y: -20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: 'easeOut' }
}
};
return (
<section className="relative py-32 mt-36 mb-20 font-sans -mx-6 md:-mx-12 lg:-mx-24 bg-white">
{/* Full-width Skewed Light Multi-Gradient Background (Blue, Red/Rose, Pink) */}
<div
className="absolute inset-0 bg-gradient-to-tr from-[#e0f2fe] via-[#ffe4e6] to-[#fce7f3] transform -skew-y-3 origin-top-left z-0 shadow-[0_20px_40px_rgba(79,70,229,0.06)] border-y border-black/10 overflow-hidden"
>
{/* Caution Tape Top */}
<div className="absolute top-0 left-0 right-0 h-9 bg-[#facc15] text-black overflow-hidden flex flex-col justify-between border-b-2 border-black z-20 select-none">
<div className="h-[4px] w-full hazard-stripes" />
<div className="flex-1 flex items-center overflow-hidden">
<div className="animate-marquee whitespace-nowrap flex items-center py-0.5">
<span className="text-[10px] font-black uppercase tracking-[0.15em]">{marqueeText}</span>
<span className="text-[10px] font-black uppercase tracking-[0.15em]">{marqueeText}</span>
</div>
</div>
<div className="h-[4px] w-full hazard-stripes" />
</div>
{/* Caution Tape Bottom */}
<div className="absolute bottom-0 left-0 right-0 h-9 bg-[#facc15] text-black overflow-hidden flex flex-col justify-between border-t-2 border-black z-20 select-none">
<div className="h-[4px] w-full hazard-stripes" />
<div className="flex-1 flex items-center overflow-hidden">
<div className="animate-marquee whitespace-nowrap flex items-center py-0.5">
<span className="text-[10px] font-black uppercase tracking-[0.15em]">{marqueeText}</span>
<span className="text-[10px] font-black uppercase tracking-[0.15em]">{marqueeText}</span>
</div>
</div>
<div className="h-[4px] w-full hazard-stripes" />
</div>
</div>
{/* Floating Pastel Glow Spheres matching the theme */}
<motion.div
animate={{
x: [0, 30, -15, 0],
y: [0, -20, 15, 0],
}}
transition={{
duration: 15,
repeat: Infinity,
ease: 'easeInOut'
}}
className="absolute top-1/4 left-10 w-[350px] h-[350px] bg-blue-300/25 blur-[120px] rounded-full pointer-events-none z-0"
/>
<motion.div
animate={{
x: [0, -40, 20, 0],
y: [0, 30, -20, 0],
}}
transition={{
duration: 18,
repeat: Infinity,
ease: 'easeInOut'
}}
className="absolute bottom-1/4 right-10 w-[400px] h-[400px] bg-rose-300/25 blur-[130px] rounded-full pointer-events-none z-0"
/>
<motion.div
animate={{
scale: [0.9, 1.1, 0.9],
opacity: [0.3, 0.6, 0.3]
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'easeInOut'
}}
className="absolute top-1/2 left-1/3 w-[200px] h-[200px] bg-pink-300/20 blur-[90px] rounded-full pointer-events-none z-0"
/>
{/* Un-skewed Content Container */}
<div className="relative z-10 max-w-7xl mx-auto px-6 md:px-12 lg:px-16">
{/* Animated Header */}
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={headerVariants}
className="text-center mb-20 relative"
>
<motion.div
whileHover={{ scale: 1.05 }}
className="inline-flex items-center gap-2 px-4 py-1.5 bg-gradient-to-r from-blue-500/10 via-rose-500/10 to-pink-500/10 text-indigo-700 border border-indigo-500/20 text-[10px] font-black uppercase tracking-widest rounded-full mb-6 cursor-default shadow-lg shadow-indigo-500/5 hover:border-indigo-400/40 transition-colors"
>
<Sparkles className="w-3.5 h-3.5 text-rose-500 animate-spin" style={{ animationDuration: '4s' }} />
Registry Live
</motion.div>
<h2 className="text-4xl md:text-6xl font-extrabold text-slate-900 tracking-tight leading-none mb-5 font-fantasy-title">
Special Events Registry
</h2>
<div className="w-24 h-[3px] bg-gradient-to-r from-blue-500 via-rose-500 to-pink-500 mx-auto mb-6 rounded-full shadow-[0_0_12px_rgba(79,70,229,0.2)]" />
<p className="text-slate-600 text-sm md:text-base max-w-xl mx-auto font-medium leading-relaxed">
Explore premium workshops, hackathons, and institutional programs selected for Rajalakshmi Institute of Technology.
</p>
</motion.div>
{/* Animated Cards Grid */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10"
>
{specialEvents.map((event) => (
<motion.div
key={event.id}
variants={cardVariants}
whileHover={{
y: -10,
scale: 1.02,
borderColor: 'rgba(79, 70, 229, 0.3)',
boxShadow: "0 25px 50px -12px rgba(79, 70, 229, 0.12), 0 0 15px rgba(79, 70, 229, 0.04)"
}}
whileTap={{ scale: 0.98 }}
onClick={() => handleRedirect(event.link)}
className="bg-white/85 backdrop-blur-md border border-slate-200 rounded-[2.2rem] p-8 transition-all duration-300 cursor-pointer group flex flex-col justify-between min-h-[290px] relative overflow-hidden shadow-xl shadow-slate-900/5"
>
{/* Animated inner glowing border gradient on hover */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 via-rose-500/5 to-pink-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
<div className="absolute -top-12 -right-12 w-32 h-32 bg-blue-400/5 blur-2xl rounded-full group-hover:bg-blue-400/10 transition-all duration-500" />
<div>
<div className="flex items-center justify-between mb-6">
{/* Glowing Icon Container */}
<div className="w-12 h-12 rounded-2xl bg-indigo-50/80 border border-indigo-100/60 flex items-center justify-center text-indigo-600 group-hover:bg-gradient-to-tr group-hover:from-blue-500 group-hover:to-pink-500 group-hover:text-white group-hover:border-transparent transition-all duration-300 group-hover:shadow-[0_0_15px_rgba(79,70,229,0.25)]">
<Zap className="w-5 h-5 group-hover:animate-bounce" />
</div>
{/* Status chip */}
<span className="text-[9px] font-black text-indigo-600 group-hover:text-indigo-800 uppercase tracking-widest border border-indigo-50 px-2.5 py-1 rounded-full bg-indigo-50/40 transition-colors">
Featured
</span>
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-3 group-hover:text-indigo-950 transition-all duration-300 tracking-tight font-fantasy-card">
{event.title}
</h3>
<p className="text-slate-700 leading-relaxed font-medium line-clamp-4 group-hover:text-slate-900 transition-colors font-fantasy-body">
{event.description}
</p>
</div>
{/* Bottom bar with action */}
<div className="mt-8 pt-5 border-t border-slate-100 flex items-center justify-between group-hover:border-indigo-200 transition-colors">
<span className="text-[10px] font-extrabold text-indigo-600 uppercase tracking-widest flex items-center gap-2 group-hover:text-indigo-800 transition-colors font-fantasy-card">
Launch Registry
<ArrowUpRight className="w-4 h-4 transform group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform duration-300 text-indigo-600 group-hover:text-pink-500" />
</span>
{/* Visual accent dot */}
<div className="w-1.5 h-1.5 rounded-full bg-indigo-600 group-hover:bg-pink-500 group-hover:animate-ping transition-colors" />
</div>
</motion.div>
))}
</motion.div>
</div>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&family=Cinzel:wght@700;900&family=Eagle+Lake&display=swap');
.font-fantasy-title {
font-family: 'Cinzel', serif;
font-weight: 900;
}
.font-fantasy-card {
font-family: 'MedievalSharp', cursive;
}
.font-fantasy-body {
font-family: 'Eagle Lake', cursive;
font-size: 13px;
}
@keyframes marquee-scroll {
0% { transform: translateX(0%); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
display: flex;
width: max-content;
animation: marquee-scroll 25s linear infinite;
}
.hazard-stripes {
background: repeating-linear-gradient(
-45deg,
#000,
#000 6px,
#facc15 6px,
#facc15 12px
);
}
`}</style>
</section>
);
};
export default SpecialEventsBanner;

View File

@@ -1,101 +0,0 @@
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

@@ -1,583 +0,0 @@
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-white 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> UPLOADING...</> :
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

@@ -1,205 +0,0 @@
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[];
bookedEventIds?: string[];
onToggleBooking?: (id: string) => void;
userRole?: string;
}
const UpcomingEventsSlider: React.FC<UpcomingEventsSliderProps> = ({
events,
bookedEventIds = [],
onToggleBooking,
userRole
}) => {
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 justify-between gap-2 mb-2">
<div className="flex items-center gap-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>
{userRole === 'STUDENT' && bookedEventIds.includes(selectedEvent.id) && (
<span className="px-2.5 py-1 bg-emerald-50 text-emerald-600 border border-emerald-200 text-[8px] font-black rounded-lg uppercase tracking-wider">
Registered
</span>
)}
</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 flex flex-col gap-3">
{userRole === 'STUDENT' && onToggleBooking && (
<>
{bookedEventIds.includes(selectedEvent.id) ? (
<button
onClick={() => {
if (window.confirm(`Are you sure you want to cancel your registration for "${selectedEvent.title}"?`)) {
onToggleBooking(selectedEvent.id);
setSelectedEvent(null);
}
}}
className="w-full py-4 bg-rose-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-700 transition-all shadow-xl shadow-rose-200 focus:outline-none focus:ring-4 focus:ring-rose-100"
>
Cancel Registration
</button>
) : (
<button
onClick={() => {
onToggleBooking(selectedEvent.id);
setSelectedEvent(null);
}}
className="w-full py-4 bg-emerald-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-200 focus:outline-none focus:ring-4 focus:ring-emerald-100"
>
Get Tickets
</button>
)}
</>
)}
<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

@@ -1,199 +0,0 @@
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

@@ -1,19 +0,0 @@
export const CLUBS = [
'General',
'UBA',
'YUVA Club',
'National Service Scheme (NSS)',
'Youth Red Cross(YRC)',
'Rotaract Club',
'Women Empowerment Club',
'Techsparks Club',
'Infinitus',
'STEAM Club',
'Fusion Language Club',
'Artist League',
'Photography Club',
'Podcast Club',
'Nippon Club',
'Telgu Club',
'Mediastic Hub'
];

View File

@@ -1,32 +0,0 @@
export interface InstitutionalEvent {
id: string;
name: string;
month: string;
semester: 'ODD' | 'EVEN';
}
export const INSTITUTIONAL_EVENTS: InstitutionalEvent[] = [
// EVEN Semester (Jan - June)
{ id: 'ie-1', name: 'New Year & Christmas Celebration', month: 'January', semester: 'EVEN' },
{ id: 'ie-2', name: 'Republic Day Celebration', month: 'January', semester: 'EVEN' },
{ id: 'ie-3', name: 'Pongal Celebration', month: 'January', semester: 'EVEN' },
{ id: 'ie-4', name: 'Yatra Banner Release', month: 'January', semester: 'EVEN' },
{ id: 'ie-5', name: 'Yatra Ethnic Day', month: 'February', semester: 'EVEN' },
{ id: 'ie-6', name: 'Yatra', month: 'February', semester: 'EVEN' },
{ id: 'ie-7', name: 'International Womens Day', month: 'March', semester: 'EVEN' },
{ id: 'ie-8', name: 'Ugathi', month: 'March', semester: 'EVEN' },
{ id: 'ie-9', name: 'Tamil New Year', month: 'April', semester: 'EVEN' },
// ODD Semester (July - Dec)
{ id: 'ie-10', name: 'Techritz', month: 'July', semester: 'ODD' },
{ id: 'ie-11', name: 'Independence Day', month: 'August', semester: 'ODD' },
{ id: 'ie-12', name: 'AI Horizon Week', month: 'August', semester: 'ODD' },
{ id: 'ie-13', name: 'Initiation Day', month: 'September', semester: 'ODD' },
{ id: 'ie-14', name: 'Onam Celebration', month: 'September', semester: 'ODD' },
{ id: 'ie-15', name: 'Teachers Day', month: 'September', semester: 'ODD' },
{ id: 'ie-16', name: 'Engineers Day', month: 'September', semester: 'ODD' },
{ id: 'ie-17', name: 'SDG Golu', month: 'September', semester: 'ODD' },
{ id: 'ie-18', name: 'Ayutha Puja', month: 'October', semester: 'ODD' },
{ id: 'ie-19', name: 'Tech Fest IIT Bombay', month: 'October', semester: 'ODD' },
{ id: 'ie-20', name: 'EDI Conclave - FICCI Flow', month: 'November', semester: 'ODD' },
];

View File

@@ -1,67 +0,0 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: number;
email: string;
fullName: string;
role: 'FACULTY' | 'HOD' | 'PRINCIPAL' | 'ADMIN' | 'PLACEMENT' | 'STUDENT';
department: string;
isClubCoordinator?: boolean;
isPlacementStaff?: boolean;
isClassIncharge?: boolean;
inchargeClass?: string;
inchargeBatch?: string;
inchargeSection?: string;
classStrength?: number;
assignedClubs?: string[];
regNo?: string;
year?: string;
section?: string;
collegeName?: string;
gender?: string;
phone?: string;
}
interface AuthContextType {
user: User | null;
login: (userData: User) => void;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const savedUser = localStorage.getItem('ems_user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
}, []);
const login = (userData: User) => {
setUser(userData);
localStorage.setItem('ems_user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('ems_user');
};
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -1,127 +0,0 @@
import React, { createContext, useContext, useState } from 'react';
import { createPortal } from 'react-dom';
import { CheckSquare, XCircle, CheckCircle, Eye } from 'lucide-react';
interface DialogOptions {
type?: 'info' | 'confirm' | 'error' | 'success';
title: string;
message: string;
onConfirm?: () => void;
onCancel?: () => void;
}
interface DialogContextType {
showAlert: (title: string, message: string, type?: 'info' | 'error' | 'success') => void;
showConfirm: (title: string, message: string, onConfirm: () => void, onCancel?: () => void) => void;
}
const DialogContext = createContext<DialogContextType | undefined>(undefined);
export const useDialog = () => {
const context = useContext(DialogContext);
if (!context) {
throw new Error('useDialog must be used within a DialogProvider');
}
return context;
};
export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [options, setOptions] = useState<DialogOptions | null>(null);
const showAlert = (title: string, message: string, type: 'info' | 'error' | 'success' = 'info') => {
setOptions({
title,
message,
type,
});
setIsOpen(true);
};
const showConfirm = (title: string, message: string, onConfirm: () => void, onCancel?: () => void) => {
setOptions({
title,
message,
type: 'confirm',
onConfirm,
onCancel,
});
setIsOpen(true);
};
const handleClose = () => {
if (options?.onCancel) {
options.onCancel();
}
setIsOpen(false);
};
const handleConfirm = () => {
if (options?.onConfirm) {
options.onConfirm();
}
setIsOpen(false);
};
return (
<DialogContext.Provider value={{ showAlert, showConfirm }}>
{children}
{isOpen && options && createPortal(
<div className="fixed inset-0 z-[100000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white rounded-[2.5rem] w-full max-w-sm p-8 text-center shadow-2xl animate-in zoom-in-95 border border-slate-100 flex flex-col justify-between">
<div>
<div className={`w-16 h-16 rounded-full flex items-center justify-center text-2xl mx-auto mb-6 ${
options.type === 'confirm' ? "bg-amber-50 text-amber-500" :
options.type === 'error' ? "bg-rose-50 text-rose-500" :
options.type === 'success' ? "bg-emerald-50 text-emerald-500" :
"bg-blue-50 text-blue-500"
}`}>
{options.type === 'confirm' && <CheckSquare className="w-8 h-8" />}
{options.type === 'error' && <XCircle className="w-8 h-8" />}
{options.type === 'success' && <CheckCircle className="w-8 h-8" />}
{(options.type === 'info' || !options.type) && <Eye className="w-8 h-8" />}
</div>
<h3 className="text-xl font-black text-slate-800 uppercase mb-2 tracking-tight">
{options.title}
</h3>
<p className="text-slate-500 text-xs font-semibold leading-relaxed mb-8">
{options.message}
</p>
</div>
<div className="flex gap-4">
{options.type === 'confirm' ? (
<>
<button
onClick={handleClose}
className="flex-1 py-3 bg-slate-100 hover:bg-slate-200 text-slate-500 rounded-xl font-black uppercase text-[10px] tracking-widest transition-all"
>
No, Cancel
</button>
<button
onClick={handleConfirm}
className="flex-1 py-3 bg-[#1A202C] hover:bg-black text-white rounded-xl font-black uppercase text-[10px] tracking-widest transition-all shadow-md"
>
Yes, Proceed
</button>
</>
) : (
<button
onClick={handleClose}
className={`w-full py-3 text-white rounded-xl font-black uppercase text-[10px] tracking-widest transition-all shadow-md ${
options.type === 'error' ? "bg-rose-500 hover:bg-rose-600 shadow-rose-100" :
options.type === 'success' ? "bg-emerald-500 hover:bg-emerald-600 shadow-emerald-100" :
"bg-[#1A202C] hover:bg-black shadow-slate-100"
}`}
>
Okay
</button>
)}
</div>
</div>
</div>,
document.body
)}
</DialogContext.Provider>
);
};

View File

@@ -1,169 +0,0 @@
# Manual Integration Guide: Google Sign-In with Firebase Auth
This document provides step-by-step instructions on how to transition from the simulated Google login to a production-ready Google Authentication system using Firebase Auth in the React frontend.
---
## Step 1: Firebase Project Setup
1. Go to the [Firebase Console](https://console.firebase.google.com/).
2. Click **Add Project** and follow the prompts to create a new project.
3. Once the project is created, click the **Web icon** (`</>`) on the project overview page to register a new web application.
4. Copy the `firebaseConfig` details provided in the Firebase Console. It will look like this:
```javascript
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.firebasestorage.app",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
```
---
## Step 2: Enable Google Authentication
1. In the left-hand navigation sidebar of the Firebase Console, go to **Build** > **Authentication**.
2. Click **Get Started** if this is the first time setting up Authentication.
3. Navigate to the **Sign-in method** tab.
4. Click **Add new provider** and select **Google**.
5. Enable the toggle, configure your public-facing project name, select a project support email, and click **Save**.
6. (Optional) Under **Authorized domains**, ensure `localhost` and your production domain are listed so OAuth redirects work.
---
## Step 3: Set Up Project Credentials in Google Cloud Console
Google Sign-in requires OAuth consent. If you see redirect issues:
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
2. Select your Firebase project from the dropdown.
3. Go to **APIs & Services** > **OAuth consent screen**.
4. Configure the publishing status to **Testing** or **Production**, add test users if in Testing, and fill out required app details.
---
## Step 4: Frontend Code Integration
### 1. Install Firebase SDK
Ensure firebase dependencies are installed in your frontend folder:
```bash
npm install firebase
```
### 2. Configure Firebase Auth Client
Create/update `frontend/src/lib/firebaseConfig.ts` with the copied configurations:
```typescript
import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.firebasestorage.app",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Firebase Auth
export const auth = getAuth(app);
export const googleProvider = new GoogleAuthProvider();
```
### 3. Implement Sign-in Action
In `LoginPage.tsx`, replace the mock handlers with real Firebase Auth sign-in triggers:
```typescript
import { signInWithPopup } from 'firebase/auth';
import { auth, googleProvider } from '../lib/firebaseConfig';
const handleGoogleSignIn = async () => {
try {
const result = await signInWithPopup(auth, googleProvider);
const user = result.user;
const email = user.email; // e.g. student.240007@cse.ritchennai.edu.in
const displayName = user.displayName || "RIT User";
// Call your backend/database lookup or sync user details
const response = await fetch(API_BASE_URL + '/api/auth/google-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, fullName: displayName })
});
if (response.ok) {
const userData = await response.json();
login(userData);
} else {
const errorData = await response.json();
alert(errorData.message || "Failed to log in.");
}
} catch (error) {
console.error("Google Sign-In Error:", error);
}
};
```
---
## Step 5: Backend Integration (Spring Boot)
To support Google authentication token validation or syncing, add a secure endpoint:
```java
@PostMapping("/api/auth/google-login")
public ResponseEntity<?> googleLogin(@RequestBody Map<String, String> payload) {
String email = payload.get("email");
String fullName = payload.get("fullName");
if (email == null || email.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "Email is required"));
}
email = email.trim().toLowerCase();
// Check if user already exists
Optional<User> existingUser = userRepository.findByEmail(email);
if (existingUser.isPresent()) {
return ResponseEntity.ok(existingUser.get());
}
// Auto-register RIT student if email matches the student format
if (email.matches("^student\\.\\d{6}@[a-zA-Z0-9&-_]+\\.ritchennai\\.edu\\.in$")) {
User newStudent = new User();
newStudent.setEmail(email);
newStudent.setFullName(fullName);
newStudent.setRole("STUDENT");
// Parse department & batch logic here
String rollNumber = email.split("@")[0].substring(8); // extracts roll e.g. "240007"
String rawDept = email.split("@")[1].split("\\.")[0]; // cse
newStudent.setRegNo(rollNumber);
// Set Department
newStudent.setDepartment(mapDepartmentDomain(rawDept));
// Set Year Batch
int joinYear = 2000 + Integer.parseInt(rollNumber.substring(0, 2));
int currentYear = LocalDate.now().getYear();
int academicOffset = LocalDate.now().getMonthValue() >= 6 ? 1 : 0;
int yearIndex = currentYear - joinYear + academicOffset;
String[] years = {"1st Year", "2nd Year", "3rd Year", "4th Year"};
newStudent.setInchargeBatch(yearIndex >= 1 && yearIndex <= 4 ? years[yearIndex - 1] : "N/A");
newStudent.setInchargeClass(newStudent.getDepartment());
newStudent.setInchargeSection("A");
// Generate random placeholder password for DB constraints
newStudent.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
userRepository.save(newStudent);
return ResponseEntity.ok(newStudent);
}
return ResponseEntity.status(401).body(Map.of("message", "Access denied. Only registered accounts can log in."));
}
```

View File

@@ -1,169 +0,0 @@
@import "tailwindcss";
@import "./App.css";
@theme {
--color-brand-indigo: #4f46e5;
--color-brand-navy: #0f4475;
--color-brand-glow: rgba(79, 70, 229, 0.1);
--color-surface-background: #f8fafc;
--color-surface-white: #ffffff;
--color-surface-input: #f9fafb;
--color-text-dark: #0f172a;
--color-text-primary: #1e293b;
--color-text-secondary: #475569;
--color-text-muted: #94a3b8;
--color-status-success: #059669;
--color-status-danger: #dc2626;
--color-status-warning: #d97706;
--font-sans: "Inter", sans-serif;
--radius-4xl: 2rem;
--radius-5xl: 2.5rem;
--radius-6xl: 3.5rem;
--shadow-premium: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
--shadow-widget: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
}
@layer base {
body {
@apply bg-surface-background text-text-primary font-sans antialiased;
}
}
@layer components {
.glass-effect {
@apply bg-white/80 backdrop-blur-md border border-white/20;
}
.premium-shadow {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.premium-shadow-sm {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
}
.sidebar-item {
@apply flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 text-text-secondary font-semibold hover:bg-brand-glow hover:text-brand-indigo cursor-pointer;
}
.sidebar-item-active {
@apply bg-brand-glow text-brand-indigo;
}
.card-widget {
@apply bg-white rounded-[2rem] p-6 border border-slate-100 shadow-sm hover:shadow-md transition-shadow duration-300;
}
.floating-sidebar {
@apply fixed left-4 top-4 bottom-4 w-64 bg-white rounded-[2.5rem] border border-slate-200/60 shadow-xl z-50 flex flex-col p-6;
}
}
::-webkit-scrollbar {
width: 5px;
}
::-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

@@ -1,3 +0,0 @@
// Dynamic API Base URL based on the current hostname
// This allows access from localhost as well as other devices on the same network
export const API_BASE_URL = `http://${window.location.hostname}:8081`;

View File

@@ -1,13 +0,0 @@
export const getCurrentAcademicYear = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth(); // 0-11
// If we are in June or later, the academic year is currentYear-(currentYear+1)
// Otherwise, it's (currentYear-1)-currentYear
if (month >= 5) { // June is index 5
return `${year}-${(year + 1).toString().slice(-2)}`;
} else {
return `${year - 1}-${year.toString().slice(-2)}`;
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +0,0 @@
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,6 +0,0 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -1,11 +0,0 @@
import './lib/firebaseBackend'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -1,588 +0,0 @@
import { API_BASE_URL } from '../lib/config';
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 { Mail, Lock, LogIn, AlertCircle, X, ArrowLeft, Shield } from 'lucide-react';
import { signInWithPopup } from 'firebase/auth';
import { auth, googleProvider } from '../lib/firebaseBackend';
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'];
const GoogleIcon = () => (
<svg className="w-5 h-5 mr-3" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1, 0, 0, 1, 0, 0)">
<path d="M21.35,11.1H12v2.7h5.38c-0.24,1.28 -0.96,2.37 -2.04,3.1v2.58h3.3c1.93,-1.78 3.04,-4.4 3.04,-7.48C21.68,11.96 21.56,11.49 21.35,11.1z" fill="#4285F4" />
<path d="M12,20.58c2.43,0 4.47,-0.8 5.96,-2.2l-2.58,-2c-0.72,0.48 -1.64,0.77 -2.58,0.77 -2.37,0 -4.38,-1.6 -5.1,-3.75H4.31v2.1a8.4,8.4 0 0,0 7.69,5.08z" fill="#34A853" />
<path d="M6.9,13.4c-0.18,-0.54 -0.29,-1.11 -0.29,-1.7 0,-0.59 0.11,-1.16 0.29,-1.7V7.9H4.31A8.4,8.4 0 0,0 3.3,11.7c0,1.38 0.33,2.69 1.01,3.8l2.59,-2.1z" fill="#FBBC05" />
<path d="M12,6.85c1.32,0 2.5,0.45 3.44,1.35l2.58,-2.58C16.46,4.1 14.43,3.32 12,3.32c-4.79,0 -8.7,2.82 -10.39,6.9l2.59,2.1c0.72,-2.15 2.73,-3.75 5.1,-3.75z" fill="#EA4335" />
</g>
</svg>
);
export const LoginPage: React.FC = () => {
const { login } = useAuth();
const [isSignUp, setIsSignUp] = useState(false);
const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null);
const [splashState, setSplashState] = useState<'logo' | 'text' | 'fade-to-white' | 'done'>('logo');
useEffect(() => {
const timer1 = setTimeout(() => {
setSplashState('text');
}, 1500);
const timer2 = setTimeout(() => {
setSplashState('fade-to-white');
}, 3000);
const timer3 = setTimeout(() => {
setSplashState('done');
}, 4500);
return () => {
clearTimeout(timer1);
clearTimeout(timer2);
clearTimeout(timer3);
};
}, []);
// Login credentials state
const [emailInput, setEmailInput] = useState('');
const [passwordInput, setPasswordInput] = useState('');
// Register state (for other colleges / external students)
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);
// Google sign in state
const [showGoogleChooser, setShowGoogleChooser] = useState(false);
const [customGoogleEmail, setCustomGoogleEmail] = useState('');
const mockAccounts = [
{ email: "student.240007@cse.ritchennai.edu.in", name: "Abiram R (3rd Year CSE)", role: "Student (Auto-Reg)" },
{ email: "student.250012@aiml.ritchennai.edu.in", name: "John Doe (2nd Year AIML)", role: "Student (Auto-Reg)" },
{ email: "faculty@rit.edu", name: "Dr. Faculty Member", role: "Faculty" },
{ email: "hod@rit.edu", name: "Prof. Head of Dept", role: "HOD" },
{ email: "principal@rit.edu", name: "Dr. College Principal", role: "Principal" },
{ email: "admin@rit.edu", name: "System Administrator", role: "Admin" },
];
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleLoginSubmit = async (e: React.FormEvent) => {
e.preventDefault();
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: emailInput,
password: passwordInput
})
});
if (response.ok) {
const userData = await response.json();
login(userData);
} else {
const errData = await response.json();
throw new Error(errData.message || "Invalid credentials.");
}
} catch (err: any) {
setErrorMessage(err.message || "An error occurred during authentication.");
} finally {
setIsSubmitting(false);
}
};
const handleGoogleSignIn = async () => {
setErrorMessage(null);
setIsSubmitting(true);
try {
const result = await signInWithPopup(auth, googleProvider);
const user = result.user;
if (user && user.email) {
const response = await fetch(API_BASE_URL + '/api/auth/google-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: user.email,
fullName: user.displayName || "RIT User"
})
});
if (response.ok) {
const userData = await response.json();
login(userData);
} else {
const errData = await response.json();
throw new Error(errData.message || "Failed to sync Google account with database.");
}
}
} catch (err: any) {
console.error("Google Auth Error:", err);
if (err.code !== 'auth/popup-closed-by-user') {
setErrorMessage(err.message || "Google Sign-In failed.");
}
} finally {
setIsSubmitting(false);
}
};
const handleGoogleLoginSubmit = async (email: string, name: string) => {
if (!email) {
alert("Please select or enter a Google account email.");
return;
}
setErrorMessage(null);
setIsSubmitting(true);
setShowGoogleChooser(false);
try {
const response = await fetch(API_BASE_URL + '/api/auth/google-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
fullName: name
})
});
if (response.ok) {
const userData = await response.json();
login(userData);
} else {
const errData = await response.json();
throw new Error(errData.message || "OAuth login failed.");
}
} catch (err: any) {
setErrorMessage(err.message || "Google Sign-In failed.");
} finally {
setIsSubmitting(false);
}
};
const handleRegisterSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMessage(null);
setIsSubmitting(true);
try {
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: 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: any) {
setErrorMessage(err.message || "An error occurred.");
} finally {
setIsSubmitting(false);
}
};
const RIT_BLUE = 'bg-[#004a99]';
const RIT_BLUE_TEXT = 'text-[#004a99]';
const RIT_BLUE_HOVER = 'hover:bg-[#003366]';
return (
<div className="min-h-screen w-full bg-slate-50 flex items-center justify-center font-sans p-6 overflow-x-hidden relative">
{/* Background radial accent */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-brand-glow rounded-full blur-[120px] opacity-40 pointer-events-none -z-10" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-indigo-50 rounded-full blur-[120px] opacity-40 pointer-events-none -z-10" />
<AnimatePresence mode="wait">
{!isSignUp ? (
// ----------------------------------------------------
// CENTRALIZED SIGN IN
// ----------------------------------------------------
<motion.div
key="login-pane"
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
transition={{ duration: 0.4 }}
className="w-full max-w-md bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-10 flex flex-col items-center relative overflow-hidden"
>
<div className="w-full text-center mb-8">
<img src={ritLogo} alt="RIT Logo" className="h-16 w-auto mx-auto mb-6 object-contain" />
<h2 className="text-3xl font-black text-slate-900 tracking-tight flex items-center justify-center gap-1.5 uppercase">
RIT EVENT HUB
</h2>
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-1">Centralized Portal Login</p>
</div>
{/* Google Sign In Button */}
<button
onClick={handleGoogleSignIn}
onDoubleClick={() => setShowGoogleChooser(true)}
title="Double-click to open mock accounts for local testing"
className="w-full py-4 px-6 bg-white border border-slate-200 hover:border-brand-indigo/30 rounded-2xl flex items-center justify-center text-xs font-black text-slate-700 uppercase tracking-widest hover:bg-slate-50 transition-all hover:scale-[1.02] active:scale-[0.98] premium-shadow-sm mb-6"
>
<GoogleIcon />
Sign in with Google
</button>
{/* Separator */}
<div className="w-full flex items-center justify-center gap-4 mb-6">
<div className="h-px bg-slate-100 flex-1" />
<span className="text-[9px] font-black text-slate-300 uppercase tracking-widest">or use credentials</span>
<div className="h-px bg-slate-100 flex-1" />
</div>
{/* Email & Password Form */}
<form onSubmit={handleLoginSubmit} className="w-full space-y-4">
<div className="relative">
<Mail className="absolute left-5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
required
type="email"
placeholder="EMAIL ADDRESS"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
className="w-full bg-slate-50 border border-transparent focus:border-brand-indigo/30 rounded-2xl pl-12 pr-6 py-4.5 text-xs font-bold outline-none transition-all"
/>
</div>
<div className="relative">
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
required
type={showPassword ? 'text' : 'password'}
placeholder="PASSWORD"
value={passwordInput}
onChange={(e) => setPasswordInput(e.target.value)}
className="w-full bg-slate-50 border border-transparent focus:border-brand-indigo/30 rounded-2xl pl-12 pr-12 py-4.5 text-xs font-bold outline-none transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-5 top-1/2 -translate-y-1/2 text-slate-300 hover:text-brand-indigo transition-colors"
>
<i className={cn("fas", showPassword ? "fa-eye-slash" : "fa-eye")}></i>
</button>
</div>
{errorMessage && (
<div className="p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2 border border-red-100/50">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{errorMessage}</span>
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className={cn(
"w-full py-5 text-white text-xs font-black rounded-2xl transition-all shadow-lg active:scale-[0.98] disabled:opacity-55 flex items-center justify-center gap-2 mt-4",
RIT_BLUE,
RIT_BLUE_HOVER
)}
>
<LogIn className="w-4 h-4" />
{isSubmitting ? 'Verifying...' : 'Sign in now...'}
</button>
</form>
<div className="mt-8 text-center space-y-3">
<button
type="button"
onClick={() => {
setIsSignUp(true);
setSignUpType('EXTERNAL');
setErrorMessage(null);
}}
className="text-xs font-bold text-slate-400 hover:text-brand-indigo transition-colors block mx-auto"
>
External Student ? Register here..
</button>
</div>
</motion.div>
) : (
// ----------------------------------------------------
// SIGN UP (Only for External Students)
// ----------------------------------------------------
<motion.div
key="signup-pane"
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
transition={{ duration: 0.4 }}
className="w-full max-w-md bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-10 flex flex-col relative overflow-hidden"
>
{/* Back Button */}
<button
onClick={() => {
setIsSignUp(false);
setSignUpType(null);
setErrorMessage(null);
}}
className="absolute top-6 left-6 w-10 h-10 bg-slate-50 hover:bg-slate-100 text-slate-500 rounded-full flex items-center justify-center transition-all border border-slate-100"
type="button"
>
<ArrowLeft className="w-4 h-4" />
</button>
<div className="text-center mb-8 pt-4">
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
Other College <span className="text-[#004a99]">Sign Up</span>
</h2>
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
</div>
<form onSubmit={handleRegisterSubmit} className="space-y-4 overflow-y-auto max-h-[450px] pr-3 py-1 scrollbar-hide">
<div className="grid grid-cols-2 gap-4">
<input required name="name" placeholder="FULL NAME" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.name} onChange={handleInputChange} />
<input required type="email" name="email" placeholder="EMAIL" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none 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-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.regNo} onChange={handleInputChange} />
<input required name="phone" placeholder="PHONE NUMBER" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none 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-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.password} onChange={handleInputChange} />
<input required type="password" name="confirmPassword" placeholder="CONFIRM" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none 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-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none 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-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.collegeLocation} onChange={handleInputChange} />
</div>
<input required name="collegeName" placeholder="COLLEGE NAME" className="w-full bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.collegeName} onChange={handleInputChange} />
<div className="relative">
<select required name="department" className="w-full bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all cursor-pointer appearance-none" value={formData.department} onChange={handleInputChange}>
<option value="">SELECT DEPARTMENT</option>
{EXTERNAL_DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
{errorMessage && (
<div className="p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2 border border-red-100/50">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{errorMessage}</span>
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className={cn(
"w-full py-5 text-white text-xs font-black rounded-2xl transition-all shadow-lg active:scale-[0.98] disabled:opacity-55 flex items-center justify-center gap-2",
RIT_BLUE,
RIT_BLUE_HOVER
)}
>
{isSubmitting ? 'Registering...' : 'Sign up now...'}
</button>
</form>
</motion.div>
)}
</AnimatePresence>
{/* ----------------------------------------------------
MOCK GOOGLE ACCOUNT CHOOSER MODAL
---------------------------------------------------- */}
<AnimatePresence>
{showGoogleChooser && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowGoogleChooser(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-md bg-white rounded-[2.5rem] premium-shadow overflow-hidden"
>
<div className="p-8 space-y-4">
<div className="flex items-center justify-between pb-4 border-b border-slate-100">
<div>
<h3 className="text-lg font-black text-brand-navy flex items-center gap-1.5 uppercase">
<Shield className="w-5 h-5 text-brand-indigo" />
Sign In with Google
</h3>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-wider mt-0.5">Mock OAuth Provider Account Chooser</p>
</div>
<button onClick={() => setShowGoogleChooser(false)} className="p-2 hover:bg-slate-50 rounded-xl text-slate-400 transition-all">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-1">
{mockAccounts.map((acc) => (
<button
key={acc.email}
onClick={() => handleGoogleLoginSubmit(acc.email, acc.name.split(' (')[0])}
className="w-full text-left p-3.5 bg-slate-50 hover:bg-brand-glow border border-slate-100 hover:border-brand-indigo/20 rounded-2xl flex items-center justify-between transition-all hover:scale-[1.01]"
>
<div>
<p className="text-xs font-black text-brand-navy leading-none">{acc.name}</p>
<p className="text-[10px] text-slate-400 font-medium mt-1">{acc.email}</p>
</div>
<span className="px-2 py-0.5 bg-white border border-slate-100 text-[8px] font-black uppercase tracking-widest text-brand-indigo rounded-md">{acc.role}</span>
</button>
))}
</div>
<div className="border-t border-slate-100 pt-4 space-y-3">
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Or enter a custom email</p>
<div className="flex gap-2">
<input
type="email"
placeholder="student.260099@cse.ritchennai.edu.in"
value={customGoogleEmail}
onChange={(e) => setCustomGoogleEmail(e.target.value)}
className="flex-1 bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-xs font-semibold focus:outline-none focus:border-brand-indigo/30"
/>
<button
onClick={() => handleGoogleLoginSubmit(customGoogleEmail, "Custom Google User")}
className="px-4 py-2.5 bg-brand-navy text-white font-black text-[9px] uppercase tracking-widest rounded-xl hover:scale-105 active:scale-95 transition-all shadow-sm"
>
OAuth Sign In
</button>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
{/* Splash Screen Overlay */}
<AnimatePresence>
{splashState !== 'done' && (
<motion.div
key="splash-overlay"
initial={{ opacity: 1, backgroundColor: '#004a99' }}
animate={{
opacity: 1,
backgroundColor: splashState === 'fade-to-white' ? '#ffffff' : '#004a99'
}}
exit={{ opacity: 0 }}
transition={{
backgroundColor: { duration: 1.5, ease: "easeInOut" },
opacity: { duration: 1.5, ease: "easeInOut" }
}}
className="fixed inset-0 flex items-center justify-center z-[200] overflow-hidden"
>
{/* Background Pattern - fade it out when transitioning to white */}
<motion.div
animate={{ opacity: splashState === 'fade-to-white' ? 0 : 0.1 }}
transition={{ duration: 1.0 }}
className="absolute inset-0 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>
</motion.div>
<AnimatePresence mode="wait">
{splashState === 'logo' && (
<motion.div
key="logo"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.1 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
className="flex flex-col items-center gap-4"
>
<img src={ritLogo} alt="RIT Logo" className="h-28 w-auto object-contain drop-shadow-[0_10px_20px_rgba(255,255,255,0.15)]" />
</motion.div>
)}
{splashState === 'text' && (
<motion.div
key="text"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
className="text-center"
>
<h1 className="text-4xl md:text-5xl font-black text-white tracking-widest uppercase">
RIT EVENT HUB
</h1>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -1,163 +0,0 @@
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;
paymentLink?: string;
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;
targetedBatch?: 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';
image?: string;
}
export interface DeptBatch {
id: number;
name: string;
department: string;
classes: string[]; // List of Year - Section strings, e.g. ["1st Year - A", "2nd Year - B"]
}

View File

@@ -1,28 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})