Compare commits
63 Commits
bede7ca962
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f368c83246 | ||
|
|
db5ac8e2ea | ||
|
|
a19c36579e | ||
|
|
6be263771d | ||
|
|
9275722e16 | ||
|
|
6740400788 | ||
|
|
6d386273fb | ||
|
|
56c3849357 | ||
|
|
f58c98cd46 | ||
|
|
8c7a3d7f1a | ||
|
|
f8047b6cd3 | ||
|
|
72c213f2b6 | ||
|
|
e6ea5cb302 | ||
|
|
64f0c5d754 | ||
|
|
d9ed15b481 | ||
|
|
87865f810f | ||
|
|
906e9c757b | ||
|
|
23bf4fd86a | ||
|
|
95c97e81fa | ||
|
|
128e033a9a | ||
|
|
4e2edb7c52 | ||
|
|
bbb2cf7d30 | ||
|
|
b712b1e2f1 | ||
|
|
1641bf2110 | ||
|
|
2f69b0a29d | ||
|
|
5ea7de558b | ||
|
|
596504840e | ||
|
|
56ce10fc55 | ||
|
|
0c86c087e4 | ||
|
|
fe7ec3d61e | ||
|
|
34ea034899 | ||
|
|
a99c54c705 | ||
|
|
d028e86bdd | ||
|
|
b5b152f43c | ||
|
|
82c6444c28 | ||
|
|
f937efce5c | ||
|
|
e15c95fafd | ||
|
|
9a0c601955 | ||
|
|
774ae7e7ae | ||
|
|
9a58b2a53e | ||
|
|
a4ad5e5815 | ||
|
|
7cd28fa974 | ||
|
|
bba33ef05b | ||
|
|
5f3b3f5973 | ||
|
|
f8ca5a3368 | ||
|
|
2d91f6f5c4 | ||
|
|
154f19d546 | ||
|
|
02d8dea9a9 | ||
|
|
d3bd00e506 | ||
|
|
e83b934e9c | ||
|
|
1eebd10f3a | ||
|
|
d827f4aab9 | ||
|
|
29377ce56e | ||
|
|
97a59ee715 | ||
|
|
3afb40d451 | ||
|
|
bb71b725d9 | ||
|
|
fcaf16405d | ||
|
|
3f277f53f4 | ||
|
|
4e0300836e | ||
|
|
122e839d36 | ||
|
|
68e8f6e002 | ||
|
|
a6efffdefd | ||
|
|
052fd76a99 |
222
IMS_NEXUS_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# IMS–Nexus Integration & Architecture Guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. How This Simulates the Actual Production Workflow
|
||||||
|
|
||||||
|
In the architecture documented in your design files, **Freshers-Hub (Nexus)** is the *Student Experience Layer* running on your VPS, while **IMS** is the *Authoritative System of Record* hosted and managed separately by the college.
|
||||||
|
|
||||||
|
### The Real-World Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Student Browser]
|
||||||
|
│
|
||||||
|
│ 1. Submits Reg No ("2114251001") + Password ("rit@2026")
|
||||||
|
▼
|
||||||
|
[Nexus Frontend (React)]
|
||||||
|
│
|
||||||
|
│ 2. POST /api/v1/ims/auth/login
|
||||||
|
▼
|
||||||
|
[Nexus Backend (Spring Boot Proxy)]
|
||||||
|
│
|
||||||
|
│ 3. Secure HTTPS Call with 2 Identities:
|
||||||
|
│ • Identity 1 (Application): "X-Nexus-App-Secret: <shared_secret>"
|
||||||
|
│ • Identity 2 (Student): Reg No & Password
|
||||||
|
▼
|
||||||
|
[College Firewall & IMS API Gateway] ◄── [Gate 1: HTTPS & Gate 2: IP Allowlist]
|
||||||
|
│
|
||||||
|
│ 4. Authenticates credentials against IMS DB
|
||||||
|
│ 5. Enforces Scopes (e.g. student.read, timetable.read)
|
||||||
|
▼
|
||||||
|
[IMS Database (Locked Source of Truth)]
|
||||||
|
│
|
||||||
|
│ 6. Returns Student Profile, Day Schedule, Room Info
|
||||||
|
▼
|
||||||
|
[Nexus Backend] ──(Session Cached)──► [Nexus Frontend Dashboard]
|
||||||
|
```
|
||||||
|
|
||||||
|
### What the Mock Layer is Simulating Today
|
||||||
|
|
||||||
|
| Production Component | How Our Code Simulates It Locally |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **IMS Server & Database** | `MockImsClientImpl.java` holds an in-memory repository of realistic student records (CSE, AI&DS, ECE), timetables, and classroom blocks. |
|
||||||
|
| **Credential Authentication** | The mock checks Reg No & Password and generates a structured session token (`ims_session_<regNo>_<uuid>`). |
|
||||||
|
| **Dynamic Timetable Engine** | Calculates current time vs. class slots to determine real-time period status (`COMPLETED`, `ONGOING`, `UPCOMING`). |
|
||||||
|
| **Security Boundary** | The frontend **never** talks to IMS directly. It always routes through the Nexus backend interface `ImsClient.java`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. On Deployment: What Actually Changes on Your End?
|
||||||
|
|
||||||
|
Because we used the **Strategy / Adapter Pattern**, your application is built so that switching to the live college server requires **zero code rewrites in the frontend, controllers, or database models**.
|
||||||
|
|
||||||
|
### Step 1: Switch the Configuration Property
|
||||||
|
In `backend/src/main/resources/application.properties` (or via Linux environment variables on your VPS):
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# 1. Flip client type from "mock" to "real"
|
||||||
|
ims.client.type=real
|
||||||
|
|
||||||
|
# 2. Provide the live IMS API Gateway URL
|
||||||
|
ims.base-url=https://api.ims.ritchennai.edu.in
|
||||||
|
|
||||||
|
# 3. Provide the shared secret given to you by the college IMS team
|
||||||
|
ims.api-key=${IMS_CLIENT_SECRET:production_secret_key_here}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: What Spring Boot Does Automatically
|
||||||
|
- Spring Boot detects `ims.client.type=real`.
|
||||||
|
- It deactivates `MockImsClientImpl` and activates `RealImsClientImpl.java`.
|
||||||
|
- `RealImsClientImpl` starts dispatching live HTTP requests with the required headers (`X-Nexus-App-Secret`, `X-Student-Id`).
|
||||||
|
|
||||||
|
### Step 3: Network & Security Setup on the VPS
|
||||||
|
1. **SSL/TLS**: Ensure your Nexus domain (e.g., `nexus.ritchennai.edu.in`) has an active HTTPS certificate.
|
||||||
|
2. **Fixed Static IP**: Give your VPS public IP (e.g., `103.xxx.xxx.xxx`) to the college network administrator for IP whitelisting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. What Information Do You Need from the IMS Team?
|
||||||
|
|
||||||
|
Since IMS is an independent standalone application managed by a separate team or vendor with no public APIs exposed yet, you need to request the following **4 items** from them:
|
||||||
|
|
||||||
|
### 📋 The Integration Checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
+─────────────────────────────────────────────────────────────────────────────+
|
||||||
|
| IMS INTEGRATION CHECKLIST |
|
||||||
|
+───┬──────────────────────────────────┬──────────────────────────────────────+
|
||||||
|
| 1 | Static IP Whitelisting | Whitelist your Nexus VPS outbound IP |
|
||||||
|
| 2 | Application Secret / API Key | A secure shared secret key / token |
|
||||||
|
| 3 | API Endpoint Base URL | e.g. https://api.ims.college.edu |
|
||||||
|
| 4 | API Contract & Response Format | JSON schema for login & timetable |
|
||||||
|
+───┴──────────────────────────────────┴──────────────────────────────────────+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Endpoints the IMS Team Needs to Expose for You
|
||||||
|
|
||||||
|
You can hand the IMS development team this exact list of endpoints:
|
||||||
|
|
||||||
|
#### 1. Student Authentication
|
||||||
|
- **Endpoint**: `POST /api/v1/auth/student-login`
|
||||||
|
- **Headers**: `X-Nexus-App-Secret: <your_secret_key>`
|
||||||
|
- **Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"regNumber": "2114251001",
|
||||||
|
"password": "student_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response Expected**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"token": "ims_jwt_token_here",
|
||||||
|
"student": {
|
||||||
|
"regNumber": "2114251001",
|
||||||
|
"name": "Anbu Kathir",
|
||||||
|
"degree": "B.E.",
|
||||||
|
"department": "Computer Science & Engineering",
|
||||||
|
"year": 1,
|
||||||
|
"semester": 2,
|
||||||
|
"section": "A",
|
||||||
|
"batch": "2025 - 2029"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Student Dashboard / Today's Timetable & Venue
|
||||||
|
- **Endpoint**: `GET /api/v1/dashboard/me` (or `GET /api/v1/timetable/me?date=YYYY-MM-DD`)
|
||||||
|
- **Headers**:
|
||||||
|
- `X-Nexus-App-Secret: <your_secret_key>`
|
||||||
|
- `X-Student-Id: 2114251001` (or `Authorization: Bearer <token>`)
|
||||||
|
- **Response Expected**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"classLocation": {
|
||||||
|
"roomNumber": "LH-204",
|
||||||
|
"buildingName": "Dr. APJ Abdul Kalam Academic Block",
|
||||||
|
"floor": "2nd Floor",
|
||||||
|
"wing": "East Wing",
|
||||||
|
"landmark": "Next to CSE Department Library"
|
||||||
|
},
|
||||||
|
"classIncharge": {
|
||||||
|
"name": "Dr. R. Arunkumar",
|
||||||
|
"cabin": "APJ Block, Cabin 208-B",
|
||||||
|
"email": "arunkumar.r@ritchennai.edu.in"
|
||||||
|
},
|
||||||
|
"todaySchedule": {
|
||||||
|
"dayOfWeek": "MONDAY",
|
||||||
|
"periods": [
|
||||||
|
{
|
||||||
|
"periodNumber": 1,
|
||||||
|
"timeSlot": "08:45 AM - 09:40 AM",
|
||||||
|
"subjectCode": "CS3201",
|
||||||
|
"subjectName": "Data Structures",
|
||||||
|
"type": "THEORY",
|
||||||
|
"facultyName": "Dr. R. Arunkumar",
|
||||||
|
"venue": "LH-204"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. What If the IMS Team Cannot Provide REST APIs Immediately?
|
||||||
|
|
||||||
|
If the IMS team is slow to build custom REST APIs, here are the standard industry workarounds in order of preference:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ If REST API is not ready today: │
|
||||||
|
└───────────────────┬────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────┼─────────────────────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
|
| Option A: | | Option B: | | Option C: |
|
||||||
|
| Read-Only DB | | Automated Nightly| | LDAP / Active |
|
||||||
|
| View / Replica | | JSON/CSV Export | | Directory Auth |
|
||||||
|
| (Direct read of | | (Sync timetable | | (Single Sign-On |
|
||||||
|
| class schedule) | | & room data) | | integration) |
|
||||||
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Option A (Read-Only Database View)**:
|
||||||
|
- Ask the database administrator for a **read-only database view** restricted strictly to timetable and room allocation tables. Your backend connects via JDBC read-only credentials.
|
||||||
|
2. **Option B (Scheduled JSON / CSV Sync)**:
|
||||||
|
- The IMS team runs a nightly cron job that dumps timetable/classroom data as an encrypted JSON/CSV file to a private S3 bucket or SFTP server. Your Spring Boot backend parses and caches it every 24 hours.
|
||||||
|
3. **Option C (LDAP / Active Directory)**:
|
||||||
|
- If the college uses Active Directory or LDAP for campus Wi-Fi/portals, students authenticate through LDAP, and timetable structures are matched using department & section mapping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Ready-to-Send Request Template for the IMS Team
|
||||||
|
|
||||||
|
You can copy and email this directly to the college IT / IMS team:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Subject: API Integration Request for Nexus Freshers-Hub Portal
|
||||||
|
|
||||||
|
Dear IMS / IT Team,
|
||||||
|
|
||||||
|
We are developing the Nexus Student Portal (Freshers-Hub) to provide students with quick access to their daily timetables, classroom venue directions, and academic tools.
|
||||||
|
|
||||||
|
To integrate seamlessly with the college IMS without duplicating data, we have implemented a secure server-to-server API client following standard enterprise security practices (IP Allowlisting + Shared Application Secret + Encrypted HTTPS).
|
||||||
|
|
||||||
|
Could you please provide us with the following integration details:
|
||||||
|
1. Staging/Production API Base URL (e.g., https://api.ims.ritchennai.edu.in)
|
||||||
|
2. Application API Key / Client Secret for the Nexus backend
|
||||||
|
3. IP Whitelisting for our VPS outbound IP: [INSERT_YOUR_VPS_IP]
|
||||||
|
4. Endpoints for:
|
||||||
|
- POST /api/v1/auth/student-login (Register Number & Password authentication)
|
||||||
|
- GET /api/v1/timetable/me (Student's daily schedule & classroom venue)
|
||||||
|
|
||||||
|
We have our Spring Boot backend proxy and mock test suites ready to plug into your API contract as soon as endpoints are available.
|
||||||
|
|
||||||
|
Thank you,
|
||||||
|
Nexus Engineering Team
|
||||||
|
```
|
||||||
105
README.md
@@ -1,32 +1,87 @@
|
|||||||
# React + TypeScript + Vite
|
# 🏫 Rajalakshmi Institute of Technology — Freshers-Hub
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
Welcome to the **Freshers-Hub Portal**, a multi-service campus assistant and student utility ecosystem designed for Rajalakshmi Institute of Technology (RIT). The platform provides student services including live transit tracking, study guides, competitive programming leaderboards, developer collaboration networks, and an automated AI assistant.
|
||||||
|
|
||||||
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)
|
## 🏗️ System Architecture & Service Stack
|
||||||
- [@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 system is built on a distributed microservice architecture utilizing four primary runtimes:
|
||||||
|
|
||||||
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 Oxlint configuration
|
│ Nginx Web Server │
|
||||||
|
│ (Ports 80 / 443) │
|
||||||
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
|
└───────────────┬───────────────┘
|
||||||
|
│
|
||||||
```json
|
┌───────────────────────┼───────────────────────┐
|
||||||
{
|
▼ ▼ ▼
|
||||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
|
||||||
"plugins": ["react", "typescript", "oxc"],
|
│ React Frontend │ │Spring Boot Backend│ │Go Chatbot Service │
|
||||||
"options": {
|
│ (Vite App) │ │ (Port 8085) │ │ (Port 8081) │
|
||||||
"typeAware": true
|
└───────────────────┘ └─────────┬─────────┘ └───────────────────┘
|
||||||
},
|
│
|
||||||
"rules": {
|
▼
|
||||||
"react/rules-of-hooks": "error",
|
┌───────────────────┐ ┌───────────────────┐
|
||||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
│ Python Gateway │◄──┤ Telegram Bot & │
|
||||||
}
|
│ (Port 8082) │ │ Discord Bot │
|
||||||
}
|
└───────────────────┘ └───────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
|
### Stack Components:
|
||||||
|
1. **Frontend**: React, TypeScript, Vite, Tailwind CSS, Leaflet Maps, and Framer Motion.
|
||||||
|
2. **Core API Backend**: Spring Boot, Java 21, Hibernate/JPA, and PostgreSQL.
|
||||||
|
3. **Go Chatbot Engine**: Go microservice performing spelling correction (Levenshtein Distance) and BM25 ranking queries.
|
||||||
|
4. **Python Bot Gateway**: FastAPI HTTP listener interfacing with Telegram and Discord bot APIs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Service Ports & Deployments
|
||||||
|
|
||||||
|
| Component | Dev Port | Production Port | Daemon Service | Description |
|
||||||
|
| :--- | :---: | :---: | :---: | :--- |
|
||||||
|
| **React App** | `5173` | `80` / `443` | `nginx` | Frontend web client |
|
||||||
|
| **Spring Boot API** | `8085` | `8085` | `springboot` | Core business logic & database API |
|
||||||
|
| **Go Chatbot API** | `8081` | `8081` | `chatbot` | Keyword lookup & campus Q&A engine |
|
||||||
|
| **Python Bot Server** | `8082` | `8082` | `telegram-bot` | Telegram/Discord webhook router |
|
||||||
|
| **PostgreSQL Database** | `5432` | `5432` | `postgresql` | Primary database cluster |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Feature Documentation Directories
|
||||||
|
|
||||||
|
For full functional breakdowns, REST API schemas, and technical setup guides for each platform feature, refer to the guides inside [docs/features/](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features):
|
||||||
|
|
||||||
|
- 📚 **[Academic Resources Sync](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/academic_resources.md)**: Syncs static uploads on the VPS directly to database catalog endpoints.
|
||||||
|
- 🛠️ **[Interactive Student Toolkit](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/student_toolkit.md)**: Contains SGPA/CGPA formulas and college internal marks calculation algorithms.
|
||||||
|
- 🚌 **[Live Bus GPS Tracking](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/live_bus_tracking.md)**: Renders live bus coordinates with custom Leaflet overlays and static JSON fail-safes.
|
||||||
|
- 🤖 **[AI Chatbot Microservice](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/ai_chatbot_engine.md)**: Custom Go BM25 NLP search engine and spelling correction pipeline details.
|
||||||
|
- 💬 **[Moderated Q&A Forum](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/community_qa_forum.md)**: Moderation workflow linking student web queries with Telegram Senior Helper channels.
|
||||||
|
- 🤝 **[Developer Collab Hub](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/developer_collab_hub.md)**: Project listings filtered by RIT subdomains and linked with Telegram/Discord bot accept/decline callback requests.
|
||||||
|
- 🏆 **[LeetCode Leaderboard](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/leetcode_leaderboard.md)**: LeetCode GraphQL polling details using spaced request queues to avoid rate limit bans.
|
||||||
|
- 🗺️ **[Interactive Campus Blueprint](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/campus_map.md)**: Percentage-based blueprint vector coordinates system.
|
||||||
|
- 👩🏫 **[Faculty Directory](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/faculty_directory.md)**: Unified campus-wide search and sorting directories for RIT professors.
|
||||||
|
- 🏆 **[Clubs & Centers Explorer](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/campus_events.md)**: Extracurricular search matching tool with a built-in interest matching quiz.
|
||||||
|
- 🚀 **[RAISE Incubator Landing](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/raise_incubator.md)**: Startup slot pitch reservation system.
|
||||||
|
- 🔑 **[Admin Panel & Service Watchdog](file:///c:/Users/deves/OneDrive/project_main/Freshers-Hub/docs/features/admin_dashboard.md)**: Operations watchdog script, notification lists, and system health status pipelines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Verification & Building
|
||||||
|
|
||||||
|
To verify that the system compiles successfully after local configurations:
|
||||||
|
|
||||||
|
1. **Verify Frontend Build**:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
2. **Verify Java Backend Compilation**:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
./mvnw clean package -DskipTests
|
||||||
|
```
|
||||||
|
3. **Verify Go Microservice Compilation**:
|
||||||
|
```bash
|
||||||
|
cd chatbot-service
|
||||||
|
go build -o chatbot
|
||||||
|
```
|
||||||
|
|||||||
1606
attendance_report.html
Normal file
@@ -69,6 +69,13 @@
|
|||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jsoup HTML Parser for SkillRack profile scraping -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
<version>1.18.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring Boot Starter Test -->
|
<!-- Spring Boot Starter Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
package com.rit.portal.controller;
|
package com.rit.portal.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.rit.portal.entity.BusRoute;
|
||||||
|
import com.rit.portal.entity.BusStop;
|
||||||
|
import com.rit.portal.entity.CommunityQuestion;
|
||||||
|
import com.rit.portal.entity.NotePyq;
|
||||||
|
import com.rit.portal.repository.BusRouteRepository;
|
||||||
|
import com.rit.portal.repository.BusStopRepository;
|
||||||
|
import com.rit.portal.repository.CommunityQuestionRepository;
|
||||||
|
import com.rit.portal.repository.NotePyqRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -12,26 +26,56 @@ import java.util.*;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/admin")
|
@RequestMapping("/api/admin")
|
||||||
|
@CrossOrigin(originPatterns = "*")
|
||||||
public class AdminController {
|
public class AdminController {
|
||||||
|
|
||||||
// Hardcoded credentials - no @Value injection needed
|
@Autowired
|
||||||
private static final String ADMIN_USER = "ritadmin";
|
private BusRouteRepository busRouteRepository;
|
||||||
private static final String ADMIN_PASS = "ritadmin2026";
|
|
||||||
|
|
||||||
private Path getRecipientsFilePath() {
|
@Autowired
|
||||||
Path vpsPath = Paths.get("/var/www/freshers-hub/scripts/recipients.txt");
|
private BusStopRepository busStopRepository;
|
||||||
if (vpsPath.getParent() != null && Files.exists(vpsPath.getParent())) {
|
|
||||||
return vpsPath;
|
|
||||||
}
|
|
||||||
Path localPath = Paths.get("scripts/recipients.txt").toAbsolutePath();
|
|
||||||
if (localPath.getParent() != null && !Files.exists(localPath.getParent())) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(localPath.getParent());
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
return localPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NotePyqRepository notePyqRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CommunityQuestionRepository communityQuestionRepository;
|
||||||
|
|
||||||
|
@Value("${admin.transport.username:Transport}")
|
||||||
|
private String transportUsername;
|
||||||
|
|
||||||
|
@Value("${admin.transport.password:RIT@2026}")
|
||||||
|
private String transportPassword;
|
||||||
|
|
||||||
|
@Value("${admin.community.username:Community}")
|
||||||
|
private String communityUsername;
|
||||||
|
|
||||||
|
@Value("${admin.community.password:RIT@2026}")
|
||||||
|
private String communityPassword;
|
||||||
|
|
||||||
|
@Value("${admin.clubs.username:Clubs}")
|
||||||
|
private String clubsUsername;
|
||||||
|
|
||||||
|
@Value("${admin.clubs.password:RIT@2026}")
|
||||||
|
private String clubsPassword;
|
||||||
|
|
||||||
|
@Value("${admin.curriculum.username:Curriculum}")
|
||||||
|
private String curriculumUsername;
|
||||||
|
|
||||||
|
@Value("${admin.curriculum.password:RIT@2026}")
|
||||||
|
private String curriculumPassword;
|
||||||
|
|
||||||
|
@Value("${admin.super.username:Admin}")
|
||||||
|
private String superAdminUsername;
|
||||||
|
|
||||||
|
@Value("${admin.super.password:RIT@2026}")
|
||||||
|
private String superAdminPassword;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 1. MULTI-ROLE ADMIN AUTHENTICATION
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, Object> payload) {
|
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, Object> payload) {
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
@@ -46,27 +90,537 @@ public class AdminController {
|
|||||||
if (p != null) inputPass = p.toString().trim();
|
if (p != null) inputPass = p.toString().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ADMIN_USER.equalsIgnoreCase(inputUser) && ADMIN_PASS.equals(inputPass)) {
|
boolean isStandardPass = "RIT@2026".equalsIgnoreCase(inputPass);
|
||||||
|
|
||||||
|
// 1. Check Transport Admin ("Transport" / "RIT@2026")
|
||||||
|
if ((transportUsername.equalsIgnoreCase(inputUser) && (transportPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"transportadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
response.put("message", "Admin login successful");
|
response.put("role", "ROLE_TRANSPORT");
|
||||||
response.put("token", "ADMIN_SESSION_TOKEN_RIT_2026");
|
response.put("username", "Transport Admin");
|
||||||
} else {
|
response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026");
|
||||||
response.put("success", false);
|
response.put("message", "Transport fleet admin login successful");
|
||||||
response.put("message", "Invalid admin credentials");
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Community & Senior Q&A Admin ("Community" / "RIT@2026" or "QA")
|
||||||
|
if ((communityUsername.equalsIgnoreCase(inputUser) && (communityPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"qa".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"communityadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"seniorqa".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_COMMUNITY");
|
||||||
|
response.put("username", "Community & Q&A Admin");
|
||||||
|
response.put("token", "COMMUNITY_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "Community & Senior Q&A admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Clubs & Centers Admin ("Clubs" / "RIT@2026" or "Club")
|
||||||
|
if ((clubsUsername.equalsIgnoreCase(inputUser) && (clubsPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"club".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"clubadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"clubsadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_CLUBS");
|
||||||
|
response.put("username", "Clubs & Centers Admin");
|
||||||
|
response.put("token", "CLUBS_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "Clubs & Centers admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check GPA Curriculum Admin ("Curriculum" / "RIT@2026" or "GPA" or "Academics")
|
||||||
|
if ((curriculumUsername.equalsIgnoreCase(inputUser) && (curriculumPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"gpa".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"gpaadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"academics".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"curriculumadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_CURRICULUM");
|
||||||
|
response.put("username", "GPA Curriculum Admin");
|
||||||
|
response.put("token", "CURRICULUM_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "GPA Curriculum admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026" or "superadmin")
|
||||||
|
if ((superAdminUsername.equalsIgnoreCase(inputUser) && (superAdminPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"superadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
("ritadmin".equalsIgnoreCase(inputUser) && "ritadmin2026".equals(inputPass))) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_SUPER_ADMIN");
|
||||||
|
response.put("username", "Super Admin");
|
||||||
|
response.put("token", "ADMIN_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "Super admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "Invalid credentials. Use 'Transport', 'Community', 'Clubs', 'Curriculum', or 'Admin' with password 'RIT@2026'.");
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.put("success", false);
|
response.put("success", false);
|
||||||
response.put("message", "Login error: " + e.getMessage());
|
response.put("message", "Login error: " + e.getMessage());
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 2. TRANSPORT & BUS FLEET MANAGEMENT
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
@GetMapping("/transport/routes")
|
||||||
|
public ResponseEntity<List<BusRoute>> getAllTransportRoutes() {
|
||||||
|
return ResponseEntity.ok(busRouteRepository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/transport/routes")
|
||||||
|
public ResponseEntity<?> createBusRoute(@RequestBody Map<String, Object> routeData) {
|
||||||
|
try {
|
||||||
|
String number = (String) routeData.get("number");
|
||||||
|
String name = (String) routeData.get("name");
|
||||||
|
String from = (String) routeData.get("from");
|
||||||
|
String to = (String) routeData.get("to");
|
||||||
|
String departureTime = (String) routeData.get("departureTime");
|
||||||
|
String arrivalTime = (String) routeData.get("arrivalTime");
|
||||||
|
String color = (String) routeData.getOrDefault("color", "#FF6B00");
|
||||||
|
|
||||||
|
if (number == null || name == null || from == null || to == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Missing required route fields"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BusRoute newRoute = BusRoute.builder()
|
||||||
|
.number(number.trim().toUpperCase())
|
||||||
|
.name(name.trim())
|
||||||
|
.from(from.trim())
|
||||||
|
.to(to.trim())
|
||||||
|
.departureTime(departureTime != null ? departureTime : "07:00 AM")
|
||||||
|
.arrivalTime(arrivalTime != null ? arrivalTime : "08:30 AM")
|
||||||
|
.color(color)
|
||||||
|
.stops(new ArrayList<>())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Save parent route first
|
||||||
|
BusRoute saved = busRouteRepository.save(newRoute);
|
||||||
|
|
||||||
|
// Handle nested stops
|
||||||
|
Object stopsObj = routeData.get("stops");
|
||||||
|
if (stopsObj instanceof List<?> rawStops) {
|
||||||
|
int order = 1;
|
||||||
|
for (Object item : rawStops) {
|
||||||
|
if (item instanceof Map<?, ?> stopMap) {
|
||||||
|
String stopName = (String) stopMap.get("name");
|
||||||
|
String time = (String) stopMap.get("time");
|
||||||
|
if (stopName != null && !stopName.isBlank()) {
|
||||||
|
BusStop stop = BusStop.builder()
|
||||||
|
.route(saved)
|
||||||
|
.name(stopName.trim())
|
||||||
|
.time(time != null ? time.trim() : "07:30 AM")
|
||||||
|
.stopOrder(order++)
|
||||||
|
.build();
|
||||||
|
busStopRepository.save(stop);
|
||||||
|
saved.getStops().add(stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("error", "Failed to create route: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/transport/routes/{id}")
|
||||||
|
public ResponseEntity<?> updateBusRoute(@PathVariable Integer id, @RequestBody Map<String, Object> routeData) {
|
||||||
|
Optional<BusRoute> optionalRoute = busRouteRepository.findById(id);
|
||||||
|
if (optionalRoute.isEmpty()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BusRoute route = optionalRoute.get();
|
||||||
|
if (routeData.containsKey("number")) route.setNumber(((String) routeData.get("number")).trim().toUpperCase());
|
||||||
|
if (routeData.containsKey("name")) route.setName(((String) routeData.get("name")).trim());
|
||||||
|
if (routeData.containsKey("from")) route.setFrom(((String) routeData.get("from")).trim());
|
||||||
|
if (routeData.containsKey("to")) route.setTo(((String) routeData.get("to")).trim());
|
||||||
|
if (routeData.containsKey("departureTime")) route.setDepartureTime((String) routeData.get("departureTime"));
|
||||||
|
if (routeData.containsKey("arrivalTime")) route.setArrivalTime((String) routeData.get("arrivalTime"));
|
||||||
|
if (routeData.containsKey("color")) route.setColor((String) routeData.get("color"));
|
||||||
|
|
||||||
|
// Clear old stops and replace
|
||||||
|
if (routeData.containsKey("stops")) {
|
||||||
|
Object stopsObj = routeData.get("stops");
|
||||||
|
if (stopsObj instanceof List<?> rawStops) {
|
||||||
|
route.getStops().clear();
|
||||||
|
busRouteRepository.save(route);
|
||||||
|
|
||||||
|
int order = 1;
|
||||||
|
for (Object item : rawStops) {
|
||||||
|
if (item instanceof Map<?, ?> stopMap) {
|
||||||
|
String stopName = (String) stopMap.get("name");
|
||||||
|
String time = (String) stopMap.get("time");
|
||||||
|
if (stopName != null && !stopName.isBlank()) {
|
||||||
|
BusStop stop = BusStop.builder()
|
||||||
|
.route(route)
|
||||||
|
.name(stopName.trim())
|
||||||
|
.time(time != null ? time.trim() : "07:30 AM")
|
||||||
|
.stopOrder(order++)
|
||||||
|
.build();
|
||||||
|
busStopRepository.save(stop);
|
||||||
|
route.getStops().add(stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BusRoute saved = busRouteRepository.save(route);
|
||||||
|
return ResponseEntity.ok(saved);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("error", "Failed to update route: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/transport/routes/{id}")
|
||||||
|
public ResponseEntity<?> deleteBusRoute(@PathVariable Integer id) {
|
||||||
|
if (!busRouteRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
busRouteRepository.deleteById(id);
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Route deleted successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 3. TELEGRAM BOT Q&A "SENIORS" & CONFIGURATION MANAGEMENT
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
private Path getTelegramConfigPath() {
|
||||||
|
// Priority 1: Production VPS path
|
||||||
|
Path vpsPath = Paths.get("/var/www/freshers-hub/telegram-bot/config.json");
|
||||||
|
if (Files.exists(vpsPath)) {
|
||||||
|
return vpsPath;
|
||||||
|
}
|
||||||
|
// Priority 2: Local relative directory
|
||||||
|
Path localPath1 = Paths.get("telegram-bot/config.json").toAbsolutePath();
|
||||||
|
if (Files.exists(localPath1)) {
|
||||||
|
return localPath1;
|
||||||
|
}
|
||||||
|
Path localPath2 = Paths.get("../telegram-bot/config.json").toAbsolutePath();
|
||||||
|
if (Files.exists(localPath2)) {
|
||||||
|
return localPath2;
|
||||||
|
}
|
||||||
|
return localPath1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> readTelegramConfigFile() {
|
||||||
|
Path path = getTelegramConfigPath();
|
||||||
|
if (Files.exists(path)) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = Files.readAllBytes(path);
|
||||||
|
Map<String, Object> loaded = objectMapper.readValue(bytes, Map.class);
|
||||||
|
if (loaded != null) {
|
||||||
|
List<Object> rawChatIds = (List<Object>) loaded.get("helper_chat_ids");
|
||||||
|
List<Map<String, Object>> helpers = (List<Map<String, Object>>) loaded.get("seniorHelpers");
|
||||||
|
|
||||||
|
if (helpers == null && rawChatIds != null) {
|
||||||
|
helpers = new ArrayList<>();
|
||||||
|
for (Object id : rawChatIds) {
|
||||||
|
Map<String, Object> h = new HashMap<>();
|
||||||
|
h.put("chatId", Long.parseLong(id.toString()));
|
||||||
|
h.put("name", "Senior Responder");
|
||||||
|
helpers.add(h);
|
||||||
|
}
|
||||||
|
loaded.put("seniorHelpers", helpers);
|
||||||
|
} else if (helpers != null && rawChatIds == null) {
|
||||||
|
List<Long> ids = new ArrayList<>();
|
||||||
|
for (Map<String, Object> h : helpers) {
|
||||||
|
if (h.get("chatId") != null) {
|
||||||
|
try {
|
||||||
|
ids.add(Long.parseLong(h.get("chatId").toString()));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded.put("helper_chat_ids", ids);
|
||||||
|
}
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> defaultCfg = new HashMap<>();
|
||||||
|
defaultCfg.put("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
|
||||||
|
defaultCfg.put("telegram_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
|
||||||
|
defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(1873240361L, 5567776672L, 8518850169L, 7238144438L)));
|
||||||
|
List<Map<String, Object>> defaultHelpers = new ArrayList<>();
|
||||||
|
defaultHelpers.add(Map.of("chatId", 1873240361L, "name", "Senior Mentor (CSE)"));
|
||||||
|
defaultHelpers.add(Map.of("chatId", 5567776672L, "name", "Senior Responder (ECE)"));
|
||||||
|
defaultHelpers.add(Map.of("chatId", 8518850169L, "name", "Senior Responder (IT)"));
|
||||||
|
defaultHelpers.add(Map.of("chatId", 7238144438L, "name", "Senior Responder (AIDS)"));
|
||||||
|
defaultCfg.put("seniorHelpers", defaultHelpers);
|
||||||
|
defaultCfg.put("spring_backend_url", "http://localhost:8085");
|
||||||
|
return defaultCfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveTelegramConfigFile(Map<String, Object> config) {
|
||||||
|
Path path = getTelegramConfigPath();
|
||||||
|
try {
|
||||||
|
if (path.getParent() != null && !Files.exists(path.getParent())) {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
}
|
||||||
|
objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(path.toString()), config);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/telegram/config")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTelegramConfig() {
|
||||||
|
return ResponseEntity.ok(readTelegramConfigFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/telegram/config")
|
||||||
|
public ResponseEntity<?> updateTelegramConfig(@RequestBody Map<String, Object> payload) {
|
||||||
|
Map<String, Object> current = readTelegramConfigFile();
|
||||||
|
if (payload.containsKey("community_bot_token")) current.put("community_bot_token", payload.get("community_bot_token"));
|
||||||
|
if (payload.containsKey("telegram_bot_token")) current.put("telegram_bot_token", payload.get("telegram_bot_token"));
|
||||||
|
if (payload.containsKey("spring_backend_url")) current.put("spring_backend_url", payload.get("spring_backend_url"));
|
||||||
|
if (payload.containsKey("bot_username")) current.put("bot_username", payload.get("bot_username"));
|
||||||
|
if (payload.containsKey("seniorHelpers")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> helpers = (List<Map<String, Object>>) payload.get("seniorHelpers");
|
||||||
|
current.put("seniorHelpers", helpers);
|
||||||
|
List<Long> ids = new ArrayList<>();
|
||||||
|
if (helpers != null) {
|
||||||
|
for (Map<String, Object> h : helpers) {
|
||||||
|
if (h.get("chatId") != null) {
|
||||||
|
try {
|
||||||
|
ids.add(Long.parseLong(h.get("chatId").toString()));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.put("helper_chat_ids", ids);
|
||||||
|
} else if (payload.containsKey("helper_chat_ids")) {
|
||||||
|
current.put("helper_chat_ids", payload.get("helper_chat_ids"));
|
||||||
|
}
|
||||||
|
saveTelegramConfigFile(current);
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "config", current));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/telegram/helpers")
|
||||||
|
public ResponseEntity<?> addSeniorHelper(@RequestBody Map<String, Object> payload) {
|
||||||
|
Object chatIdObj = payload.get("chatId");
|
||||||
|
String nameStr = payload.get("name") != null ? payload.get("name").toString().trim() : "Senior Responder";
|
||||||
|
if (chatIdObj == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "chatId is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long chatId;
|
||||||
|
try {
|
||||||
|
chatId = Long.parseLong(chatIdObj.toString().trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "chatId must be a valid integer number"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> config = readTelegramConfigFile();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
|
||||||
|
if (helpers == null) helpers = new ArrayList<>();
|
||||||
|
List<Long> helperIds = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean found = false;
|
||||||
|
for (Map<String, Object> h : helpers) {
|
||||||
|
if (h.get("chatId") != null) {
|
||||||
|
Long cId = Long.parseLong(h.get("chatId").toString());
|
||||||
|
helperIds.add(cId);
|
||||||
|
if (cId.equals(chatId)) {
|
||||||
|
h.put("name", nameStr);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
Map<String, Object> newHelper = new HashMap<>();
|
||||||
|
newHelper.put("chatId", chatId);
|
||||||
|
newHelper.put("name", nameStr);
|
||||||
|
helpers.add(newHelper);
|
||||||
|
helperIds.add(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.put("seniorHelpers", helpers);
|
||||||
|
config.put("helper_chat_ids", helperIds);
|
||||||
|
saveTelegramConfigFile(config);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers, "helper_chat_ids", helperIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/telegram/helpers/{chatId}")
|
||||||
|
public ResponseEntity<?> removeSeniorHelper(@PathVariable Long chatId) {
|
||||||
|
Map<String, Object> config = readTelegramConfigFile();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
|
||||||
|
if (helpers != null) {
|
||||||
|
helpers.removeIf(h -> h.get("chatId") != null && Long.parseLong(h.get("chatId").toString()) == chatId);
|
||||||
|
} else {
|
||||||
|
helpers = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> helperIds = new ArrayList<>();
|
||||||
|
for (Map<String, Object> h : helpers) {
|
||||||
|
if (h.get("chatId") != null) {
|
||||||
|
try {
|
||||||
|
helperIds.add(Long.parseLong(h.get("chatId").toString()));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.put("seniorHelpers", helpers);
|
||||||
|
config.put("helper_chat_ids", helperIds);
|
||||||
|
saveTelegramConfigFile(config);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers, "helper_chat_ids", helperIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/telegram/test-bot")
|
||||||
|
public ResponseEntity<?> testTelegramBot(@RequestBody Map<String, Object> payload) {
|
||||||
|
String token = (String) payload.get("token");
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
Map<String, Object> cfg = readTelegramConfigFile();
|
||||||
|
token = (String) cfg.getOrDefault("community_bot_token", "");
|
||||||
|
}
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("ok", false, "description", "Bot token is required"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
String url = "https://api.telegram.org/bot" + token.trim() + "/getMe";
|
||||||
|
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
|
||||||
|
return ResponseEntity.ok(response.getBody());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.ok(Map.of("ok", false, "description", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/telegram/test-dispatch")
|
||||||
|
public ResponseEntity<?> testTelegramDispatch(@RequestBody Map<String, Object> payload) {
|
||||||
|
String token = (String) payload.get("token");
|
||||||
|
Object chatIdObj = payload.get("chatId");
|
||||||
|
String message = (String) payload.get("message");
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
Map<String, Object> cfg = readTelegramConfigFile();
|
||||||
|
token = (String) cfg.getOrDefault("community_bot_token", "");
|
||||||
|
}
|
||||||
|
if (token == null || token.trim().isEmpty() || chatIdObj == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("ok", false, "description", "Bot token and chatId are required"));
|
||||||
|
}
|
||||||
|
if (message == null || message.trim().isEmpty()) {
|
||||||
|
message = "🔔 *RIT Nexus Admin Test Ping*\n\nThis is a verified test notification confirming your Telegram Chat ID is active and connected to the Q&A dispatch system!";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
String url = "https://api.telegram.org/bot" + token.trim() + "/sendMessage";
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("chat_id", chatIdObj);
|
||||||
|
body.put("text", message);
|
||||||
|
body.put("parse_mode", "Markdown");
|
||||||
|
ResponseEntity<Map> response = restTemplate.postForEntity(url, body, Map.class);
|
||||||
|
return ResponseEntity.ok(response.getBody());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.ok(Map.of("ok", false, "description", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/telegram/test-broadcast")
|
||||||
|
public ResponseEntity<?> testTelegramBroadcast(@RequestBody Map<String, Object> payload) {
|
||||||
|
Map<String, Object> cfg = readTelegramConfigFile();
|
||||||
|
String token = (String) payload.getOrDefault("token", cfg.get("community_bot_token"));
|
||||||
|
String message = (String) payload.getOrDefault("message", "📢 *RIT Nexus Q&A Dispatch Network Test*\n\nAll senior responders are currently being pinged to verify real-time dispatch connectivity.");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Object> rawHelpers = (List<Object>) cfg.getOrDefault("helper_chat_ids", new ArrayList<>());
|
||||||
|
int delivered = 0;
|
||||||
|
List<Map<String, Object>> results = new ArrayList<>();
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
if (token != null && !token.trim().isEmpty() && rawHelpers != null) {
|
||||||
|
for (Object idObj : rawHelpers) {
|
||||||
|
try {
|
||||||
|
String url = "https://api.telegram.org/bot" + token.trim() + "/sendMessage";
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("chat_id", idObj);
|
||||||
|
body.put("text", message);
|
||||||
|
body.put("parse_mode", "Markdown");
|
||||||
|
ResponseEntity<Map> res = restTemplate.postForEntity(url, body, Map.class);
|
||||||
|
results.add(Map.of("chatId", idObj, "status", "success", "response", res.getBody()));
|
||||||
|
delivered++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
results.add(Map.of("chatId", idObj, "status", "failed", "error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(Map.of("success", true, "delivered", delivered, "total", rawHelpers != null ? rawHelpers.size() : 0, "details", results));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 4. NOTES & PYQS STUDY MATERIALS MODERATION
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
@GetMapping("/notes")
|
||||||
|
public ResponseEntity<List<NotePyq>> getAllNotes() {
|
||||||
|
return ResponseEntity.ok(notePyqRepository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/notes/{id}")
|
||||||
|
public ResponseEntity<?> deleteNote(@PathVariable Integer id) {
|
||||||
|
if (!notePyqRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
notePyqRepository.deleteById(id);
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Note deleted successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 5. COMMUNITY QUESTIONS MODERATION
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
@GetMapping("/questions")
|
||||||
|
public ResponseEntity<List<CommunityQuestion>> getAllQuestions() {
|
||||||
|
return ResponseEntity.ok(communityQuestionRepository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/questions/{id}")
|
||||||
|
public ResponseEntity<?> deleteQuestion(@PathVariable Integer id) {
|
||||||
|
if (!communityQuestionRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
communityQuestionRepository.deleteById(id);
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Question deleted successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 6. DEPLOYMENT RECIPIENTS (GIT PUSH HEALTH NOTIFICATIONS)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
private Path getRecipientsFilePath() {
|
||||||
|
Path vpsPath = Paths.get("/var/www/freshers-hub/scripts/recipients.txt");
|
||||||
|
if (vpsPath.getParent() != null && Files.exists(vpsPath.getParent())) {
|
||||||
|
return vpsPath;
|
||||||
|
}
|
||||||
|
Path localPath = Paths.get("scripts/recipients.txt").toAbsolutePath();
|
||||||
|
if (localPath.getParent() != null && !Files.exists(localPath.getParent())) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(localPath.getParent());
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
return localPath;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/recipients")
|
@GetMapping("/recipients")
|
||||||
public ResponseEntity<List<String>> getRecipients() {
|
public ResponseEntity<List<String>> getRecipients() {
|
||||||
Path path = getRecipientsFilePath();
|
Path path = getRecipientsFilePath();
|
||||||
List<String> recipients = getRecipientsList(path);
|
return ResponseEntity.ok(getRecipientsList(path));
|
||||||
return ResponseEntity.ok(recipients);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/recipients")
|
@PostMapping("/recipients")
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.rit.portal.controller;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.ClubLike;
|
||||||
|
import com.rit.portal.repository.ClubLikeRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/clubs")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class ClubController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClubLikeRepository clubLikeRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of likes for all clubs & centers
|
||||||
|
*/
|
||||||
|
@GetMapping("/likes")
|
||||||
|
public ResponseEntity<Map<String, Long>> getAllClubLikes() {
|
||||||
|
List<Object[]> results = clubLikeRepository.countAllGroupedByClubId();
|
||||||
|
Map<String, Long> likesMap = new HashMap<>();
|
||||||
|
for (Object[] result : results) {
|
||||||
|
String clubId = (String) result[0];
|
||||||
|
Long count = (Long) result[1];
|
||||||
|
likesMap.put(clubId, count);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(likesMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of club IDs liked by a specific user or device
|
||||||
|
*/
|
||||||
|
@GetMapping("/user-likes")
|
||||||
|
public ResponseEntity<List<String>> getUserLikedClubs(@RequestParam("userIdentifier") String userIdentifier) {
|
||||||
|
if (userIdentifier == null || userIdentifier.isBlank()) {
|
||||||
|
return ResponseEntity.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
List<ClubLike> likes = clubLikeRepository.findByUserIdentifier(userIdentifier);
|
||||||
|
List<String> likedClubIds = likes.stream()
|
||||||
|
.map(ClubLike::getClubId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return ResponseEntity.ok(likedClubIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle like state for a club/center by a user or device
|
||||||
|
*/
|
||||||
|
@PostMapping("/{clubId}/like")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> toggleClubLike(
|
||||||
|
@PathVariable("clubId") String clubId,
|
||||||
|
@RequestBody(required = false) Map<String, String> payload,
|
||||||
|
@RequestParam(value = "userIdentifier", required = false) String paramUserIdentifier) {
|
||||||
|
|
||||||
|
String userIdentifier = null;
|
||||||
|
if (payload != null && payload.containsKey("userIdentifier")) {
|
||||||
|
userIdentifier = payload.get("userIdentifier");
|
||||||
|
}
|
||||||
|
if ((userIdentifier == null || userIdentifier.isBlank()) && paramUserIdentifier != null) {
|
||||||
|
userIdentifier = paramUserIdentifier;
|
||||||
|
}
|
||||||
|
if (userIdentifier == null || userIdentifier.isBlank()) {
|
||||||
|
userIdentifier = "anonymous_guest";
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<ClubLike> existingLike = clubLikeRepository.findByClubIdAndUserIdentifier(clubId, userIdentifier);
|
||||||
|
boolean isNowLiked;
|
||||||
|
|
||||||
|
if (existingLike.isPresent()) {
|
||||||
|
clubLikeRepository.delete(existingLike.get());
|
||||||
|
isNowLiked = false;
|
||||||
|
} else {
|
||||||
|
ClubLike newLike = ClubLike.builder()
|
||||||
|
.clubId(clubId)
|
||||||
|
.userIdentifier(userIdentifier)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
clubLikeRepository.save(newLike);
|
||||||
|
isNowLiked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
long newCount = clubLikeRepository.countByClubId(clubId);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("clubId", clubId);
|
||||||
|
response.put("liked", isNowLiked);
|
||||||
|
response.put("count", newCount);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ public class CollabController {
|
|||||||
request.setApplicationsCount(request.getApplicationsCount() + 1);
|
request.setApplicationsCount(request.getApplicationsCount() + 1);
|
||||||
collabRequestRepository.save(request);
|
collabRequestRepository.save(request);
|
||||||
|
|
||||||
// Notify Telegram/Discord intermediary bot asynchronously
|
// Notify Telegram intermediary bot asynchronously
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> payload = new HashMap<>();
|
Map<String, Object> payload = new HashMap<>();
|
||||||
@@ -112,7 +112,6 @@ public class CollabController {
|
|||||||
payload.put("author_name", request.getAuthorName());
|
payload.put("author_name", request.getAuthorName());
|
||||||
payload.put("contact_info", request.getContactInfo());
|
payload.put("contact_info", request.getContactInfo());
|
||||||
payload.put("telegram_chat_id", request.getTelegramChatId());
|
payload.put("telegram_chat_id", request.getTelegramChatId());
|
||||||
payload.put("discord_user_id", request.getDiscordUserId());
|
|
||||||
payload.put("applicant_name", savedApp.getApplicantName());
|
payload.put("applicant_name", savedApp.getApplicantName());
|
||||||
payload.put("applicant_email", savedApp.getApplicantEmail());
|
payload.put("applicant_email", savedApp.getApplicantEmail());
|
||||||
payload.put("applicant_dept", savedApp.getApplicantDept());
|
payload.put("applicant_dept", savedApp.getApplicantDept());
|
||||||
@@ -172,11 +171,6 @@ public class CollabController {
|
|||||||
return collabRequestRepository.findByTelegramChatIdAndStatusOrderByCreatedAtDesc(chatId, "OPEN");
|
return collabRequestRepository.findByTelegramChatIdAndStatusOrderByCreatedAtDesc(chatId, "OPEN");
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/active/discord/{userId}")
|
|
||||||
public List<CollabRequest> getActiveRequestsByDiscord(@PathVariable String userId) {
|
|
||||||
return collabRequestRepository.findByDiscordUserIdAndStatusOrderByCreatedAtDesc(userId, "OPEN");
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResponseEntity<Void> deleteOrCancelCollabRequest(@PathVariable Integer id) {
|
public ResponseEntity<Void> deleteOrCancelCollabRequest(@PathVariable Integer id) {
|
||||||
return collabRequestRepository.findById(id).map(req -> {
|
return collabRequestRepository.findById(id).map(req -> {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.rit.portal.controller;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.SkillrackProfile;
|
||||||
|
import com.rit.portal.service.SkillrackService;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/skillrack")
|
||||||
|
@CrossOrigin(originPatterns = "*")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SkillrackController {
|
||||||
|
|
||||||
|
private final SkillrackService skillrackService;
|
||||||
|
|
||||||
|
@GetMapping("/leaderboard")
|
||||||
|
public List<SkillrackProfile> getLeaderboard() {
|
||||||
|
return skillrackService.getAllRankedProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<?> registerProfile(@RequestBody RegisterRequest request) {
|
||||||
|
if (request.getSkillrackEmail() == null || request.getSkillrackEmail().trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "SkillRack email is required"));
|
||||||
|
}
|
||||||
|
if (request.getSkillrackPassword() == null || request.getSkillrackPassword().trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "SkillRack password is required"));
|
||||||
|
}
|
||||||
|
if (request.getStudentName() == null || request.getStudentName().trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Student name is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SkillrackProfile profile = skillrackService.registerOrUpdateStudent(
|
||||||
|
request.getStudentName(),
|
||||||
|
request.getSkillrackEmail(),
|
||||||
|
request.getSkillrackPassword(),
|
||||||
|
request.getDepartment() != null ? request.getDepartment() : "CSE",
|
||||||
|
request.getYear() != null ? request.getYear() : "1st Year"
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(profile);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().body(Map.of("error", "Failed to register profile: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sync")
|
||||||
|
public ResponseEntity<?> triggerSync() {
|
||||||
|
skillrackService.syncAllProfilesWithSpacing();
|
||||||
|
return ResponseEntity.ok(Map.of("message", "SkillRack background sync initiated with 5-second spacing."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class RegisterRequest {
|
||||||
|
private String studentName;
|
||||||
|
private String skillrackEmail;
|
||||||
|
private String skillrackPassword;
|
||||||
|
private String department;
|
||||||
|
private String year;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.rit.portal.controller;
|
||||||
|
|
||||||
|
import com.rit.portal.dto.ims.StudentDashboardDto.*;
|
||||||
|
import com.rit.portal.service.ims.ImsClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/ims")
|
||||||
|
public class StudentDashboardController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ImsClient imsClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/ims/auth/login
|
||||||
|
* Authenticates student via Register Number (e.g. 2114251001) and Password.
|
||||||
|
*/
|
||||||
|
@PostMapping("/auth/login")
|
||||||
|
public ResponseEntity<ImsLoginResponse> loginWithIms(@RequestBody ImsLoginRequest request) {
|
||||||
|
ImsLoginResponse response = imsClient.authenticate(request.getRegNumber(), request.getPassword());
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/ims/auth/mock-users
|
||||||
|
* Returns pre-configured mock student credentials for instant testing.
|
||||||
|
*/
|
||||||
|
@GetMapping("/auth/mock-users")
|
||||||
|
public ResponseEntity<List<MockUserCredentialDto>> getMockUsers() {
|
||||||
|
return ResponseEntity.ok(imsClient.getAvailableMockUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to resolve the student's register number from email, header, or param.
|
||||||
|
*/
|
||||||
|
private String resolveStudentId(String regNumber, String userEmail) {
|
||||||
|
if (regNumber != null && !regNumber.isBlank()) {
|
||||||
|
return regNumber.trim();
|
||||||
|
}
|
||||||
|
if (userEmail != null && !userEmail.isBlank()) {
|
||||||
|
String digits = userEmail.split("@")[0].replaceAll("[^0-9]", "");
|
||||||
|
return digits.isEmpty() ? "2114251001" : digits;
|
||||||
|
}
|
||||||
|
return "2114251001";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/ims/dashboard/me
|
||||||
|
* Returns: Timetable of the day, Class Location, Class Teachers, Student & Dept Info
|
||||||
|
*/
|
||||||
|
@GetMapping("/dashboard/me")
|
||||||
|
public ResponseEntity<DashboardResponse> getMyDashboard(
|
||||||
|
@RequestParam(required = false) String regNumber,
|
||||||
|
@RequestParam(required = false) String email,
|
||||||
|
@RequestHeader(value = "X-IMS-Student-Id", required = false) String headerStudentId,
|
||||||
|
@RequestHeader(value = "X-Dev-Student-Id", required = false) String devStudentOverride) {
|
||||||
|
|
||||||
|
String studentId = (devStudentOverride != null && !devStudentOverride.isBlank())
|
||||||
|
? devStudentOverride
|
||||||
|
: (headerStudentId != null && !headerStudentId.isBlank())
|
||||||
|
? headerStudentId
|
||||||
|
: resolveStudentId(regNumber, email);
|
||||||
|
|
||||||
|
DashboardResponse response = imsClient.getStudentDashboard(studentId);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.rit.portal.dto.ims;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StudentDashboardDto {
|
||||||
|
|
||||||
|
// ─── 0. IMS Authentication DTOs ──────────────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ImsLoginRequest {
|
||||||
|
private String regNumber; // e.g. "2114251001" or "251001"
|
||||||
|
private String password; // e.g. "rit@2026"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ImsLoginResponse {
|
||||||
|
private boolean success;
|
||||||
|
private String message;
|
||||||
|
private String token; // IMS Session / Bearer Token
|
||||||
|
private StudentInfo student;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class MockUserCredentialDto {
|
||||||
|
private String regNumber;
|
||||||
|
private String defaultPassword;
|
||||||
|
private String studentName;
|
||||||
|
private String department;
|
||||||
|
private String section;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 1. Student & Department Details ─────────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class StudentInfo {
|
||||||
|
private String studentId; // e.g. "2114251001" or "251001"
|
||||||
|
private String regNumber; // e.g. "2114251001"
|
||||||
|
private String name; // e.g. "Anbu Kathir"
|
||||||
|
private String email; // e.g. "251001@ritchennai.edu.in"
|
||||||
|
private String degree; // e.g. "B.E."
|
||||||
|
private String department; // e.g. "Computer Science & Engineering"
|
||||||
|
private String departmentCode; // e.g. "CSE"
|
||||||
|
private int year; // e.g. 1
|
||||||
|
private int semester; // e.g. 2
|
||||||
|
private String section; // e.g. "A"
|
||||||
|
private String batch; // e.g. "2025 - 2029"
|
||||||
|
private String regulation; // e.g. "2021 Regulation"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. Class & Campus Location ─────────────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ClassLocationInfo {
|
||||||
|
private String roomNumber; // e.g. "LH-204"
|
||||||
|
private String buildingName; // e.g. "Dr. APJ Abdul Kalam Academic Block"
|
||||||
|
private String floor; // e.g. "2nd Floor"
|
||||||
|
private String wing; // e.g. "East Wing"
|
||||||
|
private String landmark; // e.g. "Next to CSE Department Library, Opposite to Seminar Hall"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3. Faculty / Staff for this Class ───────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ClassFacultyMember {
|
||||||
|
private String subjectCode; // e.g. "CS3201"
|
||||||
|
private String subjectName; // e.g. "Data Structures & Algorithms"
|
||||||
|
private String facultyName; // e.g. "Dr. R. Arunkumar"
|
||||||
|
private String designation; // e.g. "Associate Professor"
|
||||||
|
private String email; // e.g. "arunkumar.r@ritchennai.edu.in"
|
||||||
|
private String officeLocation; // e.g. "APJ Block, Cabin 208-B"
|
||||||
|
private String phoneExtension; // e.g. "Ext. 312"
|
||||||
|
private String avatarUrl; // Optional photo
|
||||||
|
private boolean isClassIncharge; // true if Class Incharge
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 4. Timetable Period ─────────────────────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class TimetablePeriod {
|
||||||
|
private int periodNumber; // 1 to 6
|
||||||
|
private String timeSlot; // e.g. "08:45 AM - 09:40 AM"
|
||||||
|
private String startTime; // "08:45"
|
||||||
|
private String endTime; // "09:40"
|
||||||
|
private String subjectCode; // "CS3201"
|
||||||
|
private String subjectName; // "Data Structures & Algorithms"
|
||||||
|
private String type; // "THEORY" | "LAB" | "TUTORIAL" | "LIBRARY"
|
||||||
|
private String facultyName; // "Dr. R. Arunkumar"
|
||||||
|
private String venue; // "LH-204" or "CC-03 Lab"
|
||||||
|
private String status; // "UPCOMING" | "ONGOING" | "COMPLETED"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5. Daily Timetable ──────────────────────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class DaySchedule {
|
||||||
|
private String dayOfWeek; // "MONDAY", "TUESDAY", etc.
|
||||||
|
private LocalDate date;
|
||||||
|
private List<TimetablePeriod> periods;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Complete Dashboard Aggregate ────────────────────────────
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class DashboardResponse {
|
||||||
|
private StudentInfo student;
|
||||||
|
private ClassLocationInfo classLocation;
|
||||||
|
private List<ClassFacultyMember> facultyList;
|
||||||
|
private DaySchedule todaySchedule;
|
||||||
|
private boolean isMockData;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
backend/src/main/java/com/rit/portal/entity/ClubLike.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package com.rit.portal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "club_likes", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "unique_club_user_like", columnNames = {"club_id", "user_identifier"})
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ClubLike {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "club_id", nullable = false, length = 100)
|
||||||
|
private String clubId;
|
||||||
|
|
||||||
|
@Column(name = "user_identifier", nullable = false, length = 255)
|
||||||
|
private String userIdentifier;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
@@ -46,9 +46,6 @@ public class CollabRequest {
|
|||||||
@Column(name = "telegram_chat_id")
|
@Column(name = "telegram_chat_id")
|
||||||
private Long telegramChatId;
|
private Long telegramChatId;
|
||||||
|
|
||||||
@Column(name = "discord_user_id")
|
|
||||||
private String discordUserId;
|
|
||||||
|
|
||||||
@Column(name = "status")
|
@Column(name = "status")
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String status = "OPEN";
|
private String status = "OPEN";
|
||||||
|
|||||||
@@ -30,11 +30,14 @@ public class CommunityAnswer {
|
|||||||
private String author;
|
private String author;
|
||||||
|
|
||||||
@Column(name = "upvotes")
|
@Column(name = "upvotes")
|
||||||
|
@Builder.Default
|
||||||
private Integer upvotes = 0;
|
private Integer upvotes = 0;
|
||||||
|
|
||||||
@Column(name = "is_accepted")
|
@Column(name = "is_accepted")
|
||||||
|
@Builder.Default
|
||||||
private Boolean isAccepted = false;
|
private Boolean isAccepted = false;
|
||||||
|
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
|
@Builder.Default
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +29,19 @@ public class CommunityQuestion {
|
|||||||
private String author;
|
private String author;
|
||||||
|
|
||||||
@Column(name = "upvotes")
|
@Column(name = "upvotes")
|
||||||
|
@Builder.Default
|
||||||
private Integer upvotes = 0;
|
private Integer upvotes = 0;
|
||||||
|
|
||||||
@Column(name = "tags", columnDefinition = "text[]")
|
@Column(name = "tags", columnDefinition = "text[]")
|
||||||
|
@Builder.Default
|
||||||
private List<String> tags = new ArrayList<>();
|
private List<String> tags = new ArrayList<>();
|
||||||
|
|
||||||
@Column(name = "is_answered")
|
@Column(name = "is_answered")
|
||||||
|
@Builder.Default
|
||||||
private Boolean isAnswered = false;
|
private Boolean isAnswered = false;
|
||||||
|
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
|
@Builder.Default
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ public class NotePyq {
|
|||||||
private String fileSize;
|
private String fileSize;
|
||||||
|
|
||||||
@Column(name = "downloads_count")
|
@Column(name = "downloads_count")
|
||||||
|
@Builder.Default
|
||||||
private Integer downloadsCount = 0;
|
private Integer downloadsCount = 0;
|
||||||
|
|
||||||
@Column(name = "uploaded_at")
|
@Column(name = "uploaded_at")
|
||||||
|
@Builder.Default
|
||||||
private LocalDateTime uploadedAt = LocalDateTime.now();
|
private LocalDateTime uploadedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.rit.portal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "skillrack_profiles")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class SkillrackProfile {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "student_name", nullable = false)
|
||||||
|
private String studentName;
|
||||||
|
|
||||||
|
@Column(name = "skillrack_email", nullable = false, unique = true)
|
||||||
|
private String skillrackEmail;
|
||||||
|
|
||||||
|
@Column(name = "encrypted_password", nullable = false, length = 512)
|
||||||
|
private String encryptedPassword;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String department;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String year;
|
||||||
|
|
||||||
|
@Column(name = "total_points")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer totalPoints = 0;
|
||||||
|
|
||||||
|
@Column(name = "code_test_solved")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer codeTestSolved = 0;
|
||||||
|
|
||||||
|
@Column(name = "code_tutor_solved")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer codeTutorSolved = 0;
|
||||||
|
|
||||||
|
@Column(name = "code_track_solved")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer codeTrackSolved = 0;
|
||||||
|
|
||||||
|
@Column(name = "dc_solved")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer dcSolved = 0;
|
||||||
|
|
||||||
|
@Column(name = "gold_medals")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer goldMedals = 0;
|
||||||
|
|
||||||
|
@Column(name = "silver_medals")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer silverMedals = 0;
|
||||||
|
|
||||||
|
@Column(name = "bronze_medals")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer bronzeMedals = 0;
|
||||||
|
|
||||||
|
@Column(name = "last_updated")
|
||||||
|
private LocalDateTime lastUpdated;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
if (createdAt == null) createdAt = LocalDateTime.now();
|
||||||
|
if (lastUpdated == null) lastUpdated = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.rit.portal.repository;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.ClubLike;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ClubLikeRepository extends JpaRepository<ClubLike, Long> {
|
||||||
|
|
||||||
|
Optional<ClubLike> findByClubIdAndUserIdentifier(String clubId, String userIdentifier);
|
||||||
|
|
||||||
|
boolean existsByClubIdAndUserIdentifier(String clubId, String userIdentifier);
|
||||||
|
|
||||||
|
long countByClubId(String clubId);
|
||||||
|
|
||||||
|
List<ClubLike> findByUserIdentifier(String userIdentifier);
|
||||||
|
|
||||||
|
@Query("SELECT c.clubId, COUNT(c) FROM ClubLike c GROUP BY c.clubId")
|
||||||
|
List<Object[]> countAllGroupedByClubId();
|
||||||
|
}
|
||||||
@@ -10,5 +10,4 @@ public interface CollabRequestRepository extends JpaRepository<CollabRequest, In
|
|||||||
List<CollabRequest> findAllByOrderByCreatedAtDesc();
|
List<CollabRequest> findAllByOrderByCreatedAtDesc();
|
||||||
List<CollabRequest> findByAuthorEmailOrderByCreatedAtDesc(String authorEmail);
|
List<CollabRequest> findByAuthorEmailOrderByCreatedAtDesc(String authorEmail);
|
||||||
List<CollabRequest> findByTelegramChatIdAndStatusOrderByCreatedAtDesc(Long telegramChatId, String status);
|
List<CollabRequest> findByTelegramChatIdAndStatusOrderByCreatedAtDesc(Long telegramChatId, String status);
|
||||||
List<CollabRequest> findByDiscordUserIdAndStatusOrderByCreatedAtDesc(String discordUserId, String status);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.rit.portal.repository;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.SkillrackProfile;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SkillrackProfileRepository extends JpaRepository<SkillrackProfile, Integer> {
|
||||||
|
Optional<SkillrackProfile> findBySkillrackEmailIgnoreCase(String skillrackEmail);
|
||||||
|
List<SkillrackProfile> findAllByOrderByTotalPointsDesc();
|
||||||
|
boolean existsBySkillrackEmailIgnoreCase(String skillrackEmail);
|
||||||
|
}
|
||||||
@@ -45,8 +45,17 @@ public class BusLocationService {
|
|||||||
|
|
||||||
public List<BusLocation> getActiveLocations() {
|
public List<BusLocation> getActiveLocations() {
|
||||||
Instant threshold = Instant.now().minus(STALE_DURATION);
|
Instant threshold = Instant.now().minus(STALE_DURATION);
|
||||||
// Clean up stale entries on read
|
return locationCache.values().stream()
|
||||||
|
.filter(loc -> !loc.getLastUpdated().isBefore(threshold))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic background eviction of stale GPS coordinates every 5 minutes
|
||||||
|
*/
|
||||||
|
@org.springframework.scheduling.annotation.Scheduled(fixedRate = 300000)
|
||||||
|
public void evictStaleLocations() {
|
||||||
|
Instant threshold = Instant.now().minus(STALE_DURATION);
|
||||||
locationCache.entrySet().removeIf(entry -> entry.getValue().getLastUpdated().isBefore(threshold));
|
locationCache.entrySet().removeIf(entry -> entry.getValue().getLastUpdated().isBefore(threshold));
|
||||||
return List.copyOf(locationCache.values());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package com.rit.portal.service;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.SkillrackProfile;
|
||||||
|
import com.rit.portal.repository.SkillrackProfileRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jsoup.Connection;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class SkillrackService {
|
||||||
|
|
||||||
|
private final SkillrackProfileRepository repository;
|
||||||
|
|
||||||
|
// AES-128 key (16 bytes) — in production, move to environment variable
|
||||||
|
private static final String AES_KEY = "RitSkillR@ck2026";
|
||||||
|
private static final String SKILLRACK_BASE = "https://www.skillrack.com";
|
||||||
|
private static final String LOGIN_URL = SKILLRACK_BASE + "/faces/ui/profile.xhtml";
|
||||||
|
private static final String PROFILE_URL = SKILLRACK_BASE + "/faces/candidate/manageprofile.xhtml";
|
||||||
|
|
||||||
|
// ─── Public API ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<SkillrackProfile> getAllRankedProfiles() {
|
||||||
|
return repository.findAllByOrderByTotalPointsDesc();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SkillrackProfile registerOrUpdateStudent(String studentName, String email, String password,
|
||||||
|
String department, String year) {
|
||||||
|
String cleanEmail = email.trim().toLowerCase();
|
||||||
|
|
||||||
|
SkillrackProfile profile = repository.findBySkillrackEmailIgnoreCase(cleanEmail)
|
||||||
|
.orElse(SkillrackProfile.builder()
|
||||||
|
.studentName(studentName.trim())
|
||||||
|
.skillrackEmail(cleanEmail)
|
||||||
|
.department(department.trim())
|
||||||
|
.year(year.trim())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
profile.setStudentName(studentName.trim());
|
||||||
|
profile.setDepartment(department.trim());
|
||||||
|
profile.setYear(year.trim());
|
||||||
|
profile.setEncryptedPassword(encrypt(password));
|
||||||
|
|
||||||
|
boolean success = fetchAndUpdateProfile(profile, password);
|
||||||
|
if (!success) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Could not authenticate with SkillRack using '" + cleanEmail +
|
||||||
|
"'. Please verify your email and password are correct.");
|
||||||
|
}
|
||||||
|
return repository.save(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean fetchAndUpdateProfile(SkillrackProfile profile) {
|
||||||
|
String password = decrypt(profile.getEncryptedPassword());
|
||||||
|
if (password == null || password.isEmpty()) {
|
||||||
|
log.warn("Cannot decrypt password for SkillRack profile: {}", profile.getSkillrackEmail());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return fetchAndUpdateProfile(profile, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core scraping method using SkillRack's container security:
|
||||||
|
* 1. GET https://www.skillrack.com/faces/ui/profile.xhtml to establish JSESSIONID session
|
||||||
|
* 2. POST to https://www.skillrack.com/faces/ui/j_security_check with j_username (Login ID) & j_password
|
||||||
|
* 3. GET https://www.skillrack.com/faces/candidate/profileview.xhtml with authenticated session & parse HTML
|
||||||
|
*/
|
||||||
|
public boolean fetchAndUpdateProfile(SkillrackProfile profile, String rawPassword) {
|
||||||
|
try {
|
||||||
|
log.info("Fetching SkillRack stats for Login ID: {}", profile.getSkillrackEmail());
|
||||||
|
|
||||||
|
String loginPageUrl = SKILLRACK_BASE + "/faces/ui/profile.xhtml";
|
||||||
|
String jSecurityCheckUrl = SKILLRACK_BASE + "/faces/ui/j_security_check";
|
||||||
|
|
||||||
|
// Step 1: GET login page to establish session cookie
|
||||||
|
Connection.Response pageRes = Jsoup.connect(loginPageUrl)
|
||||||
|
.method(Connection.Method.GET)
|
||||||
|
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
|
.timeout(15000)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
Map<String, String> cookies = new HashMap<>(pageRes.cookies());
|
||||||
|
|
||||||
|
// Step 2: POST login credentials to j_security_check
|
||||||
|
Map<String, String> formData = new LinkedHashMap<>();
|
||||||
|
formData.put("j_username", profile.getSkillrackEmail().trim());
|
||||||
|
formData.put("j_password", rawPassword);
|
||||||
|
|
||||||
|
Connection.Response loginRes = Jsoup.connect(jSecurityCheckUrl)
|
||||||
|
.method(Connection.Method.POST)
|
||||||
|
.cookies(cookies)
|
||||||
|
.data(formData)
|
||||||
|
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.header("Referer", loginPageUrl)
|
||||||
|
.followRedirects(true)
|
||||||
|
.timeout(15000)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
cookies.putAll(loginRes.cookies());
|
||||||
|
|
||||||
|
// Step 3: Fetch candidate profile page with authenticated session
|
||||||
|
Connection.Response profileRes = Jsoup.connect(PROFILE_URL)
|
||||||
|
.method(Connection.Method.GET)
|
||||||
|
.cookies(cookies)
|
||||||
|
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
|
.followRedirects(true)
|
||||||
|
.timeout(15000)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
cookies.putAll(profileRes.cookies());
|
||||||
|
Document profileDoc = profileRes.parse();
|
||||||
|
String bodyText = profileDoc.body().text();
|
||||||
|
|
||||||
|
// Check if login failed (still on login page containing j_security_check form)
|
||||||
|
if (bodyText.contains("j_security_check") || bodyText.contains("Invalid User ID")) {
|
||||||
|
log.warn("SkillRack authentication failed for: {}", profile.getSkillrackEmail());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Parse profile stats from the HTML
|
||||||
|
boolean parsed = parseProfileStats(profileDoc, profile);
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
// Fallback: Try to extract from raw text with regex patterns
|
||||||
|
parsed = parseFromRawText(bodyText, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed) {
|
||||||
|
profile.setLastUpdated(LocalDateTime.now());
|
||||||
|
log.info("Successfully scraped SkillRack for {}: Points={}, CT={}, Tutor={}, Track={}, DC={}, Gold={}, Silver={}, Bronze={}",
|
||||||
|
profile.getSkillrackEmail(), profile.getTotalPoints(),
|
||||||
|
profile.getCodeTestSolved(), profile.getCodeTutorSolved(),
|
||||||
|
profile.getCodeTrackSolved(), profile.getDcSolved(),
|
||||||
|
profile.getGoldMedals(), profile.getSilverMedals(), profile.getBronzeMedals());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Could not parse SkillRack profile data for: {}", profile.getSkillrackEmail());
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to fetch SkillRack data for {}: {}", profile.getSkillrackEmail(), e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HTML Parsing ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean parseProfileStats(Document doc, SkillrackProfile profile) {
|
||||||
|
String html = doc.html();
|
||||||
|
boolean foundAnything = false;
|
||||||
|
|
||||||
|
// 1. Extract Real Name from .ui-chip-text if available
|
||||||
|
Element chipEl = doc.selectFirst(".ui-chip-text");
|
||||||
|
if (chipEl != null) {
|
||||||
|
String chipText = chipEl.text().trim();
|
||||||
|
if (chipText.contains("-")) {
|
||||||
|
String[] parts = chipText.split("-");
|
||||||
|
if (parts.length >= 1 && !parts[0].trim().isEmpty()) {
|
||||||
|
profile.setStudentName(parts[0].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Gold Medals (orange medal icon)
|
||||||
|
Matcher goldM = Pattern.compile("ion-md-medal\"\\s+style=\"color:orange\"></i>\\s*(\\d+)").matcher(html);
|
||||||
|
if (goldM.find()) {
|
||||||
|
profile.setGoldMedals(Integer.parseInt(goldM.group(1)));
|
||||||
|
foundAnything = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Silver Medals (grey medal icon)
|
||||||
|
Matcher silverM = Pattern.compile("ion-md-medal\"\\s+style=\"color:grey\"></i>\\s*(\\d+)").matcher(html);
|
||||||
|
if (silverM.find()) {
|
||||||
|
profile.setSilverMedals(Integer.parseInt(silverM.group(1)));
|
||||||
|
foundAnything = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Bronze Medals (brown medal icon)
|
||||||
|
Matcher bronzeM = Pattern.compile("ion-md-medal\"\\s+style=\"color:brown\"></i>\\s*(\\d+)").matcher(html);
|
||||||
|
if (bronzeM.find()) {
|
||||||
|
profile.setBronzeMedals(Integer.parseInt(bronzeM.group(1)));
|
||||||
|
foundAnything = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Total Points / Score (brown flag icon)
|
||||||
|
Matcher pointsM = Pattern.compile("ion-md-flag\"\\s+style=\"color:brown\"></i>\\s*(\\d+)").matcher(html);
|
||||||
|
if (pointsM.find()) {
|
||||||
|
profile.setTotalPoints(Integer.parseInt(pointsM.group(1)));
|
||||||
|
foundAnything = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundAnything;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseFromRawText(String text, SkillrackProfile profile) {
|
||||||
|
boolean found = false;
|
||||||
|
|
||||||
|
// Pattern: "Code Test 123" or "CodeTest: 123" or "Code Test Solved: 123"
|
||||||
|
found |= extractAndSet(text, "(?i)code\\s*test[^\\d]*(\\d+)", val -> profile.setCodeTestSolved(val));
|
||||||
|
found |= extractAndSet(text, "(?i)code\\s*tutor[^\\d]*(\\d+)", val -> profile.setCodeTutorSolved(val));
|
||||||
|
found |= extractAndSet(text, "(?i)code\\s*track[^\\d]*(\\d+)", val -> profile.setCodeTrackSolved(val));
|
||||||
|
found |= extractAndSet(text, "(?i)daily\\s*challenge[^\\d]*(\\d+)", val -> profile.setDcSolved(val));
|
||||||
|
found |= extractAndSet(text, "(?i)(?:dc|DC)[^\\d]*(\\d+)", val -> {
|
||||||
|
if (profile.getDcSolved() == 0) profile.setDcSolved(val);
|
||||||
|
});
|
||||||
|
found |= extractAndSet(text, "(?i)gold[^\\d]*(\\d+)", val -> profile.setGoldMedals(val));
|
||||||
|
found |= extractAndSet(text, "(?i)silver[^\\d]*(\\d+)", val -> profile.setSilverMedals(val));
|
||||||
|
found |= extractAndSet(text, "(?i)bronze[^\\d]*(\\d+)", val -> profile.setBronzeMedals(val));
|
||||||
|
found |= extractAndSet(text, "(?i)total\\s*(?:point|score)s?[^\\d]*(\\d+)", val -> profile.setTotalPoints(val));
|
||||||
|
|
||||||
|
// If total points not found, compute from components
|
||||||
|
if (profile.getTotalPoints() == 0 && found) {
|
||||||
|
int computed = profile.getCodeTestSolved() + profile.getCodeTutorSolved()
|
||||||
|
+ profile.getCodeTrackSolved() + profile.getDcSolved();
|
||||||
|
if (computed > 0) {
|
||||||
|
profile.setTotalPoints(computed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean extractAndSet(String text, String regex, java.util.function.IntConsumer setter) {
|
||||||
|
Matcher m = Pattern.compile(regex).matcher(text);
|
||||||
|
if (m.find()) {
|
||||||
|
int val = parseIntSafe(m.group(1));
|
||||||
|
if (val > 0) {
|
||||||
|
setter.accept(val);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseIntSafe(String s) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(s.replaceAll("[^\\d]", ""));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── AES Encryption Helpers ───────────────────────────────────────
|
||||||
|
|
||||||
|
private String encrypt(String plainText) {
|
||||||
|
try {
|
||||||
|
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||||
|
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getEncoder().encodeToString(encrypted);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AES encryption failed: {}", e.getMessage());
|
||||||
|
throw new RuntimeException("Encryption failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String decrypt(String encryptedBase64) {
|
||||||
|
try {
|
||||||
|
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(encryptedBase64);
|
||||||
|
byte[] decrypted = cipher.doFinal(decoded);
|
||||||
|
return new String(decrypted, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AES decryption failed: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scheduled & Manual Sync ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatic daily sync at 2:30 AM — spaced 5s apart to avoid rate limits
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 30 2 * * ?")
|
||||||
|
public void scheduledDailySync() {
|
||||||
|
log.info("Starting scheduled daily SkillRack sync...");
|
||||||
|
syncAllProfilesWithSpacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual or scheduled trigger to sync all profiles with 5-second delays.
|
||||||
|
*/
|
||||||
|
public void syncAllProfilesWithSpacing() {
|
||||||
|
List<SkillrackProfile> profiles = repository.findAll();
|
||||||
|
log.info("Queued {} SkillRack profiles for sync.", profiles.size());
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
for (int i = 0; i < profiles.size(); i++) {
|
||||||
|
SkillrackProfile p = profiles.get(i);
|
||||||
|
try {
|
||||||
|
boolean success = fetchAndUpdateProfile(p);
|
||||||
|
if (success) {
|
||||||
|
repository.save(p);
|
||||||
|
}
|
||||||
|
if (i < profiles.size() - 1) {
|
||||||
|
log.info("Waiting 5s before next SkillRack profile sync...");
|
||||||
|
Thread.sleep(5000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.error("SkillRack sync interrupted.");
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error syncing SkillRack profile {}: {}", p.getSkillrackEmail(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Finished SkillRack sync for all profiles.");
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.rit.portal.service.ims;
|
||||||
|
|
||||||
|
import com.rit.portal.dto.ims.StudentDashboardDto.*;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ImsClient {
|
||||||
|
ImsLoginResponse authenticate(String regNumber, String password);
|
||||||
|
List<MockUserCredentialDto> getAvailableMockUsers();
|
||||||
|
DashboardResponse getStudentDashboard(String studentId);
|
||||||
|
StudentInfo getStudentInfo(String studentId);
|
||||||
|
ClassLocationInfo getClassLocation(String department, String section, int semester);
|
||||||
|
List<ClassFacultyMember> getClassFaculty(String department, String section, int semester);
|
||||||
|
DaySchedule getDaySchedule(String studentId, LocalDate date);
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
package com.rit.portal.service.ims;
|
||||||
|
|
||||||
|
import com.rit.portal.dto.ims.StudentDashboardDto.*;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(name = "ims.client.type", havingValue = "mock", matchIfMissing = true)
|
||||||
|
public class MockImsClientImpl implements ImsClient {
|
||||||
|
|
||||||
|
private final Map<String, StudentInfo> studentDb = new HashMap<>();
|
||||||
|
private final Map<String, String> userPasswords = new HashMap<>();
|
||||||
|
private final Map<String, ClassLocationInfo> locationDb = new HashMap<>();
|
||||||
|
private final Map<String, List<ClassFacultyMember>> facultyDb = new HashMap<>();
|
||||||
|
private final List<MockUserCredentialDto> mockUserList = new ArrayList<>();
|
||||||
|
|
||||||
|
public MockImsClientImpl() {
|
||||||
|
seedMockData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedMockData() {
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 1. MOCK STUDENT 1: 2114251001 (CSE - Section A, Year 1, Sem 2)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
StudentInfo s1 = StudentInfo.builder()
|
||||||
|
.studentId("2114251001")
|
||||||
|
.regNumber("2114251001")
|
||||||
|
.name("Anbu Kathir")
|
||||||
|
.email("251001@ritchennai.edu.in")
|
||||||
|
.degree("B.E.")
|
||||||
|
.department("Computer Science & Engineering")
|
||||||
|
.departmentCode("CSE")
|
||||||
|
.year(1)
|
||||||
|
.semester(2)
|
||||||
|
.section("A")
|
||||||
|
.batch("2025 - 2029")
|
||||||
|
.regulation("2021 Regulation")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
studentDb.put("2114251001", s1);
|
||||||
|
studentDb.put("251001", s1);
|
||||||
|
userPasswords.put("2114251001", "rit@2026");
|
||||||
|
userPasswords.put("251001", "rit@2026");
|
||||||
|
|
||||||
|
mockUserList.add(MockUserCredentialDto.builder()
|
||||||
|
.regNumber("2114251001")
|
||||||
|
.defaultPassword("rit@2026")
|
||||||
|
.studentName("Anbu Kathir")
|
||||||
|
.department("Computer Science & Engineering")
|
||||||
|
.section("Sec A")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
locationDb.put("CSE_A_2", ClassLocationInfo.builder()
|
||||||
|
.roomNumber("LH-204")
|
||||||
|
.buildingName("Dr. APJ Abdul Kalam Academic Block")
|
||||||
|
.floor("2nd Floor")
|
||||||
|
.wing("East Wing")
|
||||||
|
.landmark("Next to CSE Department Library, Opposite to Seminar Hall")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
facultyDb.put("CSE_A_2", List.of(
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("CS3201")
|
||||||
|
.subjectName("Data Structures & Algorithms")
|
||||||
|
.facultyName("Dr. R. Arunkumar")
|
||||||
|
.designation("Associate Professor & Class Incharge")
|
||||||
|
.email("arunkumar.r@ritchennai.edu.in")
|
||||||
|
.officeLocation("APJ Block, Cabin 208-B")
|
||||||
|
.phoneExtension("Ext. 312")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Arunkumar")
|
||||||
|
.isClassIncharge(true)
|
||||||
|
.build(),
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("MA3251")
|
||||||
|
.subjectName("Discrete Mathematics")
|
||||||
|
.facultyName("Dr. S. Malathi")
|
||||||
|
.designation("Professor (Mathematics)")
|
||||||
|
.email("malathi.s@ritchennai.edu.in")
|
||||||
|
.officeLocation("Science & Humanities Block, 1st Floor")
|
||||||
|
.phoneExtension("Ext. 118")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Malathi")
|
||||||
|
.isClassIncharge(false)
|
||||||
|
.build(),
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("CS3202")
|
||||||
|
.subjectName("Digital Principles & System Design")
|
||||||
|
.facultyName("Dr. M. Balaji")
|
||||||
|
.designation("Assistant Professor")
|
||||||
|
.email("balaji.m@ritchennai.edu.in")
|
||||||
|
.officeLocation("APJ Block, Cabin 104")
|
||||||
|
.phoneExtension("Ext. 325")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Balaji")
|
||||||
|
.isClassIncharge(false)
|
||||||
|
.build(),
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("GE3251")
|
||||||
|
.subjectName("Environmental Sciences & Sustainability")
|
||||||
|
.facultyName("Dr. T. Gayathri")
|
||||||
|
.designation("Assistant Professor (Chemistry)")
|
||||||
|
.email("gayathri.t@ritchennai.edu.in")
|
||||||
|
.officeLocation("Science Block, Chemistry Cabin 3")
|
||||||
|
.phoneExtension("Ext. 142")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Gayathri")
|
||||||
|
.isClassIncharge(false)
|
||||||
|
.build(),
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("CS3271")
|
||||||
|
.subjectName("Data Structures Laboratory")
|
||||||
|
.facultyName("Dr. R. Arunkumar / Dr. K. Revathi")
|
||||||
|
.designation("Lab In-Charges")
|
||||||
|
.email("lab.cse@ritchennai.edu.in")
|
||||||
|
.officeLocation("Computing Center 3 (CC-03)")
|
||||||
|
.phoneExtension("Ext. 330")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Revathi")
|
||||||
|
.isClassIncharge(false)
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 2. MOCK STUDENT 2: 2114251002 (AI&DS - Section B, Year 1, Sem 2)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
StudentInfo s2 = StudentInfo.builder()
|
||||||
|
.studentId("2114251002")
|
||||||
|
.regNumber("2114251002")
|
||||||
|
.name("Priyadharshini M")
|
||||||
|
.email("251002@ritchennai.edu.in")
|
||||||
|
.degree("B.Tech.")
|
||||||
|
.department("Artificial Intelligence & Data Science")
|
||||||
|
.departmentCode("AIDS")
|
||||||
|
.year(1)
|
||||||
|
.semester(2)
|
||||||
|
.section("B")
|
||||||
|
.batch("2025 - 2029")
|
||||||
|
.regulation("2021 Regulation")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
studentDb.put("2114251002", s2);
|
||||||
|
studentDb.put("251002", s2);
|
||||||
|
userPasswords.put("2114251002", "rit@2026");
|
||||||
|
userPasswords.put("251002", "rit@2026");
|
||||||
|
|
||||||
|
mockUserList.add(MockUserCredentialDto.builder()
|
||||||
|
.regNumber("2114251002")
|
||||||
|
.defaultPassword("rit@2026")
|
||||||
|
.studentName("Priyadharshini M")
|
||||||
|
.department("AI & Data Science")
|
||||||
|
.section("Sec B")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
locationDb.put("AIDS_B_2", ClassLocationInfo.builder()
|
||||||
|
.roomNumber("LH-301")
|
||||||
|
.buildingName("Sir Isaac Newton Innovation Block")
|
||||||
|
.floor("3rd Floor")
|
||||||
|
.wing("North Wing")
|
||||||
|
.landmark("Opposite AI Centre of Excellence, Near Lift Lobby")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
facultyDb.put("AIDS_B_2", List.of(
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("AD3251")
|
||||||
|
.subjectName("Data Structures Design")
|
||||||
|
.facultyName("Dr. N. Saravanan")
|
||||||
|
.designation("Associate Professor & Class Incharge")
|
||||||
|
.email("saravanan.n@ritchennai.edu.in")
|
||||||
|
.officeLocation("Newton Block, Cabin 302")
|
||||||
|
.phoneExtension("Ext. 401")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Saravanan")
|
||||||
|
.isClassIncharge(true)
|
||||||
|
.build(),
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("AD3252")
|
||||||
|
.subjectName("Foundations of Artificial Intelligence")
|
||||||
|
.facultyName("Prof. J. Daniel")
|
||||||
|
.designation("Assistant Professor")
|
||||||
|
.email("daniel.j@ritchennai.edu.in")
|
||||||
|
.officeLocation("Newton Block, Cabin 306")
|
||||||
|
.phoneExtension("Ext. 405")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Daniel")
|
||||||
|
.isClassIncharge(false)
|
||||||
|
.build(),
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("MA3251")
|
||||||
|
.subjectName("Probability and Statistics")
|
||||||
|
.facultyName("Dr. V. Lakshmi")
|
||||||
|
.designation("Professor (Mathematics)")
|
||||||
|
.email("lakshmi.v@ritchennai.edu.in")
|
||||||
|
.officeLocation("Science Block, Cabin 108")
|
||||||
|
.phoneExtension("Ext. 119")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Lakshmi")
|
||||||
|
.isClassIncharge(false)
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 3. MOCK STUDENT 3: 2114251003 (ECE - Section A, Year 1, Sem 2)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
StudentInfo s3 = StudentInfo.builder()
|
||||||
|
.studentId("2114251003")
|
||||||
|
.regNumber("2114251003")
|
||||||
|
.name("Rahul V")
|
||||||
|
.email("251003@ritchennai.edu.in")
|
||||||
|
.degree("B.E.")
|
||||||
|
.department("Electronics & Communication Engineering")
|
||||||
|
.departmentCode("ECE")
|
||||||
|
.year(1)
|
||||||
|
.semester(2)
|
||||||
|
.section("A")
|
||||||
|
.batch("2025 - 2029")
|
||||||
|
.regulation("2021 Regulation")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
studentDb.put("2114251003", s3);
|
||||||
|
studentDb.put("251003", s3);
|
||||||
|
userPasswords.put("2114251003", "rit@2026");
|
||||||
|
userPasswords.put("251003", "rit@2026");
|
||||||
|
|
||||||
|
mockUserList.add(MockUserCredentialDto.builder()
|
||||||
|
.regNumber("2114251003")
|
||||||
|
.defaultPassword("rit@2026")
|
||||||
|
.studentName("Rahul V")
|
||||||
|
.department("Electronics & Comm. Engg.")
|
||||||
|
.section("Sec A")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
locationDb.put("ECE_A_2", ClassLocationInfo.builder()
|
||||||
|
.roomNumber("LH-105")
|
||||||
|
.buildingName("Rabindranath Tagore Academic Block")
|
||||||
|
.floor("1st Floor")
|
||||||
|
.wing("South Wing")
|
||||||
|
.landmark("Near Department Auditorium")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
facultyDb.put("ECE_A_2", List.of(
|
||||||
|
ClassFacultyMember.builder()
|
||||||
|
.subjectCode("EC3251")
|
||||||
|
.subjectName("Circuit Analysis")
|
||||||
|
.facultyName("Dr. I. Chandra")
|
||||||
|
.designation("Professor & Class Incharge")
|
||||||
|
.email("chandra.i@ritchennai.edu.in")
|
||||||
|
.officeLocation("Tagore Block, Cabin 112")
|
||||||
|
.phoneExtension("Ext. 502")
|
||||||
|
.avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Chandra")
|
||||||
|
.isClassIncharge(true)
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImsLoginResponse authenticate(String regNumber, String password) {
|
||||||
|
if (regNumber == null || regNumber.isBlank()) {
|
||||||
|
return ImsLoginResponse.builder()
|
||||||
|
.success(false)
|
||||||
|
.message("Register Number is required.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanedReg = regNumber.trim();
|
||||||
|
StudentInfo student = studentDb.get(cleanedReg);
|
||||||
|
|
||||||
|
if (student == null) {
|
||||||
|
String digits = cleanedReg.replaceAll("[^0-9]", "");
|
||||||
|
student = studentDb.get(digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (student == null) {
|
||||||
|
student = StudentInfo.builder()
|
||||||
|
.studentId(cleanedReg)
|
||||||
|
.regNumber(cleanedReg)
|
||||||
|
.name("Student (" + cleanedReg + ")")
|
||||||
|
.email(cleanedReg + "@ritchennai.edu.in")
|
||||||
|
.degree("B.E.")
|
||||||
|
.department("Computer Science & Engineering")
|
||||||
|
.departmentCode("CSE")
|
||||||
|
.year(1)
|
||||||
|
.semester(2)
|
||||||
|
.section("A")
|
||||||
|
.batch("2025 - 2029")
|
||||||
|
.regulation("2021 Regulation")
|
||||||
|
.build();
|
||||||
|
studentDb.put(cleanedReg, student);
|
||||||
|
userPasswords.put(cleanedReg, "rit@2026");
|
||||||
|
}
|
||||||
|
|
||||||
|
String expectedPassword = userPasswords.getOrDefault(cleanedReg, "rit@2026");
|
||||||
|
boolean isValid = password != null && (
|
||||||
|
password.equals(expectedPassword) ||
|
||||||
|
password.equals("rit@2026") ||
|
||||||
|
password.equals("password") ||
|
||||||
|
password.equals("123456")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return ImsLoginResponse.builder()
|
||||||
|
.success(false)
|
||||||
|
.message("Invalid Register Number or Password. Default password: rit@2026")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = "ims_session_" + student.getRegNumber() + "_" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
|
||||||
|
return ImsLoginResponse.builder()
|
||||||
|
.success(true)
|
||||||
|
.message("IMS Login Successful")
|
||||||
|
.token(token)
|
||||||
|
.student(student)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MockUserCredentialDto> getAvailableMockUsers() {
|
||||||
|
return Collections.unmodifiableList(mockUserList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DashboardResponse getStudentDashboard(String studentId) {
|
||||||
|
StudentInfo student = getStudentInfo(studentId);
|
||||||
|
String classKey = student.getDepartmentCode() + "_" + student.getSection() + "_" + student.getSemester();
|
||||||
|
|
||||||
|
ClassLocationInfo location = locationDb.getOrDefault(classKey, locationDb.get("CSE_A_2"));
|
||||||
|
List<ClassFacultyMember> faculty = facultyDb.getOrDefault(classKey, facultyDb.get("CSE_A_2"));
|
||||||
|
DaySchedule schedule = getDaySchedule(studentId, LocalDate.now());
|
||||||
|
|
||||||
|
return DashboardResponse.builder()
|
||||||
|
.student(student)
|
||||||
|
.classLocation(location)
|
||||||
|
.facultyList(faculty)
|
||||||
|
.todaySchedule(schedule)
|
||||||
|
.isMockData(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudentInfo getStudentInfo(String studentId) {
|
||||||
|
if (studentId == null || studentId.isBlank()) {
|
||||||
|
return studentDb.get("2114251001");
|
||||||
|
}
|
||||||
|
String cleaned = studentId.trim();
|
||||||
|
StudentInfo student = studentDb.get(cleaned);
|
||||||
|
if (student != null) return student;
|
||||||
|
|
||||||
|
String digits = cleaned.replaceAll("[^0-9]", "");
|
||||||
|
return studentDb.getOrDefault(digits, studentDb.get("2114251001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLocationInfo getClassLocation(String department, String section, int semester) {
|
||||||
|
String key = department + "_" + section + "_" + semester;
|
||||||
|
return locationDb.getOrDefault(key, locationDb.get("CSE_A_2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClassFacultyMember> getClassFaculty(String department, String section, int semester) {
|
||||||
|
String key = department + "_" + section + "_" + semester;
|
||||||
|
return facultyDb.getOrDefault(key, facultyDb.get("CSE_A_2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DaySchedule getDaySchedule(String studentId, LocalDate date) {
|
||||||
|
DayOfWeek day = date.getDayOfWeek();
|
||||||
|
String dayName = (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) ? "MONDAY" : day.name();
|
||||||
|
|
||||||
|
LocalTime now = LocalTime.now();
|
||||||
|
List<TimetablePeriod> periods = buildTimetableForDay(studentId, dayName, now);
|
||||||
|
|
||||||
|
return DaySchedule.builder()
|
||||||
|
.dayOfWeek(dayName)
|
||||||
|
.date(date)
|
||||||
|
.periods(periods)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TimetablePeriod> buildTimetableForDay(String studentId, String dayOfWeek, LocalTime now) {
|
||||||
|
List<TimetablePeriod> list = new ArrayList<>();
|
||||||
|
|
||||||
|
String[][] slots = {
|
||||||
|
{"1", "08:45 AM - 09:40 AM", "08:45", "09:40"},
|
||||||
|
{"2", "09:40 AM - 10:35 AM", "09:40", "10:35"},
|
||||||
|
{"3", "10:50 AM - 11:45 AM", "10:50", "11:45"},
|
||||||
|
{"4", "11:45 AM - 12:40 PM", "11:45", "12:40"},
|
||||||
|
{"5", "01:30 PM - 02:25 PM", "13:30", "14:25"},
|
||||||
|
{"6", "02:25 PM - 03:20 PM", "14:25", "15:20"}
|
||||||
|
};
|
||||||
|
|
||||||
|
boolean isAids = studentId != null && studentId.contains("251002");
|
||||||
|
String room = isAids ? "LH-301" : "LH-204";
|
||||||
|
String lab = isAids ? "AI-02 Lab" : "CC-03 Lab";
|
||||||
|
|
||||||
|
String[][] subjects;
|
||||||
|
if ("WEDNESDAY".equalsIgnoreCase(dayOfWeek) || "FRIDAY".equalsIgnoreCase(dayOfWeek)) {
|
||||||
|
subjects = new String[][]{
|
||||||
|
{isAids ? "AD3251" : "CS3201", isAids ? "Data Structures Design" : "Data Structures & Algorithms", "THEORY", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", room},
|
||||||
|
{"MA3251", isAids ? "Probability & Stats" : "Discrete Mathematics", "THEORY", isAids ? "Dr. V. Lakshmi" : "Dr. S. Malathi", room},
|
||||||
|
{isAids ? "AD3271" : "CS3271", isAids ? "AI & DS Practical Lab" : "Data Structures Laboratory", "LAB", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", lab},
|
||||||
|
{isAids ? "AD3271" : "CS3271", isAids ? "AI & DS Practical Lab" : "Data Structures Laboratory", "LAB", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", lab},
|
||||||
|
{isAids ? "AD3252" : "CS3202", isAids ? "Foundations of AI" : "Digital Principles & System Design", "THEORY", isAids ? "Prof. J. Daniel" : "Dr. M. Balaji", room},
|
||||||
|
{"GE3251", "Environmental Sciences", "THEORY", "Dr. T. Gayathri", room}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
subjects = new String[][]{
|
||||||
|
{"MA3251", isAids ? "Probability & Stats" : "Discrete Mathematics", "THEORY", isAids ? "Dr. V. Lakshmi" : "Dr. S. Malathi", room},
|
||||||
|
{isAids ? "AD3251" : "CS3201", isAids ? "Data Structures Design" : "Data Structures & Algorithms", "THEORY", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", room},
|
||||||
|
{isAids ? "AD3252" : "CS3202", isAids ? "Foundations of AI" : "Digital Principles & System Design", "THEORY", isAids ? "Prof. J. Daniel" : "Dr. M. Balaji", room},
|
||||||
|
{"GE3251", "Environmental Sciences", "THEORY", "Dr. T. Gayathri", room},
|
||||||
|
{isAids ? "AD3251" : "CS3201", isAids ? "AI Problem Solving Tutorial" : "DSA Tutorial & Problem Solving", "TUTORIAL", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", room},
|
||||||
|
{"LIB", "Digital Library & Research Hours", "LIBRARY", "Chief Librarian", "Central Library - 2nd Floor"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < slots.length; i++) {
|
||||||
|
LocalTime start = LocalTime.parse(slots[i][2]);
|
||||||
|
LocalTime end = LocalTime.parse(slots[i][3]);
|
||||||
|
|
||||||
|
String status = "UPCOMING";
|
||||||
|
if (now.isAfter(end)) {
|
||||||
|
status = "COMPLETED";
|
||||||
|
} else if (now.isAfter(start) && now.isBefore(end)) {
|
||||||
|
status = "ONGOING";
|
||||||
|
}
|
||||||
|
|
||||||
|
list.add(TimetablePeriod.builder()
|
||||||
|
.periodNumber(Integer.parseInt(slots[i][0]))
|
||||||
|
.timeSlot(slots[i][1])
|
||||||
|
.startTime(slots[i][2])
|
||||||
|
.endTime(slots[i][3])
|
||||||
|
.subjectCode(subjects[i][0])
|
||||||
|
.subjectName(subjects[i][1])
|
||||||
|
.type(subjects[i][2])
|
||||||
|
.facultyName(subjects[i][3])
|
||||||
|
.venue(subjects[i][4])
|
||||||
|
.status(status)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.rit.portal.service.ims;
|
||||||
|
|
||||||
|
import com.rit.portal.dto.ims.StudentDashboardDto.*;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(name = "ims.client.type", havingValue = "real")
|
||||||
|
public class RealImsClientImpl implements ImsClient {
|
||||||
|
|
||||||
|
@Value("${ims.base-url:https://api.ims.ritchennai.edu.in}")
|
||||||
|
private String imsBaseUrl;
|
||||||
|
|
||||||
|
@Value("${ims.api-key:}")
|
||||||
|
private String imsApiKey;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
private HttpHeaders createHeaders(String studentId) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("X-Nexus-App-Secret", imsApiKey);
|
||||||
|
if (studentId != null && !studentId.isBlank()) {
|
||||||
|
headers.set("X-Student-Id", studentId);
|
||||||
|
}
|
||||||
|
headers.set("Accept", "application/json");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImsLoginResponse authenticate(String regNumber, String password) {
|
||||||
|
String url = imsBaseUrl + "/api/v1/auth/student-login";
|
||||||
|
ImsLoginRequest request = ImsLoginRequest.builder()
|
||||||
|
.regNumber(regNumber)
|
||||||
|
.password(password)
|
||||||
|
.build();
|
||||||
|
HttpEntity<ImsLoginRequest> entity = new HttpEntity<>(request, createHeaders(null));
|
||||||
|
ResponseEntity<ImsLoginResponse> resp = restTemplate.exchange(url, HttpMethod.POST, entity, ImsLoginResponse.class);
|
||||||
|
return resp.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MockUserCredentialDto> getAvailableMockUsers() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DashboardResponse getStudentDashboard(String studentId) {
|
||||||
|
String url = imsBaseUrl + "/api/v1/dashboard/me";
|
||||||
|
HttpEntity<?> entity = new HttpEntity<>(createHeaders(studentId));
|
||||||
|
ResponseEntity<DashboardResponse> resp = restTemplate.exchange(url, HttpMethod.GET, entity, DashboardResponse.class);
|
||||||
|
DashboardResponse body = resp.getBody();
|
||||||
|
if (body != null) {
|
||||||
|
body.setMockData(false);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudentInfo getStudentInfo(String studentId) {
|
||||||
|
String url = imsBaseUrl + "/api/v1/students/me";
|
||||||
|
HttpEntity<?> entity = new HttpEntity<>(createHeaders(studentId));
|
||||||
|
ResponseEntity<StudentInfo> resp = restTemplate.exchange(url, HttpMethod.GET, entity, StudentInfo.class);
|
||||||
|
return resp.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLocationInfo getClassLocation(String department, String section, int semester) {
|
||||||
|
String url = String.format("%s/api/v1/classes/location?dept=%s&sec=%s&sem=%d", imsBaseUrl, department, section, semester);
|
||||||
|
HttpEntity<?> entity = new HttpEntity<>(createHeaders(null));
|
||||||
|
return restTemplate.exchange(url, HttpMethod.GET, entity, ClassLocationInfo.class).getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClassFacultyMember> getClassFaculty(String department, String section, int semester) {
|
||||||
|
String url = String.format("%s/api/v1/classes/faculty?dept=%s&sec=%s&sem=%d", imsBaseUrl, department, section, semester);
|
||||||
|
HttpEntity<?> entity = new HttpEntity<>(createHeaders(null));
|
||||||
|
ClassFacultyMember[] array = restTemplate.exchange(url, HttpMethod.GET, entity, ClassFacultyMember[].class).getBody();
|
||||||
|
return array != null ? Arrays.asList(array) : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DaySchedule getDaySchedule(String studentId, LocalDate date) {
|
||||||
|
String url = String.format("%s/api/v1/timetable/me?date=%s", imsBaseUrl, date.toString());
|
||||||
|
HttpEntity<?> entity = new HttpEntity<>(createHeaders(studentId));
|
||||||
|
return restTemplate.exchange(url, HttpMethod.GET, entity, DaySchedule.class).getBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,40 @@
|
|||||||
# ─── DATABASE CONNECTION CONFIGURATION ───
|
# ─── DATABASE CONNECTION CONFIGURATION ───
|
||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=disable
|
spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=disable
|
||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
spring.datasource.password=${DB_PASSWORD:Devesh@54}
|
spring.datasource.password=${DB_PASSWORD:Amudiesh22@.}
|
||||||
|
|
||||||
|
# ─── HIKARICP CONNECTION POOL OPTIMIZATIONS ───
|
||||||
|
spring.datasource.hikari.maximum-pool-size=30
|
||||||
|
spring.datasource.hikari.minimum-idle=10
|
||||||
|
spring.datasource.hikari.idle-timeout=300000
|
||||||
|
spring.datasource.hikari.max-lifetime=1200000
|
||||||
|
spring.datasource.hikari.connection-timeout=20000
|
||||||
|
|
||||||
# ─── JPA / HIBERNATE SETTINGS ───
|
# ─── JPA / HIBERNATE SETTINGS ───
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.jpa.show-sql=true
|
spring.jpa.show-sql=false
|
||||||
spring.jpa.properties.hibernate.format_sql=true
|
spring.jpa.properties.hibernate.format_sql=false
|
||||||
|
|
||||||
# ─── SERVER PORT ───
|
# ─── SERVER PORT & HTTP GZIP COMPRESSION ───
|
||||||
server.port=8085
|
server.port=8085
|
||||||
|
server.compression.enabled=true
|
||||||
|
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css
|
||||||
|
server.compression.min-response-size=1024
|
||||||
|
|
||||||
# ─── GOOGLE OAUTH2 CONFIGURATION ───
|
# ─── GOOGLE OAUTH2 CONFIGURATION ───
|
||||||
google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
|
google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
|
||||||
|
|
||||||
# ─── ADMIN PORTAL CREDENTIALS ───
|
# ─── ADMIN PORTAL MULTI-ROLE CREDENTIALS ───
|
||||||
|
admin.transport.username=${TRANSPORT_USERNAME:Transport}
|
||||||
|
admin.transport.password=${TRANSPORT_PASSWORD:RIT@2026}
|
||||||
|
admin.super.username=${SUPER_ADMIN_USERNAME:Admin}
|
||||||
|
admin.super.password=${SUPER_ADMIN_PASSWORD:RIT@2026}
|
||||||
admin.username=${ADMIN_USERNAME:ritadmin}
|
admin.username=${ADMIN_USERNAME:ritadmin}
|
||||||
admin.password=${ADMIN_PASSWORD:ritadmin2026}
|
admin.password=${ADMIN_PASSWORD:ritadmin2026}
|
||||||
|
|
||||||
|
# ─── IMS INTEGRATION CONFIGURATION ───
|
||||||
|
# Options: "mock" (active now for testing & simulation) | "real" (for live college IMS)
|
||||||
|
ims.client.type=mock
|
||||||
|
ims.base-url=${IMS_BASE_URL:https://api.ims.ritchennai.edu.in}
|
||||||
|
ims.api-key=${IMS_API_KEY:rit_nexus_vps_secret_key_2026}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.rit.portal;
|
||||||
|
|
||||||
|
import com.rit.portal.model.BusLocation;
|
||||||
|
import com.rit.portal.service.BusLocationService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class BusLocationServiceTest {
|
||||||
|
|
||||||
|
private BusLocationService busLocationService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
busLocationService = new BusLocationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateAndGetLocation() {
|
||||||
|
busLocationService.updateLocation("24A", 13.0827, 80.2707);
|
||||||
|
|
||||||
|
BusLocation loc = busLocationService.getLocation("24A");
|
||||||
|
assertNotNull(loc);
|
||||||
|
assertEquals("24A", loc.getRouteNumber());
|
||||||
|
assertEquals(13.0827, loc.getLatitude());
|
||||||
|
assertEquals(80.2707, loc.getLongitude());
|
||||||
|
assertFalse(loc.isStopped());
|
||||||
|
assertNotNull(loc.getLastUpdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStopLocation() {
|
||||||
|
busLocationService.updateLocation("15B", 12.9716, 77.5946);
|
||||||
|
busLocationService.stopLocation("15B");
|
||||||
|
|
||||||
|
BusLocation loc = busLocationService.getLocation("15B");
|
||||||
|
assertNotNull(loc);
|
||||||
|
assertTrue(loc.isStopped());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetNonExistentLocationReturnsNull() {
|
||||||
|
BusLocation loc = busLocationService.getLocation("NON_EXISTENT");
|
||||||
|
assertNull(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetActiveLocations() {
|
||||||
|
busLocationService.updateLocation("R1", 13.0, 80.0);
|
||||||
|
busLocationService.updateLocation("R2", 13.1, 80.1);
|
||||||
|
|
||||||
|
List<BusLocation> active = busLocationService.getActiveLocations();
|
||||||
|
assertEquals(2, active.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/test/java/com/rit/portal/EntityMappingTest.java
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package com.rit.portal;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.CommunityAnswer;
|
||||||
|
import com.rit.portal.entity.CommunityQuestion;
|
||||||
|
import com.rit.portal.entity.NotePyq;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class EntityMappingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNotePyqBuilderDefaults() {
|
||||||
|
NotePyq note = NotePyq.builder()
|
||||||
|
.title("Data Structures Notes")
|
||||||
|
.subject("Data Structures")
|
||||||
|
.department("CSE")
|
||||||
|
.semester(3)
|
||||||
|
.fileType("notes")
|
||||||
|
.downloadUrl("https://example.com/ds.pdf")
|
||||||
|
.fileSize("2.4 MB")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertNotNull(note);
|
||||||
|
assertEquals(0, note.getDownloadsCount());
|
||||||
|
assertNotNull(note.getUploadedAt());
|
||||||
|
assertTrue(note.getUploadedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommunityQuestionBuilderDefaults() {
|
||||||
|
CommunityQuestion question = CommunityQuestion.builder()
|
||||||
|
.title("How to prepare for CAT-1?")
|
||||||
|
.body("Looking for reference materials and past questions.")
|
||||||
|
.author("Student123")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertNotNull(question);
|
||||||
|
assertEquals(0, question.getUpvotes());
|
||||||
|
assertFalse(question.getIsAnswered());
|
||||||
|
assertNotNull(question.getTags());
|
||||||
|
assertNotNull(question.getAnswers());
|
||||||
|
assertNotNull(question.getCreatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommunityAnswerBuilderDefaults() {
|
||||||
|
CommunityAnswer answer = CommunityAnswer.builder()
|
||||||
|
.body("Focus on Unit 1 and Unit 2 standard questions.")
|
||||||
|
.author("Senior456")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertNotNull(answer);
|
||||||
|
assertEquals(0, answer.getUpvotes());
|
||||||
|
assertFalse(answer.getIsAccepted());
|
||||||
|
assertNotNull(answer.getCreatedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
1540
grade_report.html
Normal file
25
package-lock.json
generated
@@ -1614,21 +1614,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.11.2",
|
"version": "1.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||||
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
|
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/core/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/wasi-threads": "1.2.2",
|
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.11.2",
|
"version": "1.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||||
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
|
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
BIN
public/clubs/center_ai.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/clubs/center_apple_tech.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/clubs/center_ar_vr.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
public/clubs/center_cloud_computing.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/clubs/center_cybersecurity.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
public/clubs/center_data_science.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/clubs/center_ev_energy.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
public/clubs/center_image_processing.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
public/clubs/center_iot.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
public/clubs/center_radar_healthcare.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/clubs/center_semiconductor.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
public/clubs/center_smart_manufacturing.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
public/clubs/center_space.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/clubs/center_zf_transportation.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
public/clubs/grover_center_quantum.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/map.png
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 8.8 MiB |
3543
real_timetable.html
Normal file
10
schema.sql
@@ -135,3 +135,13 @@ CREATE TABLE IF NOT EXISTS confessions (
|
|||||||
reactions_count INT DEFAULT 0,
|
reactions_count INT DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 10. Club & Center Likes Table
|
||||||
|
CREATE TABLE IF NOT EXISTS club_likes (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
club_id VARCHAR(100) NOT NULL,
|
||||||
|
user_identifier VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT unique_club_user_like UNIQUE (club_id, user_identifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
|||||||
18
scratch/inspect_table_cells.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('real_timetable.html', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find rows in the timetable table
|
||||||
|
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL)
|
||||||
|
print(f"Total tr rows: {len(rows)}")
|
||||||
|
|
||||||
|
for idx, r in enumerate(rows):
|
||||||
|
if 'tuesday' in r.lower() or 'tue' in r.lower():
|
||||||
|
print(f"\n--- ROW {idx} (TUESDAY) ---")
|
||||||
|
tds = re.findall(r'<td[^>]*>(.*?)</td>', r, re.DOTALL)
|
||||||
|
print(f"Total td cells in Tuesday row: {len(tds)}")
|
||||||
|
for col_i, td in enumerate(tds):
|
||||||
|
clean_td = ' '.join(re.sub(r'<[^>]+>', ' ', td).split())
|
||||||
|
forms_in_td = re.findall(r'<form[^>]*>(.*?)</form>', td, re.DOTALL)
|
||||||
|
print(f" Col {col_i}: forms_count={len(forms_in_td)} | text={clean_td[:80]}")
|
||||||
24
scratch/inspect_tt_periods.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('real_timetable.html', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
forms = re.findall(r'<form[^>]*class="[^"]*period_form[^"]*"[^>]*>(.*?)</form>', content, re.DOTALL)
|
||||||
|
print(f"Total forms found: {len(forms)}")
|
||||||
|
|
||||||
|
by_day = {}
|
||||||
|
for form in forms:
|
||||||
|
day_m = re.search(r'name="day"\s+value="([^"]+)"', form)
|
||||||
|
per_m = re.search(r'name="period"\s+value="([^"]+)"', form)
|
||||||
|
day = day_m.group(1).lower() if day_m else 'unknown'
|
||||||
|
per = per_m.group(1) if per_m else '0'
|
||||||
|
by_day.setdefault(day, []).append((per, form))
|
||||||
|
|
||||||
|
for day in sorted(by_day.keys()):
|
||||||
|
print(f"\n=== DAY: {day.upper()} (Count: {len(by_day[day])}) ===")
|
||||||
|
for per, form in by_day[day]:
|
||||||
|
primaries = re.findall(r'class="[^"]*text-primary[^"]*"[^>]*>(.*?)</span>', form, re.DOTALL)
|
||||||
|
clean_p = [re.sub(r'<[^>]+>', '', p).strip() for p in primaries]
|
||||||
|
full_txt = re.sub(r'<[^>]+>', ' ', form).strip()
|
||||||
|
full_txt = ' '.join(full_txt.split())
|
||||||
|
print(f" Period {per}: primaries={clean_p} | full_txt={full_txt[:100]}")
|
||||||
13
scratch/inspect_tue_context.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('real_timetable.html', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
print("File length:", len(content))
|
||||||
|
# Search for Tuesday in content
|
||||||
|
tue_matches = [m.start() for m in re.finditer(r'tuesday', content, re.IGNORECASE)]
|
||||||
|
print("Tuesday matches at indices:", tue_matches)
|
||||||
|
|
||||||
|
for pos in tue_matches[:3]:
|
||||||
|
print("\n--- Context around Tuesday ---")
|
||||||
|
print(content[max(0, pos-200):min(len(content), pos+400)])
|
||||||
53
scratch/test_lab_assignment_marks.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import http.cookiejar
|
||||||
|
import re
|
||||||
|
|
||||||
|
cj = http.cookiejar.CookieJar()
|
||||||
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||||
|
|
||||||
|
login_url = "https://ims.rajalakshmi.edu.in/ims/login"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: GET login page
|
||||||
|
req = urllib.request.Request(login_url, headers=headers)
|
||||||
|
with opener.open(req) as resp:
|
||||||
|
html = resp.read().decode('utf-8')
|
||||||
|
|
||||||
|
csrf_m = re.search(r'name="_token"\s+value="([^"]+)"', html)
|
||||||
|
csrf_token = csrf_m.group(1) if csrf_m else ''
|
||||||
|
print(f"CSRF Token: {csrf_token}")
|
||||||
|
|
||||||
|
# Step 2: POST login
|
||||||
|
post_data = urllib.parse.urlencode({
|
||||||
|
'_token': csrf_token,
|
||||||
|
'username': '2117240070293',
|
||||||
|
'password': '7010406809'
|
||||||
|
}).encode('utf-8')
|
||||||
|
|
||||||
|
req_post = urllib.request.Request(login_url, data=post_data, headers=headers)
|
||||||
|
with opener.open(req_post) as resp_post:
|
||||||
|
dash_html = resp_post.read().decode('utf-8')
|
||||||
|
|
||||||
|
print(f"Login success! Dash HTML length: {len(dash_html)}")
|
||||||
|
|
||||||
|
# Step 3: Find all <a> hrefs containing lab or assignment or mark or report
|
||||||
|
all_links = re.findall(r'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>', dash_html, re.DOTALL)
|
||||||
|
print(f"Total links found: {len(all_links)}")
|
||||||
|
|
||||||
|
for href, label in all_links:
|
||||||
|
clean_label = re.sub(r'<[^>]+>', '', label).strip()
|
||||||
|
clean_label_lower = clean_label.lower()
|
||||||
|
if 'lab' in clean_label_lower or 'assignment' in clean_label_lower or 'mark' in clean_label_lower or 'grade' in clean_label_lower or 'cat' in clean_label_lower:
|
||||||
|
print(f"Link: '{clean_label}' -> Href: '{href}'")
|
||||||
|
|
||||||
|
# Also search for all hrefs in the full HTML to be sure
|
||||||
|
all_hrefs = re.findall(r'href="([^"]+)"', dash_html)
|
||||||
|
print("\n--- ALL UNIQUE HREFS IN DASHBOARD ---")
|
||||||
|
for h in set(all_hrefs):
|
||||||
|
if 'admin' in h or 'student' in h or 'mark' in h or 'report' in h:
|
||||||
|
print(" ", h)
|
||||||
58
src/App.tsx
@@ -1,28 +1,35 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, lazy, Suspense } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AuthProvider } from '@/context/AuthContext';
|
import { AuthProvider } from '@/context/AuthContext';
|
||||||
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
|
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
|
||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Home from '@/pages/Home/Home';
|
import PageLoader from '@/components/PageLoader/PageLoader';
|
||||||
import Notes from '@/pages/Notes/Notes';
|
import ErrorBoundary from '@/components/ErrorBoundary/ErrorBoundary';
|
||||||
import Toolkit from '@/pages/Toolkit/Toolkit';
|
|
||||||
import AIAssistant from '@/pages/AIAssistant/AIAssistant';
|
// Code-split route components with dynamic imports for minimal initial bundle size
|
||||||
import Campus from '@/pages/Campus/Campus';
|
const Home = lazy(() => import('@/pages/Home/Home'));
|
||||||
import Events from '@/pages/Events/Events';
|
const Notes = lazy(() => import('@/pages/Notes/Notes'));
|
||||||
import Community from '@/pages/Community/Community';
|
const Toolkit = lazy(() => import('@/pages/Toolkit/Toolkit'));
|
||||||
import BusRoutes from '@/pages/BusRoutes/BusRoutes';
|
const AIAssistant = lazy(() => import('@/pages/AIAssistant/AIAssistant'));
|
||||||
import Faculty from '@/pages/Faculty/Faculty';
|
const Campus = lazy(() => import('@/pages/Campus/Campus'));
|
||||||
import DevCollab from '@/pages/DevCollab/DevCollab';
|
const Events = lazy(() => import('@/pages/Events/Events'));
|
||||||
import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard';
|
const Community = lazy(() => import('@/pages/Community/Community'));
|
||||||
import AdminDashboard from '@/pages/Admin/AdminDashboard';
|
const BusRoutes = lazy(() => import('@/pages/BusRoutes/BusRoutes'));
|
||||||
import Raise from '@/pages/Raise/Raise';
|
const Faculty = lazy(() => import('@/pages/Faculty/Faculty'));
|
||||||
|
const DevCollab = lazy(() => import('@/pages/DevCollab/DevCollab'));
|
||||||
|
const LeetcodeLeaderboard = lazy(() => import('@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard'));
|
||||||
|
const AdminDashboard = lazy(() => import('@/pages/Admin/AdminDashboard'));
|
||||||
|
const Raise = lazy(() => import('@/pages/Raise/Raise'));
|
||||||
|
const StudentDashboard = lazy(() => import('@/pages/StudentDashboard/StudentDashboard'));
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
gcTime: 10 * 60 * 1000,
|
gcTime: 10 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -39,10 +46,12 @@ function ScrollToTop() {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Standalone Admin Route */}
|
{/* Standalone Admin Route */}
|
||||||
<Route path="/admin" element={<AdminDashboard />} />
|
<Route path="/admin" element={<AdminDashboard />} />
|
||||||
@@ -61,6 +70,10 @@ function App() {
|
|||||||
<Route path="/faculty" element={<Faculty />} />
|
<Route path="/faculty" element={<Faculty />} />
|
||||||
<Route path="/events" element={<Events />} />
|
<Route path="/events" element={<Events />} />
|
||||||
|
|
||||||
|
{/* Student Dashboard & Timetable — Real IMS Reg No & Password */}
|
||||||
|
<Route path="/dashboard" element={<StudentDashboard />} />
|
||||||
|
<Route path="/timetable" element={<StudentDashboard />} />
|
||||||
|
|
||||||
{/* Protected Routes — Require Google Sign-In */}
|
{/* Protected Routes — Require Google Sign-In */}
|
||||||
<Route
|
<Route
|
||||||
path="/community"
|
path="/community"
|
||||||
@@ -89,8 +102,19 @@ function App() {
|
|||||||
path="/leetcode"
|
path="/leetcode"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
title="RIT LeetCode Leaderboard"
|
title="RIT Student Leaderboard"
|
||||||
description="Track campus competitive programming rankings and see top RIT coders."
|
description="Track campus competitive programming rankings (LeetCode & SkillRack) and verified skill certifications."
|
||||||
|
>
|
||||||
|
<LeetcodeLeaderboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/student-leaderboard"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute
|
||||||
|
title="RIT Student Leaderboard"
|
||||||
|
description="Track campus competitive programming rankings (LeetCode & SkillRack) and verified skill certifications."
|
||||||
>
|
>
|
||||||
<LeetcodeLeaderboard />
|
<LeetcodeLeaderboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
@@ -98,9 +122,11 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ export function StaggerContainer({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
animate="visible"
|
||||||
viewport={{ once: true, margin: '-50px' }}
|
|
||||||
variants={{
|
variants={{
|
||||||
hidden: {},
|
hidden: {},
|
||||||
visible: {
|
visible: {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { X, ShieldCheck, Sparkles } from 'lucide-react';
|
import {
|
||||||
|
X, ShieldCheck, Sparkles, User, KeyRound,
|
||||||
|
ArrowRight, AlertCircle, GraduationCap, Bus, Shield
|
||||||
|
} from 'lucide-react';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface GoogleAuthModalProps {
|
interface GoogleAuthModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -10,7 +14,14 @@ interface GoogleAuthModalProps {
|
|||||||
|
|
||||||
export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) {
|
export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) {
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
const { isAuthenticated } = useAuth();
|
const navigate = useNavigate();
|
||||||
|
const { isAuthenticated, loginWithCredentials } = useAuth();
|
||||||
|
|
||||||
|
const [usernameInput, setUsernameInput] = useState('');
|
||||||
|
const [passwordInput, setPasswordInput] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [activeMode, setActiveMode] = useState<'credentials' | 'google'>('credentials');
|
||||||
|
|
||||||
// Close modal automatically if user gets authenticated
|
// Close modal automatically if user gets authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -21,7 +32,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
|||||||
|
|
||||||
// Render Google Button inside modal when open
|
// Render Google Button inside modal when open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen || activeMode !== 'google') return;
|
||||||
|
|
||||||
const renderGoogleBtn = () => {
|
const renderGoogleBtn = () => {
|
||||||
if (window.google && buttonRef.current) {
|
if (window.google && buttonRef.current) {
|
||||||
@@ -38,10 +49,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render immediately if script is loaded
|
|
||||||
renderGoogleBtn();
|
renderGoogleBtn();
|
||||||
|
|
||||||
// Or poll briefly if script is loading
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (window.google) {
|
if (window.google) {
|
||||||
renderGoogleBtn();
|
renderGoogleBtn();
|
||||||
@@ -50,7 +58,40 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
|||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [isOpen]);
|
}, [isOpen, activeMode]);
|
||||||
|
|
||||||
|
const handleSubmitCredentials = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!usernameInput.trim() || !passwordInput.trim()) {
|
||||||
|
setErrorMessage('Please enter both username/register number and password.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim());
|
||||||
|
if (result.success) {
|
||||||
|
onClose();
|
||||||
|
if (result.redirectTo) {
|
||||||
|
navigate(result.redirectTo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrorMessage(result.message || 'Invalid credentials.');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMessage(err?.message || 'Authentication error.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickFill = (user: string, pass: string) => {
|
||||||
|
setUsernameInput(user);
|
||||||
|
setPasswordInput(pass);
|
||||||
|
setErrorMessage(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -62,20 +103,20 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute inset-0 bg-slate-950/70 backdrop-blur-md"
|
className="absolute inset-0 bg-slate-950/80 backdrop-blur-md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal Container */}
|
{/* Modal Container */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
initial={{ opacity: 0, scale: 0.92, y: 15 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
exit={{ opacity: 0, scale: 0.92, y: 15 }}
|
||||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.3 }}
|
transition={{ type: 'spring', bounce: 0.18, duration: 0.3 }}
|
||||||
className="relative w-full max-w-md bg-slate-900 border border-slate-700/80 rounded-3xl shadow-2xl p-8 z-10 text-center overflow-hidden"
|
className="relative w-full max-w-md bg-slate-900 border border-white/10 rounded-3xl shadow-2xl p-6 sm:p-7 z-10 text-slate-100 overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* Background Orbs */}
|
{/* Background Glow */}
|
||||||
<div className="absolute -top-20 -right-20 w-40 h-40 bg-orange-500/20 rounded-full blur-2xl pointer-events-none" />
|
<div className="absolute -top-20 -right-20 w-44 h-44 bg-orange-500/15 rounded-full blur-3xl pointer-events-none" />
|
||||||
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-blue-500/20 rounded-full blur-2xl pointer-events-none" />
|
<div className="absolute -bottom-20 -left-20 w-44 h-44 bg-indigo-500/15 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
@@ -85,38 +126,149 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
|||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Icon Header */}
|
{/* Header */}
|
||||||
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center mx-auto mb-5">
|
<div className="text-center mb-5">
|
||||||
<Sparkles className="w-8 h-8 text-[#F97316]" />
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-lg shadow-orange-500/20 mx-auto mb-3">
|
||||||
|
<div className="w-full h-full bg-slate-950/60 rounded-[14px] flex items-center justify-center">
|
||||||
|
<Sparkles className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title & Description */}
|
<h2 className="text-xl font-extrabold text-white tracking-tight">
|
||||||
<h2
|
|
||||||
className="text-2xl font-bold text-white mb-2"
|
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
Sign In to Freshers Hub
|
Sign In to Freshers Hub
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
Enter your student register number or admin credentials.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p
|
{/* Tab Selector */}
|
||||||
className="text-xs text-slate-300 mb-6 leading-relaxed"
|
<div className="flex rounded-xl bg-slate-950/80 p-1 border border-white/5 mb-5 text-xs font-semibold">
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveMode('credentials')}
|
||||||
|
className={`flex-1 py-2 rounded-lg transition-all cursor-pointer ${
|
||||||
|
activeMode === 'credentials'
|
||||||
|
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-sm'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
Credentials (Student / Admin)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveMode('google')}
|
||||||
|
className={`flex-1 py-2 rounded-lg transition-all cursor-pointer ${
|
||||||
|
activeMode === 'google'
|
||||||
|
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-sm'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Google Sign In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ERROR MESSAGE */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="mb-4 p-3 rounded-xl bg-rose-500/10 border border-rose-500/20 flex items-start gap-2 text-xs text-rose-300">
|
||||||
|
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 1. CREDENTIALS SIGN IN FORM */}
|
||||||
|
{activeMode === 'credentials' && (
|
||||||
|
<form onSubmit={handleSubmitCredentials} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
|
||||||
|
<User className="w-3.5 h-3.5 text-orange-400" />
|
||||||
|
Register Number / Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={usernameInput}
|
||||||
|
onChange={(e) => setUsernameInput(e.target.value)}
|
||||||
|
placeholder="e.g. 2114251001 or Transport or Admin"
|
||||||
|
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
|
||||||
|
<KeyRound className="w-3.5 h-3.5 text-orange-400" />
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordInput}
|
||||||
|
onChange={(e) => setPasswordInput(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full py-3 px-4 rounded-xl text-white font-bold text-xs bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Sign In</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Quick Demo Fill Buttons for Admins */}
|
||||||
|
<div className="pt-3 border-t border-white/10 space-y-1.5">
|
||||||
|
<span className="text-[10px] text-slate-400 font-semibold block text-center uppercase tracking-wider">
|
||||||
|
Official Admin Portals:
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5 text-[11px]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleQuickFill('Transport', 'RIT@2026')}
|
||||||
|
className="p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="truncate">🚌 Transport Admin</span>
|
||||||
|
<span className="text-[9px] text-blue-400 font-mono">Select</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleQuickFill('Admin', 'RIT@2026')}
|
||||||
|
className="p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="truncate">⚡ Super Admin</span>
|
||||||
|
<span className="text-[9px] text-emerald-400 font-mono">Select</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2. GOOGLE SIGN IN */}
|
||||||
|
{activeMode === 'google' && (
|
||||||
|
<div className="space-y-4 py-2 text-center">
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">
|
||||||
Sign in with your Google account. Use your{' '}
|
Sign in with your Google account. Use your{' '}
|
||||||
<strong className="text-[#F97316]">@ritchennai.edu.in</strong> college email to unlock the{' '}
|
<strong className="text-orange-400">@ritchennai.edu.in</strong> email for automatic student verification.
|
||||||
<strong className="text-emerald-400">Verified Student</strong> badge and post collaboration requests!
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Google Sign-In Button Container */}
|
<div className="flex justify-center my-4 min-h-[44px]">
|
||||||
<div className="flex justify-center my-6 min-h-[44px]">
|
|
||||||
<div ref={buttonRef} />
|
<div ref={buttonRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Verified Student Callout */}
|
<div className="pt-3 border-t border-white/10 flex items-center justify-center gap-1.5 text-[11px] text-slate-400">
|
||||||
<div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-center gap-2 text-[11px] text-slate-400">
|
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||||
<ShieldCheck className="w-4 h-4 text-emerald-400 shrink-0" />
|
<span>Google OAuth 2.0 Single Sign-On</span>
|
||||||
<span>Official RIT Student Verification via Google</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export default function ProtectedRoute({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
className="text-2xl sm:text-3xl font-bold text-[#1E293B] mb-3"
|
className="text-2xl sm:text-3xl font-extrabold text-[#1E293B] mb-3"
|
||||||
style={{ fontFamily: 'Playfair Display, serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
91
src/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||||
|
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('[ErrorBoundary caught error]:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleReset = () => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleGoHome = () => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[70vh] flex items-center justify-center p-6">
|
||||||
|
<div className="max-w-md w-full bg-card/80 backdrop-blur-xl border border-destructive/20 rounded-2xl p-8 shadow-2xl text-center space-y-6">
|
||||||
|
<div className="w-16 h-16 mx-auto rounded-2xl bg-destructive/10 border border-destructive/20 flex items-center justify-center text-destructive shadow-inner">
|
||||||
|
<AlertTriangle className="w-8 h-8 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl font-bold tracking-tight text-foreground">
|
||||||
|
Something went wrong
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
An unexpected error occurred while rendering this section. You can try refreshing the page or navigating back home.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.error && (
|
||||||
|
<div className="p-3 rounded-lg bg-muted/50 border border-border text-left overflow-x-auto text-xs font-mono text-muted-foreground max-h-24">
|
||||||
|
{this.state.error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={this.handleReset}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium transition-all shadow-lg shadow-orange-600/25 hover:shadow-orange-600/40 active:scale-95 cursor-pointer"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={this.handleGoHome}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl bg-secondary hover:bg-secondary/80 text-foreground text-sm font-medium transition-all active:scale-95 border border-border cursor-pointer"
|
||||||
|
>
|
||||||
|
<Home className="w-4 h-4" />
|
||||||
|
Go to Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -36,20 +36,65 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
{ name: 'Yahoo Mail', url: `https://compose.mail.yahoo.com/?to=${faculty.email}`, icon: Mail, color: 'text-purple-600', bg: 'bg-purple-50' },
|
{ name: 'Yahoo Mail', url: `https://compose.mail.yahoo.com/?to=${faculty.email}`, icon: Mail, color: 'text-purple-600', bg: 'bg-purple-50' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const parseToListItems = (text: string | undefined): string[] => {
|
||||||
|
if (!text) return [];
|
||||||
|
const cleaned = text.replace(/<br\s*\/?>/gi, '\n');
|
||||||
|
const lines = cleaned.split(/\n+/);
|
||||||
|
const items: string[] = [];
|
||||||
|
lines.forEach((line) => {
|
||||||
|
let subParts: string[];
|
||||||
|
if (line.includes(';')) {
|
||||||
|
subParts = line.split(';');
|
||||||
|
} else if (line.includes(',') && !line.includes('(')) {
|
||||||
|
subParts = line.split(',');
|
||||||
|
} else {
|
||||||
|
subParts = [line];
|
||||||
|
}
|
||||||
|
subParts.forEach((part) => {
|
||||||
|
let trimmed = part.trim();
|
||||||
|
trimmed = trimmed.replace(/^[\d+.)\-•\s*]+/, '').trim();
|
||||||
|
if (trimmed && trimmed !== '—' && trimmed !== 'Nil' && trimmed !== 'nil') {
|
||||||
|
items.push(trimmed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPointList = (value: string | undefined, placeholder: string = 'Not specified') => {
|
||||||
|
const items = parseToListItems(value);
|
||||||
|
if (items.length <= 1) {
|
||||||
|
return (
|
||||||
|
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
{items[0] || placeholder}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ul className="space-y-1.5 mt-1 text-left">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-2 text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-[#FF6B00] mt-2 shrink-0" />
|
||||||
|
<span className="leading-tight">{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -6, boxShadow: '0 20px 40px -10px rgba(0,0,0,0.1)' }}
|
whileHover={{ y: -4 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="bg-white border border-[#E8ECF4] rounded-[20px] p-5 flex flex-col gap-4 relative"
|
className="bg-white border border-[#E9E5EE] rounded-2xl p-5 flex flex-col gap-4 relative shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)] hover:shadow-[0_12px_35px_-6px_rgba(19,9,36,0.08)] hover:border-[#FF6B00]/30"
|
||||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
|
||||||
>
|
>
|
||||||
{/* Avatar + Name */}
|
{/* Avatar + Name */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0 shadow-sm"
|
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0 shadow-sm"
|
||||||
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
|
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
@@ -57,10 +102,10 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-[#22C55E] border-2 border-white rounded-full z-10 shadow-sm" />
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-[#22C55E] border-2 border-white rounded-full z-10 shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-[#1E293B] text-sm leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<h3 className="font-bold text-[#1A0B2E] text-sm leading-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{faculty.name}
|
{faculty.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-[#FF7A00] font-medium mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-xs text-[#FF6B00] font-semibold mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
{faculty.designation}
|
{faculty.designation}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,27 +113,27 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
|
|
||||||
{/* Details */}
|
{/* Details */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2 text-xs text-[#475569]">
|
<div className="flex items-center gap-2 text-xs text-[#4A3E5E]">
|
||||||
<Building2 className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
|
<Building2 className="w-3.5 h-3.5 text-[#9E91B6] shrink-0" />
|
||||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
|
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact & Actions */}
|
{/* Contact & Actions */}
|
||||||
<div className="flex items-center gap-2 pt-2 border-t border-[#E5E7EB] mt-auto">
|
<div className="flex items-center gap-2 pt-2 border-t border-[#E9E5EE] mt-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowEmailOptions(true)}
|
onClick={() => setShowEmailOptions(true)}
|
||||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium text-[#475569] transition-all hover:bg-gray-50 border border-transparent hover:border-gray-200"
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-semibold text-[#4A3E5E] transition-all hover:bg-purple-50/50 border border-transparent hover:border-[#E9E5EE]"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
aria-label={`Email ${faculty.name}`}
|
aria-label={`Email ${faculty.name}`}
|
||||||
>
|
>
|
||||||
<Mail className="w-3.5 h-3.5" />
|
<Mail className="w-3.5 h-3.5 text-[#FF6B00]" />
|
||||||
Email
|
Email
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowModal(true)}
|
onClick={() => setShowModal(true)}
|
||||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all hover:bg-[#FFF7ED]"
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-bold transition-all hover:bg-[#FFF7ED]"
|
||||||
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
|
style={{ color: '#FF6B00', fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
View Profile
|
View Profile
|
||||||
<ChevronRight className="w-3.5 h-3.5" />
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
@@ -135,11 +180,11 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-32 h-32 rounded-full flex items-center justify-center text-white font-bold text-5xl mb-5 shadow-lg"
|
className="w-32 h-32 rounded-full flex items-center justify-center text-white font-bold text-5xl mb-5 shadow-lg"
|
||||||
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
|
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-center text-[#1E293B] mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<h2 className="text-2xl font-bold text-center text-[#1E293B] mb-1" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{faculty.name}
|
{faculty.name}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base font-semibold text-[#FF7A00] text-center" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-base font-semibold text-[#FF7A00] text-center" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
@@ -193,17 +238,13 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
<div className="w-10 h-10 rounded-xl bg-orange-50 flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-xl bg-orange-50 flex items-center justify-center shrink-0">
|
||||||
<User className="w-5 h-5 text-[#FF7A00]" />
|
<User className="w-5 h-5 text-[#FF7A00]" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Professional Details</h3>
|
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Professional Details</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Department</p>
|
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Department</p>
|
||||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</p>
|
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
|
||||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Specialization</p>
|
|
||||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.specialization || 'Not specified'}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Qualification</p>
|
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Qualification</p>
|
||||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.qualification || 'Not specified'}</p>
|
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.qualification || 'Not specified'}</p>
|
||||||
@@ -213,11 +254,15 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.experience || 'Not specified'}</p>
|
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.experience || 'Not specified'}</p>
|
||||||
</div>
|
</div>
|
||||||
{faculty.joinedInstitution && (
|
{faculty.joinedInstitution && (
|
||||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50 md:col-span-2">
|
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Joined Institution</p>
|
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Joined Institution</p>
|
||||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.joinedInstitution}</p>
|
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.joinedInstitution}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50 md:col-span-2">
|
||||||
|
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Interest / Domain / Specialization</p>
|
||||||
|
{renderPointList(faculty.interest || faculty.specialization)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,17 +272,19 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
<div className="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
|
||||||
<BookOpen className="w-5 h-5 text-indigo-500" />
|
<BookOpen className="w-5 h-5 text-indigo-500" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Academic Information</h3>
|
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Academic Information</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="p-5 rounded-xl bg-indigo-50/30 border border-indigo-100/50">
|
<div className="p-5 rounded-xl bg-indigo-50/30 border border-indigo-100/50">
|
||||||
<p className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Subjects Handling</p>
|
<p className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Subjects Handling</p>
|
||||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.subjectsHandling || 'Not specified'}</p>
|
{renderPointList(faculty.subjectsHandling)}
|
||||||
</div>
|
</div>
|
||||||
|
{faculty.researchAreas && (
|
||||||
<div className="p-5 rounded-xl bg-teal-50/30 border border-teal-100/50">
|
<div className="p-5 rounded-xl bg-teal-50/30 border border-teal-100/50">
|
||||||
<p className="text-xs font-bold text-teal-500 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Research Areas</p>
|
<p className="text-xs font-bold text-teal-500 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Research Areas</p>
|
||||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.researchAreas || 'Not specified'}</p>
|
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.researchAreas}</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{faculty.studentsGuided && (
|
{faculty.studentsGuided && (
|
||||||
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50">
|
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50">
|
||||||
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Students Guided</p>
|
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Students Guided</p>
|
||||||
@@ -267,7 +314,7 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
<div className="w-10 h-10 rounded-xl bg-pink-50 flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-xl bg-pink-50 flex items-center justify-center shrink-0">
|
||||||
<GraduationCap className="w-5 h-5 text-pink-500" />
|
<GraduationCap className="w-5 h-5 text-pink-500" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Achievements</h3>
|
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Achievements</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{faculty.achievements && faculty.achievements.length > 0 && (
|
{faculty.achievements && faculty.achievements.length > 0 && (
|
||||||
@@ -295,30 +342,36 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Section 4: Office Info */}
|
{/* Section 4: Office Info */}
|
||||||
|
{(faculty.office || faculty.officeHours) && (
|
||||||
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
|
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="w-10 h-10 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
|
||||||
<Clock className="w-5 h-5 text-emerald-500" />
|
<Clock className="w-5 h-5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Office Information</h3>
|
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Office Information</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{faculty.officeHours && (
|
||||||
<div className="p-5 rounded-xl bg-emerald-50/50 border border-emerald-100 flex items-start gap-4">
|
<div className="p-5 rounded-xl bg-emerald-50/50 border border-emerald-100 flex items-start gap-4">
|
||||||
<Clock className="w-6 h-6 text-emerald-500 shrink-0 mt-0.5" />
|
<Clock className="w-6 h-6 text-emerald-500 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold text-emerald-600 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Hours</p>
|
<p className="text-xs font-bold text-emerald-600 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Hours</p>
|
||||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.officeHours || 'Not specified'}</p>
|
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.officeHours}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{faculty.office && (
|
||||||
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50 flex items-start gap-4">
|
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50 flex items-start gap-4">
|
||||||
<Building2 className="w-6 h-6 text-blue-500 shrink-0 mt-0.5" />
|
<Building2 className="w-6 h-6 text-blue-500 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Location</p>
|
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Location</p>
|
||||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office || 'Not specified'}</p>
|
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -349,7 +402,7 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Choose Mail App</h3>
|
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Choose Mail App</h3>
|
||||||
<p className="text-xs text-[#64748B] mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Compose email to {faculty.name}</p>
|
<p className="text-xs text-[#64748B] mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Compose email to {faculty.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setShowEmailOptions(false)} className="p-2 rounded-full hover:bg-gray-100 transition-colors">
|
<button onClick={() => setShowEmailOptions(false)} className="p-2 rounded-full hover:bg-gray-100 transition-colors">
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ export default function FeatureCard({ feature }: FeatureCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -6, boxShadow: '0 20px 50px -12px rgba(0,0,0,0.15)' }}
|
whileHover={{ y: -4 }}
|
||||||
transition={{ duration: 0.25 }}
|
transition={{ duration: 0.25 }}
|
||||||
className="bg-white border border-[#E5E7EB] rounded-2xl p-6 flex flex-col gap-4 group cursor-pointer"
|
className="bg-white border border-[#E9E5EE] rounded-2xl p-6 flex flex-col gap-4 group cursor-pointer shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)] hover:shadow-[0_12px_35px_-6px_rgba(19,9,36,0.08)] hover:border-[#FF6B00]/30"
|
||||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300 group-hover:scale-110"
|
className="w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300 group-hover:scale-105"
|
||||||
style={{ backgroundColor: feature.bgColor, color: feature.color }}
|
style={{ backgroundColor: feature.bgColor, color: feature.color }}
|
||||||
>
|
>
|
||||||
{IconComponent && <IconComponent className="w-6 h-6" />}
|
{IconComponent && <IconComponent className="w-6 h-6" />}
|
||||||
@@ -29,12 +28,12 @@ export default function FeatureCard({ feature }: FeatureCardProps) {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3
|
<h3
|
||||||
className="text-base font-semibold text-[#1E293B] mb-1.5"
|
className="text-base font-bold text-[#1A0B2E] mb-1.5"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{feature.title}
|
{feature.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-[#94A3B8] leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-sm text-[#4A3E5E] leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
{feature.description}
|
{feature.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,9 +41,9 @@ export default function FeatureCard({ feature }: FeatureCardProps) {
|
|||||||
{/* Arrow */}
|
{/* Arrow */}
|
||||||
<Link
|
<Link
|
||||||
to={feature.path}
|
to={feature.path}
|
||||||
className="flex items-center gap-1.5 text-sm font-semibold transition-all duration-200 w-fit"
|
className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider transition-all duration-200 w-fit"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Plus Jakarta Sans, sans-serif',
|
||||||
color: feature.color,
|
color: feature.color,
|
||||||
}}
|
}}
|
||||||
aria-label={`Explore ${feature.title}`}
|
aria-label={`Explore ${feature.title}`}
|
||||||
|
|||||||
@@ -1,109 +1,239 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { GraduationCap, MapPin, Phone, Mail, ArrowRight } from 'lucide-react';
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
GraduationCap, MapPin, Phone, Mail, ArrowRight, Sparkles,
|
||||||
|
BookOpen, Bot, Map, Bus, UserCheck, Users, MessageCircle, Wrench, Code2, Trophy
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
const quickLinks = [
|
const exploreLinks = [
|
||||||
{ label: 'Home', path: '/' },
|
{ label: 'Notes & PYQs', path: '/notes', icon: BookOpen },
|
||||||
{ label: 'Notes & PYQs', path: '/notes' },
|
{ label: 'Student Toolkit', path: '/toolkit', icon: Wrench },
|
||||||
{ label: 'RIT Chatbot', path: '/ai-assistant' },
|
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
|
||||||
{ label: '3D Campus Map', path: '/campus' },
|
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
|
||||||
{ label: 'Bus Routes', path: '/bus-routes' },
|
{ label: 'Student Leaderboard', path: '/student-leaderboard', icon: Trophy },
|
||||||
{ label: 'Faculty Directory', path: '/faculty' },
|
];
|
||||||
{ label: 'Events', path: '/events' },
|
|
||||||
{ label: 'Community', path: '/community' },
|
const campusLinks = [
|
||||||
{ label: 'Dev Hub', path: '/collab' },
|
{ label: 'Campus Map', path: '/campus', icon: Map },
|
||||||
|
{ label: 'Bus Routes', path: '/bus-routes', icon: Bus },
|
||||||
|
{ label: 'Clubs & Centers', path: '/events', icon: Users },
|
||||||
|
{ label: 'Freshers Q&A', path: '/community', icon: MessageCircle },
|
||||||
|
{ label: 'Dev Collab Hub', path: '/collab', icon: Code2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
|
const location = useLocation();
|
||||||
|
const isHome = location.pathname === '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-[#0F172A] text-white border-t border-slate-800">
|
<div className="w-full relative z-20">
|
||||||
{/* CTA Banner - Enterprise Deep Indigo */}
|
{/* ─── 1. CTA Banner (Floating Overlay Box - Only on Home Page) ────────── */}
|
||||||
<div className="bg-gradient-to-r from-indigo-700 via-indigo-800 to-purple-900 py-12 border-b border-indigo-500/20">
|
{isHome && (
|
||||||
<div className="container-custom flex flex-col md:flex-row items-center justify-between gap-6 px-4">
|
<div className="container-custom relative z-20 -mb-20 lg:-mb-24 px-4">
|
||||||
<div>
|
<motion.div
|
||||||
<h3 className="text-2xl font-bold text-white" style={{ fontFamily: 'Playfair Display, serif' }}>
|
whileHover={{ y: -3 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="relative overflow-hidden rounded-3xl p-8 lg:p-12 shadow-2xl border border-orange-400/30"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #EA580C 0%, #F97316 50%, #FB923C 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Ambient Background Lighting */}
|
||||||
|
<div className="absolute -right-16 -top-16 w-64 h-64 bg-white/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="absolute -left-16 -bottom-16 w-64 h-64 bg-black/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6">
|
||||||
|
<div className="text-center md:text-left">
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-3.5 py-1 rounded-full bg-white/15 backdrop-blur-md text-white text-xs font-bold uppercase tracking-wider mb-3 border border-white/20">
|
||||||
|
<Sparkles className="w-3.5 h-3.5 text-amber-200" />
|
||||||
|
<span>RIT Student Portal</span>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
className="text-2xl lg:text-3xl xl:text-4xl font-extrabold text-white tracking-tight"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
Ready to explore RIT?
|
Ready to explore RIT?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-indigo-100/90 mt-1 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p
|
||||||
Everything a fresher needs — all in one place.
|
className="text-orange-100 text-sm lg:text-base mt-2 max-w-xl font-medium"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
Everything a fresher needs — semester notes, campus map, chatbot, bus routes, and developer tools all in one place.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
|
||||||
to="/ai-assistant"
|
<Link to="/ai-assistant" className="shrink-0">
|
||||||
className="flex items-center gap-2 bg-white hover:bg-slate-100 text-indigo-950 px-6 py-3 rounded-xl font-bold transition-all shadow-md hover:scale-105"
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05, boxShadow: '0 20px 30px -10px rgba(0,0,0,0.3)' }}
|
||||||
|
whileTap={{ scale: 0.96 }}
|
||||||
|
className="flex items-center gap-2.5 bg-white text-[#EA580C] px-7 py-3.5 rounded-2xl font-bold text-sm lg:text-base shadow-xl transition-all cursor-pointer group"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
>
|
>
|
||||||
<span>Ask Chatbot</span>
|
<span>Ask Chatbot</span>
|
||||||
<ArrowRight className="w-4 h-4 text-indigo-700" />
|
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
||||||
|
</motion.button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Footer */}
|
{/* ─── 2. Main Footer Section (Deep Royal Dark Purple) ──────────── */}
|
||||||
<div className="container-custom py-12 px-4">
|
<footer className={`bg-[#130924] text-slate-300 relative z-10 pb-24 lg:pb-28 ${isHome ? 'pt-32 lg:pt-36' : 'pt-28 lg:pt-36'}`}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
<div className="container-custom">
|
||||||
{/* Brand */}
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-12 gap-12 xl:gap-14 items-start">
|
||||||
<div>
|
{/* Column 1: Brand Info (xl:col-span-4) */}
|
||||||
<div className="flex items-center gap-2.5 mb-4">
|
<div className="xl:col-span-4 flex flex-col gap-5">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center bg-indigo-600 border border-indigo-400/30 text-white shadow-md">
|
<Link to="/" className="flex items-center gap-3.5 group w-fit">
|
||||||
<GraduationCap className="w-5 h-5" />
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-orange-500 to-amber-500 flex items-center justify-center shadow-lg shadow-orange-500/25 group-hover:scale-105 transition-transform shrink-0">
|
||||||
|
<GraduationCap className="w-6.5 h-6.5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-col">
|
||||||
<div className="font-bold text-white text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Freshers Hub</div>
|
<span className="font-extrabold text-white text-xl tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
<div className="text-[10px] text-slate-400">Rajalakshmi Institute of Technology</div>
|
RIT Freshers Hub
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-semibold text-purple-300/80 uppercase tracking-wider mt-0.5">
|
||||||
|
Rajalakshmi Institute of Technology
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
<p className="text-slate-400 text-sm leading-relaxed mb-5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
|
||||||
Your all-in-one guide to campus life at RIT. Notes, chatbot, bus routes, 3D campus map, and developer hub — built for students.
|
<p className="text-slate-300/80 text-sm lg:text-base leading-relaxed max-w-md" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Your official student companion for RIT campus life. Access study resources, competitive coding ranks, project collaboration, and campus navigation.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5 mt-2 flex-wrap">
|
||||||
|
<span className="px-3.5 py-1.5 rounded-xl bg-[#1E0C36] border border-[#3A1968] text-xs font-semibold text-orange-400">
|
||||||
|
Official Student Portal
|
||||||
|
</span>
|
||||||
|
<span className="px-3.5 py-1.5 rounded-xl bg-emerald-500/15 border border-emerald-500/25 text-xs font-semibold text-emerald-400">
|
||||||
|
Active 24/7
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Links */}
|
{/* Column 2: Academics & Tools (xl:col-span-2) */}
|
||||||
<div>
|
<div className="xl:col-span-2">
|
||||||
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<h4
|
||||||
Quick Navigation
|
className="!text-white font-extrabold text-xs lg:text-sm uppercase tracking-widest mb-6 lg:mb-7 flex items-center gap-2.5"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-orange-500 shadow-xs shadow-orange-500/50" />
|
||||||
|
Academics
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<ul className="space-y-4 lg:space-y-4.5">
|
||||||
{quickLinks.map((link) => (
|
{exploreLinks.map((link) => {
|
||||||
|
const Icon = link.icon;
|
||||||
|
return (
|
||||||
|
<li key={link.path}>
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
|
||||||
to={link.path}
|
to={link.path}
|
||||||
className="text-slate-400 hover:text-indigo-400 text-sm transition-colors flex items-center gap-1.5 group py-1"
|
className="text-slate-300/80 hover:text-orange-400 text-sm lg:text-base font-medium transition-colors inline-flex items-center gap-3 group py-1"
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
>
|
>
|
||||||
<span className="w-1 h-1 rounded-full bg-indigo-500 opacity-0 group-hover:opacity-100 transition-opacity" />
|
<Icon className="w-4.5 h-4.5 text-purple-400/60 group-hover:text-orange-400 transition-colors shrink-0" />
|
||||||
{link.label}
|
<span>{link.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
</li>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact */}
|
{/* Column 3: Campus Life (xl:col-span-3) */}
|
||||||
<div>
|
<div className="xl:col-span-3">
|
||||||
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<h4
|
||||||
Campus Location & Contact
|
className="!text-white font-extrabold text-xs lg:text-sm uppercase tracking-widest mb-6 lg:mb-7 flex items-center gap-2.5"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-orange-500 shadow-xs shadow-orange-500/50" />
|
||||||
|
Campus Life
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-4 lg:space-y-4.5">
|
||||||
<li className="flex items-start gap-3 text-slate-400 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
{campusLinks.map((link) => {
|
||||||
<MapPin className="w-4 h-4 mt-0.5 shrink-0 text-indigo-400" />
|
const Icon = link.icon;
|
||||||
<span>Rajalakshmi Institute of Technology,<br />Kuthambakkam, Chennai - 600 124</span>
|
return (
|
||||||
|
<li key={link.path}>
|
||||||
|
<Link
|
||||||
|
to={link.path}
|
||||||
|
className="text-slate-300/80 hover:text-orange-400 text-sm lg:text-base font-medium transition-colors inline-flex items-center gap-3 group py-1"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
<Icon className="w-4.5 h-4.5 text-purple-400/60 group-hover:text-orange-400 transition-colors shrink-0" />
|
||||||
|
<span>{link.label}</span>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-3 text-slate-400 text-sm">
|
);
|
||||||
<Phone className="w-4 h-4 shrink-0 text-indigo-400" />
|
})}
|
||||||
<a href="tel:+914423422890" className="hover:text-indigo-400 transition-colors">+91 44 2342 2890</a>
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 4: Reach Us (xl:col-span-3) */}
|
||||||
|
<div className="xl:col-span-3">
|
||||||
|
<h4
|
||||||
|
className="!text-white font-extrabold text-xs lg:text-sm uppercase tracking-widest mb-6 lg:mb-7 flex items-center gap-2.5"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-orange-500 shadow-xs shadow-orange-500/50" />
|
||||||
|
Reach Us
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-5 lg:space-y-6">
|
||||||
|
<li className="flex items-start gap-4 text-slate-300 text-sm lg:text-base leading-relaxed">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-orange-500/15 border border-orange-500/30 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
|
<MapPin className="w-4.5 h-4.5 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-semibold text-white">Rajalakshmi Institute of Technology</span>
|
||||||
|
<span className="text-slate-400 text-xs lg:text-sm mt-1">Kuthambakkam, Chennai - 600 124</span>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-3 text-slate-400 text-sm">
|
|
||||||
<Mail className="w-4 h-4 shrink-0 text-indigo-400" />
|
<li className="flex items-center gap-4 text-slate-300 text-sm lg:text-base">
|
||||||
<a href="mailto:info@rit.ac.in" className="hover:text-indigo-400 transition-colors">info@rit.ac.in</a>
|
<div className="w-9 h-9 rounded-xl bg-orange-500/15 border border-orange-500/30 flex items-center justify-center shrink-0">
|
||||||
|
<Phone className="w-4.5 h-4.5 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<a href="tel:+914423422890" className="hover:text-orange-400 transition-colors font-medium">
|
||||||
|
+91 44 2342 2890
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="flex items-center gap-4 text-slate-300 text-sm lg:text-base">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-orange-500/15 border border-orange-500/30 flex items-center justify-center shrink-0">
|
||||||
|
<Mail className="w-4.5 h-4.5 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<a href="mailto:info@rit.ac.in" className="hover:text-orange-400 transition-colors font-medium">
|
||||||
|
info@rit.ac.in
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 pt-6 border-t border-slate-800 text-center text-xs text-slate-500 flex flex-col sm:flex-row items-center justify-between gap-3">
|
{/* Bottom Copyright Bar */}
|
||||||
<span>© {new Date().getFullYear()} Rajalakshmi Institute of Technology. All rights reserved.</span>
|
<div className="border-t border-[#2A104E] bg-[#090314] py-9 lg:py-10 px-4 mt-20">
|
||||||
<span className="text-slate-400">RIT Student Portal v2.0</span>
|
<div className="container-custom flex flex-col sm:flex-row items-center justify-between gap-5">
|
||||||
|
<p className="text-purple-300/70 text-sm lg:text-base font-medium" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
© {new Date().getFullYear()} RIT Freshers Hub. Built for Rajalakshmi Institute of Technology Students.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6 text-sm lg:text-base font-medium text-purple-300/70">
|
||||||
|
<a href="#" className="hover:text-orange-400 transition-colors">
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
<span className="text-purple-900">·</span>
|
||||||
|
<a href="#" className="hover:text-orange-400 transition-colors">
|
||||||
|
Terms of Use
|
||||||
|
</a>
|
||||||
|
<span className="text-purple-900">·</span>
|
||||||
|
<a href="#" className="hover:text-orange-400 transition-colors">
|
||||||
|
Accessibility
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,416 +0,0 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Bell,
|
|
||||||
Menu,
|
|
||||||
Calendar,
|
|
||||||
LogIn,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
BookOpen,
|
|
||||||
Bus,
|
|
||||||
Users,
|
|
||||||
Calendar as ClubIcon,
|
|
||||||
Code2,
|
|
||||||
Trophy,
|
|
||||||
Bot,
|
|
||||||
Rocket,
|
|
||||||
ChevronRight
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { BUS_ROUTES, FACULTY_DATA, CLUBS_DATA } from '@/constants';
|
|
||||||
|
|
||||||
interface HeaderProps {
|
|
||||||
onToggleSidebar: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotificationItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
time: string;
|
|
||||||
read: boolean;
|
|
||||||
type: 'academic' | 'dev' | 'campus' | 'bus';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Header({ onToggleSidebar }: HeaderProps) {
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
||||||
const [isNotifOpen, setIsNotifOpen] = useState(false);
|
|
||||||
const [currentDate, setCurrentDate] = useState('');
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle } = useAuth();
|
|
||||||
const notifRef = useRef<HTMLDivElement>(null);
|
|
||||||
const searchRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [notifications, setNotifications] = useState<NotificationItem[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Moodle Notes & PYQs Updated',
|
|
||||||
message: '1st to 8th Semester notes and previous year question papers are published on Moodle.',
|
|
||||||
time: '10m ago',
|
|
||||||
read: false,
|
|
||||||
type: 'academic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Dev Collab Hub Open',
|
|
||||||
message: 'Post open-source project ideas or find co-developers across departments.',
|
|
||||||
time: '1h ago',
|
|
||||||
read: false,
|
|
||||||
type: 'dev',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'LeetCode Campus Rankings Live',
|
|
||||||
message: 'Monthly competitive programming leaderboard has been updated.',
|
|
||||||
time: '3h ago',
|
|
||||||
read: false,
|
|
||||||
type: 'academic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: 'Campus Bus Schedule Confirmed',
|
|
||||||
message: 'All 4 campus bus routes operating on 7:00 AM morning schedule.',
|
|
||||||
time: '1d ago',
|
|
||||||
read: true,
|
|
||||||
type: 'bus',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const unreadCount = notifications.filter((n) => !n.read).length;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const now = new Date();
|
|
||||||
const formatted = now.toLocaleDateString('en-US', {
|
|
||||||
weekday: 'short',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
setCurrentDate(formatted);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Listen for ⌘K shortcut
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsSearchOpen((prev) => !prev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Close dropdowns on outside click
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
if (notifRef.current && !notifRef.current.contains(e.target as Node)) {
|
|
||||||
setIsNotifOpen(false);
|
|
||||||
}
|
|
||||||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
|
||||||
setIsSearchOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
|
||||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearNotifications = () => {
|
|
||||||
setNotifications([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Search indexing & results calculation
|
|
||||||
const searchResults = (() => {
|
|
||||||
if (!searchQuery.trim()) return [];
|
|
||||||
const q = searchQuery.toLowerCase();
|
|
||||||
const results: { title: string; desc: string; path: string; icon: any; category: string }[] = [];
|
|
||||||
|
|
||||||
// Check modules
|
|
||||||
if ('notes pyqs moodle syllabus question paper'.includes(q)) {
|
|
||||||
results.push({ title: 'Academic Notes & PYQs', desc: 'Semester course notes & past papers', path: '/notes', icon: BookOpen, category: 'Academics' });
|
|
||||||
}
|
|
||||||
if ('bus route timing transport travel pickup'.includes(q)) {
|
|
||||||
results.push({ title: 'Campus Bus Routes', desc: 'Schedules and stops for all campus buses', path: '/bus-routes', icon: Bus, category: 'Utilities' });
|
|
||||||
}
|
|
||||||
if ('chatbot assistant question help contact rules'.includes(q)) {
|
|
||||||
results.push({ title: 'Campus Chatbot', desc: 'Instant Q&A about campus life & rules', path: '/ai-assistant', icon: Bot, category: 'Academics' });
|
|
||||||
}
|
|
||||||
if ('collab dev project developer team coding github'.includes(q)) {
|
|
||||||
results.push({ title: 'Dev Collab Hub', desc: 'Find co-developers & build projects', path: '/collab', icon: Code2, category: 'Dev' });
|
|
||||||
}
|
|
||||||
if ('leetcode ranking leaderboard coding rank'.includes(q)) {
|
|
||||||
results.push({ title: 'LeetCode Leaderboard', desc: 'Campus competitive coding rankings', path: '/leetcode', icon: Trophy, category: 'Dev' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search Bus Routes
|
|
||||||
BUS_ROUTES.forEach((r) => {
|
|
||||||
if (r.name.toLowerCase().includes(q) || r.number.toLowerCase().includes(q) || r.from.toLowerCase().includes(q)) {
|
|
||||||
results.push({ title: `${r.number}: ${r.name}`, desc: `Departure: ${r.departureTime} from ${r.from}`, path: '/bus-routes', icon: Bus, category: 'Bus Route' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search Faculty
|
|
||||||
FACULTY_DATA.forEach((f) => {
|
|
||||||
if (f.name.toLowerCase().includes(q) || f.department.toLowerCase().includes(q) || (f.specialization && f.specialization.toLowerCase().includes(q))) {
|
|
||||||
results.push({ title: f.name, desc: `${f.designation} — ${f.department}`, path: '/faculty', icon: Users, category: 'Faculty' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search Clubs
|
|
||||||
CLUBS_DATA.forEach((c) => {
|
|
||||||
if (c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q)) {
|
|
||||||
results.push({ title: c.name, desc: c.description, path: '/events', icon: ClubIcon, category: 'Club' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return results.slice(0, 6);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const handleSelectResult = (path: string) => {
|
|
||||||
setIsSearchOpen(false);
|
|
||||||
setSearchQuery('');
|
|
||||||
navigate(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 z-30 w-full bg-white/90 backdrop-blur-md border-b border-slate-200/80 px-4 lg:px-8 py-3 flex items-center justify-between shadow-xs">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Mobile Toggle */}
|
|
||||||
<button
|
|
||||||
onClick={onToggleSidebar}
|
|
||||||
className="lg:hidden p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors cursor-pointer"
|
|
||||||
title="Toggle Navigation Menu"
|
|
||||||
>
|
|
||||||
<Menu className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Global Interactive Search Input Container */}
|
|
||||||
<div className="relative" ref={searchRef}>
|
|
||||||
<div
|
|
||||||
onClick={() => setIsSearchOpen(true)}
|
|
||||||
className="relative w-64 sm:w-80 md:w-96 cursor-text"
|
|
||||||
>
|
|
||||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearchQuery(e.target.value);
|
|
||||||
setIsSearchOpen(true);
|
|
||||||
}}
|
|
||||||
onFocus={() => setIsSearchOpen(true)}
|
|
||||||
placeholder="Search notes, faculty, bus routes, clubs..."
|
|
||||||
className="w-full pl-10 pr-12 py-2 text-xs font-medium bg-slate-100/90 hover:bg-slate-100 focus:bg-white border border-transparent focus:border-indigo-500/40 rounded-xl text-slate-800 placeholder-slate-400 outline-none transition-all shadow-inner"
|
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
|
||||||
/>
|
|
||||||
<span className="hidden sm:inline-flex absolute right-3 top-1/2 -translate-y-1/2 text-[10px] font-semibold text-slate-400 bg-white border border-slate-200 px-1.5 py-0.5 rounded-md shadow-2xs">
|
|
||||||
⌘K
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search Live Results Dropdown Overlay */}
|
|
||||||
{isSearchOpen && (
|
|
||||||
<div className="absolute top-full left-0 mt-2 w-full sm:w-[420px] bg-white rounded-2xl border border-slate-200 shadow-xl overflow-hidden z-50 p-2 text-left">
|
|
||||||
<div className="flex items-center justify-between px-3 py-2 border-b border-slate-100">
|
|
||||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">
|
|
||||||
{searchQuery.trim() ? 'Search Results' : 'Quick Navigation'}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsSearchOpen(false)}
|
|
||||||
className="p-1 rounded-md text-slate-400 hover:text-slate-600 hover:bg-slate-100"
|
|
||||||
>
|
|
||||||
<X className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-72 overflow-y-auto custom-scrollbar p-1 space-y-1">
|
|
||||||
{searchResults.length > 0 ? (
|
|
||||||
searchResults.map((res, i) => {
|
|
||||||
const Icon = res.icon;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
onClick={() => handleSelectResult(res.path)}
|
|
||||||
className="flex items-center justify-between p-2.5 rounded-xl hover:bg-indigo-50/70 cursor-pointer transition-colors group"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<div className="w-8 h-8 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center shrink-0">
|
|
||||||
<Icon className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="text-xs font-bold text-slate-900 group-hover:text-indigo-700 truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
{res.title}
|
|
||||||
</span>
|
|
||||||
<span className="text-[11px] text-slate-500 truncate">{res.desc}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-[9px] font-bold px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 shrink-0">
|
|
||||||
{res.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="p-4 text-center space-y-2">
|
|
||||||
<span className="text-xs text-slate-500 block">
|
|
||||||
{searchQuery.trim() ? `No matches found for "${searchQuery}"` : 'Type to search notes, faculty, routes & clubs'}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap justify-center gap-1.5 pt-1">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSelectResult('/notes')}
|
|
||||||
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-100 text-slate-700 font-medium hover:bg-indigo-50 hover:text-indigo-600"
|
|
||||||
>
|
|
||||||
Notes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSelectResult('/bus-routes')}
|
|
||||||
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-100 text-slate-700 font-medium hover:bg-indigo-50 hover:text-indigo-600"
|
|
||||||
>
|
|
||||||
Bus Routes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSelectResult('/faculty')}
|
|
||||||
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-100 text-slate-700 font-medium hover:bg-indigo-50 hover:text-indigo-600"
|
|
||||||
>
|
|
||||||
Faculty
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Header Status Bar */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Date Display Badge */}
|
|
||||||
<div className="hidden md:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-100 text-slate-600 text-xs font-medium border border-slate-200/60">
|
|
||||||
<Calendar className="w-3.5 h-3.5 text-indigo-600" />
|
|
||||||
<span>{currentDate}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Functional Notifications Dropdown */}
|
|
||||||
<div className="relative" ref={notifRef}>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsNotifOpen((prev) => !prev)}
|
|
||||||
className="relative p-2 rounded-xl bg-slate-100 hover:bg-indigo-50 text-slate-600 hover:text-indigo-600 transition-colors cursor-pointer border border-slate-200/60"
|
|
||||||
title="Notifications"
|
|
||||||
>
|
|
||||||
<Bell className="w-4 h-4" />
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-indigo-600 ring-2 ring-white" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Notifications Panel Dropdown */}
|
|
||||||
{isNotifOpen && (
|
|
||||||
<div className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white rounded-2xl border border-slate-200 shadow-xl overflow-hidden z-50 p-3 text-left">
|
|
||||||
<div className="flex items-center justify-between pb-2 border-b border-slate-100">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xs font-bold text-slate-900" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
Notifications
|
|
||||||
</span>
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700">
|
|
||||||
{unreadCount} new
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={markAllAsRead}
|
|
||||||
className="text-[11px] font-semibold text-indigo-600 hover:text-indigo-800"
|
|
||||||
>
|
|
||||||
Mark all read
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{notifications.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={clearNotifications}
|
|
||||||
className="text-[11px] font-semibold text-slate-400 hover:text-slate-600"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-72 overflow-y-auto custom-scrollbar pt-2 space-y-2">
|
|
||||||
{notifications.length > 0 ? (
|
|
||||||
notifications.map((n) => (
|
|
||||||
<div
|
|
||||||
key={n.id}
|
|
||||||
className={`p-2.5 rounded-xl border transition-colors ${
|
|
||||||
n.read ? 'bg-slate-50/60 border-slate-100' : 'bg-indigo-50/40 border-indigo-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-xs font-bold text-slate-900 leading-snug" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
{n.title}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-slate-400 font-medium">{n.time}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] text-slate-600 leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
|
||||||
{n.message}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="p-6 text-center text-xs text-slate-400">
|
|
||||||
No new notifications
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Auth Quick Badge */}
|
|
||||||
{isAuthenticated && user ? (
|
|
||||||
<div className="flex items-center gap-2 pl-2 border-l border-slate-200">
|
|
||||||
{user.pictureUrl ? (
|
|
||||||
<img
|
|
||||||
src={user.pictureUrl}
|
|
||||||
alt={user.name}
|
|
||||||
className="w-8 h-8 rounded-full border border-indigo-300 object-cover"
|
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-8 h-8 rounded-full bg-indigo-600 text-white font-bold text-xs flex items-center justify-center">
|
|
||||||
{user.name.charAt(0)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="hidden xl:flex flex-col text-left">
|
|
||||||
<span className="text-xs font-bold text-slate-800 leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
{user.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-indigo-600 font-semibold">
|
|
||||||
{isVerifiedStudent ? 'Verified Student' : 'RIT Account'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={loginWithGoogle}
|
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold shadow-sm transition-all cursor-pointer"
|
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
<LogIn className="w-3.5 h-3.5" />
|
|
||||||
<span>Sign In</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,166 +1,254 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Menu, X, LogIn, LogOut, ShieldCheck, ChevronDown, Rocket, Sparkles } from 'lucide-react';
|
import {
|
||||||
import { NAV_LINKS } from '@/constants';
|
Menu,
|
||||||
|
X,
|
||||||
|
LogIn,
|
||||||
|
LogOut,
|
||||||
|
ShieldCheck,
|
||||||
|
ChevronDown,
|
||||||
|
Map,
|
||||||
|
Bus,
|
||||||
|
UserCheck,
|
||||||
|
MessageCircle,
|
||||||
|
Rocket,
|
||||||
|
BookOpen,
|
||||||
|
Wrench,
|
||||||
|
Bot,
|
||||||
|
Users,
|
||||||
|
Code2,
|
||||||
|
Trophy,
|
||||||
|
Ticket,
|
||||||
|
Award,
|
||||||
|
GraduationCap,
|
||||||
|
LayoutDashboard,
|
||||||
|
Home as HomeIcon,
|
||||||
|
ShieldAlert,
|
||||||
|
} from 'lucide-react';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
icon?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIMARY_LINKS: NavItem[] = [
|
||||||
|
{ label: 'Home', path: '/', icon: HomeIcon },
|
||||||
|
{ label: 'Student dashboard', path: '/dashboard', icon: LayoutDashboard },
|
||||||
|
{ label: 'Notes', path: '/notes', icon: BookOpen },
|
||||||
|
{ label: 'Bus Routes', path: '/bus-routes', icon: Bus },
|
||||||
|
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
|
||||||
|
{ label: 'Community', path: '/community', icon: MessageCircle },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MORE_LINKS: NavItem[] = [
|
||||||
|
{ label: 'Chatbot', path: '/ai-assistant', icon: Bot },
|
||||||
|
{ label: 'Toolkit', path: '/toolkit', icon: Wrench },
|
||||||
|
{ label: 'Campus Map', path: '/campus', icon: Map },
|
||||||
|
{ label: 'Clubs & Centers', path: '/events', icon: Users },
|
||||||
|
{ label: 'Dev Collab', path: '/collab', icon: Code2 },
|
||||||
|
{ label: 'Student Leaderboard', path: '/student-leaderboard', icon: Trophy },
|
||||||
|
{ label: 'Student Gate Pass', path: '/notes?portal=gatepass', icon: Ticket },
|
||||||
|
{ label: 'Student Proficiency', path: '/notes?portal=proficiency', icon: Award },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALL_MOBILE_LINKS: NavItem[] = [
|
||||||
|
...PRIMARY_LINKS,
|
||||||
|
...MORE_LINKS,
|
||||||
|
{ label: 'RAISE Incubator', path: '/raise', icon: Rocket },
|
||||||
|
];
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||||
|
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
|
const { user, isAuthenticated, isVerifiedStudent, isAdmin, isTransportAdmin, openAuthModal, logout } = useAuth();
|
||||||
const profileRef = useRef<HTMLDivElement>(null);
|
const profileRef = useRef<HTMLDivElement>(null);
|
||||||
|
const moreRef = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => setIsScrolled(window.scrollY > 20);
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMobileOpen(false);
|
setIsMobileOpen(false);
|
||||||
|
setIsMoreOpen(false);
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
// Close profile dropdown when clicking outside
|
// Close dropdowns when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (profileRef.current && !profileRef.current.contains(e.target as Node)) {
|
if (profileRef.current && !profileRef.current.contains(e.target as Node)) {
|
||||||
setIsProfileOpen(false);
|
setIsProfileOpen(false);
|
||||||
}
|
}
|
||||||
|
if (moreRef.current && !moreRef.current.contains(e.target as Node)) {
|
||||||
|
setIsMoreOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const checkIsActive = (linkPath: string) => {
|
const checkIsActive = (linkPath: string) => {
|
||||||
const [pathName, searchString] = linkPath.split('?');
|
if (linkPath === '/') return location.pathname === '/';
|
||||||
if (location.pathname !== pathName) return false;
|
if (linkPath === '/notes') return location.pathname === '/notes' && !location.search.includes('portal=');
|
||||||
|
if (linkPath.includes('?')) {
|
||||||
if (pathName === '/notes') {
|
return location.pathname + location.search === linkPath;
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const hasToolkit = params.has('toolkit') || params.get('section') === 'toolkit';
|
|
||||||
|
|
||||||
if (searchString) {
|
|
||||||
return hasToolkit;
|
|
||||||
} else {
|
|
||||||
return !hasToolkit;
|
|
||||||
}
|
}
|
||||||
}
|
return location.pathname.startsWith(linkPath);
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMoreActive = MORE_LINKS.some((link) => checkIsActive(link.path));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Unified Floating Navbar Container */}
|
||||||
|
<header className="fixed top-3.5 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-[1440px] pointer-events-none">
|
||||||
<motion.nav
|
<motion.nav
|
||||||
initial={{ y: -100, opacity: 0 }}
|
initial={{ y: -60, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
transition={{ duration: 0.45, ease: 'easeOut' }}
|
||||||
className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-6xl transition-all duration-300 shadow-[0_8px_32px_rgba(0,0,0,0.25)]"
|
className="relative pointer-events-auto flex items-center justify-between gap-2 lg:gap-4 px-3 sm:px-4 py-2 rounded-2xl bg-slate-950/90 backdrop-blur-xl border border-white/10 shadow-[0_8px_30px_rgba(0,0,0,0.35)] transition-all"
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(to right, rgba(15, 23, 42, 0.45) 0%, rgba(30, 41, 59, 0.35) 50%, rgba(15, 23, 42, 0.45) 100%)',
|
|
||||||
backdropFilter: 'blur(16px)',
|
|
||||||
WebkitBackdropFilter: 'blur(16px)',
|
|
||||||
borderRadius: '20px',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-5 py-2.5">
|
{/* Left: Logo & College Brand */}
|
||||||
{/* Logo - RIT Event Hub Style */}
|
<Link to="/" className="flex items-center gap-2.5 group shrink-0">
|
||||||
<a href="/" className="flex items-center gap-2 group">
|
|
||||||
{/* Logo Image */}
|
|
||||||
<motion.img
|
<motion.img
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.06 }}
|
||||||
src="/logo.png"
|
src="/logo.png"
|
||||||
alt="RIT Logo"
|
alt="RIT Logo"
|
||||||
className="w-10 h-10 object-contain rounded-full shrink-0"
|
className="w-8 h-8 lg:w-9 lg:h-9 object-contain rounded-full shrink-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="font-extrabold text-white text-2xl tracking-tighter" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<span
|
||||||
|
className="font-extrabold text-white text-lg lg:text-xl tracking-tight group-hover:text-[#FF6B00] transition-colors"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
rit
|
rit
|
||||||
</span>
|
</span>
|
||||||
<div className="h-7 w-[1px] bg-white/20 mx-2" />
|
<div className="h-4 lg:h-5 w-[1px] bg-white/20 mx-2" />
|
||||||
<div className="flex flex-col text-[8.5px] font-bold text-slate-200 leading-tight tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<div
|
||||||
<span>RAJALAKSHMI</span>
|
className="hidden xl:flex flex-col text-[7.5px] lg:text-[8px] font-bold text-slate-300 leading-[1.15] tracking-wider uppercase"
|
||||||
<span>INSTITUTE OF</span>
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
<span>TECHNOLOGY</span>
|
>
|
||||||
|
<span>Rajalakshmi</span>
|
||||||
|
<span>Institute of</span>
|
||||||
|
<span>Technology</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Nav */}
|
{/* Center: Desktop Navigation Links */}
|
||||||
<div className="hidden md:flex items-center gap-1">
|
<div className="hidden lg:flex items-center gap-1 xl:gap-1.5 min-w-0">
|
||||||
{NAV_LINKS.map((link) => {
|
{PRIMARY_LINKS.map((link) => {
|
||||||
const isActive = checkIsActive(link.path);
|
const isActive = checkIsActive(link.path);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
key={link.path}
|
||||||
to={link.path}
|
to={link.path}
|
||||||
className="relative px-1.5 lg:px-2.5 xl:px-3.5 py-1.5 rounded-lg text-[10px] lg:text-[11px] xl:text-xs font-semibold uppercase tracking-wider transition-all duration-200"
|
className={`relative px-2.5 xl:px-3 py-1.5 rounded-xl text-xs font-semibold tracking-wide transition-all duration-200 whitespace-nowrap ${
|
||||||
style={{
|
isActive ? 'text-white' : 'text-slate-300 hover:text-white hover:bg-white/10'
|
||||||
fontFamily: 'Poppins, sans-serif',
|
}`}
|
||||||
color: isActive ? '#FFFFFF' : '#CBD5E1',
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="nav-active"
|
layoutId="nav-pill-active"
|
||||||
className="absolute inset-0 rounded-xl"
|
className="absolute inset-0 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#F97316] shadow-sm"
|
||||||
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
|
transition={{ type: 'spring', bounce: 0.15, duration: 0.35 }}
|
||||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.4 }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="relative z-10">{link.label}</span>
|
<span className="relative z-10">{link.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* "More" Dropdown Menu */}
|
||||||
|
<div className="relative" ref={moreRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMoreOpen(!isMoreOpen)}
|
||||||
|
className={`relative flex items-center gap-1 px-2.5 xl:px-3 py-1.5 rounded-xl text-xs font-semibold tracking-wide transition-all cursor-pointer ${
|
||||||
|
isMoreActive ? 'text-white' : 'text-slate-300 hover:text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
{isMoreActive && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="nav-pill-active"
|
||||||
|
className="absolute inset-0 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#F97316] shadow-sm"
|
||||||
|
transition={{ type: 'spring', bounce: 0.15, duration: 0.35 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="relative z-10">More</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-3.5 h-3.5 relative z-10 transition-transform duration-200 ${
|
||||||
|
isMoreOpen ? 'rotate-180 text-white' : 'text-slate-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isMoreOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 8, scale: 0.96 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 8, scale: 0.96 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute right-0 top-full mt-2 w-52 bg-slate-900/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl p-1.5 z-50 overflow-hidden"
|
||||||
|
>
|
||||||
|
{MORE_LINKS.map((item) => {
|
||||||
|
const IconComponent = item.icon;
|
||||||
|
const active = checkIsActive(item.path);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
onClick={() => setIsMoreOpen(false)}
|
||||||
|
className={`flex items-center gap-2.5 px-3 py-2 rounded-xl text-xs font-medium transition-all ${
|
||||||
|
active
|
||||||
|
? 'bg-white/15 text-orange-400 font-semibold'
|
||||||
|
: 'text-slate-300 hover:text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
{IconComponent && <IconComponent className={`w-4 h-4 shrink-0 ${active ? 'text-orange-400' : 'text-slate-300'}`} />}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side: Auth + Mobile Toggle */}
|
{/* Right: Auth Profile / Unified Sign In */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="hidden lg:flex items-center gap-2 shrink-0">
|
||||||
{/* Pitch your Idea - RAISE Button */}
|
|
||||||
<Link
|
|
||||||
to="/raise"
|
|
||||||
className="hidden lg:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-rose-500 via-pink-500 to-rose-600 hover:from-rose-600 hover:to-pink-600 text-white text-[11px] font-bold shadow-md shadow-rose-500/25 border border-white/20 transition-all duration-200 hover:scale-[1.03] active:scale-95 whitespace-nowrap"
|
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
<Sparkles className="w-3.5 h-3.5 text-amber-200" />
|
|
||||||
<span>Pitch your Idea - RAISE</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Auth Section */}
|
|
||||||
{isAuthenticated && user ? (
|
{isAuthenticated && user ? (
|
||||||
<div className="relative" ref={profileRef}>
|
<div className="relative" ref={profileRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
||||||
className="flex items-center gap-2 px-2 py-1.5 rounded-xl hover:bg-white/10 transition-all cursor-pointer"
|
className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-slate-800/80 hover:bg-slate-800 border border-white/10 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
{user.pictureUrl ? (
|
{user.pictureUrl ? (
|
||||||
<img
|
<img
|
||||||
src={user.pictureUrl}
|
src={user.pictureUrl}
|
||||||
alt={user.name}
|
alt={user.name}
|
||||||
className="w-8 h-8 rounded-full border-2 border-white/30 object-cover"
|
className="w-6 h-6 rounded-full border border-white/30 object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-white font-bold text-sm">
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-white font-bold text-xs ${
|
||||||
{user.name.charAt(0).toUpperCase()}
|
isAdmin ? 'bg-indigo-600' : 'bg-gradient-to-br from-[#FF6B00] to-[#F97316]'
|
||||||
|
}`}>
|
||||||
|
{isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="hidden lg:flex flex-col items-start">
|
<span
|
||||||
<span className="text-white text-[11px] font-semibold leading-tight max-w-[100px] truncate">
|
className="text-white text-xs font-semibold max-w-[120px] truncate"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
{user.name.split(' ')[0]}
|
{user.name.split(' ')[0]}
|
||||||
</span>
|
</span>
|
||||||
{isVerifiedStudent && (
|
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
||||||
<span className="flex items-center gap-0.5 text-[9px] text-emerald-400 font-bold">
|
|
||||||
<ShieldCheck className="w-2.5 h-2.5" /> Verified
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ChevronDown className="w-3 h-3 text-slate-400 hidden lg:block" />
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Profile Dropdown */}
|
{/* Profile Dropdown */}
|
||||||
@@ -171,9 +259,9 @@ export default function Navbar() {
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 8, scale: 0.95 }}
|
exit={{ opacity: 0, y: 8, scale: 0.95 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className="absolute right-0 top-full mt-2 w-64 bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden z-50"
|
className="absolute right-0 top-full mt-2 w-64 bg-slate-900 border border-white/10 rounded-2xl shadow-2xl overflow-hidden z-50"
|
||||||
>
|
>
|
||||||
<div className="p-4 border-b border-slate-700/60">
|
<div className="p-4 border-b border-white/10">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{user.pictureUrl ? (
|
{user.pictureUrl ? (
|
||||||
<img
|
<img
|
||||||
@@ -183,8 +271,10 @@ export default function Navbar() {
|
|||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-white font-bold text-lg">
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-lg ${
|
||||||
{user.name.charAt(0).toUpperCase()}
|
isAdmin ? 'bg-indigo-600' : 'bg-gradient-to-br from-[#FF6B00] to-[#F97316]'
|
||||||
|
}`}>
|
||||||
|
{isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -192,28 +282,53 @@ export default function Navbar() {
|
|||||||
<p className="text-slate-400 text-[11px] truncate">{user.email}</p>
|
<p className="text-slate-400 text-[11px] truncate">{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isVerifiedStudent ? (
|
|
||||||
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-500/15 border border-emerald-500/25">
|
{/* Role Badges */}
|
||||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
{isAdmin ? (
|
||||||
<span className="text-[11px] font-bold text-emerald-400">Verified RIT Student</span>
|
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-indigo-500/15 border border-indigo-500/25">
|
||||||
</div>
|
<ShieldAlert className="w-3.5 h-3.5 text-indigo-400" />
|
||||||
) : (
|
<span className="text-[11px] font-bold text-indigo-400">
|
||||||
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-amber-500/15 border border-amber-500/25">
|
{isTransportAdmin ? 'Transport Fleet Manager' : 'System Super Admin'}
|
||||||
<span className="text-[11px] font-semibold text-amber-400">
|
|
||||||
Sign in with @ritchennai.edu.in for verified badge
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : isVerifiedStudent ? (
|
||||||
|
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-500/15 border border-emerald-500/25">
|
||||||
|
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||||
|
<span className="text-[11px] font-bold text-emerald-400">
|
||||||
|
Verified Student {user.regNumber ? `(${user.regNumber})` : ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2">
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
{isAdmin ? (
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
onClick={() => setIsProfileOpen(false)}
|
||||||
|
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-indigo-500/15 text-xs font-semibold transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<ShieldCheck className="w-4 h-4 text-indigo-400" />
|
||||||
|
Admin Console
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
onClick={() => setIsProfileOpen(false)}
|
||||||
|
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 hover:border-orange-500/20 text-xs font-semibold transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<GraduationCap className="w-4 h-4 text-orange-400" />
|
||||||
|
Student Dashboard
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout();
|
logout();
|
||||||
setIsProfileOpen(false);
|
setIsProfileOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-slate-300 hover:text-white hover:bg-red-500/15 text-xs font-semibold transition-all cursor-pointer"
|
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-red-500/15 text-xs font-semibold transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4" />
|
<LogOut className="w-4 h-4 text-red-400" />
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,28 +340,29 @@ export default function Navbar() {
|
|||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.03 }}
|
whileHover={{ scale: 1.03 }}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={loginWithGoogle}
|
onClick={openAuthModal}
|
||||||
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl text-white text-[11px] font-semibold border border-white/15 hover:border-orange-500/40 hover:bg-white/5 transition-all cursor-pointer"
|
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-white text-xs font-bold bg-gradient-to-r from-[#FF6B00] to-[#F97316] shadow-sm hover:opacity-95 transition-all cursor-pointer"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
<LogIn className="w-3.5 h-3.5" />
|
<LogIn className="w-3.5 h-3.5 text-white" />
|
||||||
Sign In
|
<span>Sign In</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile Toggle */}
|
{/* Mobile Hamburger Toggle */}
|
||||||
|
<div className="lg:hidden flex items-center">
|
||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={() => setIsMobileOpen(!isMobileOpen)}
|
onClick={() => setIsMobileOpen(!isMobileOpen)}
|
||||||
className="md:hidden w-11 h-11 rounded-xl flex items-center justify-center text-slate-200 hover:text-white bg-slate-800/60 hover:bg-slate-700/60 border border-slate-700/50 transition-all active:scale-95"
|
className="w-9 h-9 rounded-xl flex items-center justify-center text-slate-200 hover:text-white bg-slate-800/80 border border-white/10 transition-all active:scale-95"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
{isMobileOpen ? <X className="w-5 h-5 stroke-[2.5]" /> : <Menu className="w-5 h-5 stroke-[2.5]" />}
|
{isMobileOpen ? <X className="w-5 h-5 stroke-[2.5]" /> : <Menu className="w-5 h-5 stroke-[2.5]" />}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
{/* Mobile Menu Dropdown */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isMobileOpen && (
|
{isMobileOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -254,73 +370,117 @@ export default function Navbar() {
|
|||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
transition={{ duration: 0.25 }}
|
transition={{ duration: 0.25 }}
|
||||||
className="overflow-hidden md:hidden max-h-[80vh] overflow-y-auto"
|
className="absolute left-0 top-full mt-2 w-full overflow-hidden lg:hidden max-h-[82vh] overflow-y-auto z-50 rounded-2xl border border-white/10 bg-slate-950/95 backdrop-blur-2xl shadow-2xl"
|
||||||
>
|
>
|
||||||
<div className="px-4 pb-5 pt-3 grid grid-cols-2 gap-2 border-t border-white/10 bg-slate-950/90 backdrop-blur-2xl rounded-b-2xl">
|
<div className="p-4 space-y-3">
|
||||||
{NAV_LINKS.map((link) => {
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{ALL_MOBILE_LINKS.map((link) => {
|
||||||
const isActive = checkIsActive(link.path);
|
const isActive = checkIsActive(link.path);
|
||||||
|
const isRaise = link.path === '/raise';
|
||||||
|
const IconComp = link.icon;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
key={link.path}
|
||||||
to={link.path}
|
to={link.path}
|
||||||
onClick={() => setIsMobileOpen(false)}
|
onClick={() => setIsMobileOpen(false)}
|
||||||
className="px-3.5 py-3 rounded-xl text-xs font-semibold uppercase tracking-wider flex items-center justify-center text-center transition-all min-h-[44px]"
|
className={`px-3 py-2.5 rounded-xl text-xs font-semibold flex items-center gap-2 transition-all ${
|
||||||
style={{
|
isRaise
|
||||||
fontFamily: 'Poppins, sans-serif',
|
? 'bg-gradient-to-r from-[#EC4899] via-[#F43F5E] to-[#E11D48] text-white font-bold shadow-md shadow-pink-500/30 col-span-2 justify-center'
|
||||||
color: isActive ? '#FFFFFF' : '#CBD5E1',
|
: isActive
|
||||||
backgroundColor: isActive ? '#F97316' : 'rgba(255, 255, 255, 0.05)',
|
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-sm'
|
||||||
border: isActive ? '1px solid #FB923C' : '1px solid rgba(255, 255, 255, 0.08)',
|
: 'bg-white/5 text-slate-200 hover:bg-white/10 border border-white/5'
|
||||||
}}
|
}`}
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{link.label}
|
{IconComp && <IconComp className="w-3.5 h-3.5 opacity-80" />}
|
||||||
|
<span className="truncate">{link.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
{/* Mobile RAISE Button */}
|
|
||||||
<Link
|
|
||||||
to="/raise"
|
|
||||||
onClick={() => setIsMobileOpen(false)}
|
|
||||||
className="col-span-2 mt-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-white text-xs font-bold bg-gradient-to-r from-rose-500 to-pink-600 border border-rose-400/40 shadow-md shadow-rose-500/20 transition-all active:scale-95"
|
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
<Sparkles className="w-4 h-4 text-amber-200" />
|
|
||||||
Pitch your Idea - RAISE
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Mobile Auth Button */}
|
{/* Mobile Auth Button */}
|
||||||
|
<div className="pt-2 border-t border-white/10">
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loginWithGoogle();
|
openAuthModal();
|
||||||
setIsMobileOpen(false);
|
setIsMobileOpen(false);
|
||||||
}}
|
}}
|
||||||
className="col-span-2 mt-2 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-white text-xs font-semibold border border-orange-500/40 bg-orange-500/10 hover:bg-orange-500/20 transition-all cursor-pointer"
|
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-white text-xs font-semibold border border-orange-500/40 bg-gradient-to-r from-[#FF6B00] to-[#F97316] transition-all cursor-pointer"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4 text-white" />
|
||||||
Sign In with Google
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between p-2 rounded-xl bg-white/5 text-xs text-slate-300">
|
||||||
|
<span className="font-bold text-white">{user?.name}</span>
|
||||||
|
<span className="text-[10px] text-orange-400 font-mono">
|
||||||
|
{isAdmin ? 'Admin' : (user?.regNumber || 'Student')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
onClick={() => setIsMobileOpen(false)}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-indigo-600 text-white text-xs font-bold"
|
||||||
|
>
|
||||||
|
Go to Admin Console
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
onClick={() => setIsMobileOpen(false)}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-orange-500 text-white text-xs font-bold"
|
||||||
|
>
|
||||||
|
Go to Student Dashboard
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout();
|
logout();
|
||||||
setIsMobileOpen(false);
|
setIsMobileOpen(false);
|
||||||
}}
|
}}
|
||||||
className="col-span-2 mt-2 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-red-400 text-xs font-semibold border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition-all cursor-pointer"
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-red-400 text-xs font-semibold border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition-all cursor-pointer"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4" />
|
<LogOut className="w-4 h-4" />
|
||||||
Sign Out ({user?.name.split(' ')[0]})
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.nav>
|
|
||||||
|
|
||||||
{/* Spacer */}
|
{/* Floating RAISE Incubator Button directly below right side of navbar */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: -10, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.35, ease: 'easeOut', delay: 0.1 }}
|
||||||
|
className="absolute right-0 sm:right-1 lg:right-1 top-[calc(100%+8px)] pointer-events-auto hidden lg:block z-40"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/raise"
|
||||||
|
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-extrabold text-white transition-all shadow-lg hover:scale-105 active:scale-95 cursor-pointer backdrop-blur-xl ${
|
||||||
|
checkIsActive('/raise')
|
||||||
|
? 'bg-gradient-to-r from-pink-600 via-rose-600 to-pink-700 ring-2 ring-pink-300 shadow-pink-500/50'
|
||||||
|
: 'bg-gradient-to-r from-[#EC4899] via-[#F43F5E] to-[#E11D48] hover:from-[#DB2777] hover:to-[#BE123C] shadow-pink-500/40 border border-pink-400/40'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
<Rocket className="w-4 h-4 text-white animate-pulse" />
|
||||||
|
<span>RAISE Incubator</span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</motion.nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Spacer to prevent content overlap */}
|
||||||
<div className="h-20" />
|
<div className="h-20" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
24
src/components/PageLoader/PageLoader.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const PageLoader: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[60vh] flex flex-col items-center justify-center p-6 space-y-4">
|
||||||
|
<div className="relative flex items-center justify-center">
|
||||||
|
{/* Outer glowing ripple */}
|
||||||
|
<div className="absolute w-16 h-16 rounded-full bg-orange-500/20 animate-ping" />
|
||||||
|
{/* Spinning gradient ring */}
|
||||||
|
<div className="w-12 h-12 rounded-full border-3 border-transparent border-t-orange-500 border-r-orange-400 animate-spin" />
|
||||||
|
{/* Center dot */}
|
||||||
|
<div className="absolute w-4 h-4 rounded-full bg-gradient-to-tr from-orange-600 to-amber-400 shadow-lg shadow-orange-500/50" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center space-y-1">
|
||||||
|
<p className="text-sm font-semibold tracking-wide bg-gradient-to-r from-orange-600 via-amber-500 to-orange-400 bg-clip-text text-transparent animate-pulse">
|
||||||
|
Loading Freshers Hub
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Preparing your campus experience...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageLoader;
|
||||||
@@ -28,12 +28,12 @@ export default function SectionTitle({
|
|||||||
className={`inline-flex items-center gap-2 mb-3 ${isCenter ? 'mx-auto' : ''}`}
|
className={`inline-flex items-center gap-2 mb-3 ${isCenter ? 'mx-auto' : ''}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="px-3 py-1 rounded-full text-xs font-semibold tracking-wider uppercase"
|
className="px-3 py-1 rounded-full text-xs font-bold tracking-wider uppercase"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Plus Jakarta Sans, sans-serif',
|
||||||
backgroundColor: '#FFF7ED',
|
backgroundColor: '#FFF7ED',
|
||||||
color: '#F97316',
|
color: '#FF6B00',
|
||||||
border: '1px solid #FED7AA',
|
border: '1px solid #FFEDD5',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
@@ -46,15 +46,15 @@ export default function SectionTitle({
|
|||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
className="text-3xl md:text-4xl font-bold"
|
className="text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight"
|
||||||
style={{ fontFamily: 'Playfair Display, serif', color: '#1E293B' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif', color: '#1A0B2E' }}
|
||||||
>
|
>
|
||||||
{title}{' '}
|
{title}{' '}
|
||||||
{highlight && (
|
{highlight && (
|
||||||
<span
|
<span
|
||||||
className="relative inline-block"
|
className="relative inline-block"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
background: 'linear-gradient(135deg, #FF6B00, #F97316)',
|
||||||
WebkitBackgroundClip: 'text',
|
WebkitBackgroundClip: 'text',
|
||||||
WebkitTextFillColor: 'transparent',
|
WebkitTextFillColor: 'transparent',
|
||||||
backgroundClip: 'text',
|
backgroundClip: 'text',
|
||||||
@@ -71,7 +71,7 @@ export default function SectionTitle({
|
|||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
className={`mt-3 text-base text-[#475569] max-w-xl ${isCenter ? 'mx-auto' : ''}`}
|
className={`mt-3 text-base text-[#4A3E5E] max-w-xl ${isCenter ? 'mx-auto' : ''}`}
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
>
|
>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import {
|
|
||||||
LayoutDashboard,
|
|
||||||
BookOpen,
|
|
||||||
Wrench,
|
|
||||||
Bot,
|
|
||||||
MapPin,
|
|
||||||
Bus,
|
|
||||||
Users,
|
|
||||||
Calendar,
|
|
||||||
MessageSquare,
|
|
||||||
Code2,
|
|
||||||
Trophy,
|
|
||||||
Rocket,
|
|
||||||
LogOut,
|
|
||||||
LogIn,
|
|
||||||
ShieldCheck,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
GitBranch,
|
|
||||||
X
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
|
|
||||||
interface SidebarProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
isCollapsed: boolean;
|
|
||||||
onToggleCollapse: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NavItem {
|
|
||||||
label: string;
|
|
||||||
path: string;
|
|
||||||
icon: any;
|
|
||||||
highlight?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NavGroup {
|
|
||||||
title: string;
|
|
||||||
items: NavItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Sidebar({
|
|
||||||
isOpen,
|
|
||||||
onToggle,
|
|
||||||
isCollapsed,
|
|
||||||
onToggleCollapse,
|
|
||||||
}: SidebarProps) {
|
|
||||||
const location = useLocation();
|
|
||||||
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
|
|
||||||
|
|
||||||
const checkIsActive = (linkPath: string) => {
|
|
||||||
const [pathName, searchString] = linkPath.split('?');
|
|
||||||
if (location.pathname !== pathName) return false;
|
|
||||||
|
|
||||||
if (pathName === '/notes') {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const hasToolkit = params.has('toolkit') || params.get('section') === 'toolkit';
|
|
||||||
if (searchString) {
|
|
||||||
return hasToolkit;
|
|
||||||
} else {
|
|
||||||
return !hasToolkit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const navGroups: NavGroup[] = [
|
|
||||||
{
|
|
||||||
title: 'ACADEMICS & PORTAL',
|
|
||||||
items: [
|
|
||||||
{ label: 'Dashboard', path: '/', icon: LayoutDashboard },
|
|
||||||
{ label: 'Notes & PYQs', path: '/notes', icon: BookOpen },
|
|
||||||
{ label: 'Student Toolkit', path: '/notes?section=toolkit', icon: Wrench },
|
|
||||||
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'CAMPUS & UTILITIES',
|
|
||||||
items: [
|
|
||||||
{ label: 'Bus Tracker', path: '/bus-routes', icon: Bus },
|
|
||||||
{ label: 'Campus Map', path: '/campus', icon: MapPin },
|
|
||||||
{ label: 'Faculty Directory', path: '/faculty', icon: Users },
|
|
||||||
{ label: 'Student Clubs', path: '/events', icon: Calendar },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'COMMUNITY & DEV',
|
|
||||||
items: [
|
|
||||||
{ label: 'Freshers Q&A', path: '/community', icon: MessageSquare },
|
|
||||||
{ label: 'Dev Collab Hub', path: '/collab', icon: Code2, highlight: true },
|
|
||||||
{ label: 'LeetCode Board', path: '/leetcode', icon: Trophy },
|
|
||||||
{ label: 'RAISE Incubator', path: '/raise', icon: Rocket },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Mobile Backdrop Overlay */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
onClick={onToggle}
|
|
||||||
className="fixed inset-0 bg-black/60 backdrop-blur-xs z-40 lg:hidden"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Main Sidebar Container - Sleek Professional Deep Indigo */}
|
|
||||||
<aside
|
|
||||||
className={`fixed top-0 left-0 z-50 h-screen bg-[#0F172A] text-white transition-all duration-300 ease-in-out lg:translate-x-0 ${
|
|
||||||
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
||||||
} ${
|
|
||||||
isCollapsed ? 'w-20' : 'w-64 xl:w-72'
|
|
||||||
} flex flex-col justify-between border-r border-slate-800 shadow-[8px_0_30px_rgba(0,0,0,0.3)]`}
|
|
||||||
>
|
|
||||||
{/* Top Branding & Collapse Button */}
|
|
||||||
<div className="p-4 sm:p-5 flex flex-col gap-5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Link to="/" className="flex items-center gap-3 group min-w-0">
|
|
||||||
<div className="p-2 rounded-2xl bg-indigo-950/80 border border-indigo-500/20 shadow-sm shrink-0">
|
|
||||||
<img
|
|
||||||
src="/logo.png"
|
|
||||||
alt="RIT Logo"
|
|
||||||
className="w-7 h-7 object-contain rounded-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="font-bold text-white text-lg tracking-tight truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
RIT Portal
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-indigo-300/70 font-medium tracking-wider uppercase truncate" style={{ fontFamily: 'Inter, sans-serif' }}>
|
|
||||||
Freshers Hub
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Mobile Close Button */}
|
|
||||||
<button
|
|
||||||
onClick={onToggle}
|
|
||||||
className="lg:hidden p-1.5 rounded-xl bg-slate-800 text-slate-200 hover:bg-slate-700"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Desktop Collapse Toggle Button */}
|
|
||||||
<button
|
|
||||||
onClick={onToggleCollapse}
|
|
||||||
className="hidden lg:flex p-1.5 rounded-xl bg-slate-800/80 hover:bg-indigo-900/60 text-indigo-300 hover:text-white border border-slate-700/60 transition-colors"
|
|
||||||
title={isCollapsed ? 'Expand Sidebar' : 'Collapse Sidebar'}
|
|
||||||
>
|
|
||||||
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Dev Collab Launch CTA inside Sidebar (Expanded only) */}
|
|
||||||
{!isCollapsed && (
|
|
||||||
<Link
|
|
||||||
to="/collab"
|
|
||||||
className="w-full flex items-center justify-between p-3 rounded-2xl bg-indigo-600 hover:bg-indigo-500 border border-indigo-400/30 text-white group transition-all duration-200 shadow-md shadow-indigo-950/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
|
||||||
<div className="w-7 h-7 rounded-xl bg-white/20 border border-white/30 flex items-center justify-center text-white shrink-0">
|
|
||||||
<GitBranch className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col text-left min-w-0">
|
|
||||||
<span className="text-xs font-bold leading-tight truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
Dev Collab Hub
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-indigo-100/80 truncate">Co-Developer Projects</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ChevronRight className="w-4 h-4 text-indigo-200 group-hover:translate-x-0.5 transition-transform" />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Navigation Groups */}
|
|
||||||
<nav className="flex flex-col gap-5 overflow-y-auto max-h-[calc(100vh-320px)] custom-scrollbar pr-0.5">
|
|
||||||
{navGroups.map((group) => (
|
|
||||||
<div key={group.title} className="flex flex-col gap-1">
|
|
||||||
{!isCollapsed && (
|
|
||||||
<span
|
|
||||||
className="px-3 text-[10px] font-bold text-slate-400/70 tracking-widest uppercase mb-1"
|
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
|
||||||
>
|
|
||||||
{group.title}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{group.items.map((item) => {
|
|
||||||
const isActive = checkIsActive(item.path);
|
|
||||||
const Icon = item.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
title={isCollapsed ? item.label : undefined}
|
|
||||||
className={`relative flex items-center ${
|
|
||||||
isCollapsed ? 'justify-center px-0 py-3' : 'justify-between px-3 py-2.5'
|
|
||||||
} rounded-xl text-xs font-semibold transition-all duration-200 group ${
|
|
||||||
isActive
|
|
||||||
? 'bg-indigo-600 text-white shadow-sm border border-indigo-400/30'
|
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-800/60'
|
|
||||||
}`}
|
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Icon
|
|
||||||
className={`w-4 h-4 shrink-0 ${
|
|
||||||
isActive ? 'text-white' : 'text-slate-400 group-hover:text-slate-200'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{!isCollapsed && <span>{item.label}</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isCollapsed && item.highlight && !isActive && (
|
|
||||||
<span className="w-2 h-2 rounded-full bg-indigo-400 animate-pulse" />
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer User Profile & Auth Section */}
|
|
||||||
<div className="p-3 border-t border-slate-800 bg-slate-950/60">
|
|
||||||
{isAuthenticated && user ? (
|
|
||||||
<div className={`flex items-center ${isCollapsed ? 'justify-center p-2' : 'justify-between p-2.5'} rounded-2xl bg-white/5 border border-white/10`}>
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
|
||||||
{user.pictureUrl ? (
|
|
||||||
<img
|
|
||||||
src={user.pictureUrl}
|
|
||||||
alt={user.name}
|
|
||||||
className="w-8 h-8 rounded-xl border border-indigo-400/40 object-cover shrink-0"
|
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-8 h-8 rounded-xl bg-indigo-600 text-white font-bold text-xs flex items-center justify-center shrink-0">
|
|
||||||
{user.name.charAt(0).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="flex flex-col min-w-0 text-left">
|
|
||||||
<span className="text-xs font-bold text-white truncate" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
|
||||||
{user.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-slate-400 truncate">
|
|
||||||
{isVerifiedStudent ? 'Verified Student' : 'RIT Account'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isCollapsed && (
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
title="Sign Out"
|
|
||||||
className="p-1.5 rounded-xl hover:bg-white/10 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<LogOut className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={loginWithGoogle}
|
|
||||||
title={isCollapsed ? 'Student Sign In' : undefined}
|
|
||||||
className={`w-full flex items-center justify-center gap-2 ${
|
|
||||||
isCollapsed ? 'p-2.5' : 'p-2.5'
|
|
||||||
} rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition-all cursor-pointer`}
|
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
|
||||||
>
|
|
||||||
<LogIn className="w-4 h-4 shrink-0" />
|
|
||||||
{!isCollapsed && <span>Student Sign In</span>}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
793
src/constants/departmentCurriculum.ts
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
export interface SubjectCurriculum {
|
||||||
|
name: string;
|
||||||
|
credits: number;
|
||||||
|
code?: string;
|
||||||
|
isElective?: boolean;
|
||||||
|
electiveSlot?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROFESSIONAL_ELECTIVES_LIST: Array<{ code: string; name: string; credits: number }> = [
|
||||||
|
{ code: 'AD23V11', name: 'Agriculture Data Analytics', credits: 3 },
|
||||||
|
{ code: 'AD23V12', name: 'Big Data Analytics', credits: 3 },
|
||||||
|
{ code: 'AD23V13', name: 'Data warehousing and Data Mining', credits: 3 },
|
||||||
|
{ code: 'AD23V14', name: 'Health Care Analytics', credits: 3 },
|
||||||
|
{ code: 'AD23V15', name: 'Image and Video Analytics', credits: 3 },
|
||||||
|
{ code: 'AD23V16', name: 'Web and Social Media Analytics', credits: 3 },
|
||||||
|
{ code: 'AD23V17', name: 'Predictive Analytics', credits: 3 },
|
||||||
|
{ code: 'AD23V18', name: 'Sentiment Analysis', credits: 3 },
|
||||||
|
{ code: 'AD23V21', name: 'AI in IoT', credits: 3 },
|
||||||
|
{ code: 'AD23V22', name: 'Applied Machine Learning', credits: 3 },
|
||||||
|
{ code: 'AD23V23', name: 'Cognitive Computing', credits: 3 },
|
||||||
|
{ code: 'AD23V24', name: 'Computer Vision', credits: 3 },
|
||||||
|
{ code: 'AD23V25', name: 'Ethics For AI', credits: 3 },
|
||||||
|
{ code: 'AD23V26', name: 'Game Theory', credits: 3 },
|
||||||
|
{ code: 'AD23V27', name: 'Quantum Computing Techniques', credits: 3 },
|
||||||
|
{ code: 'AD23V28', name: 'Responsive AI', credits: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEPARTMENT_CODE_MAP: Record<string, string> = {
|
||||||
|
'Artificial Intelligence & Data Science': 'AIDS',
|
||||||
|
'Artificial Intelligence & Machine Learning': 'AIML',
|
||||||
|
'Biotechnology': 'BIOTECH',
|
||||||
|
'Communication and Computer Engineering': 'CCE',
|
||||||
|
'Computer Science & Business Systems': 'CSBS',
|
||||||
|
'Computer Science & Engineering': 'CSE',
|
||||||
|
'Electronics & Communication Engineering': 'ECE',
|
||||||
|
'Electronics Engineering VLSI (Design and Technology)': 'VLSI',
|
||||||
|
'Mechanical Engineering': 'MECH',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEPARTMENT_CURRICULUM: Record<string, Record<number, SubjectCurriculum[]>> = {
|
||||||
|
AIDS: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Physics for Information Science (PH23111)", credits: 3 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Basic Electrical and Electronics Engineering (GE23113)", credits: 3 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Physics Laboratory (PH23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23122)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Engineering Chemistry (CY23211)", credits: 3 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Tamil & Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Engineering Graphics (GE23231)", credits: 4 },
|
||||||
|
{ name: "Data Structures Design (AD23231)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Discrete Mathematics (MA23311)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence (AL23311)", credits: 3 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Database Management Systems (CS23411)", credits: 3 },
|
||||||
|
{ name: "Digital Principles and Computer Organization (EC23331)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence Laboratory (AL23321)", credits: 1 },
|
||||||
|
{ name: "Database Management Systems Laboratory (CS23421)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "Data Wrangling (AD231C1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23411)", credits: 2 },
|
||||||
|
{ name: "Probability and Statistics (MA23411)", credits: 4 },
|
||||||
|
{ name: "Data Analytics (AD23411)", credits: 3 },
|
||||||
|
{ name: "Machine Learning (AL23411)", credits: 3 },
|
||||||
|
{ name: "Operating Systems (CS23412)", credits: 3 },
|
||||||
|
{ name: "Design and Analysis of Algorithms (CS23431)", credits: 4 },
|
||||||
|
{ name: "Data Analytics Laboratory (AD23421)", credits: 1 },
|
||||||
|
{ name: "Machine Learning Laboratory (AL23421)", credits: 1 },
|
||||||
|
{ name: "Operating Systems Laboratory (CS23422)", credits: 1 },
|
||||||
|
{ name: "Introduction to Azure Machine Learning (AD231C1)", credits: 1 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Deep Learning (AD23511)", credits: 3 },
|
||||||
|
{ name: "Computer Networks (CS23511)", credits: 3 },
|
||||||
|
{ name: "Data Exploration and Visualization (AD23532)", credits: 4 },
|
||||||
|
{ name: "Business Analytics (CB23531)", credits: 4 },
|
||||||
|
{ name: "Disaster Risk Reduction and Management", credits: 0 },
|
||||||
|
{ name: "Big Data Analytics (AD23531)", credits: 3 },
|
||||||
|
{ name: "Image & Video Analytics (AD23V15)", credits: 3 },
|
||||||
|
{ name: "Tableau - Data Visualization (AD23IC3)", credits: 1 },
|
||||||
|
{ name: "Deep Learning Laboratory (AD23521)", credits: 1 },
|
||||||
|
{ name: "Computer Networks Laboratory (CS23521)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT (EC23631)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Software Engineering (CS23631)", credits: 4 },
|
||||||
|
{ name: "Mini Project (AD23621)", credits: 2 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Data and Information Security (CB23511)", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Internship / Certification Course (AD23721)", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work (AD23821)", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
AIML: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Engineering Chemistry (CY23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Engineering Graphics (GE23131)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23122)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Physics for Information Science (PH23211)", credits: 3 },
|
||||||
|
{ name: "Basic Electrical and Electronics Engineering (GE23211)", credits: 3 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Data Structures and Design (AD23231)", credits: 4 },
|
||||||
|
{ name: "Physics Laboratory (PH23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Discrete Mathematics (MA23311)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence (AL23311)", credits: 3 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Database Management Systems (CS23411)", credits: 3 },
|
||||||
|
{ name: "Digital Principles and Computer Organization (EC23331)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence Laboratory (AL23321)", credits: 1 },
|
||||||
|
{ name: "Database Management Systems Laboratory (CS23421)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "Data Wrangling (AD231C1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Environmental Sciences and Sustainability (GE23411)", credits: 2 },
|
||||||
|
{ name: "Formal Language and Automata Theory (AD23412)", credits: 4 },
|
||||||
|
{ name: "Data Analytics (AD23411)", credits: 3 },
|
||||||
|
{ name: "Machine Learning (AL23411)", credits: 3 },
|
||||||
|
{ name: "Operating Systems (CS23412)", credits: 3 },
|
||||||
|
{ name: "Design and Analysis of Algorithms (CS23431)", credits: 4 },
|
||||||
|
{ name: "Machine Learning Laboratory (AL23421)", credits: 1 },
|
||||||
|
{ name: "Data Analytics Laboratory (AD23421)", credits: 1 },
|
||||||
|
{ name: "Operating Systems Laboratory (CS23422)", credits: 1 },
|
||||||
|
{ name: "Introduction to Azure Machine Learning (AD231C1)", credits: 1 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Deep Learning for Computer Vision (AL23511)", credits: 3 },
|
||||||
|
{ name: "Computer Networks (CS23511)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Natural Language Processing (AL23531)", credits: 4 },
|
||||||
|
{ name: "Big Data Analytics (AD23531)", credits: 4 },
|
||||||
|
{ name: "Deep Learning for Computer Vision Laboratory (AL23521)", credits: 1 },
|
||||||
|
{ name: "Computer Networks Laboratory (CS23521)", credits: 1 },
|
||||||
|
{ name: "Industry Oriented Course III", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT (EC23631)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Software Engineering (CS23631)", credits: 4 },
|
||||||
|
{ name: "Mini Project (AL23621)", credits: 2 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Cryptography and Cyber Security (CS23513)", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Internship / Certification Course (AL23721)", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work (AL23821)", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
BIOTECH: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Engineering Chemistry (CY23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Engineering Graphics (GE23131)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Physics for Information Science (PH23211)", credits: 3 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Basic Electrical and Electronics Engineering (GE23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Physics Laboratory (PH23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23223)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 1", credits: 2 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Discrete Mathematics (MA23311)", credits: 4 },
|
||||||
|
{ name: "Fundamentals of Economics and Financial Accounting (CB23311)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Data Structures and Algorithms (CS23314)", credits: 3 },
|
||||||
|
{ name: "Digital Principles and Computer Organization (EC23331)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "Data Structures and Algorithms Laboratory (CS23324)", credits: 1 },
|
||||||
|
{ name: "Design Thinking (CB23IC1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23411)", credits: 2 },
|
||||||
|
{ name: "Probability and Statistics (MA23411)", credits: 4 },
|
||||||
|
{ name: "Introduction to Business Systems (CB23411)", credits: 3 },
|
||||||
|
{ name: "Database Management Systems (CS23411)", credits: 3 },
|
||||||
|
{ name: "Operating Systems (CS23412)", credits: 3 },
|
||||||
|
{ name: "Artificial Intelligence and Machine Learning (AL23431)", credits: 4 },
|
||||||
|
{ name: "Database Management Systems Laboratory (CS23421)", credits: 1 },
|
||||||
|
{ name: "Operating Systems Laboratory (CS23422)", credits: 1 },
|
||||||
|
{ name: "Enterprise Resource Planning (CB23IC2)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 3 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Data and Information Security (CB23511)", credits: 3 },
|
||||||
|
{ name: "Fundamentals of Management (CB23512)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Deep Learning Techniques (AD23531)", credits: 4 },
|
||||||
|
{ name: "Business Analytics (CB23531)", credits: 4 },
|
||||||
|
{ name: "Data and Information Security Laboratory (CB23521)", credits: 1 },
|
||||||
|
{ name: "Business Communication Laboratory – I (GE23521)", credits: 1 },
|
||||||
|
{ name: "Visualization Tools using R (CB23IC3)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT (EC23631)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Software Engineering (CS23631)", credits: 4 },
|
||||||
|
{ name: "Mini Project (CB23621)", credits: 2 },
|
||||||
|
{ name: "Business Communication Laboratory – II (GE23621)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Cryptography and Cyber Security (CS23513)", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Cloud Computing (CS23731)", credits: 4 },
|
||||||
|
{ name: "Internship / Certification Course (CB23721)", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work (CB23821)", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
CSBS: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Engineering Chemistry (CY23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Engineering Graphics (GE23131)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Physics for Information Science (PH23211)", credits: 3 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Basic Electrical and Electronics Engineering (GE23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Physics Laboratory (PH23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23223)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 1", credits: 2 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Discrete Mathematics (MA23311)", credits: 4 },
|
||||||
|
{ name: "Fundamentals of Economics and Financial Accounting (CB23311)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Data Structures and Algorithms (CS23314)", credits: 3 },
|
||||||
|
{ name: "Digital Principles and Computer Organization (EC23331)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "Data Structures and Algorithms Laboratory (CS23324)", credits: 1 },
|
||||||
|
{ name: "Design Thinking (CB23IC1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23411)", credits: 2 },
|
||||||
|
{ name: "Probability and Statistics (MA23411)", credits: 4 },
|
||||||
|
{ name: "Introduction to Business Systems (CB23411)", credits: 3 },
|
||||||
|
{ name: "Database Management Systems (CS23411)", credits: 3 },
|
||||||
|
{ name: "Operating Systems (CS23412)", credits: 3 },
|
||||||
|
{ name: "Artificial Intelligence and Machine Learning (AL23431)", credits: 4 },
|
||||||
|
{ name: "Database Management Systems Laboratory (CS23421)", credits: 1 },
|
||||||
|
{ name: "Operating Systems Laboratory (CS23422)", credits: 1 },
|
||||||
|
{ name: "Enterprise Resource Planning (CB23IC2)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 3 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Data and Information Security (CB23511)", credits: 3 },
|
||||||
|
{ name: "Fundamentals of Management (CB23512)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Deep Learning Techniques (AD23531)", credits: 4 },
|
||||||
|
{ name: "Business Analytics (CB23531)", credits: 4 },
|
||||||
|
{ name: "Data and Information Security Laboratory (CB23521)", credits: 1 },
|
||||||
|
{ name: "Business Communication Laboratory – I (GE23521)", credits: 1 },
|
||||||
|
{ name: "Visualization Tools using R (CB23IC3)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT (EC23631)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Software Engineering (CS23631)", credits: 4 },
|
||||||
|
{ name: "Mini Project (CB23621)", credits: 2 },
|
||||||
|
{ name: "Business Communication Laboratory – II (GE23621)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Cryptography and Cyber Security (CS23513)", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Cloud Computing (CS23731)", credits: 4 },
|
||||||
|
{ name: "Internship / Certification Course (CB23721)", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work (CB23821)", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
CCE: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Physics for Electronics Engineering (PH23112)", credits: 3 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Circuit Analysis (EC23111)", credits: 4 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23122)", credits: 1 },
|
||||||
|
{ name: "Physics Laboratory (PH23121)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Engineering Chemistry (CY23211)", credits: 3 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Electronic Devices and Circuits (EC23231)", credits: 4 },
|
||||||
|
{ name: "Engineering Graphics (GE23231)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 1", credits: 1 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23311)", credits: 2 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Wide Band-gap Devices (EV23311)", credits: 3 },
|
||||||
|
{ name: "Digital System Design (EC23312)", credits: 3 },
|
||||||
|
{ name: "Signals and Systems (EC23313)", credits: 4 },
|
||||||
|
{ name: "Digital System Design Laboratory (EC23321)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "PCB Design (EC23IC1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Random Process and Linear Algebra (MA23412)", credits: 4 },
|
||||||
|
{ name: "Microprocessors and Microcontrollers (EV23411)", credits: 3 },
|
||||||
|
{ name: "Electromagnetic Fields (EC23412)", credits: 3 },
|
||||||
|
{ name: "Linear Integrated Circuits (EC23413)", credits: 3 },
|
||||||
|
{ name: "Digital Signal Processing (EC23431)", credits: 4 },
|
||||||
|
{ name: "Networks and Security (EC23432)", credits: 3 },
|
||||||
|
{ name: "Microprocessors and Microcontrollers Laboratory (EV23421)", credits: 1 },
|
||||||
|
{ name: "Linear Integrated Circuits Laboratory (EC23422)", credits: 1 },
|
||||||
|
{ name: "Design Thinking for Engineers (CC23IC2)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 2 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "ASIC Design (EV23511)", credits: 3 },
|
||||||
|
{ name: "VLSI and Chip Design (EC23512)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT Design (EC23531)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence and Machine Learning (AL23431)", credits: 4 },
|
||||||
|
{ name: "VLSI Laboratory (EC23521)", credits: 1 },
|
||||||
|
{ name: "Introduction to ARM Based System Design (EV23IC3)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "CMOS Analog and Mixed Signal IC Design (EV23611)", credits: 3 },
|
||||||
|
{ name: "Digital Logic Synthesis using HDL (EV23612)", credits: 4 },
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Mini Project (EC23621)", credits: 2 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Internship / Certification Course", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
CSE: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Engineering Chemistry (CY23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Engineering Graphics (GE23131)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Physics for Information Science (PH23211)", credits: 3 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Basic Electrical and Electronics Engineering (GE23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Physics Laboratory (PH23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23223)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 1", credits: 2 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23311)", credits: 2 },
|
||||||
|
{ name: "Discrete Mathematics (MA23311)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence (AL23311)", credits: 3 },
|
||||||
|
{ name: "Data Structures (CS23311)", credits: 3 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Digital Principles and Computer Organization (EC23331)", credits: 4 },
|
||||||
|
{ name: "Data Structures Laboratory (CS23321)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "Design Thinking for Software Engineers", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Database Management Systems (CS23411)", credits: 3 },
|
||||||
|
{ name: "Operating Systems (CS23412)", credits: 3 },
|
||||||
|
{ name: "Theory of Computation (CS23413)", credits: 3 },
|
||||||
|
{ name: "Software Development Practices (CS23414)", credits: 3 },
|
||||||
|
{ name: "Machine Learning Techniques (AL23432)", credits: 4 },
|
||||||
|
{ name: "Design and Analysis of Algorithms (CS23431)", credits: 4 },
|
||||||
|
{ name: "Database Management Systems Laboratory (CS23421)", credits: 1 },
|
||||||
|
{ name: "Operating Systems Laboratory (CS23422)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 3 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Computer Networks (CS23511)", credits: 3 },
|
||||||
|
{ name: "Compiler Design (CS23512)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Deep Learning Techniques (AD23531)", credits: 4 },
|
||||||
|
{ name: "Data Exploration and Visualization (AD23532)", credits: 4 },
|
||||||
|
{ name: "Computer Networks Laboratory (CS23521)", credits: 1 },
|
||||||
|
{ name: "Industry Oriented Course", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT (EC23631)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Software Engineering (CS23631)", credits: 4 },
|
||||||
|
{ name: "Mini Project", credits: 2 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Cryptography and Cyber Security (CS23513)", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Natural Language Processing (AL23531)", credits: 4 },
|
||||||
|
{ name: "Internship / Certification Course", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
ECE: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Physics for Electronics Engineering (PH23112)", credits: 3 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Circuit Analysis (EC23111)", credits: 4 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Physics Laboratory (PH23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23122)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Engineering Chemistry (CY23211)", credits: 3 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Electronic Devices and Circuits (EC23231)", credits: 4 },
|
||||||
|
{ name: "Engineering Graphics (GE23231)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 1", credits: 2 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23311)", credits: 2 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Control Systems (EC23311)", credits: 4 },
|
||||||
|
{ name: "Digital System Design (EC23312)", credits: 3 },
|
||||||
|
{ name: "Signals and Systems (EC23313)", credits: 4 },
|
||||||
|
{ name: "Digital System Design Laboratory (EC23321)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "PCB Design (EC23IC1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Random Process and Linear Algebra (MA23412)", credits: 4 },
|
||||||
|
{ name: "Communication Systems (EC23411)", credits: 3 },
|
||||||
|
{ name: "Electromagnetic Fields (EC23412)", credits: 3 },
|
||||||
|
{ name: "Linear Integrated Circuits (EC23413)", credits: 3 },
|
||||||
|
{ name: "Digital Signal Processing (EC23431)", credits: 4 },
|
||||||
|
{ name: "Networks and Security (EC23432)", credits: 3 },
|
||||||
|
{ name: "Communication Systems Laboratory (EC23421)", credits: 1 },
|
||||||
|
{ name: "Linear Integrated Circuits Laboratory (EC23422)", credits: 1 },
|
||||||
|
{ name: "Cyber Security (EC23IC2)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 3 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Transmission Lines & RF Systems (EC23511)", credits: 3 },
|
||||||
|
{ name: "VLSI and Chip Design (EC23512)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT Design (EC23531)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence and Machine Learning (AL23431)", credits: 4 },
|
||||||
|
{ name: "VLSI Laboratory (EV23521)", credits: 1 },
|
||||||
|
{ name: "Introduction to Robotics (EC23IC3)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Antenna Theory (EC23611)", credits: 3 },
|
||||||
|
{ name: "Wireless Communication (EC23612)", credits: 3 },
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Mini Project (EC23621)", credits: 2 },
|
||||||
|
{ name: "Wireless Communication Laboratory (EC23622)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Ad Hoc and Wireless Sensor Networks (EC23711)", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Internship / Certification Course", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
VLSI: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Physics for Electronics Engineering (PH23112)", credits: 3 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Circuit Analysis (EC23111)", credits: 4 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23122)", credits: 1 },
|
||||||
|
{ name: "Physics Laboratory (PH23121)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Engineering Chemistry (CY23211)", credits: 3 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Electronic Devices and Circuits (EC23231)", credits: 4 },
|
||||||
|
{ name: "Engineering Graphics (GE23231)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Environmental Science and Sustainability (GE23311)", credits: 2 },
|
||||||
|
{ name: "Wide Band-gap Devices (EV23311)", credits: 3 },
|
||||||
|
{ name: "Digital System Design (EC23312)", credits: 3 },
|
||||||
|
{ name: "Signals and Systems (EC23313)", credits: 4 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Digital System Design Laboratory (EC23321)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 },
|
||||||
|
{ name: "PCB Design (EC23IC1)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Random Process and Linear Algebra (MA23412)", credits: 4 },
|
||||||
|
{ name: "Microprocessors and Microcontrollers (EV23411)", credits: 3 },
|
||||||
|
{ name: "Electromagnetic Fields (EC23412)", credits: 3 },
|
||||||
|
{ name: "Linear Integrated Circuits (EC23413)", credits: 3 },
|
||||||
|
{ name: "Digital Signal Processing (EC23431)", credits: 4 },
|
||||||
|
{ name: "Networks and Security (EC23432)", credits: 3 },
|
||||||
|
{ name: "Microprocessors and Microcontrollers Laboratory (EV23421)", credits: 1 },
|
||||||
|
{ name: "Linear Integrated Circuits Laboratory (EC23422)", credits: 1 },
|
||||||
|
{ name: "Design Thinking for Engineers (CC23IC2)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 2 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "ASIC Design (EV23511)", credits: 3 },
|
||||||
|
{ name: "VLSI and Chip Design (EC23512)", credits: 3 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Embedded Systems and IoT Design (EC23531)", credits: 4 },
|
||||||
|
{ name: "Artificial Intelligence and Machine Learning (AL23431)", credits: 4 },
|
||||||
|
{ name: "VLSI Laboratory (EC23521)", credits: 1 },
|
||||||
|
{ name: "Introduction to ARM Based System Design (EV23IC3)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "CMOS Analog and Mixed Signal IC Design (EV23611)", credits: 3 },
|
||||||
|
{ name: "Digital Logic Synthesis using HDL (EV23612)", credits: 4 },
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Mini Project", credits: 2 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Internship / Certification Course", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work", credits: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
MECH: {
|
||||||
|
1: [
|
||||||
|
{ name: "Communicative English (HS23111)", credits: 3 },
|
||||||
|
{ name: "Matrices and Calculus (MA23111)", credits: 4 },
|
||||||
|
{ name: "Electrical, Electronics and Instrumentation Engineering (GE23114)", credits: 3 },
|
||||||
|
{ name: "Problem Solving and C Programming (GE23111)", credits: 3 },
|
||||||
|
{ name: "Materials Science (PH23113)", credits: 3 },
|
||||||
|
{ name: "Heritage of Tamils (GE23112)", credits: 0 },
|
||||||
|
{ name: "Physics Laboratory (PH23121)", credits: 1 },
|
||||||
|
{ name: "Problem Solving and C Programming Laboratory (GE23121)", credits: 1 },
|
||||||
|
{ name: "Engineering Practices Laboratory (GE23122)", credits: 1 },
|
||||||
|
{ name: "Basic Electrical and Electronics Engineering Laboratory (GE23123)", credits: 1 }
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{ name: "Professional English (HS23211)", credits: 2 },
|
||||||
|
{ name: "Engineering Chemistry (CY23211)", credits: 3 },
|
||||||
|
{ name: "Statistics and Numerical Methods (MA23211)", credits: 4 },
|
||||||
|
{ name: "Python for Data Science (AD23211)", credits: 3 },
|
||||||
|
{ name: "Tamils and Technology (GE23213)", credits: 0 },
|
||||||
|
{ name: "Engineering Graphics (GE23231)", credits: 4 },
|
||||||
|
{ name: "Chemistry Laboratory (CY23221)", credits: 1 },
|
||||||
|
{ name: "Python for Data Science Laboratory (AD23221)", credits: 1 },
|
||||||
|
{ name: "Communication Laboratory / Foreign Language (GE23221)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 1", credits: 2 }
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{ name: "Transforms and Partial Differential Equations (MA23313)", credits: 4 },
|
||||||
|
{ name: "Engineering Mechanics (ME23311)", credits: 3 },
|
||||||
|
{ name: "Engineering Thermodynamics (ME23312)", credits: 4 },
|
||||||
|
{ name: "Fluid Mechanics and Machinery (ME23313)", credits: 4 },
|
||||||
|
{ name: "Manufacturing Processes (ME23314)", credits: 3 },
|
||||||
|
{ name: "Object Oriented Programming (CS23312)", credits: 3 },
|
||||||
|
{ name: "Computer Aided Machine Drawing Laboratory (ME23321)", credits: 1 },
|
||||||
|
{ name: "Object Oriented Programming Laboratory (CS23322)", credits: 1 }
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
{ name: "Environmental Science for Mechanical Engineers (GE23412)", credits: 2 },
|
||||||
|
{ name: "Engineering Materials and Metallurgy (ME23411)", credits: 3 },
|
||||||
|
{ name: "Manufacturing Technology (ME23412)", credits: 3 },
|
||||||
|
{ name: "Mechanics of Solids (ME23413)", credits: 3 },
|
||||||
|
{ name: "Theory of Machines (ME23414)", credits: 4 },
|
||||||
|
{ name: "Thermal Engineering (ME23431)", credits: 4 },
|
||||||
|
{ name: "Manufacturing Technology Laboratory (ME23421)", credits: 1 },
|
||||||
|
{ name: "Strength of Materials and Fluid Machinery Laboratory (ME23422)", credits: 1 },
|
||||||
|
{ name: "Introduction to Product Lifecycle Management (ME23IC1)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 2", credits: 3 }
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
{ name: "Design of Machine Elements (ME23511)", credits: 4 },
|
||||||
|
{ name: "Heat and Mass Transfer (ME23512)", credits: 4 },
|
||||||
|
{ name: "Professional Elective I", credits: 3, isElective: true, electiveSlot: 1 },
|
||||||
|
{ name: "Professional Elective II", credits: 3, isElective: true, electiveSlot: 2 },
|
||||||
|
{ name: "Mandatory Course I", credits: 0 },
|
||||||
|
{ name: "Mechatronics (ME23531)", credits: 4 },
|
||||||
|
{ name: "Heat Transfer Laboratory (ME23521)", credits: 1 },
|
||||||
|
{ name: "Professional / Skill Development (ME23522)", credits: 1 },
|
||||||
|
{ name: "Introduction to Supply Chain Management (ME23IC2)", credits: 1 }
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
{ name: "Design of Transmission Systems (ME23611)", credits: 3 },
|
||||||
|
{ name: "Professional Elective III", credits: 3, isElective: true, electiveSlot: 3 },
|
||||||
|
{ name: "Professional Elective IV", credits: 3, isElective: true, electiveSlot: 4 },
|
||||||
|
{ name: "Open Elective I", credits: 3 },
|
||||||
|
{ name: "Open Elective II", credits: 3 },
|
||||||
|
{ name: "Mandatory Course II", credits: 0 },
|
||||||
|
{ name: "Metrology and Measurements (ME23631)", credits: 4 },
|
||||||
|
{ name: "Design and Fabrication Project (ME23621)", credits: 2 },
|
||||||
|
{ name: "CAD and CAM Laboratory (ME23622)", credits: 1 },
|
||||||
|
{ name: "Dynamics Laboratory (ME23623)", credits: 1 },
|
||||||
|
{ name: "Introduction to HVAC Systems (ME23IC3)", credits: 1 },
|
||||||
|
{ name: "NCC / Service Club Credit Course Level 3", credits: 3 }
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
{ name: "Human Values and Ethics (GE23711)", credits: 2 },
|
||||||
|
{ name: "Elective – Management", credits: 3 },
|
||||||
|
{ name: "Industrial Robotics (ME23712)", credits: 4 },
|
||||||
|
{ name: "Finite Element Analysis (ME23713)", credits: 4 },
|
||||||
|
{ name: "Professional Elective V", credits: 3, isElective: true, electiveSlot: 5 },
|
||||||
|
{ name: "Professional Elective VI", credits: 3, isElective: true, electiveSlot: 6 },
|
||||||
|
{ name: "Internship / Certification Course (ME23721)", credits: 2 }
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
{ name: "Open Elective III", credits: 3 },
|
||||||
|
{ name: "Project Work", credits: 10 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, useCallback } from 'rea
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { getBackendUrl } from '@/lib/utils';
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
import GoogleAuthModal from '@/components/Auth/GoogleAuthModal';
|
import GoogleAuthModal from '@/components/Auth/GoogleAuthModal';
|
||||||
|
import { getStoredImsSession, clearImsSession, saveImsSession, loginWithIms, type StudentInfo } from '@/services/imsService';
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// Types
|
// Types
|
||||||
@@ -13,15 +14,31 @@ export interface AuthUser {
|
|||||||
name: string;
|
name: string;
|
||||||
pictureUrl?: string;
|
pictureUrl?: string;
|
||||||
verifiedStudent: boolean;
|
verifiedStudent: boolean;
|
||||||
|
role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_COMMUNITY' | 'ROLE_CLUBS' | 'ROLE_CURRICULUM' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER';
|
||||||
|
regNumber?: string;
|
||||||
|
department?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginCredentialsResult {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
redirectTo?: string;
|
||||||
|
role?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: AuthUser | null;
|
user: AuthUser | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isVerifiedStudent: boolean;
|
isVerifiedStudent: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
isTransportAdmin: boolean;
|
||||||
|
isCommunityAdmin: boolean;
|
||||||
|
isClubsAdmin: boolean;
|
||||||
|
isCurriculumAdmin: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isAuthModalOpen: boolean;
|
isAuthModalOpen: boolean;
|
||||||
loginWithGoogle: () => void;
|
loginWithGoogle: () => void;
|
||||||
|
loginWithCredentials: (username: string, password: string) => Promise<LoginCredentialsResult>;
|
||||||
openAuthModal: () => void;
|
openAuthModal: () => void;
|
||||||
closeAuthModal: () => void;
|
closeAuthModal: () => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
@@ -43,9 +60,15 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isVerifiedStudent: false,
|
isVerifiedStudent: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isTransportAdmin: false,
|
||||||
|
isCommunityAdmin: false,
|
||||||
|
isClubsAdmin: false,
|
||||||
|
isCurriculumAdmin: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isAuthModalOpen: false,
|
isAuthModalOpen: false,
|
||||||
loginWithGoogle: () => {},
|
loginWithGoogle: () => {},
|
||||||
|
loginWithCredentials: async () => ({ success: false }),
|
||||||
openAuthModal: () => {},
|
openAuthModal: () => {},
|
||||||
closeAuthModal: () => {},
|
closeAuthModal: () => {},
|
||||||
logout: () => {},
|
logout: () => {},
|
||||||
@@ -54,29 +77,13 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
export const useAuth = () => useContext(AuthContext);
|
export const useAuth = () => useContext(AuthContext);
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// Google GSI global type
|
// Storage Keys
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|
||||||
declare global {
|
const GOOGLE_STORAGE_KEY = 'rit_auth_user';
|
||||||
interface Window {
|
const ADMIN_TOKEN_KEY = 'RIT_ADMIN_TOKEN';
|
||||||
google?: {
|
const ADMIN_ROLE_KEY = 'RIT_ADMIN_ROLE';
|
||||||
accounts: {
|
const ADMIN_USER_KEY = 'RIT_ADMIN_USER';
|
||||||
id: {
|
|
||||||
initialize: (config: Record<string, unknown>) => void;
|
|
||||||
prompt: (callback?: (notification: { isNotDisplayed: () => boolean; isSkippedMoment: () => boolean }) => void) => void;
|
|
||||||
renderButton: (element: HTMLElement, config: Record<string, unknown>) => void;
|
|
||||||
revoke: (email: string, callback: () => void) => void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
// Provider
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'rit_auth_user';
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<AuthUser | null>(null);
|
const [user, setUser] = useState<AuthUser | null>(null);
|
||||||
@@ -86,7 +93,54 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []);
|
const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []);
|
||||||
const closeAuthModal = useCallback(() => setIsAuthModalOpen(false), []);
|
const closeAuthModal = useCallback(() => setIsAuthModalOpen(false), []);
|
||||||
|
|
||||||
// Handle the credential response from Google GSI
|
// Restore any active session on app load
|
||||||
|
useEffect(() => {
|
||||||
|
// 1. Check Admin Session
|
||||||
|
const adminToken = localStorage.getItem(ADMIN_TOKEN_KEY);
|
||||||
|
const adminRole = localStorage.getItem(ADMIN_ROLE_KEY);
|
||||||
|
const adminUser = localStorage.getItem(ADMIN_USER_KEY);
|
||||||
|
if (adminToken && adminRole) {
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: adminUser || (adminRole === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin'),
|
||||||
|
email: adminRole === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role: adminRole as any,
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check IMS Student Session
|
||||||
|
const imsSession = getStoredImsSession();
|
||||||
|
if (imsSession?.student) {
|
||||||
|
setUser({
|
||||||
|
id: Number(imsSession.student.regNumber) || 1,
|
||||||
|
name: imsSession.student.name,
|
||||||
|
email: imsSession.student.email,
|
||||||
|
regNumber: imsSession.student.regNumber,
|
||||||
|
department: imsSession.student.department,
|
||||||
|
verifiedStudent: true,
|
||||||
|
role: 'ROLE_STUDENT',
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Google Session
|
||||||
|
const googleStored = localStorage.getItem(GOOGLE_STORAGE_KEY);
|
||||||
|
if (googleStored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(googleStored) as AuthUser;
|
||||||
|
setUser({ ...parsed, role: 'ROLE_USER' });
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(GOOGLE_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle Google OAuth GSI
|
||||||
const handleCredentialResponse = useCallback(async (response: { credential: string }) => {
|
const handleCredentialResponse = useCallback(async (response: { credential: string }) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(getBackendUrl('/api/auth/google'), {
|
const res = await fetch(getBackendUrl('/api/auth/google'), {
|
||||||
@@ -95,15 +149,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
body: JSON.stringify({ credential: response.credential }),
|
body: JSON.stringify({ credential: response.credential }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (res.ok) {
|
||||||
console.error('Auth failed:', res.status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData: AuthUser = await res.json();
|
const userData: AuthUser = await res.json();
|
||||||
setUser(userData);
|
const userObj: AuthUser = { ...userData, role: 'ROLE_USER' };
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
|
setUser(userObj);
|
||||||
|
localStorage.setItem(GOOGLE_STORAGE_KEY, JSON.stringify(userObj));
|
||||||
setIsAuthModalOpen(false);
|
setIsAuthModalOpen(false);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Google auth error:', err);
|
console.error('Google auth error:', err);
|
||||||
}
|
}
|
||||||
@@ -111,42 +163,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Initialize Google GSI
|
// Initialize Google GSI
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 1. Restore session from localStorage
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (stored) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(stored) as AuthUser;
|
|
||||||
setUser(parsed);
|
|
||||||
|
|
||||||
// Validate session is still valid against backend
|
|
||||||
fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`))
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
|
||||||
setUser(null);
|
|
||||||
} else {
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((freshData) => {
|
|
||||||
if (freshData) {
|
|
||||||
setUser(freshData);
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Backend offline, keep cached session
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
// 2. Initialize Google GSI when the script loads
|
|
||||||
const initGSI = () => {
|
const initGSI = () => {
|
||||||
if (!window.google || !GOOGLE_CLIENT_ID) return;
|
if (!window.google || !GOOGLE_CLIENT_ID) return;
|
||||||
|
|
||||||
window.google.accounts.id.initialize({
|
window.google.accounts.id.initialize({
|
||||||
client_id: GOOGLE_CLIENT_ID,
|
client_id: GOOGLE_CLIENT_ID,
|
||||||
callback: handleCredentialResponse,
|
callback: handleCredentialResponse,
|
||||||
@@ -169,16 +187,191 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [handleCredentialResponse]);
|
}, [handleCredentialResponse]);
|
||||||
|
|
||||||
const loginWithGoogle = useCallback(() => {
|
const loginWithGoogle = useCallback(() => {
|
||||||
// Open the clean Google Sign-In modal directly
|
|
||||||
setIsAuthModalOpen(true);
|
setIsAuthModalOpen(true);
|
||||||
|
|
||||||
// Also trigger One Tap prompt as a secondary background hint if supported
|
|
||||||
if (window.google) {
|
if (window.google) {
|
||||||
try {
|
try {
|
||||||
window.google.accounts.id.prompt();
|
window.google.accounts.id.prompt();
|
||||||
} catch {
|
} catch {}
|
||||||
// Silently ignore if browser blocks One Tap
|
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// UNIFIED CREDENTIALS SIGN-IN (Students & Admins)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
const loginWithCredentials = useCallback(async (username: string, password: string): Promise<LoginCredentialsResult> => {
|
||||||
|
const cleanedUser = username.trim();
|
||||||
|
const cleanedPass = password.trim();
|
||||||
|
|
||||||
|
if (!cleanedUser || !cleanedPass) {
|
||||||
|
return { success: false, message: 'Please enter both username/register number and password.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdminUsername = [
|
||||||
|
'admin', 'transport', 'transportadmin',
|
||||||
|
'community', 'communityadmin', 'qa', 'seniorqa',
|
||||||
|
'clubs', 'club', 'clubsadmin',
|
||||||
|
'curriculum', 'curriculumadmin', 'gpa', 'gpaadmin', 'academics',
|
||||||
|
'ritadmin', 'superadmin'
|
||||||
|
].includes(cleanedUser.toLowerCase());
|
||||||
|
|
||||||
|
// 1. First, check if credentials match Admin / Role Endpoints
|
||||||
|
if (isAdminUsername) {
|
||||||
|
try {
|
||||||
|
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: cleanedUser, password: cleanedPass }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (adminRes.ok) {
|
||||||
|
const data = await adminRes.json();
|
||||||
|
if (data.success && data.token && data.role) {
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, data.role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, data.username);
|
||||||
|
|
||||||
|
const adminUserObj: AuthUser = {
|
||||||
|
id: 999999,
|
||||||
|
name: data.username,
|
||||||
|
email: `${cleanedUser.toLowerCase()}@ritchennai.edu.in`,
|
||||||
|
verifiedStudent: false,
|
||||||
|
role: data.role,
|
||||||
|
};
|
||||||
|
setUser(adminUserObj);
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
role: data.role,
|
||||||
|
redirectTo: '/admin',
|
||||||
|
message: `${data.username} logged in successfully`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If admin endpoint fails or is offline, check fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStdPass = cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026';
|
||||||
|
|
||||||
|
// Direct local check for Admin credentials fallbacks
|
||||||
|
if (['transport', 'transportadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_TRANSPORT';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'Transport Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'Transport Admin',
|
||||||
|
email: 'transport@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['community', 'qa', 'communityadmin', 'seniorqa'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_COMMUNITY';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'COMMUNITY_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'Community & Q&A Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'Community & Q&A Admin',
|
||||||
|
email: 'community@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['clubs', 'club', 'clubsadmin', 'clubadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_CLUBS';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'CLUBS_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'Clubs & Centers Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'Clubs & Centers Admin',
|
||||||
|
email: 'clubs@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['curriculum', 'gpa', 'academics', 'gpaadmin', 'curriculumadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_CURRICULUM';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'CURRICULUM_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'GPA Curriculum Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'GPA Curriculum Admin',
|
||||||
|
email: 'curriculum@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(['admin', 'superadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) ||
|
||||||
|
(cleanedUser.toLowerCase() === 'ritadmin' && (cleanedPass === 'ritadmin2026' || isStdPass))
|
||||||
|
) {
|
||||||
|
const role = 'ROLE_SUPER_ADMIN';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'Super Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'Super Admin',
|
||||||
|
email: 'admin@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Next, check IMS Student Login
|
||||||
|
try {
|
||||||
|
const imsResult = await loginWithIms(cleanedUser, cleanedPass);
|
||||||
|
if (imsResult.success && imsResult.student) {
|
||||||
|
const studentObj: AuthUser = {
|
||||||
|
id: Number(imsResult.student.regNumber) || 1,
|
||||||
|
name: imsResult.student.name,
|
||||||
|
email: imsResult.student.email,
|
||||||
|
regNumber: imsResult.student.regNumber,
|
||||||
|
department: imsResult.student.department,
|
||||||
|
verifiedStudent: true,
|
||||||
|
role: 'ROLE_STUDENT',
|
||||||
|
};
|
||||||
|
setUser(studentObj);
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
role: 'ROLE_STUDENT',
|
||||||
|
redirectTo: '/dashboard',
|
||||||
|
message: `Welcome ${imsResult.student.name}!`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: imsResult.message || 'Authentication failed. Please check your credentials.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: err.message || 'Network error while attempting login. Please try again.',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -189,19 +382,36 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
setUser(null);
|
setUser(null);
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(GOOGLE_STORAGE_KEY);
|
||||||
|
localStorage.removeItem(ADMIN_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(ADMIN_ROLE_KEY);
|
||||||
|
localStorage.removeItem(ADMIN_USER_KEY);
|
||||||
|
clearImsSession();
|
||||||
setIsAuthModalOpen(false);
|
setIsAuthModalOpen(false);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
const isSuperAdmin = user?.role === 'ROLE_SUPER_ADMIN';
|
||||||
|
const isTransportAdmin = user?.role === 'ROLE_TRANSPORT';
|
||||||
|
const isCommunityAdmin = user?.role === 'ROLE_COMMUNITY';
|
||||||
|
const isClubsAdmin = user?.role === 'ROLE_CLUBS';
|
||||||
|
const isCurriculumAdmin = user?.role === 'ROLE_CURRICULUM';
|
||||||
|
const isAdmin = isSuperAdmin || isTransportAdmin || isCommunityAdmin || isClubsAdmin || isCurriculumAdmin;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
isAuthenticated: !!user,
|
isAuthenticated: !!user,
|
||||||
isVerifiedStudent: !!user?.verifiedStudent,
|
isVerifiedStudent: !!user?.verifiedStudent,
|
||||||
|
isAdmin,
|
||||||
|
isTransportAdmin,
|
||||||
|
isCommunityAdmin,
|
||||||
|
isClubsAdmin,
|
||||||
|
isCurriculumAdmin,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAuthModalOpen,
|
isAuthModalOpen,
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
|
loginWithCredentials,
|
||||||
openAuthModal,
|
openAuthModal,
|
||||||
closeAuthModal,
|
closeAuthModal,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
223
src/index.css
@@ -1,26 +1,32 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=Archivo+Black&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=Inter:wght@300;400;500;600;700;800&family=Poppins:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-primary: #F97316;
|
--color-primary: #FF6B00;
|
||||||
--color-primary-light: #FB923C;
|
--color-primary-light: #F97316;
|
||||||
--color-primary-dark: #EA580C;
|
--color-primary-dark: #EA580C;
|
||||||
--color-primary-50: #FFF7ED;
|
--color-primary-50: #FFF7ED;
|
||||||
--color-primary-100: #FFEDD5;
|
--color-primary-100: #FFEDD5;
|
||||||
--color-primary-200: #FED7AA;
|
--color-primary-200: #FED7AA;
|
||||||
--color-secondary: #FB923C;
|
--color-secondary: #F97316;
|
||||||
--color-background: #FAFAFA;
|
|
||||||
|
--color-dark-purple: #130924;
|
||||||
|
--color-dark-purple-light: #1E0C36;
|
||||||
|
--color-dark-purple-surface: #2A104E;
|
||||||
|
--color-dark-purple-border: #3A1968;
|
||||||
|
|
||||||
|
--color-background: #FAF9FC;
|
||||||
--color-surface: #FFFFFF;
|
--color-surface: #FFFFFF;
|
||||||
--color-border: #E5E7EB;
|
--color-border: #E9E5EE;
|
||||||
--color-heading: #1E293B;
|
--color-heading: #1A0B2E;
|
||||||
--color-body: #475569;
|
--color-body: #4A3E5E;
|
||||||
--color-muted: #94A3B8;
|
--color-muted: #9E91B6;
|
||||||
--color-accent: #FFF7ED;
|
--color-accent: #FFF7ED;
|
||||||
|
|
||||||
--font-heading: 'Playfair Display', Georgia, serif;
|
--font-heading: 'Plus Jakarta Sans', 'Inter', system-ui, sans-serif;
|
||||||
--font-body: 'Inter', system-ui, sans-serif;
|
--font-body: 'Inter', system-ui, sans-serif;
|
||||||
--font-button: 'Poppins', system-ui, sans-serif;
|
--font-button: 'Plus Jakarta Sans', 'Poppins', system-ui, sans-serif;
|
||||||
|
|
||||||
--radius-sm: 8px;
|
--radius-sm: 8px;
|
||||||
--radius-md: 12px;
|
--radius-md: 12px;
|
||||||
@@ -30,19 +36,17 @@
|
|||||||
--radius-3xl: 32px;
|
--radius-3xl: 32px;
|
||||||
--radius-full: 9999px;
|
--radius-full: 9999px;
|
||||||
|
|
||||||
--shadow-soft: 0 2px 15px -3px rgba(0,0,0,0.07), 0 10px 20px -2px rgba(0,0,0,0.04);
|
--shadow-soft: 0 4px 20px -4px rgba(19, 9, 36, 0.05), 0 1px 3px rgba(19, 9, 36, 0.02);
|
||||||
--shadow-medium: 0 4px 25px -5px rgba(0,0,0,0.1), 0 10px 30px -5px rgba(0,0,0,0.07);
|
--shadow-medium: 0 8px 30px -6px rgba(19, 9, 36, 0.08), 0 2px 6px rgba(19, 9, 36, 0.03);
|
||||||
--shadow-strong: 0 8px 40px -8px rgba(0,0,0,0.15), 0 20px 45px -8px rgba(0,0,0,0.1);
|
--shadow-strong: 0 16px 45px -8px rgba(19, 9, 36, 0.15), 0 4px 12px rgba(19, 9, 36, 0.05);
|
||||||
--shadow-orange: 0 8px 30px -4px rgba(249,115,22,0.3);
|
--shadow-orange: 0 8px 30px -4px rgba(255, 107, 0, 0.35);
|
||||||
|
|
||||||
--color-brand-orange: #ff5722;
|
--color-brand-orange: #FF6B00;
|
||||||
--color-brand-pink: #e91e63;
|
--color-brand-purple: #130924;
|
||||||
--color-brand-purple: #9c27b0;
|
|
||||||
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tailwind Preflight handles resets natively. Custom modifications go here: */
|
/* Base resets & typography */
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -50,8 +54,8 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
background-color: #FAFAFA;
|
background-color: #FAF9FC;
|
||||||
color: #475569;
|
color: #4A3E5E;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -62,10 +66,10 @@ body {
|
|||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: #FAF9FC;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #F97316;
|
background: #FF6B00;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
@@ -74,20 +78,23 @@ body {
|
|||||||
|
|
||||||
/* Selection */
|
/* Selection */
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgba(249, 115, 22, 0.2);
|
background-color: rgba(255, 107, 0, 0.2);
|
||||||
color: #1E293B;
|
color: #1A0B2E;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography base */
|
/* Typography base */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
@layer base {
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: var(--font-heading);
|
font-family: var(--font-heading);
|
||||||
color: #1E293B;
|
color: #1A0B2E;
|
||||||
line-height: 1.2;
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus ring */
|
/* Focus ring */
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid #F97316;
|
outline: 2px solid #FF6B00;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +117,12 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translateY(0px); }
|
0%, 100% { transform: translateY(0px); }
|
||||||
50% { transform: translateY(-10px); }
|
50% { transform: translateY(-8px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-orange {
|
@keyframes pulse-orange {
|
||||||
0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
|
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 0, 0.4); }
|
||||||
70% { box-shadow: 0 0 0 10px rgba(249, 115, 22, 0); }
|
70% { box-shadow: 0 0 0 10px rgba(255, 107, 0, 0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
@@ -123,12 +130,6 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
100% { background-position: 200% 0; }
|
100% { background-position: 200% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gradientShift {
|
|
||||||
0% { background-position: 0% 50%; }
|
|
||||||
50% { background-position: 100% 50%; }
|
|
||||||
100% { background-position: 0% 50%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 4s ease-in-out infinite;
|
animation: float 4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -149,36 +150,37 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
/* Glass effect */
|
/* Glass effect */
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.85);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-dark {
|
.glass-dark {
|
||||||
background: rgba(30, 41, 59, 0.8);
|
background: rgba(19, 9, 36, 0.85);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card base */
|
/* Card base */
|
||||||
.card-base {
|
.card-base {
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
border: 1px solid #E5E7EB;
|
border: 1px solid #E9E5EE;
|
||||||
box-shadow: 0 2px 15px -3px rgba(0,0,0,0.07), 0 10px 20px -2px rgba(0,0,0,0.04);
|
box-shadow: 0 4px 20px -4px rgba(19, 9, 36, 0.04), 0 1px 3px rgba(19, 9, 36, 0.02);
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-base:hover {
|
.card-base:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 8px 40px -8px rgba(0,0,0,0.15), 0 20px 45px -8px rgba(0,0,0,0.1);
|
box-shadow: 0 12px 35px -6px rgba(19, 9, 36, 0.08), 0 4px 12px -2px rgba(19, 9, 36, 0.04);
|
||||||
|
border-color: rgba(255, 107, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient text */
|
/* Gradient text */
|
||||||
.gradient-text {
|
.gradient-text {
|
||||||
background: linear-gradient(135deg, #F97316, #FB923C, #FBBF24);
|
background: linear-gradient(135deg, #FF6B00 0%, #F97316 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -187,16 +189,17 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
/* Orange button */
|
/* Orange button */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
font-family: var(--font-button);
|
font-family: var(--font-button);
|
||||||
background: linear-gradient(135deg, #F97316, #FB923C);
|
background: linear-gradient(135deg, #FF6B00 0%, #F97316 100%);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px 28px;
|
padding: 12px 28px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
box-shadow: 0 4px 15px rgba(249, 115, 22, 0.3);
|
box-shadow: 0 4px 16px rgba(255, 107, 0, 0.28);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -205,8 +208,8 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 25px rgba(249, 115, 22, 0.4);
|
box-shadow: 0 8px 24px rgba(255, 107, 0, 0.38);
|
||||||
background: linear-gradient(135deg, #EA580C, #F97316);
|
background: linear-gradient(135deg, #EA580C 0%, #FF6B00 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:active {
|
.btn-primary:active {
|
||||||
@@ -217,12 +220,12 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
font-family: var(--font-button);
|
font-family: var(--font-button);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #1E293B;
|
color: #1A0B2E;
|
||||||
border: 2px solid #E5E7EB;
|
border: 1.5px solid #E9E5EE;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 10px 26px;
|
padding: 10px 26px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -232,8 +235,9 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
border-color: #F97316;
|
border-color: #FF6B00;
|
||||||
color: #F97316;
|
color: #FF6B00;
|
||||||
|
background-color: rgba(255, 107, 0, 0.04);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,23 +267,13 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
/* Orange dot pattern background */
|
/* Orange dot pattern background */
|
||||||
.dot-pattern {
|
.dot-pattern {
|
||||||
background-image: radial-gradient(circle, #F9731615 1px, transparent 1px);
|
background-image: radial-gradient(circle, rgba(255, 107, 0, 0.08) 1px, transparent 1px);
|
||||||
background-size: 24px 24px;
|
background-size: 24px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Noise texture overlay */
|
|
||||||
.noise-overlay::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0.03;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat bubble */
|
/* Chat bubble */
|
||||||
.chat-bubble-user {
|
.chat-bubble-user {
|
||||||
background: linear-gradient(135deg, #F97316, #FB923C);
|
background: linear-gradient(135deg, #FF6B00, #F97316);
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 18px 18px 4px 18px;
|
border-radius: 18px 18px 4px 18px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
@@ -289,91 +283,26 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai {
|
.chat-bubble-ai {
|
||||||
background: #F8FAFC;
|
background: #FFFFFF;
|
||||||
color: #1E293B;
|
color: #1A0B2E;
|
||||||
border-radius: 18px 18px 18px 4px;
|
border-radius: 18px 18px 18px 4px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
border: 1px solid #E5E7EB;
|
border: 1px solid #E9E5EE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verified card style input focus state */
|
/* Verified card style input focus state */
|
||||||
.verified-focus-input {
|
.verified-focus-input {
|
||||||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
border: 1.5px solid #E5E7EB !important;
|
border: 1.5px solid #E9E5EE !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verified-focus-input:focus {
|
.verified-focus-input:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
border-color: #F97316 !important;
|
border-color: #FF6B00 !important;
|
||||||
box-shadow: none !important;
|
box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RAISE Incubator Website Styles */
|
|
||||||
.raise-website {
|
|
||||||
--font-sans: 'Inter', sans-serif;
|
|
||||||
--font-heading: 'Plus Jakarta Sans', sans-serif;
|
|
||||||
--color-primary: #101010;
|
|
||||||
}
|
|
||||||
|
|
||||||
.raise-website body {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
background-color: #fafafa;
|
|
||||||
color: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
.raise-website h1,
|
|
||||||
.raise-website h2,
|
|
||||||
.raise-website h3,
|
|
||||||
.raise-website h4,
|
|
||||||
.raise-website h5,
|
|
||||||
.raise-website h6 {
|
|
||||||
font-family: var(--font-heading) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gradient {
|
|
||||||
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 50%, #06b6d4 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-blue {
|
|
||||||
filter: invert(29%) sepia(92%) saturate(2377%) hue-rotate(215deg) brightness(93%) contrast(97%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-black {
|
|
||||||
filter: brightness(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glassmorphism {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-premium {
|
|
||||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.02),
|
|
||||||
0 1px 3px rgba(0, 0, 0, 0.05),
|
|
||||||
0 20px 40px rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-premium-hover {
|
|
||||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
}
|
|
||||||
.shadow-premium-hover:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04),
|
|
||||||
0 2px 8px rgba(0, 0, 0, 0.06),
|
|
||||||
0 30px 60px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-bg {
|
|
||||||
background-size: 40px 40px;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(to right, rgba(0, 0, 0, 0.02) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(0, 0, 0, 0.02) 1px, transparent 1px);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { Outlet, useSearchParams, useLocation } from 'react-router-dom';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Sidebar from '@/components/Sidebar/Sidebar';
|
import Navbar from '@/components/Navbar/Navbar';
|
||||||
import Header from '@/components/Header/Header';
|
|
||||||
import Footer from '@/components/Footer/Footer';
|
import Footer from '@/components/Footer/Footer';
|
||||||
|
|
||||||
const pageVariants = {
|
const pageVariants = {
|
||||||
@@ -12,42 +10,13 @@ const pageVariants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
|
||||||
return localStorage.getItem('rit_sidebar_collapsed') === 'true';
|
|
||||||
});
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const toggleSidebar = () => setIsSidebarOpen((prev) => !prev);
|
const hideFooter = location.pathname === '/notes' && searchParams.has('toolkit');
|
||||||
const toggleSidebarCollapse = () => {
|
|
||||||
setIsSidebarCollapsed((prev) => {
|
|
||||||
const next = !prev;
|
|
||||||
localStorage.setItem('rit_sidebar_collapsed', String(next));
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col lg:flex-row font-sans selection:bg-purple-700 selection:text-white">
|
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
|
||||||
{/* Left Sidebar Navigation */}
|
<Navbar />
|
||||||
<Sidebar
|
|
||||||
isOpen={isSidebarOpen}
|
|
||||||
onToggle={toggleSidebar}
|
|
||||||
isCollapsed={isSidebarCollapsed}
|
|
||||||
onToggleCollapse={toggleSidebarCollapse}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Main Right Viewport Wrapper */}
|
|
||||||
<div
|
|
||||||
className={`flex-1 flex flex-col min-w-0 transition-all duration-300 ${
|
|
||||||
isSidebarCollapsed ? 'lg:pl-20' : 'lg:pl-64 xl:pl-72'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* Top Header */}
|
|
||||||
<Header onToggleSidebar={toggleSidebar} />
|
|
||||||
|
|
||||||
{/* Dynamic Page Content */}
|
|
||||||
<div className="flex-1 p-4 sm:p-6 lg:p-8">
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.main
|
<motion.main
|
||||||
key={location.pathname}
|
key={location.pathname}
|
||||||
@@ -55,17 +24,12 @@ export default function MainLayout() {
|
|||||||
initial="initial"
|
initial="initial"
|
||||||
animate="enter"
|
animate="enter"
|
||||||
exit="exit"
|
exit="exit"
|
||||||
transition={{ duration: 0.25, ease: 'easeInOut' }}
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
className="max-w-7xl mx-auto"
|
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</motion.main>
|
</motion.main>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
{!hideFooter && <Footer />}
|
||||||
|
|
||||||
{/* Global Footer */}
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,17 +109,17 @@ export default function AIAssistant() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-[#FAFAFA]">
|
<div className="min-h-screen flex flex-col bg-[#FAF9FC]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white border-b border-[#E5E7EB] py-4 relative z-10 shadow-xs">
|
<div className="bg-white border-b border-[#E9E5EE] py-4 relative z-10 shadow-xs">
|
||||||
<div className="container-custom flex items-center justify-between">
|
<div className="container-custom flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-sm" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-md shadow-orange-500/20" style={{ background: 'linear-gradient(135deg, #FF6B00, #F97316)' }}>
|
||||||
<Bot className="w-5.5 h-5.5 text-white" />
|
<Bot className="w-5.5 h-5.5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-bold text-[#1E293B] text-base" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Assistant</h1>
|
<h1 className="font-extrabold text-[#1A0B2E] text-base" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>RIT Assistant</h1>
|
||||||
<div className="flex items-center gap-1.5 text-xs text-[#64748B]">
|
<div className="flex items-center gap-1.5 text-xs text-[#9E91B6]">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
Campus Knowledge Engine Online
|
Campus Knowledge Engine Online
|
||||||
</div>
|
</div>
|
||||||
@@ -139,19 +139,6 @@ export default function AIAssistant() {
|
|||||||
<Send className="w-3.5 h-3.5" />
|
<Send className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">Telegram Bot</span>
|
<span className="hidden sm:inline">Telegram Bot</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
href="https://discord.com/api/oauth2/authorize?client_id=1476942599167414506&permissions=8&scope=bot"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-medium text-white transition-all shadow-xs hover:brightness-105 active:scale-95"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, #5865F2, #7289DA)',
|
|
||||||
fontFamily: 'Inter, sans-serif'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Bot className="w-3.5 h-3.5" />
|
|
||||||
<span className="hidden sm:inline">Discord Bot</span>
|
|
||||||
</a>
|
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.03 }}
|
whileHover={{ scale: 1.03 }}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
@@ -187,8 +174,8 @@ export default function AIAssistant() {
|
|||||||
{/* Chat Container */}
|
{/* Chat Container */}
|
||||||
<div
|
<div
|
||||||
ref={chatContainerRef}
|
ref={chatContainerRef}
|
||||||
className="flex-1 bg-white rounded-3xl border border-[#E5E7EB] p-4 sm:p-6 flex flex-col gap-4 overflow-y-auto"
|
className="flex-1 bg-white rounded-2xl border border-[#E9E5EE] p-4 sm:p-6 flex flex-col gap-4 overflow-y-auto"
|
||||||
style={{ minHeight: '420px', maxHeight: '65vh', boxShadow: '0 8px 30px -6px rgba(0,0,0,0.04)' }}
|
style={{ minHeight: '420px', maxHeight: '65vh', boxShadow: '0 4px 20px_-4px_rgba(19,9,36,0.04)' }}
|
||||||
>
|
>
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -199,17 +186,17 @@ export default function AIAssistant() {
|
|||||||
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
|
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
|
||||||
>
|
>
|
||||||
{msg.role === 'assistant' && (
|
{msg.role === 'assistant' && (
|
||||||
<div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0 mt-1 shadow-xs" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
|
<div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0 mt-1 shadow-sm" style={{ background: 'linear-gradient(135deg, #FF6B00, #F97316)' }}>
|
||||||
<Bot className="w-4 h-4 text-white" />
|
<Bot className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="max-w-[85%] sm:max-w-[78%] px-4 py-3 text-sm leading-relaxed"
|
className="max-w-[85%] sm:max-w-[78%] px-4 py-3 text-sm leading-relaxed"
|
||||||
style={{
|
style={{
|
||||||
background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #EA580C)' : '#F8FAFC',
|
background: msg.role === 'user' ? 'linear-gradient(135deg, #FF6B00, #F97316)' : '#FFFFFF',
|
||||||
color: msg.role === 'user' ? 'white' : '#1E293B',
|
color: msg.role === 'user' ? 'white' : '#1A0B2E',
|
||||||
borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px',
|
borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px',
|
||||||
border: msg.role === 'assistant' ? '1px solid #E5E7EB' : 'none',
|
border: msg.role === 'assistant' ? '1px solid #E9E5EE' : 'none',
|
||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Inter, sans-serif',
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: formatContent(msg.content) }}
|
dangerouslySetInnerHTML={{ __html: formatContent(msg.content) }}
|
||||||
|
|||||||
@@ -15,14 +15,29 @@ export default function BusRoutes() {
|
|||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 0. Check local storage if edited by Transport Admin
|
||||||
|
const localAdminRoutes = localStorage.getItem('RIT_LOCAL_BUS_ROUTES');
|
||||||
|
if (localAdminRoutes) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(localAdminRoutes);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
setBusRoutes(parsed);
|
||||||
|
setSelectedBusRoute(parsed[0]);
|
||||||
|
setLoadingBus(false);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Instantly load static bus routes (0ms loading delay)
|
// 1. Instantly load static bus routes (0ms loading delay)
|
||||||
fetch('/bus_routes.json')
|
fetch('/bus_routes.json')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
if (!localStorage.getItem('RIT_LOCAL_BUS_ROUTES')) {
|
||||||
setBusRoutes(data);
|
setBusRoutes(data);
|
||||||
setSelectedBusRoute(data[0]);
|
setSelectedBusRoute(data[0]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => console.error('Local JSON load error:', err))
|
.catch((err) => console.error('Local JSON load error:', err))
|
||||||
.finally(() => setLoadingBus(false));
|
.finally(() => setLoadingBus(false));
|
||||||
@@ -50,18 +65,18 @@ export default function BusRoutes() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative overflow-hidden bg-slate-950 text-white">
|
<div className="min-h-screen relative overflow-hidden bg-[#0A0414] text-white">
|
||||||
{/* Sleek Dark Header */}
|
{/* Sleek Dark Header */}
|
||||||
<div className="bg-slate-900 border-b border-slate-800/80 py-4 px-4 sm:px-8 z-30 relative">
|
<div className="bg-[#130924] border-b border-[#3A1968]/80 py-4 px-4 sm:px-8 z-30 relative">
|
||||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="max-w-7xl mx-auto flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-400 mb-1">
|
<div className="flex items-center gap-2 text-xs text-purple-300/80 mb-1">
|
||||||
<Link to="/" className="hover:text-orange-400">Home</Link>
|
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
|
||||||
<ChevronRight className="w-3 h-3" />
|
<ChevronRight className="w-3 h-3" />
|
||||||
<span className="text-orange-400 font-semibold">Live Bus Tracking</span>
|
<span className="text-[#FF6B00] font-semibold">Live Bus Tracking</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-white tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<h1 className="text-xl sm:text-2xl font-extrabold text-white tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
RIT Live Transport Map
|
RIT Live Transport Map
|
||||||
</h1>
|
</h1>
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[11px] font-bold">
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[11px] font-bold">
|
||||||
@@ -74,25 +89,25 @@ export default function BusRoutes() {
|
|||||||
{/* Controls: Search + Toggle Drawer Button */}
|
{/* Controls: Search + Toggle Drawer Button */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex-1 sm:w-64">
|
<div className="relative flex-1 sm:w-64">
|
||||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-purple-300/60" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search route or bus no..."
|
placeholder="Search route or bus no..."
|
||||||
value={busSearch}
|
value={busSearch}
|
||||||
onChange={(e) => setBusSearch(e.target.value)}
|
onChange={(e) => setBusSearch(e.target.value)}
|
||||||
className="w-full pl-9 pr-4 py-2 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-400 focus:outline-none focus:border-orange-500 transition-colors"
|
className="w-full pl-9 pr-4 py-2 rounded-xl bg-[#1E0C36] border border-[#3A1968] text-xs text-white placeholder-purple-300/50 focus:outline-none focus:border-[#FF6B00] transition-colors"
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
|
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-bold border transition-all cursor-pointer ${
|
||||||
isSidebarOpen
|
isSidebarOpen
|
||||||
? 'bg-orange-500 text-white border-orange-400 shadow-lg shadow-orange-500/20'
|
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white border-orange-400 shadow-lg shadow-orange-500/20'
|
||||||
: 'bg-slate-800 text-slate-200 border-slate-700 hover:bg-slate-700'
|
: 'bg-[#1E0C36] text-purple-100 border-[#3A1968] hover:bg-[#2A104E]'
|
||||||
}`}
|
}`}
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{isSidebarOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
|
{isSidebarOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
|
|||||||
@@ -32,38 +32,37 @@ const BLUEPRINT_LEGEND = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const LOCATION_SPOTS: LocationSpot[] = [
|
const LOCATION_SPOTS: LocationSpot[] = [
|
||||||
{ name: 'Girls Hostel', x: 16.0, y: 8.5, category: 'Hostel / Mess', desc: 'Residential Quarter for Women', area: 'Top Left' },
|
{ name: 'Girls Hostel', x: 29.4, y: 7.3, category: 'Hostel / Mess', desc: 'Residential Quarter for Women', area: 'Top Left' },
|
||||||
{ name: 'Playground', x: 40.0, y: 11.0, category: 'Amenity', desc: 'Main Sports & Athletic Field', area: 'Top Center' },
|
{ name: 'Playground', x: 48.5, y: 8.8, category: 'Amenity', desc: 'Main Sports & Athletic Field', area: 'Top Center' },
|
||||||
{ name: 'Canteen', x: 67.0, y: 8.5, category: 'Amenity', desc: 'Food Court & Refreshments', area: 'Top Right' },
|
{ name: 'Canteen', x: 68.3, y: 8.3, category: 'Amenity', desc: 'Food Court & Refreshments', area: 'Top Right' },
|
||||||
{ name: 'Steve Jobs Block', x: 16.0, y: 21.0, category: 'Admin / Facility', desc: 'Auditorium, Computer Labs & Staff Rooms', area: 'West Wing' },
|
{ name: 'Steve Jobs Block', x: 27.2, y: 17.1, category: 'Admin / Facility', desc: 'Auditorium, Computer Labs & Staff Rooms', area: 'West Wing' },
|
||||||
{ name: 'Girls Mess', x: 14.5, y: 31.0, category: 'Hostel / Mess', desc: 'Dedicated Dining Hall for Women', area: 'West Wing' },
|
{ name: 'Girls Mess', x: 25.7, y: 25.9, category: 'Hostel / Mess', desc: 'Dedicated Dining Hall for Women', area: 'West Wing' },
|
||||||
{ name: 'MEC Lab', x: 14.0, y: 39.5, category: 'Lab / Academic', desc: 'Mechanical Engineering Laboratory', area: 'West Wing' },
|
{ name: 'MEC Lab', x: 23.5, y: 34.7, category: 'Lab / Academic', desc: 'Mechanical Engineering Laboratory', area: 'West Wing' },
|
||||||
{ name: 'EPL Lab', x: 13.0, y: 47.5, category: 'Lab / Academic', desc: 'Engineering Practices Laboratory', area: 'West Wing' },
|
{ name: 'EPL Lab', x: 21.3, y: 43.5, category: 'Lab / Academic', desc: 'Engineering Practices Laboratory', area: 'West Wing' },
|
||||||
{ name: 'Nikola Tesla Block', x: 40.5, y: 36.5, category: 'B Block', desc: 'Main Electrical & Engineering Research Block', area: 'Central Block' },
|
{ name: 'Astronaut Statue', x: 51.4, y: 16.1, category: 'Amenity', desc: 'Iconic Landmark Monument', area: 'Central Park' },
|
||||||
{ name: 'Astronaut Statue', x: 51.0, y: 23.0, category: 'Amenity', desc: 'Iconic Landmark Monument', area: 'Central Park' },
|
{ name: 'Campus Centre', x: 65.3, y: 20.5, category: 'Admin / Facility', desc: 'Student Activity & Administrative Center', area: 'North East' },
|
||||||
{ name: 'Campus Centre', x: 67.0, y: 23.0, category: 'Admin / Facility', desc: 'Student Activity & Administrative Center', area: 'North East' },
|
{ name: 'Nikola Tesla Block', x: 43.3, y: 34.7, category: 'B Block', desc: 'Main Electrical & Engineering Research Block', area: 'Central Block' },
|
||||||
{ name: 'Hut', x: 70.0, y: 37.0, category: 'Amenity', desc: 'Student Discussion & Rest Area', area: 'Central East' },
|
{ name: 'Hut', x: 66.8, y: 31.3, category: 'Amenity', desc: 'Student Discussion & Rest Area', area: 'Central East' },
|
||||||
{ name: 'Homi Jehangir Bhabha Block', x: 87.0, y: 24.0, category: 'A Block', desc: 'Science & Physics Research Block', area: 'East Wing' },
|
{ name: 'Homi Jehangir Bhabha Block', x: 79.3, y: 22.9, category: 'A Block', desc: 'Science & Physics Research Block', area: 'East Wing' },
|
||||||
{ name: 'Boys Mess', x: 87.0, y: 17.5, category: 'Hostel / Mess', desc: 'Ground Floor Dining Hall for Men (Homi Bhabha Block)', area: 'East Wing' },
|
{ name: 'C Block', x: 78.6, y: 37.6, category: 'C Block', desc: 'Labs, Classrooms & Staff Rooms', area: 'East Wing' },
|
||||||
{ name: 'Staff Mess', x: 87.0, y: 30.5, category: 'Amenity', desc: 'First Floor Dining Hall for Faculty & Staff (Homi Bhabha Block)', area: 'East Wing' },
|
{ name: 'Alan Turing Block', x: 66.8, y: 50.8, category: 'Lab / Academic', desc: 'Computer Science & AI Labs', area: 'East Wing' },
|
||||||
{ name: 'C Block', x: 87.0, y: 38.0, category: 'C Block', desc: 'Labs, Classrooms & Staff Rooms', area: 'East Wing' },
|
{ name: 'Arcade', x: 36.7, y: 53.2, category: 'Amenity', desc: 'Student Hub & Stores', area: 'Central South' },
|
||||||
{ name: 'Alan Turing Block', x: 77.0, y: 53.0, category: 'Lab / Academic', desc: 'Computer Science & AI Labs', area: 'East Wing' },
|
{ name: 'Green Building', x: 27.2, y: 63.5, category: 'Admin / Facility', desc: 'Eco-Friendly Academic Complex & Library', area: 'South West' },
|
||||||
{ name: 'Arcade', x: 35.0, y: 57.0, category: 'Amenity', desc: 'Student Hub & Stores', area: 'Central South' },
|
{ name: 'Security Booth', x: 44.8, y: 65.9, category: 'Amenity', desc: 'Main Gate Security Checkpoint', area: 'South Entrance' },
|
||||||
{ name: 'Green Building', x: 23.0, y: 71.0, category: 'Admin / Facility', desc: 'Eco-Friendly Academic Complex & Library', area: 'South West' },
|
{ name: 'ATM', x: 44.3, y: 73.0, category: 'Amenity', desc: '24/7 Campus Banking ATM', area: 'South Entrance' },
|
||||||
{ name: 'Security Booth', x: 49.0, y: 77.0, category: 'Amenity', desc: 'Main Gate Security Checkpoint', area: 'South Entrance' },
|
{ name: 'Bike Parking', x: 65.3, y: 61.5, category: 'Parking Area', desc: 'Two-Wheeler Vehicle Parking', area: 'South East' },
|
||||||
{ name: 'ATM', x: 49.0, y: 86.0, category: 'Amenity', desc: '24/7 Campus Banking ATM', area: 'South Entrance' },
|
{ name: 'Generator Room', x: 63.1, y: 71.8, category: 'Admin / Facility', desc: 'Campus Power & Utility Station', area: 'South East' },
|
||||||
{ name: 'Bike Parking', x: 75.0, y: 67.0, category: 'Parking Area', desc: 'Two-Wheeler Vehicle Parking', area: 'South East' },
|
{ name: 'Side Gate', x: 22.8, y: 75.0, category: 'Amenity', desc: 'Secondary Campus Gate', area: 'South West' },
|
||||||
{ name: 'Generator Room', x: 76.0, y: 81.0, category: 'Admin / Facility', desc: 'Campus Power & Utility Station', area: 'South East' },
|
{ name: 'Main Gate', x: 49.2, y: 77.9, category: 'Amenity', desc: 'Primary Campus Entrance', area: 'South Entrance' },
|
||||||
{ name: 'Side Gate', x: 23.0, y: 93.0, category: 'Amenity', desc: 'Secondary Campus Gate', area: 'South West' },
|
{ name: 'Bus Stop', x: 49.6, y: 92.8, category: 'Amenity', desc: 'Public & College Bus Boarding Point', area: 'Highway' },
|
||||||
{ name: 'Main Gate', x: 59.0, y: 93.0, category: 'Amenity', desc: 'Primary Campus Entrance', area: 'South Entrance' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const CATEGORY_MAP_SPOTS: Record<string, string[]> = {
|
const CATEGORY_MAP_SPOTS: Record<string, string[]> = {
|
||||||
dept: ['Homi Jehangir Bhabha Block', 'Nikola Tesla Block', 'C Block', 'Steve Jobs Block', 'Alan Turing Block', 'Green Building'],
|
dept: ['Homi Jehangir Bhabha Block', 'Nikola Tesla Block', 'C Block', 'Steve Jobs Block', 'Alan Turing Block', 'Green Building'],
|
||||||
library: ['Green Building'],
|
library: ['Green Building'],
|
||||||
labs: ['MEC Lab', 'EPL Lab', 'Steve Jobs Block', 'Alan Turing Block', 'Nikola Tesla Block', 'C Block'],
|
labs: ['MEC Lab', 'EPL Lab', 'Steve Jobs Block', 'Alan Turing Block', 'Nikola Tesla Block', 'C Block'],
|
||||||
hostel: ['Girls Hostel', 'Boys Mess'],
|
hostel: ['Girls Hostel', 'Girls Mess'],
|
||||||
canteen: ['Canteen', 'Girls Mess', 'Boys Mess', 'Staff Mess', 'Arcade'],
|
canteen: ['Canteen', 'Girls Mess', 'Arcade'],
|
||||||
auditorium: ['Steve Jobs Block', 'Campus Centre'],
|
auditorium: ['Steve Jobs Block', 'Campus Centre'],
|
||||||
sports: ['Playground'],
|
sports: ['Playground'],
|
||||||
};
|
};
|
||||||
@@ -148,27 +147,27 @@ export default function Campus() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
||||||
<div className="container-custom">
|
<div className="container-custom">
|
||||||
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
|
<div className="flex items-center gap-2 text-xs text-[#9E91B6] mb-3">
|
||||||
<Link to="/" className="hover:text-[#F97316]">Home</Link>
|
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
|
||||||
<ChevronRight className="w-3 h-3" />
|
<ChevronRight className="w-3 h-3" />
|
||||||
<span className="text-[#F97316]">Campus Map</span>
|
<span className="text-[#FF6B00] font-semibold">Campus Map</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h1 className="text-3xl md:text-4xl font-extrabold text-[#1A0B2E] mb-2" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
Explore{' '}
|
Explore{' '}
|
||||||
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
<span style={{ background: 'linear-gradient(135deg, #FF6B00, #F97316)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
Campus Map
|
Campus Map
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[#475569] text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-[#4A3E5E] text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Navigate campus layout, click location categories or blocks to spot exact places on the blueprint.
|
Navigate campus layout, click location categories or blocks to spot exact places on the blueprint.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container-custom py-8">
|
<div className="container-custom pt-8 pb-20 md:pb-28">
|
||||||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
||||||
{/* Map Viewer Header Bar */}
|
{/* Map Viewer Header Bar */}
|
||||||
<div ref={mapRef} className="bg-[#0F172A] rounded-t-3xl p-4 md:p-6 text-white border-b border-slate-800 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
<div ref={mapRef} className="bg-[#130924] rounded-t-3xl p-4 md:p-6 text-white border-b border-purple-900/40 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400 text-xs font-semibold border border-orange-500/30">
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400 text-xs font-semibold border border-orange-500/30">
|
||||||
@@ -178,10 +177,10 @@ export default function Campus() {
|
|||||||
<CheckCircle2 className="w-3 h-3" /> Interactive Location Spotter
|
<CheckCircle2 className="w-3 h-3" /> Interactive Location Spotter
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl md:text-2xl font-bold tracking-tight" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h2 className="text-xl md:text-2xl font-extrabold tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
Rajalakshmi Institute of Technology
|
Rajalakshmi Institute of Technology
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs md:text-sm text-slate-400 flex items-center gap-1.5 mt-0.5">
|
<p className="text-xs md:text-sm text-purple-300/80 flex items-center gap-1.5 mt-0.5">
|
||||||
<MapPin className="w-3.5 h-3.5 text-orange-400" />
|
<MapPin className="w-3.5 h-3.5 text-orange-400" />
|
||||||
Kuthambakkam, Chennai • Interactive Map Spotting Enabled
|
Kuthambakkam, Chennai • Interactive Map Spotting Enabled
|
||||||
</p>
|
</p>
|
||||||
@@ -189,23 +188,23 @@ export default function Campus() {
|
|||||||
|
|
||||||
{/* Toolbar Controls */}
|
{/* Toolbar Controls */}
|
||||||
<div className="flex items-center flex-wrap gap-2">
|
<div className="flex items-center flex-wrap gap-2">
|
||||||
<div className="flex items-center bg-slate-800/80 rounded-xl p-1 border border-slate-700">
|
<div className="flex items-center bg-[#1E0C36] rounded-xl p-1 border border-[#3A1968]">
|
||||||
<button onClick={handleZoomOut} title="Zoom Out" className="p-2 text-slate-300 hover:text-white hover:bg-slate-700/60 rounded-lg transition-colors cursor-pointer">
|
<button onClick={handleZoomOut} title="Zoom Out" className="p-2 text-purple-200 hover:text-white hover:bg-purple-900/40 rounded-lg transition-colors cursor-pointer">
|
||||||
<ZoomOut className="w-4 h-4" />
|
<ZoomOut className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleResetZoom} title="Reset Zoom" className="px-2 text-xs font-semibold text-orange-400 hover:text-orange-300 transition-colors cursor-pointer">
|
<button onClick={handleResetZoom} title="Reset Zoom" className="px-2 text-xs font-semibold text-orange-400 hover:text-orange-300 transition-colors cursor-pointer">
|
||||||
{Math.round(zoomLevel * 100)}%
|
{Math.round(zoomLevel * 100)}%
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleZoomIn} title="Zoom In" className="p-2 text-slate-300 hover:text-white hover:bg-slate-700/60 rounded-lg transition-colors cursor-pointer">
|
<button onClick={handleZoomIn} title="Zoom In" className="p-2 text-purple-200 hover:text-white hover:bg-purple-900/40 rounded-lg transition-colors cursor-pointer">
|
||||||
<ZoomIn className="w-4 h-4" />
|
<ZoomIn className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={() => setIsFullscreen(true)} className="p-2 bg-slate-800/80 hover:bg-slate-700/80 text-slate-200 hover:text-white rounded-xl border border-slate-700 transition-colors cursor-pointer" title="Expand to Fullscreen">
|
<button onClick={() => setIsFullscreen(true)} className="p-2 bg-[#1E0C36] hover:bg-[#2A104E] text-purple-200 hover:text-white rounded-xl border border-[#3A1968] transition-colors cursor-pointer" title="Expand to Fullscreen">
|
||||||
<Maximize2 className="w-4 h-4" />
|
<Maximize2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={handleDownload} className="flex items-center gap-1.5 px-3 py-2 bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white text-xs font-semibold rounded-xl shadow-lg shadow-orange-500/20 transition-all cursor-pointer" title="Download Blueprint Image">
|
<button onClick={handleDownload} className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:from-[#EA580C] hover:to-[#FF6B00] text-white text-xs font-bold rounded-xl shadow-lg shadow-orange-500/20 transition-all cursor-pointer" title="Download Blueprint Image">
|
||||||
<Download className="w-3.5 h-3.5" />
|
<Download className="w-3.5 h-3.5" />
|
||||||
Download
|
Download
|
||||||
</button>
|
</button>
|
||||||
@@ -269,9 +268,9 @@ export default function Campus() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Location Categories Bar */}
|
{/* Quick Location Categories Bar */}
|
||||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] p-6 md:p-8 mb-8 shadow-xs">
|
<div className="bg-white rounded-2xl border border-[#E9E5EE] p-6 md:p-8 mb-8 shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)]">
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h3 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>Quick Location Categories</h3>
|
<h3 className="text-xl font-bold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Quick Location Categories</h3>
|
||||||
{activeCategory && (
|
{activeCategory && (
|
||||||
<button onClick={() => setActiveCategory(null)} className="text-xs font-semibold text-orange-500 hover:text-orange-600 flex items-center gap-1 cursor-pointer"><X className="w-3.5 h-3.5" /> Clear Spot Filter</button>
|
<button onClick={() => setActiveCategory(null)} className="text-xs font-semibold text-orange-500 hover:text-orange-600 flex items-center gap-1 cursor-pointer"><X className="w-3.5 h-3.5" /> Clear Spot Filter</button>
|
||||||
)}
|
)}
|
||||||
@@ -282,10 +281,10 @@ export default function Campus() {
|
|||||||
const isCatActive = activeCategory === loc.id;
|
const isCatActive = activeCategory === loc.id;
|
||||||
return (
|
return (
|
||||||
<motion.button key={loc.id} whileHover={{ y: -3 }} whileTap={{ scale: 0.95 }} onClick={() => handleCategoryClick(loc.id)} className={`flex flex-col items-center gap-2.5 p-3 rounded-2xl transition-all border cursor-pointer group ${isCatActive ? 'border-orange-500 bg-orange-500 text-white shadow-lg ring-2 ring-orange-400/30' : 'border-transparent hover:border-[#FED7AA] hover:bg-orange-50/50'}`}>
|
<motion.button key={loc.id} whileHover={{ y: -3 }} whileTap={{ scale: 0.95 }} onClick={() => handleCategoryClick(loc.id)} className={`flex flex-col items-center gap-2.5 p-3 rounded-2xl transition-all border cursor-pointer group ${isCatActive ? 'border-orange-500 bg-orange-500 text-white shadow-lg ring-2 ring-orange-400/30' : 'border-transparent hover:border-[#FED7AA] hover:bg-orange-50/50'}`}>
|
||||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center border transition-colors ${isCatActive ? 'bg-white text-orange-500 border-white' : 'bg-[#F8FAFC] group-hover:bg-orange-100 text-[#F97316] border-[#E5E7EB] group-hover:border-orange-300'}`}>
|
<div className={`w-12 h-12 rounded-full flex items-center justify-center border transition-colors ${isCatActive ? 'bg-white text-orange-500 border-white' : 'bg-[#FAF9FC] group-hover:bg-orange-100 text-[#FF6B00] border-[#E9E5EE] group-hover:border-orange-300'}`}>
|
||||||
{Icon && <Icon className="w-5 h-5" />}
|
{Icon && <Icon className="w-5 h-5" />}
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-[11px] font-semibold text-center leading-tight ${isCatActive ? 'text-white' : 'text-[#475569] group-hover:text-[#F97316]'}`} style={{ fontFamily: 'Inter, sans-serif' }}>{loc.name}</span>
|
<span className={`text-[11px] font-semibold text-center leading-tight ${isCatActive ? 'text-white' : 'text-[#4A3E5E] group-hover:text-[#FF6B00]'}`} style={{ fontFamily: 'Inter, sans-serif' }}>{loc.name}</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -293,16 +292,16 @@ export default function Campus() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Blueprint Legend & Color Key */}
|
{/* Blueprint Legend & Color Key */}
|
||||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] p-6 md:p-8 mb-8 shadow-xs">
|
<div className="bg-white rounded-2xl border border-[#E9E5EE] p-6 md:p-8 mb-8 shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)]">
|
||||||
<div className="flex items-center gap-2 mb-5">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
<Layers className="w-5 h-5 text-[#F97316]" />
|
<Layers className="w-5 h-5 text-[#FF6B00]" />
|
||||||
<h3 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>Blueprint Map Legend</h3>
|
<h3 className="text-xl font-bold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Blueprint Map Legend</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{BLUEPRINT_LEGEND.map((item) => {
|
{BLUEPRINT_LEGEND.map((item) => {
|
||||||
const isActive = activeLegend === item.id;
|
const isActive = activeLegend === item.id;
|
||||||
return (
|
return (
|
||||||
<button key={item.id} onClick={() => { setActiveLegend(item.id); setActiveCategory(null); setSelectedFacility(null); scrollToMap(); }} className={`flex items-center gap-2.5 px-4 py-2 rounded-full border transition-all cursor-pointer ${isActive ? 'border-[#F97316] bg-orange-500 text-white shadow-md scale-105' : `${item.border} ${item.bg} ${item.text} hover:scale-105`}`}>
|
<button key={item.id} onClick={() => { setActiveLegend(item.id); setActiveCategory(null); setSelectedFacility(null); scrollToMap(); }} className={`flex items-center gap-2.5 px-4 py-2 rounded-full border transition-all cursor-pointer ${isActive ? 'border-[#FF6B00] bg-orange-500 text-white shadow-md scale-105' : `${item.border} ${item.bg} ${item.text} hover:scale-105`}`}>
|
||||||
<span className="w-3.5 h-3.5 rounded-full shrink-0 shadow-xs border border-black/10" style={{ backgroundColor: isActive ? 'white' : item.color }} />
|
<span className="w-3.5 h-3.5 rounded-full shrink-0 shadow-xs border border-black/10" style={{ backgroundColor: isActive ? 'white' : item.color }} />
|
||||||
<span className="text-xs font-semibold" style={{ fontFamily: 'Inter, sans-serif' }}>{item.label}</span>
|
<span className="text-xs font-semibold" style={{ fontFamily: 'Inter, sans-serif' }}>{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -312,28 +311,28 @@ export default function Campus() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Blueprint Facilities Index */}
|
{/* Blueprint Facilities Index */}
|
||||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] p-6 md:p-8 mb-8 shadow-xs">
|
<div className="bg-white rounded-2xl border border-[#E9E5EE] p-6 md:p-8 mb-8 shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)]">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 mb-6">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Building2 className="w-5 h-5 text-[#F97316]" />
|
<Building2 className="w-5 h-5 text-[#FF6B00]" />
|
||||||
<h3 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>Campus Blocks & Facilities Index</h3>
|
<h3 className="text-xl font-bold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Campus Blocks & Facilities Index</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[#64748B] mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>Click any facility card below to spot its exact location on the campus map image.</p>
|
<p className="text-xs text-[#9E91B6] mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>Click any facility card below to spot its exact location on the campus map image.</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-semibold text-[#F97316] bg-orange-50 px-3.5 py-1.5 rounded-full border border-orange-200">{filteredFacilities.length} Locations Mapped</span>
|
<span className="text-xs font-semibold text-[#FF6B00] bg-orange-50 px-3.5 py-1.5 rounded-full border border-orange-200">{filteredFacilities.length} Locations Mapped</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
{filteredFacilities.map((facility) => {
|
{filteredFacilities.map((facility) => {
|
||||||
const isSelected = selectedFacility?.name === facility.name;
|
const isSelected = selectedFacility?.name === facility.name;
|
||||||
const isHighlighted = highlightedSpotNames.has(facility.name);
|
const isHighlighted = highlightedSpotNames.has(facility.name);
|
||||||
return (
|
return (
|
||||||
<motion.button key={facility.name} whileHover={{ scale: 1.02, y: -2 }} whileTap={{ scale: 0.98 }} onClick={() => { if (isSelected) setSelectedFacility(null); else { setSelectedFacility(facility); setActiveCategory(null); scrollToMap(); } }} className={`text-left p-4 rounded-2xl border transition-all cursor-pointer ${isSelected ? 'border-[#F97316] bg-orange-500 text-white shadow-lg ring-2 ring-orange-400/40' : isHighlighted ? 'border-[#F97316] bg-orange-50/90 shadow-md' : 'border-[#E5E7EB] bg-[#F8FAFC] hover:bg-white hover:border-orange-200 hover:shadow-xs'}`}>
|
<motion.button key={facility.name} whileHover={{ scale: 1.02, y: -2 }} whileTap={{ scale: 0.98 }} onClick={() => { if (isSelected) setSelectedFacility(null); else { setSelectedFacility(facility); setActiveCategory(null); scrollToMap(); } }} className={`text-left p-4 rounded-2xl border transition-all cursor-pointer ${isSelected ? 'border-[#FF6B00] bg-orange-500 text-white shadow-lg ring-2 ring-orange-400/40' : isHighlighted ? 'border-[#FF6B00] bg-orange-50/90 shadow-md' : 'border-[#E9E5EE] bg-[#FAF9FC] hover:bg-white hover:border-orange-200 hover:shadow-xs'}`}>
|
||||||
<div className="flex items-start justify-between gap-1 mb-1.5">
|
<div className="flex items-start justify-between gap-1 mb-1.5">
|
||||||
<span className={`text-sm font-bold ${isSelected ? 'text-white' : 'text-[#1E293B]'}`} style={{ fontFamily: 'Poppins, sans-serif' }}>{facility.name}</span>
|
<span className={`text-sm font-bold ${isSelected ? 'text-white' : 'text-[#1A0B2E]'}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{facility.name}</span>
|
||||||
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full shrink-0 border ${isSelected ? 'bg-white/20 border-white/30 text-white' : 'bg-white border-[#E5E7EB] text-[#475569]'}`}>{facility.area}</span>
|
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full shrink-0 border ${isSelected ? 'bg-white/20 border-white/30 text-white' : 'bg-white border-[#E9E5EE] text-[#4A3E5E]'}`}>{facility.area}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-xs line-clamp-1 ${isSelected ? 'text-orange-100' : 'text-[#64748B]'}`} style={{ fontFamily: 'Inter, sans-serif' }}>{facility.desc}</p>
|
<p className={`text-xs line-clamp-1 ${isSelected ? 'text-orange-100' : 'text-[#9E91B6]'}`} style={{ fontFamily: 'Inter, sans-serif' }}>{facility.desc}</p>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { getBackendUrl } from '@/lib/utils';
|
|||||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
|
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
|
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library', 'cat exam', 'semester', 'labs', 'exams'];
|
const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library', 'cat exam', 'semester', 'labs', 'exams'];
|
||||||
|
|
||||||
const AVATARS: Record<string, string> = {
|
const AVATARS: Record<string, string> = {
|
||||||
@@ -261,24 +263,24 @@ export default function Community() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#FAFBFD' }}>
|
<div className="min-h-screen bg-[#FAF9FC]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white border-b border-slate-100 py-12">
|
<div className="bg-white border-b border-[#E9E5EE] py-12">
|
||||||
<div className="container-custom">
|
<div className="container-custom">
|
||||||
<h1 className="text-3xl md:text-4xl font-bold mb-2 tracking-tight" style={{ fontFamily: 'Playfair Display, serif', color: '#1E293B' }}>
|
<h1 className="text-3xl md:text-4xl font-extrabold mb-2 tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif', color: '#1A0B2E' }}>
|
||||||
RIT Community
|
RIT Community
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-500 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-[#4A3E5E] text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Ask questions and connect with fellow RIT students.
|
Ask questions and connect with fellow RIT students.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container-custom py-10">
|
<div className="container-custom pt-10 pb-20 md:pb-28">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
<div className="lg:col-span-2"> {/* Ask Question Box */}
|
<div className="lg:col-span-2"> {/* Ask Question Box */}
|
||||||
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-6 mb-8" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
|
<div className="bg-white border border-[#E9E5EE] rounded-2xl p-6 mb-8 shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)]">
|
||||||
<h3 className="text-base font-semibold text-[#1E293B] mb-3 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Ask a Question</h3>
|
<h3 className="text-base font-bold text-[#1A0B2E] mb-3 tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Ask a Question</h3>
|
||||||
|
|
||||||
{showSuccessBanner && (
|
{showSuccessBanner && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -452,7 +454,7 @@ export default function Community() {
|
|||||||
|
|
||||||
<h3 className={`font-bold text-[#1E293B] mb-1 hover:text-[#F97316] transition-colors tracking-tight ${isFeatured ? 'text-base md:text-lg' : 'text-sm md:text-base'
|
<h3 className={`font-bold text-[#1E293B] mb-1 hover:text-[#F97316] transition-colors tracking-tight ${isFeatured ? 'text-base md:text-lg' : 'text-sm md:text-base'
|
||||||
}`}
|
}`}
|
||||||
style={{ fontFamily: 'Playfair Display, serif' }}>
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{q.title}
|
{q.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export interface CollabRequestItem {
|
|||||||
acceptedCount?: number;
|
acceptedCount?: number;
|
||||||
contactInfo?: string;
|
contactInfo?: string;
|
||||||
telegramChatId?: number;
|
telegramChatId?: number;
|
||||||
discordUserId?: string;
|
|
||||||
status: string;
|
status: string;
|
||||||
applicationsCount: number;
|
applicationsCount: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -432,23 +431,23 @@ export default function DevCollab() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
||||||
<div className="container-custom">
|
<div className="container-custom">
|
||||||
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
|
<div className="flex items-center gap-2 text-xs text-[#9E91B6] mb-3">
|
||||||
<Link to="/" className="hover:text-indigo-600">Home</Link>
|
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
|
||||||
<ChevronRight className="w-3 h-3" />
|
<ChevronRight className="w-3 h-3" />
|
||||||
<span className="text-indigo-600">Developer Collab Hub</span>
|
<span className="text-[#FF6B00] font-semibold">Developer Collab Hub</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-indigo-50 border border-indigo-200 text-indigo-600 text-xs font-semibold mb-3">
|
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-[#FF6B00] text-xs font-bold mb-3">
|
||||||
<Code2 className="w-4 h-4 text-indigo-600" /> RIT Developers Environment
|
<Code2 className="w-4 h-4" /> RIT Developers Environment
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h1 className="text-3xl md:text-4xl font-extrabold text-[#1A0B2E] mb-2" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
Find Project{' '}
|
Find Project{' '}
|
||||||
<span style={{ background: 'linear-gradient(135deg, #4F46E5, #6366F1)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
<span style={{ background: 'linear-gradient(135deg, #FF6B00, #F97316)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
Collaborators
|
Collaborators
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[#475569] text-sm max-w-2xl" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-[#4A3E5E] text-sm max-w-2xl" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Post open-source projects, search for co-developers, or manage candidate applications cleanly on the website or via Telegram!
|
Post open-source projects, search for co-developers, or manage candidate applications cleanly on the website or via Telegram!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -457,8 +456,8 @@ export default function DevCollab() {
|
|||||||
whileHover={{ scale: 1.03 }}
|
whileHover={{ scale: 1.03 }}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={() => requireVerifiedStudent(() => setShowPostModal(true))}
|
onClick={() => requireVerifiedStudent(() => setShowPostModal(true))}
|
||||||
className="flex items-center gap-2.5 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm shadow-lg shadow-indigo-500/25 shrink-0 cursor-pointer"
|
className="flex items-center gap-2.5 px-6 py-3.5 rounded-xl text-white font-bold text-sm shadow-lg shadow-orange-500/25 shrink-0 cursor-pointer"
|
||||||
style={{ background: 'linear-gradient(135deg, #4F46E5, #6366F1)', fontFamily: 'Poppins, sans-serif' }}
|
style={{ background: 'linear-gradient(135deg, #FF6B00, #F97316)', fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
{isVerifiedStudent && <ShieldCheck className="w-4 h-4" />}
|
{isVerifiedStudent && <ShieldCheck className="w-4 h-4" />}
|
||||||
<Plus className="w-4.5 h-4.5" />
|
<Plus className="w-4.5 h-4.5" />
|
||||||
@@ -467,15 +466,15 @@ export default function DevCollab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dashboard Tabs Bar */}
|
{/* Dashboard Tabs Bar */}
|
||||||
<div className="flex items-center gap-3 mt-8 border-b border-slate-200 overflow-x-auto pb-px">
|
<div className="flex items-center gap-3 mt-8 border-b border-[#E9E5EE] overflow-x-auto pb-px">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('explore')}
|
onClick={() => setActiveTab('explore')}
|
||||||
className={`px-5 py-3 text-xs font-bold transition-all border-b-2 flex items-center gap-2 cursor-pointer ${
|
className={`px-5 py-3 text-xs font-bold transition-all border-b-2 flex items-center gap-2 cursor-pointer ${
|
||||||
activeTab === 'explore'
|
activeTab === 'explore'
|
||||||
? 'border-[#F97316] text-[#F97316]'
|
? 'border-[#FF6B00] text-[#FF6B00]'
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-800'
|
: 'border-transparent text-purple-400 hover:text-[#1A0B2E]'
|
||||||
}`}
|
}`}
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
<Code2 className="w-4 h-4" />
|
<Code2 className="w-4 h-4" />
|
||||||
Explore Projects ({requests.length})
|
Explore Projects ({requests.length})
|
||||||
|
|||||||
@@ -1,36 +1,156 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Search, Users, Filter, ArrowUpDown, BookOpen, ChevronRight, AlertTriangle } from 'lucide-react';
|
import { Search, Users, Filter, ArrowUpDown, BookOpen, ChevronRight, Sparkles, X } from 'lucide-react';
|
||||||
import FacultyCard from '@/components/FacultyCard/FacultyCard';
|
import FacultyCard from '@/components/FacultyCard/FacultyCard';
|
||||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
import { FACULTY_DATA, DEPARTMENTS } from '@/constants';
|
import { FACULTY_DATA, DEPARTMENTS } from '@/constants';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
|
import { getStoredImsSession } from '@/services/imsService';
|
||||||
|
|
||||||
|
const DEPT_MAP: Record<string, { abrv: string; full: string }> = {
|
||||||
|
'Computer Science & Engineering': { abrv: 'CSE', full: 'Computer Science & Engineering' },
|
||||||
|
'Computer Science & Business Systems': { abrv: 'CSBS', full: 'Computer Science & Business Systems' },
|
||||||
|
'Artificial Intelligence & Machine Learning': { abrv: 'AIML', full: 'Artificial Intelligence & Machine Learning' },
|
||||||
|
'Electronics & Communication Engineering': { abrv: 'ECE', full: 'Electronics & Communication Engineering' },
|
||||||
|
'Mechanical Engineering': { abrv: 'MECH', full: 'Mechanical Engineering' },
|
||||||
|
'Civil Engineering': { abrv: 'CIVIL', full: 'Civil Engineering' },
|
||||||
|
'Artificial Intelligence & Data Science': { abrv: 'AI & DS', full: 'Artificial Intelligence & Data Science' },
|
||||||
|
'Electrical & Electronics Engineering': { abrv: 'EEE', full: 'Electrical & Electronics Engineering' },
|
||||||
|
'Communication and Computer Engineering': { abrv: 'CCE', full: 'Communication and Computer Engineering' },
|
||||||
|
'Electronics Engineering VLSI (Design and Technology)': { abrv: 'EE VLSI (D&T)', full: 'Electronics Engineering VLSI (Design and Technology)' },
|
||||||
|
'Humanities & Sciences': { abrv: 'H&S', full: 'Humanities & Sciences' },
|
||||||
|
'M.Tech (Data Science)': { abrv: 'M.TECH-DS', full: 'M.Tech (Data Science)' },
|
||||||
|
'Mathematics': { abrv: 'MATHS', full: 'Mathematics' },
|
||||||
|
'M.E. (VLSI Design)': { abrv: 'MEVLSI', full: 'M.E. (VLSI Design)' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeDepartmentName(input?: string | null): string | null {
|
||||||
|
if (!input) return null;
|
||||||
|
const cleaned = input.trim();
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (DEPARTMENTS.includes(cleaned)) return cleaned;
|
||||||
|
|
||||||
|
// Search by abbreviation or substring
|
||||||
|
const upper = cleaned.toUpperCase();
|
||||||
|
for (const [full, info] of Object.entries(DEPT_MAP)) {
|
||||||
|
if (info.abrv.toUpperCase() === upper || full.toUpperCase() === upper || full.toLowerCase().includes(cleaned.toLowerCase())) {
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Faculty() {
|
export default function Faculty() {
|
||||||
const [searchFaculty, setSearchFaculty] = useState('');
|
const [searchParams] = useSearchParams();
|
||||||
const [selectedDept, setSelectedDept] = useState('All Departments');
|
const [searchFaculty, setSearchFaculty] = useState(() => searchParams.get('search') || '');
|
||||||
const [sortBy, setSortBy] = useState('Name A-Z');
|
const [sortBy, setSortBy] = useState('Name A-Z');
|
||||||
|
|
||||||
|
// Initial department detection (from URL query string or logged-in IMS student session)
|
||||||
|
const initialDepartment = useMemo(() => {
|
||||||
|
if (searchParams.has('search')) return 'All Departments';
|
||||||
|
|
||||||
|
const queryDept = searchParams.get('dept');
|
||||||
|
const matchedQuery = normalizeDepartmentName(queryDept);
|
||||||
|
if (matchedQuery) return matchedQuery;
|
||||||
|
|
||||||
|
const session = getStoredImsSession();
|
||||||
|
if (session?.student?.department) {
|
||||||
|
const matchedSession = normalizeDepartmentName(session.student.department);
|
||||||
|
if (matchedSession) return matchedSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'All Departments';
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const [selectedDept, setSelectedDept] = useState<string>(initialDepartment);
|
||||||
|
|
||||||
|
// Update selectedDept and searchFaculty if URL parameter changes
|
||||||
|
useEffect(() => {
|
||||||
|
const querySearch = searchParams.get('search');
|
||||||
|
if (querySearch !== null) {
|
||||||
|
setSearchFaculty(querySearch);
|
||||||
|
setSelectedDept('All Departments');
|
||||||
|
} else {
|
||||||
|
setSelectedDept(initialDepartment);
|
||||||
|
}
|
||||||
|
}, [searchParams, initialDepartment]);
|
||||||
|
|
||||||
|
const studentSession = useMemo(() => getStoredImsSession(), []);
|
||||||
|
const isStudentDeptAutoFiltered = selectedDept !== 'All Departments' && (
|
||||||
|
selectedDept === studentSession?.student?.department ||
|
||||||
|
searchParams.has('dept')
|
||||||
|
);
|
||||||
|
|
||||||
|
const [facultyList, setFacultyList] = useState<any[]>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('RIT_LOCAL_FACULTY');
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return FACULTY_DATA;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(getBackendUrl('/api/faculty'))
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
setFacultyList(data);
|
||||||
|
localStorage.setItem('RIT_LOCAL_FACULTY', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredAndSortedFaculty = useMemo(() => {
|
const filteredAndSortedFaculty = useMemo(() => {
|
||||||
const filtered = FACULTY_DATA.filter((f) => {
|
const filtered = facultyList.filter((f) => {
|
||||||
const searchLower = searchFaculty.trim().toLowerCase();
|
const searchLower = searchFaculty.trim().toLowerCase();
|
||||||
const matchSearch =
|
const matchSearch =
|
||||||
f.name.toLowerCase().includes(searchLower) ||
|
(f.name && f.name.toLowerCase().includes(searchLower)) ||
|
||||||
f.department.toLowerCase().includes(searchLower) ||
|
(f.department && f.department.toLowerCase().includes(searchLower)) ||
|
||||||
f.designation.toLowerCase().includes(searchLower) ||
|
(f.designation && f.designation.toLowerCase().includes(searchLower)) ||
|
||||||
(f.specialization && f.specialization.toLowerCase().includes(searchLower));
|
(f.specialization && f.specialization.toLowerCase().includes(searchLower)) ||
|
||||||
|
(f.interest && f.interest.toLowerCase().includes(searchLower));
|
||||||
|
|
||||||
const matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
|
const matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
|
||||||
return matchSearch && matchDept;
|
return matchSearch && matchDept;
|
||||||
});
|
});
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
return filtered.sort((a, b) => {
|
||||||
if (sortBy === 'Name A-Z') return a.name.localeCompare(b.name);
|
if (sortBy === 'Name A-Z') return (a.name || '').localeCompare(b.name || '');
|
||||||
if (sortBy === 'Department') return a.department.localeCompare(b.department);
|
if (sortBy === 'Department') return (a.department || '').localeCompare(b.department || '');
|
||||||
if (sortBy === 'Designation') return a.designation.localeCompare(b.designation);
|
if (sortBy === 'Designation') return (a.designation || '').localeCompare(b.designation || '');
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}, [searchFaculty, selectedDept, sortBy]);
|
}, [facultyList, searchFaculty, selectedDept, sortBy]);
|
||||||
|
|
||||||
|
const groupedFaculty = useMemo(() => {
|
||||||
|
const groups: Record<string, typeof filteredAndSortedFaculty> = {};
|
||||||
|
filteredAndSortedFaculty.forEach((faculty) => {
|
||||||
|
if (!groups[faculty.department]) {
|
||||||
|
groups[faculty.department] = [];
|
||||||
|
}
|
||||||
|
groups[faculty.department].push(faculty);
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}, [filteredAndSortedFaculty]);
|
||||||
|
|
||||||
|
const departmentOrder = useMemo(() => {
|
||||||
|
const presentDepts = Object.keys(groupedFaculty);
|
||||||
|
const sorted = DEPARTMENTS.filter(d => d !== 'All Departments');
|
||||||
|
presentDepts.forEach(d => {
|
||||||
|
if (!sorted.includes(d)) sorted.push(d);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}, [groupedFaculty]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative overflow-hidden bg-[#F8FAFC]">
|
<div className="min-h-screen relative overflow-hidden bg-[#F8FAFC]">
|
||||||
@@ -44,58 +164,67 @@ export default function Faculty() {
|
|||||||
|
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
<div className="bg-white border-b border-[#E9E5EE] py-10">
|
||||||
<div className="container-custom">
|
<div className="container-custom">
|
||||||
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
|
<div className="flex items-center gap-2 text-xs text-[#9E91B6] mb-3">
|
||||||
<Link to="/" className="hover:text-[#F97316]">Home</Link>
|
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
|
||||||
<ChevronRight className="w-3 h-3" />
|
<ChevronRight className="w-3 h-3" />
|
||||||
<span className="text-[#F97316]">Faculty Directory</span>
|
<span className="text-[#FF6B00] font-semibold">Faculty Directory</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h1 className="text-3xl md:text-4xl font-extrabold text-[#1A0B2E] mb-2" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
Faculty{' '}
|
Faculty{' '}
|
||||||
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
<span style={{ background: 'linear-gradient(135deg, #FF6B00, #F97316)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
Directory
|
Directory
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[#475569] text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-[#4A3E5E] text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Browse and connect with experienced faculty members across all departments.
|
Browse and connect with experienced faculty members across all departments.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container-custom py-8 space-y-8">
|
<div className="container-custom pt-8 pb-20 md:pb-28 space-y-6">
|
||||||
{/* Under Development Banner */}
|
|
||||||
|
{/* Active Student Department Filter Badge */}
|
||||||
|
{isStudentDeptAutoFiltered && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="bg-amber-50 border border-amber-200 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-amber-900 shadow-2xs"
|
className="bg-orange-50 border border-orange-200 rounded-2xl p-4 flex items-center justify-between gap-3 text-orange-950 shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center shrink-0">
|
||||||
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
<Sparkles className="w-5 h-5 text-[#FF6B00]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-bold uppercase tracking-wider text-amber-700" style={{ fontFamily: 'Inter, sans-serif' }}>Under Development</div>
|
<div className="text-xs font-bold uppercase tracking-wider text-orange-700">Student Department Auto-Filter</div>
|
||||||
<div className="text-xs font-medium text-amber-900" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<div className="text-xs font-medium text-orange-950">
|
||||||
Currently displaying sample mock faculty profiles. Official RIT faculty directory integration is in progress.
|
Showing faculty for your department: <strong>{selectedDept}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="px-3 py-1 rounded-full bg-amber-100 border border-amber-300 text-[11px] font-bold text-amber-800 shrink-0">
|
<button
|
||||||
Mock Data Active
|
onClick={() => setSelectedDept('All Departments')}
|
||||||
</span>
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white border border-orange-300 text-xs font-bold text-orange-800 hover:bg-orange-100 transition-colors cursor-pointer shrink-0"
|
||||||
|
>
|
||||||
|
<span>View All Departments</span>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Faculty Hero Section */}
|
{/* Faculty Hero Section */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||||
<div className="lg:col-span-5 bg-white rounded-3xl p-8 border border-[#E8ECF4] shadow-sm flex flex-col justify-center relative overflow-hidden">
|
<div className="lg:col-span-5 bg-white rounded-2xl p-8 border border-[#E9E5EE] shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)] flex flex-col justify-center relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-bl-full -z-10" />
|
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-bl-full -z-10" />
|
||||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#FF7A00] to-[#FB923C] flex items-center justify-center text-white mb-6 shadow-md">
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] flex items-center justify-center text-white mb-6 shadow-md shadow-orange-500/20">
|
||||||
<Users className="w-7 h-7" />
|
<Users className="w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-[#1E293B] mb-3" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
<h2 className="text-3xl font-extrabold text-[#1A0B2E] mb-3" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
Faculty Directory
|
Faculty Directory
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#64748B] text-xs leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-[#4A3E5E] text-xs leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Browse and connect with experienced faculty members. Search by name, department, or specialization.
|
Browse and connect with experienced faculty members. Search by name, department, or specialization.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,16 +241,31 @@ export default function Faculty() {
|
|||||||
{ abrv: 'CIVIL', full: 'Civil Engineering', color: 'emerald' },
|
{ abrv: 'CIVIL', full: 'Civil Engineering', color: 'emerald' },
|
||||||
{ abrv: 'AI & DS', full: 'Artificial Intelligence & Data Science', color: 'pink' },
|
{ abrv: 'AI & DS', full: 'Artificial Intelligence & Data Science', color: 'pink' },
|
||||||
{ abrv: 'EEE', full: 'Electrical & Electronics Engineering', color: 'yellow' },
|
{ abrv: 'EEE', full: 'Electrical & Electronics Engineering', color: 'yellow' },
|
||||||
].map(d => {
|
{ abrv: 'CCE', full: 'Communication and Computer Engineering', color: 'rose' },
|
||||||
|
{ abrv: 'EE VLSI (D&T)', full: 'Electronics Engineering VLSI (Design and Technology)', color: 'teal' },
|
||||||
|
{ abrv: 'H&S', full: 'Humanities & Sciences', color: 'indigo' },
|
||||||
|
{ abrv: 'M.TECH-DS', full: 'M.Tech (Data Science)', color: 'fuchsia' },
|
||||||
|
{ abrv: 'MATHS', full: 'Mathematics', color: 'blue' },
|
||||||
|
{ abrv: 'MEVLSI', full: 'M.E. (VLSI Design)', color: 'rose' },
|
||||||
|
].filter(d => FACULTY_DATA.filter(f => f.department === d.full).length > 0).map(d => {
|
||||||
const count = FACULTY_DATA.filter(f => f.department === d.full).length;
|
const count = FACULTY_DATA.filter(f => f.department === d.full).length;
|
||||||
|
const isSelected = selectedDept === d.full;
|
||||||
return (
|
return (
|
||||||
<div key={d.abrv} className={`p-4 rounded-2xl bg-${d.color}-50 border border-${d.color}-100 flex flex-col gap-1 transition-transform hover:scale-105 cursor-default`}>
|
<button
|
||||||
|
key={d.abrv}
|
||||||
|
onClick={() => setSelectedDept(d.full)}
|
||||||
|
className={`p-4 rounded-2xl border flex flex-col gap-1 transition-all text-left cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-[#FF6B00] text-white border-[#FF6B00] shadow-md'
|
||||||
|
: `bg-${d.color}-50 border-${d.color}-100 hover:scale-102`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className={`text-xs font-bold text-${d.color}-700`} style={{ fontFamily: 'Poppins, sans-serif' }}>{d.abrv}</span>
|
<span className={`text-xs font-bold ${isSelected ? 'text-white' : `text-${d.color}-700`}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{d.abrv}</span>
|
||||||
<BookOpen className={`w-3.5 h-3.5 text-${d.color}-400`} />
|
<BookOpen className={`w-3.5 h-3.5 ${isSelected ? 'text-white/80' : `text-${d.color}-400`}`} />
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>{count}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className={`text-2xl font-bold ${isSelected ? 'text-white' : 'text-[#1E293B]'}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{count}</span>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +278,7 @@ export default function Faculty() {
|
|||||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#94A3B8]" />
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#94A3B8]" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by faculty name, department, designation or specialization..."
|
placeholder="Search by faculty name, department, designation or interest..."
|
||||||
value={searchFaculty}
|
value={searchFaculty}
|
||||||
onChange={(e) => setSearchFaculty(e.target.value)}
|
onChange={(e) => setSearchFaculty(e.target.value)}
|
||||||
className="w-full pl-12 pr-4 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm text-[#1E293B] placeholder-[#94A3B8] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all"
|
className="w-full pl-12 pr-4 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm text-[#1E293B] placeholder-[#94A3B8] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all"
|
||||||
@@ -150,7 +294,7 @@ export default function Faculty() {
|
|||||||
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
|
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
|
||||||
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
|
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
|
||||||
>
|
>
|
||||||
{DEPARTMENTS.map((dept) => {
|
{Array.from(new Set(['All Departments', ...DEPARTMENTS, ...FACULTY_DATA.map(f => f.department)])).filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => {
|
||||||
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
|
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
|
||||||
return <option key={dept} value={dept}>{dept} ({count})</option>;
|
return <option key={dept} value={dept}>{dept} ({count})</option>;
|
||||||
})}
|
})}
|
||||||
@@ -172,15 +316,38 @@ export default function Faculty() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid */}
|
{/* Grouped Grid */}
|
||||||
{filteredAndSortedFaculty.length > 0 ? (
|
{filteredAndSortedFaculty.length > 0 ? (
|
||||||
<StaggerContainer key={`${searchFaculty}-${selectedDept}-${sortBy}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-20">
|
<div className="space-y-12 pb-20">
|
||||||
{filteredAndSortedFaculty.map((faculty) => (
|
{departmentOrder.map((deptName) => {
|
||||||
|
const facultyInDept = groupedFaculty[deptName];
|
||||||
|
if (!facultyInDept || facultyInDept.length === 0) return null;
|
||||||
|
const deptInfo = DEPT_MAP[deptName] || { abrv: deptName, full: deptName };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={deptName} className="space-y-6">
|
||||||
|
{/* Department Header */}
|
||||||
|
<div className="border-l-4 border-[#FF6B00] pl-4 py-1">
|
||||||
|
<h2 className="text-xl font-bold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
{deptInfo.abrv}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-[#9E91B6] font-semibold mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
{deptInfo.full}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Department Grid */}
|
||||||
|
<StaggerContainer key={`${deptName}-${searchFaculty}-${selectedDept}-${sortBy}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{facultyInDept.map((faculty) => (
|
||||||
<StaggerItem key={faculty.id}>
|
<StaggerItem key={faculty.id}>
|
||||||
<FacultyCard faculty={faculty} />
|
<FacultyCard faculty={faculty} />
|
||||||
</StaggerItem>
|
</StaggerItem>
|
||||||
))}
|
))}
|
||||||
</StaggerContainer>
|
</StaggerContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-20 text-center flex flex-col items-center justify-center bg-white rounded-3xl border border-[#E8ECF4] shadow-sm">
|
<div className="py-20 text-center flex flex-col items-center justify-center bg-white rounded-3xl border border-[#E8ECF4] shadow-sm">
|
||||||
<Users className="w-12 h-12 text-[#CBD5E1] mb-4" />
|
<Users className="w-12 h-12 text-[#CBD5E1] mb-4" />
|
||||||
@@ -194,7 +361,7 @@ export default function Faculty() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="px-6 py-3 bg-white/80 backdrop-blur-md border border-[#E8ECF4] shadow-[0_8px_30px_rgb(0,0,0,0.08)] rounded-full flex items-center gap-2"
|
className="px-6 py-3 bg-white/80 backdrop-blur-md border border-[#E8ECF4] shadow-[0_8px_30px_rgb(0,0,0,0.08)] rounded-full flex items-center gap-2 pointer-events-auto"
|
||||||
>
|
>
|
||||||
<span className="text-lg">👥</span>
|
<span className="text-lg">👥</span>
|
||||||
<span className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<span className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
|||||||
@@ -1,93 +1,159 @@
|
|||||||
|
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
BookOpen,
|
ArrowRight, BookOpen, Bot, GraduationCap,
|
||||||
Bot,
|
CheckCircle, Zap, Shield, Trophy, Award, Cpu, Users, Sparkles
|
||||||
Bus,
|
|
||||||
Calendar,
|
|
||||||
Code2,
|
|
||||||
Trophy
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
||||||
|
import FeatureCard from '@/components/FeatureCard/FeatureCard';
|
||||||
|
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
|
import { FEATURES } from '@/constants';
|
||||||
|
|
||||||
|
import LazyVideoHero from '@/components/LazyVideoHero/LazyVideoHero';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const quickLaunchModules = [
|
const { scrollY } = useScroll();
|
||||||
{ label: 'Bus Routes', path: '/bus-routes', icon: Bus, desc: 'Schedules & Stops' },
|
const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]);
|
||||||
{ label: 'Notes & PYQs', path: '/notes', icon: BookOpen, desc: 'Moodle Materials' },
|
const heroY = useTransform(scrollY, [0, 450], [0, -60]);
|
||||||
{ label: 'Campus Chatbot', path: '/ai-assistant', icon: Bot, desc: 'Instant Q&A' },
|
|
||||||
{ label: 'Student Clubs', path: '/events', icon: Calendar, desc: 'Clubs & Events' },
|
|
||||||
{ label: 'LeetCode Ranking', path: '/leetcode', icon: Trophy, desc: 'Coder Leaderboard' },
|
|
||||||
{ label: 'Dev Collab', path: '/collab', icon: Code2, desc: 'Co-Developers' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden rounded-3xl h-[calc(100vh-120px)] min-h-[500px] flex flex-col justify-between p-6 sm:p-10 text-white shadow-2xl border border-slate-800 bg-[#0F172A]">
|
<div className="relative bg-[#FAFAFA] min-h-screen overflow-hidden">
|
||||||
{/* Background Video (Low Bitrate & AutoPlay Muted) */}
|
{/* ─── Hero Section with IntersectionObserver Lazy Video ─────────────────── */}
|
||||||
<video
|
<LazyVideoHero>
|
||||||
src="/campus-video.mp4"
|
<section className="relative pt-20 pb-24 z-10">
|
||||||
autoPlay
|
<motion.div
|
||||||
loop
|
className="container-custom relative z-20 flex flex-col items-center text-center px-4"
|
||||||
muted
|
style={{ opacity: heroOpacity, y: heroY }}
|
||||||
playsInline
|
>
|
||||||
preload="auto"
|
{/* Heading */}
|
||||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
<motion.h1
|
||||||
/>
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className="text-3xl md:text-5xl font-medium tracking-wide mb-2"
|
||||||
|
style={{ color: '#FFFFFF', fontFamily: 'Plus Jakarta Sans, sans-serif', textShadow: '0 2px 14px rgba(0,0,0,0.95)' }}
|
||||||
|
>
|
||||||
|
Welcome to
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
{/* Clean Dark Vignette Overlay */}
|
<motion.h2
|
||||||
<div className="absolute inset-0 bg-slate-950/45 backdrop-blur-[1px]" />
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
{/* Top Branding Badge */}
|
transition={{ duration: 0.7, delay: 0.2 }}
|
||||||
<div className="relative z-10 text-center sm:text-left">
|
className="text-5xl md:text-7xl font-extrabold tracking-tight leading-tight mb-6"
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-black/40 border border-white/20 text-xs font-semibold text-white backdrop-blur-md">
|
style={{ color: '#FFFFFF', fontFamily: 'Plus Jakarta Sans, sans-serif', textShadow: '0 4px 24px rgba(0,0,0,0.95)' }}
|
||||||
<span>Rajalakshmi Institute of Technology</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Center Main Hero Title */}
|
|
||||||
<div className="relative z-10 max-w-3xl mx-auto text-center space-y-4 my-auto py-4">
|
|
||||||
<h1
|
|
||||||
className="text-4xl sm:text-6xl font-black tracking-tight leading-tight text-white drop-shadow-[0_4px_24px_rgba(0,0,0,0.95)]"
|
|
||||||
style={{ fontFamily: 'Playfair Display, serif', color: '#FFFFFF', textShadow: '0 4px 24px rgba(0,0,0,0.95)' }}
|
|
||||||
>
|
>
|
||||||
RIT Freshers Hub
|
RIT Freshers Hub
|
||||||
</h1>
|
</motion.h2>
|
||||||
<p
|
|
||||||
className="text-sm sm:text-base text-slate-100 font-medium leading-relaxed max-w-2xl mx-auto drop-shadow-md"
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.7, delay: 0.3 }}
|
||||||
|
className="max-w-2xl text-slate-100 text-base md:text-lg font-medium leading-relaxed mb-8 drop-shadow-md"
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
>
|
>
|
||||||
Centralized campus portal — academic notes, chatbot, bus routes, faculty directory & student developer tools.
|
Your centralized gateway to campus life at Rajalakshmi Institute of Technology — academic notes, AI assistant, bus routes, faculty directory, and campus navigation.
|
||||||
</p>
|
</motion.p>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Transparent Glassmorphic Quick Launch Bar */}
|
{/* Call to Actions */}
|
||||||
<div className="relative z-10 pt-2">
|
<motion.div
|
||||||
<div className="text-center mb-3">
|
initial={{ opacity: 0, y: 15 }}
|
||||||
<span className="text-[11px] font-bold text-slate-300 uppercase tracking-widest bg-black/50 px-3.5 py-1.5 rounded-full border border-white/15 backdrop-blur-md">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
Quick Launch Portal Modules
|
transition={{ duration: 0.7, delay: 0.4 }}
|
||||||
</span>
|
className="flex flex-wrap justify-center gap-4"
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4">
|
|
||||||
{quickLaunchModules.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.label}
|
|
||||||
to={item.path}
|
|
||||||
className="flex flex-col items-center justify-center p-3.5 sm:p-4 rounded-2xl bg-white/10 hover:bg-white/25 border border-white/20 backdrop-blur-md text-white transition-all cursor-pointer hover:scale-[1.04] shadow-lg group text-center"
|
|
||||||
>
|
>
|
||||||
<div className="w-9 h-9 rounded-xl bg-white/20 flex items-center justify-center mb-2 group-hover:bg-indigo-600 transition-colors">
|
<a href="#built-for-freshers">
|
||||||
<Icon className="w-4 h-4 text-white" />
|
<motion.button
|
||||||
</div>
|
whileHover={{ scale: 1.04, boxShadow: '0 10px 30px -5px rgba(255,107,0,0.4)' }}
|
||||||
<span className="text-xs sm:text-sm font-bold truncate max-w-full" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
whileTap={{ scale: 0.96 }}
|
||||||
{item.label}
|
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-bold text-sm cursor-pointer shadow-md"
|
||||||
</span>
|
style={{
|
||||||
<span className="text-[10px] sm:text-[11px] text-slate-300 truncate max-w-full">
|
fontFamily: 'Plus Jakarta Sans, sans-serif',
|
||||||
{item.desc}
|
background: 'linear-gradient(135deg, #FF6B00, #F97316)',
|
||||||
</span>
|
}}
|
||||||
|
>
|
||||||
|
<BookOpen className="w-4 h-4" />
|
||||||
|
Explore Hub
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</motion.button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<Link to="/ai-assistant">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.04, borderColor: '#FF6B00', backgroundColor: 'rgba(255,255,255,0.2)' }}
|
||||||
|
whileTap={{ scale: 0.96 }}
|
||||||
|
className="flex items-center gap-2 px-8 py-3.5 rounded-xl font-bold text-sm border-2 border-white/40 text-white bg-black/30 backdrop-blur-md transition-all cursor-pointer shadow-2xs"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
<Bot className="w-4 h-4 text-[#FF6B00]" />
|
||||||
|
Ask Assistant
|
||||||
|
</motion.button>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
</motion.div>
|
||||||
})}
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
</LazyVideoHero>
|
||||||
|
|
||||||
|
{/* Sleek Stats Card Section */}
|
||||||
|
<section className="relative z-30 -mt-6 px-4">
|
||||||
|
<div className="container-custom max-w-5xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.6 }}
|
||||||
|
className="rounded-2xl border border-[#E9E5EE] p-6 md:p-8 bg-white shadow-[0_4px_20px_-4px_rgba(19,9,36,0.05)]"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-6 text-center">
|
||||||
|
{[
|
||||||
|
{ value: '20+', label: 'SPORTS', icon: Trophy },
|
||||||
|
{ value: '100+', label: 'FACULTY', icon: GraduationCap },
|
||||||
|
{ value: '33+', label: 'CLUBS & CENTERS', icon: Award },
|
||||||
|
{ value: '24/7', label: 'ASSISTANT', icon: Cpu },
|
||||||
|
{ value: '5000+', label: 'STUDENTS', icon: Users },
|
||||||
|
].map((stat, i) => (
|
||||||
|
<div key={i} className="flex flex-col items-center justify-center p-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-[#FFF7ED] flex items-center justify-center mb-2.5">
|
||||||
|
<stat.icon className="w-5 h-5 text-[#FF6B00]" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-2xl md:text-3xl font-extrabold text-[#1A0B2E] mb-0.5"
|
||||||
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
|
>
|
||||||
|
{stat.value}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-[11px] font-bold tracking-wider text-[#9E91B6] uppercase"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
{stat.label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ─── Features Section ─────────────────────────────────────────────────── */}
|
||||||
|
<section id="built-for-freshers" className="section-padding relative z-20">
|
||||||
|
<div className="container-custom">
|
||||||
|
<SectionTitle
|
||||||
|
tag="Everything You Need"
|
||||||
|
title="Built for"
|
||||||
|
highlight="RIT Freshers"
|
||||||
|
subtitle="From notes to AI assistance — we've got everything you need to navigate campus life with confidence."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{FEATURES.map((feature) => (
|
||||||
|
<StaggerItem key={feature.id}>
|
||||||
|
<FeatureCard feature={feature} />
|
||||||
|
</StaggerItem>
|
||||||
|
))}
|
||||||
|
</StaggerContainer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Search, Download, Filter, BookOpen, FileText, ScrollText, BookMarked, ChevronRight, ArrowLeft, ExternalLink, Code, Globe, GitBranch, Cloud, Palette, Shield, DollarSign, BarChart3, Users, Megaphone, TrendingUp, UserCheck, ClipboardList, ShoppingCart, Cpu, Activity, LayoutGrid, Layers, Zap, Settings, Gauge, Radio, Wifi, Bot, Sun, Wrench, Binary, BatteryCharging, CircuitBoard, ShieldCheck, BrainCircuit, FlaskConical, Lock, Terminal, Dna, Microscope, Sparkles, Smartphone, Monitor, Server, CheckCircle2, Database, Briefcase, AlertTriangle, RefreshCw } from 'lucide-react';
|
import { Search, Download, Filter, BookOpen, FileText, ScrollText, BookMarked, ChevronRight, ArrowLeft, ExternalLink, Code, Globe, GitBranch, Cloud, Palette, Shield, DollarSign, BarChart3, Users, Megaphone, TrendingUp, UserCheck, ClipboardList, ShoppingCart, Cpu, Activity, LayoutGrid, Layers, Zap, Settings, Gauge, Radio, Wifi, Bot, Sun, Wrench, Binary, BatteryCharging, CircuitBoard, ShieldCheck, BrainCircuit, FlaskConical, Lock, Terminal, Dna, Microscope, Sparkles, Smartphone, Monitor, Server, CheckCircle2, Database, Briefcase, AlertTriangle, RefreshCw, Ticket, Award, QrCode } from 'lucide-react';
|
||||||
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
||||||
|
|
||||||
const MOODLE_URL = 'http://182.74.17.142/moodle/login/index.php';
|
const MOODLE_URL = 'http://182.74.17.142/moodle/login/index.php';
|
||||||
|
const GATE_PASS_URL = 'http://182.74.17.142/gatepass/login';
|
||||||
|
const PROFICIENCY_URL = 'http://182.74.17.142/proficiency/login';
|
||||||
import { getBackendUrl } from '@/lib/utils';
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
import { TOOLKIT_ITEMS, DEPARTMENTS } from '@/constants';
|
import { TOOLKIT_ITEMS, DEPARTMENTS } from '@/constants';
|
||||||
@@ -39,7 +41,7 @@ const SUBJECTS_BY_SEM: Record<number, string[]> = {
|
|||||||
|
|
||||||
const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.ComponentType<any> }> = {
|
const TYPE_COLORS: Record<string, { bg: string; text: string; icon: React.ComponentType<any> }> = {
|
||||||
notes: { bg: '#EFF6FF', text: '#3B82F6', icon: BookOpen },
|
notes: { bg: '#EFF6FF', text: '#3B82F6', icon: BookOpen },
|
||||||
pyq: { bg: '#EEF2FF', text: '#6366F1', icon: FileText },
|
pyq: { bg: '#FFF7ED', text: '#F97316', icon: FileText },
|
||||||
syllabus: { bg: '#ECFDF5', text: '#10B981', icon: ScrollText },
|
syllabus: { bg: '#ECFDF5', text: '#10B981', icon: ScrollText },
|
||||||
assignment: { bg: '#FDF2F8', text: '#EC4899', icon: BookMarked },
|
assignment: { bg: '#FDF2F8', text: '#EC4899', icon: BookMarked },
|
||||||
};
|
};
|
||||||
@@ -309,7 +311,7 @@ const CS_TOOLS_CATEGORIES = [
|
|||||||
bg: '#FEFCE8',
|
bg: '#FEFCE8',
|
||||||
tools: [
|
tools: [
|
||||||
{ name: 'Documentation', desc: 'Confluence, Notion, MkDocs, and Docusaurus.', url: 'https://docusaurus.io/' },
|
{ name: 'Documentation', desc: 'Confluence, Notion, MkDocs, and Docusaurus.', url: 'https://docusaurus.io/' },
|
||||||
{ name: 'Team Communication', desc: 'Slack, Microsoft Teams, Discord, and Mattermost.', url: 'https://slack.com/' },
|
{ name: 'Team Communication', desc: 'Slack, Microsoft Teams, and Mattermost.', url: 'https://slack.com/' },
|
||||||
{ name: 'Project Management', desc: 'Jira, Trello, Asana, and Linear ticket tracking.', url: 'https://linear.app/' },
|
{ name: 'Project Management', desc: 'Jira, Trello, Asana, and Linear ticket tracking.', url: 'https://linear.app/' },
|
||||||
{ name: 'Whiteboarding', desc: 'Miro, FigJam, Excalidraw, and Lucidchart.', url: 'https://excalidraw.com/' },
|
{ name: 'Whiteboarding', desc: 'Miro, FigJam, Excalidraw, and Lucidchart.', url: 'https://excalidraw.com/' },
|
||||||
{ name: 'Knowledge Management', desc: 'Obsidian, Notion, and Slite workspace wiki.', url: 'https://obsidian.md/' }
|
{ name: 'Knowledge Management', desc: 'Obsidian, Notion, and Slite workspace wiki.', url: 'https://obsidian.md/' }
|
||||||
@@ -1329,40 +1331,55 @@ export default function Notes() {
|
|||||||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState<number | null>(null);
|
const [selectedLanguage, setSelectedLanguage] = useState<number | null>(null);
|
||||||
const [selectedTool, setSelectedTool] = useState<{ name: string; desc: string; url: string; category: string; bg: string; color: string } | null>(null);
|
const [selectedTool, setSelectedTool] = useState<{ name: string; desc: string; url: string; category: string; bg: string; color: string } | null>(null);
|
||||||
|
const [activePortalTab, setActivePortalTab] = useState<'moodle' | 'gatepass' | 'proficiency'>('moodle');
|
||||||
|
|
||||||
const [notes, setNotes] = useState<any[]>([]);
|
const [notes, setNotes] = useState<any[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchNotes = async () => {
|
const fetchNotes = async () => {
|
||||||
|
let localFallback = NOTES_DATA;
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('RIT_LOCAL_NOTES');
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) localFallback = parsed;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getBackendUrl('/api/notes'));
|
const response = await fetch(getBackendUrl('/api/notes'));
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
const mappedData = data.map((n: any) => ({
|
const mappedData = data.map((n: any) => ({
|
||||||
id: String(n.id),
|
id: String(n.id),
|
||||||
title: n.title,
|
title: n.title,
|
||||||
subject: n.subject,
|
subject: n.subject,
|
||||||
department: n.department,
|
department: n.department,
|
||||||
semester: n.semester,
|
semester: n.semester,
|
||||||
type: n.fileType,
|
type: n.fileType || 'notes',
|
||||||
downloads: n.downloadsCount || 0,
|
downloads: n.downloadsCount || 0,
|
||||||
fileSize: n.fileSize,
|
fileSize: n.fileSize || '2.4 MB',
|
||||||
uploadedAt: n.uploadedAt ? n.uploadedAt.split('T')[0] : '',
|
uploadedAt: n.uploadedAt ? n.uploadedAt.split('T')[0] : '',
|
||||||
downloadUrl: n.downloadUrl
|
downloadUrl: n.downloadUrl || '#'
|
||||||
}));
|
}));
|
||||||
setNotes(mappedData);
|
setNotes(mappedData);
|
||||||
} else {
|
return;
|
||||||
setNotes(NOTES_DATA);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Error fetching notes:', err);
|
setNotes(localFallback);
|
||||||
setNotes(NOTES_DATA);
|
} catch {
|
||||||
|
setNotes(localFallback);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchNotes();
|
fetchNotes();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const portalParam = searchParams.get('portal');
|
||||||
|
if (portalParam === 'gatepass' || portalParam === 'proficiency' || portalParam === 'moodle') {
|
||||||
|
setActivePortalTab(portalParam as any);
|
||||||
|
}
|
||||||
if (searchParams.get('section') === 'toolkit') {
|
if (searchParams.get('section') === 'toolkit') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toolkitRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
toolkitRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
@@ -1407,6 +1424,42 @@ export default function Notes() {
|
|||||||
return matchSearch && matchSem && matchDept && matchType && matchSubject;
|
return matchSearch && matchSem && matchDept && matchType && matchSubject;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const portalParam = searchParams.get('portal');
|
||||||
|
let headerBreadcrumb = 'Notes & PYQs';
|
||||||
|
let headerTitle = (
|
||||||
|
<>
|
||||||
|
Notes &{' '}
|
||||||
|
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
|
PYQs
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
let headerSubtitle = 'Access semester-wise notes, previous year question papers, and study materials.';
|
||||||
|
|
||||||
|
if (portalParam === 'gatepass') {
|
||||||
|
headerBreadcrumb = 'Student Gate Pass';
|
||||||
|
headerTitle = (
|
||||||
|
<>
|
||||||
|
Student{' '}
|
||||||
|
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
|
Gate Pass
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
headerSubtitle = 'Official digital campus out-pass system for day scholars and hostellers.';
|
||||||
|
} else if (portalParam === 'proficiency') {
|
||||||
|
headerBreadcrumb = 'Student Proficiency';
|
||||||
|
headerTitle = (
|
||||||
|
<>
|
||||||
|
Student{' '}
|
||||||
|
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
|
Proficiency
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
headerSubtitle = 'Official RIT skill evaluation platform for tracking domain competencies and certifications.';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -1417,23 +1470,20 @@ export default function Notes() {
|
|||||||
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
<span>Home</span>
|
<span>Home</span>
|
||||||
<ChevronRight className="w-3 h-3" />
|
<ChevronRight className="w-3 h-3" />
|
||||||
<span className="text-[#F97316]">Notes & PYQs</span>
|
<span className="text-[#F97316]">{headerBreadcrumb}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h1 className="text-3xl md:text-4xl font-extrabold text-[#1E293B] mb-2" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
Notes &{' '}
|
{headerTitle}
|
||||||
<span style={{ background: 'linear-gradient(135deg, #6366F1, #4F46E5)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
|
||||||
PYQs
|
|
||||||
</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Access semester-wise notes, previous year question papers, and study materials.
|
{headerSubtitle}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container-custom py-10">
|
<div className="container-custom pt-10 pb-32 lg:pb-44">
|
||||||
|
|
||||||
|
|
||||||
{selectedToolkit ? (
|
{selectedToolkit ? (
|
||||||
@@ -1486,7 +1536,7 @@ export default function Notes() {
|
|||||||
: 'Back to Toolkit'}
|
: 'Back to Toolkit'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h2 className="text-3xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#1E293B] mb-2" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{selectedTool !== null ? (
|
{selectedTool !== null ? (
|
||||||
<>
|
<>
|
||||||
{selectedTool.name}{' '}
|
{selectedTool.name}{' '}
|
||||||
@@ -1548,7 +1598,7 @@ export default function Notes() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-2xl md:text-3xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h3 className="text-2xl md:text-3xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{selectedTool.name}
|
{selectedTool.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span
|
<span
|
||||||
@@ -1675,7 +1725,7 @@ export default function Notes() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-2xl md:text-3xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>
|
<h3 className="text-2xl md:text-3xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{PROGRAMMING_LANGUAGES_GUIDES[selectedLanguage].name}
|
{PROGRAMMING_LANGUAGES_GUIDES[selectedLanguage].name}
|
||||||
</h3>
|
</h3>
|
||||||
<span
|
<span
|
||||||
@@ -1919,7 +1969,10 @@ export default function Notes() {
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
{/* Moodle Academic Portal Hero Hub */}
|
|
||||||
|
|
||||||
|
{/* 1. MOODLE LMS CARD */}
|
||||||
|
{activePortalTab === 'moodle' && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 16 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -1927,19 +1980,16 @@ export default function Notes() {
|
|||||||
className="bg-white rounded-3xl border border-[#E5E7EB] p-8 md:p-10 shadow-sm relative overflow-hidden mb-8"
|
className="bg-white rounded-3xl border border-[#E5E7EB] p-8 md:p-10 shadow-sm relative overflow-hidden mb-8"
|
||||||
style={{ boxShadow: '0 4px 25px -5px rgba(0,0,0,0.05)' }}
|
style={{ boxShadow: '0 4px 25px -5px rgba(0,0,0,0.05)' }}
|
||||||
>
|
>
|
||||||
{/* Subtle Decorative Background Glow */}
|
|
||||||
<div className="absolute -right-20 -top-20 w-80 h-80 bg-orange-500/5 rounded-full blur-3xl pointer-events-none" />
|
<div className="absolute -right-20 -top-20 w-80 h-80 bg-orange-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
<div className="absolute -left-20 -bottom-20 w-80 h-80 bg-amber-500/5 rounded-full blur-3xl pointer-events-none" />
|
<div className="absolute -left-20 -bottom-20 w-80 h-80 bg-amber-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
||||||
{/* Header Badge */}
|
|
||||||
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-xs font-bold text-[#F97316] mb-6">
|
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-xs font-bold text-[#F97316] mb-6">
|
||||||
<BookOpen className="w-4 h-4 text-[#F97316]" />
|
<BookOpen className="w-4 h-4 text-[#F97316]" />
|
||||||
<span>Official RIT Learning Management System</span>
|
<span>Official RIT Learning Management System</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Heading */}
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#1E293B] mb-4 leading-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
<h2 className="text-3xl sm:text-4xl font-extrabold text-[#1E293B] mb-4 leading-tight" style={{ fontFamily: 'Playfair Display, serif' }}>
|
|
||||||
Access Academic Notes & Study Materials on{' '}
|
Access Academic Notes & Study Materials on{' '}
|
||||||
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
Moodle
|
Moodle
|
||||||
@@ -1950,7 +2000,6 @@ export default function Notes() {
|
|||||||
All official semester notes, previous year question papers (PYQs), syllabus data, and lab manuals are hosted directly on the official Rajalakshmi Institute of Technology Moodle server.
|
All official semester notes, previous year question papers (PYQs), syllabus data, and lab manuals are hosted directly on the official Rajalakshmi Institute of Technology Moodle server.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Primary Launch Action Buttons */}
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-10">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-10">
|
||||||
<a
|
<a
|
||||||
href={MOODLE_URL}
|
href={MOODLE_URL}
|
||||||
@@ -1979,7 +2028,6 @@ export default function Notes() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feature Highlights Grid */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-left border-t border-[#F1F5F9] pt-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-left border-t border-[#F1F5F9] pt-8">
|
||||||
<div className="p-4 rounded-2xl bg-[#F8FAFC] border border-[#E5E7EB]/70 flex flex-col gap-2">
|
<div className="p-4 rounded-2xl bg-[#F8FAFC] border border-[#E5E7EB]/70 flex flex-col gap-2">
|
||||||
<div className="w-8 h-8 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-600">
|
<div className="w-8 h-8 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-600">
|
||||||
@@ -2017,13 +2065,205 @@ export default function Notes() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Technical Security Note */}
|
{/* 2. STUDENT GATE PASS CARD */}
|
||||||
<div className="mt-6 text-[11px] text-[#94A3B8] leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
{activePortalTab === 'gatepass' && (
|
||||||
ℹ️ <span className="font-semibold text-[#64748B]">Security Note:</span> To protect student session privacy and prevent clickjacking attacks, RIT's Moodle server enforces opening the portal directly in a secure browser tab rather than inside third-party frames (<code className="bg-slate-100 px-1 py-0.5 rounded text-[10px]">X-Frame-Options: SAMEORIGIN</code>).
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="bg-white rounded-3xl border border-[#E5E7EB] p-8 md:p-10 shadow-sm relative overflow-hidden mb-8"
|
||||||
|
style={{ boxShadow: '0 4px 25px -5px rgba(0,0,0,0.05)' }}
|
||||||
|
>
|
||||||
|
<div className="absolute -right-20 -top-20 w-80 h-80 bg-orange-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="absolute -left-20 -bottom-20 w-80 h-80 bg-amber-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-xs font-bold text-[#F97316] mb-6">
|
||||||
|
<Ticket className="w-4 h-4 text-[#F97316]" />
|
||||||
|
<span>Official RIT Campus Gate Pass Portal</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#1E293B] mb-4 leading-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
Request & Manage Out-Passes on{' '}
|
||||||
|
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
|
Student Gate Pass
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-base text-[#64748B] mb-8 leading-relaxed max-w-2xl mx-auto" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Official digital campus out-pass system for day scholars and hostellers. Submit leave requests, track warden approvals, and present digital QR passes to campus security.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-10">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
alert('Student Gate Pass Portal dummy link triggered! You can provide the live URL anytime to connect it.');
|
||||||
|
}}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-3 px-8 py-4 rounded-2xl text-base font-bold text-white shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all cursor-pointer"
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Poppins, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-5 h-5" />
|
||||||
|
Launch Student Gate Pass Portal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(GATE_PASS_URL);
|
||||||
|
alert('Student Gate Pass URL copied to clipboard!');
|
||||||
|
}}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-4 rounded-2xl border border-[#E5E7EB] hover:border-[#F97316] bg-[#F8FAFC] hover:bg-white text-sm font-semibold text-[#475569] hover:text-[#F97316] transition-all cursor-pointer"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
<Globe className="w-4 h-4 text-[#64748B]" />
|
||||||
|
Copy Gate Pass Direct URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-left border-t border-[#F1F5F9] pt-8">
|
||||||
|
<div className="p-4 rounded-2xl bg-emerald-50/50 border border-emerald-100 flex flex-col gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-600">
|
||||||
|
<QrCode className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
Digital QR Clearance
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Generate real-time encrypted QR code for security verification at campus gates.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-2xl bg-emerald-50/50 border border-emerald-100 flex flex-col gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-teal-500/10 border border-teal-500/20 flex items-center justify-center text-teal-600">
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
Warden & Parent Approval
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Automated SMS and email notifications to wardens, mentors, and registered guardians.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-2xl bg-emerald-50/50 border border-emerald-100 flex flex-col gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-green-500/10 border border-green-500/20 flex items-center justify-center text-green-600">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
Verified RIT Student ID
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Authenticates securely with official college Roll Number & registered portal password.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 3. STUDENT PROFICIENCY CARD */}
|
||||||
|
{activePortalTab === 'proficiency' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="bg-white rounded-3xl border border-[#E5E7EB] p-8 md:p-10 shadow-sm relative overflow-hidden mb-8"
|
||||||
|
style={{ boxShadow: '0 4px 25px -5px rgba(0,0,0,0.05)' }}
|
||||||
|
>
|
||||||
|
<div className="absolute -right-20 -top-20 w-80 h-80 bg-orange-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="absolute -left-20 -bottom-20 w-80 h-80 bg-amber-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-xs font-bold text-[#F97316] mb-6">
|
||||||
|
<Award className="w-4 h-4 text-[#F97316]" />
|
||||||
|
<span>Official RIT Skill Evaluation & Readiness System</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#1E293B] mb-4 leading-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
Track Skill Milestones & Credentials on{' '}
|
||||||
|
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||||
|
Student Proficiency
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-base text-[#64748B] mb-8 leading-relaxed max-w-2xl mx-auto" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Official RIT skill evaluation platform for tracking domain competencies, technical certifications, placement readiness scores, and verified proficiency badges.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-10">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
alert('Student Proficiency Portal dummy link triggered! You can provide the live URL anytime to connect it.');
|
||||||
|
}}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-3 px-8 py-4 rounded-2xl text-base font-bold text-white shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all cursor-pointer"
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Poppins, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-5 h-5" />
|
||||||
|
Launch Student Proficiency Portal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(PROFICIENCY_URL);
|
||||||
|
alert('Student Proficiency URL copied to clipboard!');
|
||||||
|
}}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-4 rounded-2xl border border-[#E5E7EB] hover:border-[#F97316] bg-[#F8FAFC] hover:bg-white text-sm font-semibold text-[#475569] hover:text-[#F97316] transition-all cursor-pointer"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
<Globe className="w-4 h-4 text-[#64748B]" />
|
||||||
|
Copy Proficiency Direct URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-left border-t border-[#F1F5F9] pt-8">
|
||||||
|
<div className="p-4 rounded-2xl bg-purple-50/50 border border-purple-100 flex flex-col gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-600">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
Skill Radar & Domain Scores
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Visualize competency scores across programming, core engineering, and analytical reasoning.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-2xl bg-purple-50/50 border border-purple-100 flex flex-col gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-600">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
Placement Readiness
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Live feedback on mock interview performances, coding assessments, and company cutoffs.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-2xl bg-purple-50/50 border border-purple-100 flex flex-col gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-violet-500/10 border border-violet-500/20 flex items-center justify-center text-violet-600">
|
||||||
|
<Award className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||||
|
Verified Credentials
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Upload, verify, and generate shareable badges for NPTEL, Coursera & RIT certification credits.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
1845
src/pages/StudentDashboard/StudentDashboard.tsx
Normal file
83
src/services/clubService.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
|
|
||||||
|
const DEVICE_ID_KEY = 'rit_device_identifier';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a persistent user identifier for likes.
|
||||||
|
* If user is authenticated, uses email. Otherwise uses a persistent UUID in localStorage.
|
||||||
|
*/
|
||||||
|
export function getClubUserIdentifier(userEmail?: string | null): string {
|
||||||
|
if (userEmail && userEmail.trim()) {
|
||||||
|
return userEmail.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let deviceId = localStorage.getItem(DEVICE_ID_KEY);
|
||||||
|
if (!deviceId) {
|
||||||
|
deviceId = 'guest_' + Math.random().toString(36).substring(2, 11) + Date.now().toString(36);
|
||||||
|
localStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||||
|
}
|
||||||
|
return deviceId;
|
||||||
|
} catch {
|
||||||
|
return 'guest_fallback_user';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch total counts of likes for all clubs & centers from database
|
||||||
|
*/
|
||||||
|
export async function fetchClubLikes(): Promise<Record<string, number>> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(getBackendUrl('/api/clubs/likes'));
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch likes: ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return data || {};
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Unable to fetch club likes from database backend:', err);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user's liked club IDs from database
|
||||||
|
*/
|
||||||
|
export async function fetchUserLikedClubs(userIdentifier: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
getBackendUrl(`/api/clubs/user-likes?userIdentifier=${encodeURIComponent(userIdentifier)}`)
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch user likes: ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Unable to fetch user liked clubs from database backend:', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle like status for a club/center in database
|
||||||
|
*/
|
||||||
|
export async function toggleClubLikeInDb(
|
||||||
|
clubId: string,
|
||||||
|
userIdentifier: string
|
||||||
|
): Promise<{ liked: boolean; count: number } | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(getBackendUrl(`/api/clubs/${encodeURIComponent(clubId)}/like`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userIdentifier }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Toggle like failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to persist like for club ${clubId} in database:`, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
1281
src/services/imsService.ts
Normal file
@@ -28,13 +28,14 @@ export interface Stat {
|
|||||||
export interface Faculty {
|
export interface Faculty {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
designation: string;
|
designation?: string;
|
||||||
department: string;
|
department: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
office?: string;
|
office?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
specialization?: string;
|
specialization?: string;
|
||||||
|
interest?: string;
|
||||||
qualification?: string;
|
qualification?: string;
|
||||||
experience?: string;
|
experience?: string;
|
||||||
subjectsHandling?: string;
|
subjectsHandling?: string;
|
||||||
@@ -101,7 +102,7 @@ export interface Event {
|
|||||||
isUpcoming: boolean;
|
isUpcoming: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Clubs ───────────────────────────────────────────────────────────────────
|
// ─── Clubs & Centers ──────────────────────────────────────────────────────────
|
||||||
export interface ClubSocialLinks {
|
export interface ClubSocialLinks {
|
||||||
instagram?: string;
|
instagram?: string;
|
||||||
linkedin?: string;
|
linkedin?: string;
|
||||||
@@ -117,6 +118,7 @@ export interface Club {
|
|||||||
description: string;
|
description: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
category: string;
|
category: string;
|
||||||
|
type?: 'Club' | 'Center';
|
||||||
members: number;
|
members: number;
|
||||||
presidentName?: string;
|
presidentName?: string;
|
||||||
vicePresidentName?: string;
|
vicePresidentName?: string;
|
||||||
@@ -177,3 +179,9 @@ export interface ApiResponse<T> {
|
|||||||
message: string;
|
message: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
google?: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
{
|
{
|
||||||
"community_bot_token": "",
|
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"helper_chat_ids": [971749136, 5567776672,1873240361],
|
"bot_username": "FreshersCommunityBot",
|
||||||
"discord_bot_token": "",
|
"helper_chat_ids": [
|
||||||
"discord_helper_user_ids": [789393727641878568],
|
1873240361,
|
||||||
|
5567776672,
|
||||||
|
8518850169,
|
||||||
|
7238144438
|
||||||
|
],
|
||||||
|
"seniorHelpers": [
|
||||||
|
{
|
||||||
|
"chatId": 1873240361,
|
||||||
|
"name": "Senior Mentor (CSE)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chatId": 5567776672,
|
||||||
|
"name": "Senior Responder (ECE)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chatId": 8518850169,
|
||||||
|
"name": "Senior Responder (IT)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chatId": 7238144438,
|
||||||
|
"name": "Senior Responder (AIDS)"
|
||||||
|
}
|
||||||
|
],
|
||||||
"spring_backend_url": "http://localhost:8085"
|
"spring_backend_url": "http://localhost:8085"
|
||||||
}
|
}
|
||||||
@@ -2,4 +2,3 @@ requests>=2.28.0
|
|||||||
fastapi>=0.95.0
|
fastapi>=0.95.0
|
||||||
uvicorn>=0.20.0
|
uvicorn>=0.20.0
|
||||||
pydantic>=2.0
|
pydantic>=2.0
|
||||||
discord.py>=2.3.0
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import asyncio
|
import asyncio
|
||||||
import discord
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
@@ -38,8 +37,8 @@ CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
|
|||||||
def load_config():
|
def load_config():
|
||||||
if not os.path.exists(CONFIG_PATH):
|
if not os.path.exists(CONFIG_PATH):
|
||||||
default_config = {
|
default_config = {
|
||||||
"community_bot_token": "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo",
|
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"telegram_bot_token": "8913773505:AAHASuKLLOto3Ax573_dxg8bnvQy2ML6yLk",
|
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"helper_chat_ids": [],
|
"helper_chat_ids": [],
|
||||||
"spring_backend_url": "http://localhost:8085"
|
"spring_backend_url": "http://localhost:8085"
|
||||||
}
|
}
|
||||||
@@ -51,9 +50,8 @@ def load_config():
|
|||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
COMMUNITY_BOT_TOKEN = os.environ.get("COMMUNITY_BOT_TOKEN") or config.get("community_bot_token", "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo")
|
COMMUNITY_BOT_TOKEN = os.environ.get("COMMUNITY_BOT_TOKEN") or config.get("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY")
|
||||||
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or config.get("telegram_bot_token")
|
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or config.get("telegram_bot_token")
|
||||||
DISCORD_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") or config.get("discord_bot_token")
|
|
||||||
BACKEND_URL = os.environ.get("SPRING_BACKEND_URL") or config.get("spring_backend_url", "http://localhost:8085")
|
BACKEND_URL = os.environ.get("SPRING_BACKEND_URL") or config.get("spring_backend_url", "http://localhost:8085")
|
||||||
|
|
||||||
# Database Setup
|
# Database Setup
|
||||||
@@ -485,6 +483,33 @@ def telegram_polling_thread():
|
|||||||
|
|
||||||
logging.info(f"Received message from chat {chat_id}: '{text}'")
|
logging.info(f"Received message from chat {chat_id}: '{text}'")
|
||||||
|
|
||||||
|
# Handle Community Q&A reply if this is a reply to a forwarded question
|
||||||
|
reply_to = message.get("reply_to_message")
|
||||||
|
if reply_to:
|
||||||
|
original_message_id = reply_to["message_id"]
|
||||||
|
question_id = get_question_id(chat_id, original_message_id)
|
||||||
|
if question_id:
|
||||||
|
first_name = message["from"].get("first_name", "")
|
||||||
|
last_name = message["from"].get("last_name", "")
|
||||||
|
author_name = f"{first_name} {last_name}".strip() or "Senior Helper"
|
||||||
|
|
||||||
|
logging.info(f"Submitting answer for question {question_id} by helper '{author_name}'")
|
||||||
|
backend_endpoint = f"{backend_url}/api/questions/{question_id}/answers"
|
||||||
|
answer_payload = {
|
||||||
|
"body": text,
|
||||||
|
"author": author_name
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
res = requests.post(backend_endpoint, json=answer_payload, timeout=10)
|
||||||
|
if res.status_code in [200, 201]:
|
||||||
|
send_telegram_message(chat_id, "✅ *Answer posted successfully to the Q&A board!*", reply_to_message_id=message["message_id"], token=bot_token)
|
||||||
|
else:
|
||||||
|
send_telegram_message(chat_id, f"❌ *Failed to post answer to backend.* (Status: {res.status_code})\nResponse: {res.text[:100]}", reply_to_message_id=message["message_id"], token=bot_token)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error calling backend endpoint {backend_endpoint}: {e}")
|
||||||
|
send_telegram_message(chat_id, f"❌ *Connection error to backend.* ({e})", reply_to_message_id=message["message_id"], token=bot_token)
|
||||||
|
continue
|
||||||
|
|
||||||
# Handle /remove command in Telegram
|
# Handle /remove command in Telegram
|
||||||
if text.lower() in ["/remove", "/cancel"]:
|
if text.lower() in ["/remove", "/cancel"]:
|
||||||
try:
|
try:
|
||||||
@@ -611,292 +636,8 @@ def telegram_polling_thread():
|
|||||||
logging.error(f"Error in 24/7 chatbot polling loop: {e}")
|
logging.error(f"Error in 24/7 chatbot polling loop: {e}")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
# Discord Action View for Collaboration Applications
|
|
||||||
class DiscordCollabActionView(discord.ui.View):
|
|
||||||
def __init__(self, application_id: int, applicant_contact: str):
|
|
||||||
super().__init__(timeout=86400)
|
|
||||||
self.application_id = application_id
|
|
||||||
self.applicant_contact = applicant_contact
|
|
||||||
|
|
||||||
@discord.ui.button(label="Accept Collaboration", style=discord.ButtonStyle.success, emoji="✅")
|
|
||||||
async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
||||||
try:
|
|
||||||
requests.put(f"{BACKEND_URL}/api/collab/applications/{self.application_id}/status?status=ACCEPTED", timeout=5)
|
|
||||||
await interaction.response.send_message(
|
|
||||||
f"✅ **Collaboration Request Accepted!**\nDirect contact details: **{self.applicant_contact}**",
|
|
||||||
ephemeral=False
|
|
||||||
)
|
|
||||||
for item in self.children:
|
|
||||||
item.disabled = True
|
|
||||||
await interaction.message.edit(view=self)
|
|
||||||
except Exception as e:
|
|
||||||
await interaction.response.send_message(f"❌ Error updating status: {e}", ephemeral=True)
|
|
||||||
|
|
||||||
@discord.ui.button(label="Decline Request", style=discord.ButtonStyle.danger, emoji="❌")
|
|
||||||
async def decline_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
||||||
try:
|
|
||||||
requests.put(f"{BACKEND_URL}/api/collab/applications/{self.application_id}/status?status=REJECTED", timeout=5)
|
|
||||||
await interaction.response.send_message("❌ **Collaboration Request Declined.**", ephemeral=False)
|
|
||||||
for item in self.children:
|
|
||||||
item.disabled = True
|
|
||||||
await interaction.message.edit(view=self)
|
|
||||||
except Exception as e:
|
|
||||||
await interaction.response.send_message(f"❌ Error updating status: {e}", ephemeral=True)
|
|
||||||
|
|
||||||
async def broadcast_discord_collab_application(application_id: int, project_idea: str, tag: str, applicant_name: str, applicant_dept: str, applicant_year: str, applicant_contact: str, message: str, user_ids: list):
|
|
||||||
formatted_msg = (
|
|
||||||
f"🤝 **New Collaboration Request for your Project!**\n\n"
|
|
||||||
f"📌 **Project Idea:** {project_idea}\n"
|
|
||||||
f"🏷️ **Tag:** `{tag}`\n\n"
|
|
||||||
f"👤 **Applicant:** {applicant_name} ({applicant_dept}, {applicant_year})\n"
|
|
||||||
f"💬 **Message:** {message or 'No message provided'}\n"
|
|
||||||
f"📱 **Contact Info:** {applicant_contact}\n\n"
|
|
||||||
f"Click a button below to respond:"
|
|
||||||
)
|
|
||||||
view = DiscordCollabActionView(application_id, applicant_contact)
|
|
||||||
for user_id_val in user_ids:
|
|
||||||
try:
|
|
||||||
user_id = int(user_id_val)
|
|
||||||
user = await discord_client.fetch_user(user_id)
|
|
||||||
if user:
|
|
||||||
await user.send(content=formatted_msg, view=view)
|
|
||||||
logging.info(f"Sent Discord collab notification DM to user {user_id}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to send Discord collab DM to {user_id_val}: {e}")
|
|
||||||
|
|
||||||
# Discord Remove Select View
|
|
||||||
class DiscordCollabRemoveSelect(discord.ui.Select):
|
|
||||||
def __init__(self, active_requests: list):
|
|
||||||
options = []
|
|
||||||
for req in active_requests:
|
|
||||||
req_id = str(req["id"])
|
|
||||||
snippet = req.get("projectIdea", "Project")[:45]
|
|
||||||
options.append(discord.SelectOption(
|
|
||||||
label=f"Cancel: {snippet}",
|
|
||||||
value=req_id,
|
|
||||||
description=f"Tag: {req.get('tag')} | Dept: {req.get('department')}",
|
|
||||||
emoji="❌"
|
|
||||||
))
|
|
||||||
super().__init__(placeholder="Select a request to remove...", min_values=1, max_values=1, options=options)
|
|
||||||
|
|
||||||
async def callback(self, interaction: discord.Interaction):
|
|
||||||
req_id = self.values[0]
|
|
||||||
try:
|
|
||||||
res = requests.delete(f"{BACKEND_URL}/api/collab/{req_id}", timeout=5)
|
|
||||||
if res.status_code in [200, 204]:
|
|
||||||
await interaction.response.send_message("✅ **Collaboration Request cancelled and removed from RIT Dev Hub.**", ephemeral=False)
|
|
||||||
else:
|
|
||||||
await interaction.response.send_message("❌ Failed to cancel request.", ephemeral=True)
|
|
||||||
except Exception as e:
|
|
||||||
await interaction.response.send_message(f"❌ Error: {e}", ephemeral=True)
|
|
||||||
|
|
||||||
class DiscordCollabRemoveView(discord.ui.View):
|
|
||||||
def __init__(self, active_requests: list):
|
|
||||||
super().__init__(timeout=180)
|
|
||||||
self.add_item(DiscordCollabRemoveSelect(active_requests))
|
|
||||||
|
|
||||||
# Discord Native Interactive UI Components (Modal & Dropdown View)
|
|
||||||
class CollabModal(discord.ui.Modal, title="Post Collaboration Request"):
|
|
||||||
author_name = discord.ui.TextInput(label="1) Your Name", placeholder="e.g. Priyan Sharma", required=True)
|
|
||||||
dept_and_year = discord.ui.TextInput(label="2) Dept & Year", placeholder="e.g. CSE, 1st Year", required=True, default="CSE, 1st Year")
|
|
||||||
collaborators_needed = discord.ui.TextInput(label="3) Collaborators Needed", placeholder="e.g. 2", required=True, default="1")
|
|
||||||
project_idea = discord.ui.TextInput(label="4) Project Idea & Details", style=discord.TextStyle.paragraph, placeholder="Describe your project idea...", required=True)
|
|
||||||
github_link = discord.ui.TextInput(label="5) GitHub Link (Optional)", placeholder="https://github.com/...", required=False)
|
|
||||||
|
|
||||||
def __init__(self, tag: str):
|
|
||||||
super().__init__()
|
|
||||||
self.selected_tag = tag
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
|
||||||
try:
|
|
||||||
num_needed = int(self.collaborators_needed.value)
|
|
||||||
except ValueError:
|
|
||||||
num_needed = 1
|
|
||||||
|
|
||||||
dept_val = "CSE"
|
|
||||||
year_val = "1st Year"
|
|
||||||
if "," in self.dept_and_year.value:
|
|
||||||
parts = [p.strip() for p in self.dept_and_year.value.split(",", 1)]
|
|
||||||
dept_val = parts[0]
|
|
||||||
year_val = parts[1]
|
|
||||||
else:
|
|
||||||
dept_val = self.dept_and_year.value.strip()
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"authorName": self.author_name.value,
|
|
||||||
"department": dept_val,
|
|
||||||
"year": year_val,
|
|
||||||
"collaboratorsNeeded": num_needed,
|
|
||||||
"tag": self.selected_tag,
|
|
||||||
"projectIdea": self.project_idea.value,
|
|
||||||
"githubLink": self.github_link.value or None,
|
|
||||||
"contactInfo": f"Discord: {interaction.user.name}",
|
|
||||||
"discordUserId": str(interaction.user.id)
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
res = requests.post(f"{BACKEND_URL}/api/collab", json=payload, timeout=10)
|
|
||||||
if res.status_code in [200, 201]:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
f"🎉 **Collaboration Request posted live to RIT Dev Hub!**\n"
|
|
||||||
f"👤 **Author:** {self.author_name.value} ({dept_val}, {year_val})\n"
|
|
||||||
f"📌 **Project:** {self.project_idea.value}\n"
|
|
||||||
f"🏷️ **Tag:** `{self.selected_tag}`\n"
|
|
||||||
f"👥 **Collaborators Needed:** `{num_needed}`",
|
|
||||||
ephemeral=False
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await interaction.response.send_message(f"❌ Failed to save request (Status: {res.status_code})", ephemeral=True)
|
|
||||||
except Exception as e:
|
|
||||||
await interaction.response.send_message(f"❌ Error connecting to backend: {e}", ephemeral=True)
|
|
||||||
|
|
||||||
class CollabTagSelect(discord.ui.Select):
|
|
||||||
def __init__(self):
|
|
||||||
options = [
|
|
||||||
discord.SelectOption(
|
|
||||||
label="Co-developing from scratch",
|
|
||||||
value="looking for co-developing a project from scratch",
|
|
||||||
description="Build a brand new project from scratch together",
|
|
||||||
emoji="🚀"
|
|
||||||
),
|
|
||||||
discord.SelectOption(
|
|
||||||
label="Beta Testers needed",
|
|
||||||
value="looking for beta testers",
|
|
||||||
description="Test early builds and provide user feedback",
|
|
||||||
emoji="🧪"
|
|
||||||
),
|
|
||||||
discord.SelectOption(
|
|
||||||
label="Open-Source Contributors",
|
|
||||||
value="looking for Open-source Collaborators/Contributers",
|
|
||||||
description="Open repository seeking PRs and contributors",
|
|
||||||
emoji="🌐"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
super().__init__(placeholder="Scroll down to select a tag for your request...", min_values=1, max_values=1, options=options)
|
|
||||||
|
|
||||||
async def callback(self, interaction: discord.Interaction):
|
|
||||||
selected_tag = self.values[0]
|
|
||||||
modal = CollabModal(tag=selected_tag)
|
|
||||||
await interaction.response.send_modal(modal)
|
|
||||||
|
|
||||||
class CollabView(discord.ui.View):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(timeout=180)
|
|
||||||
self.add_item(CollabTagSelect())
|
|
||||||
|
|
||||||
# Discord Bot Client Setup
|
|
||||||
intents = discord.Intents.default()
|
|
||||||
intents.messages = True
|
|
||||||
intents.message_content = True
|
|
||||||
|
|
||||||
discord_client = discord.Client(intents=intents)
|
|
||||||
discord_loop = None
|
|
||||||
|
|
||||||
@discord_client.event
|
|
||||||
async def on_ready():
|
|
||||||
logging.info(f"Discord Bot logged in as {discord_client.user}!")
|
|
||||||
|
|
||||||
@discord_client.event
|
|
||||||
async def on_message(message):
|
|
||||||
if message.author == discord_client.user:
|
|
||||||
return
|
|
||||||
|
|
||||||
is_dm = isinstance(message.channel, discord.DMChannel)
|
|
||||||
is_mention = discord_client.user in message.mentions
|
|
||||||
|
|
||||||
if not is_dm and not is_mention:
|
|
||||||
return
|
|
||||||
|
|
||||||
content = message.content
|
|
||||||
if is_mention:
|
|
||||||
mention_str = f"<@{discord_client.user.id}>"
|
|
||||||
mention_nick_str = f"<@!{discord_client.user.id}>"
|
|
||||||
content = content.replace(mention_str, "").replace(mention_nick_str, "").strip()
|
|
||||||
|
|
||||||
# Discord /collab & /remove commands disabled as requested
|
|
||||||
if content.lower().startswith("/collab") or content.lower().startswith("/remove") or content.lower().startswith("/cancel"):
|
|
||||||
await message.reply("ℹ️ **Dev Hub collaboration requests via Discord are currently disabled.** Please use the RIT Dev Hub website (https://rit-services.in/dev-collab) or the Telegram bot (@Ritchatbot_bot).")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if this message is a reply to a question DM sent to a helper
|
|
||||||
if message.reference and message.reference.message_id:
|
|
||||||
question_id = get_question_id(message.author.id, message.reference.message_id)
|
|
||||||
if question_id:
|
|
||||||
author_name = message.author.display_name or message.author.name or "Senior Helper"
|
|
||||||
logging.info(f"Submitting answer for question {question_id} by Discord helper '{author_name}'")
|
|
||||||
backend_endpoint = f"{BACKEND_URL}/api/questions/{question_id}/answers"
|
|
||||||
answer_payload = {
|
|
||||||
"body": content,
|
|
||||||
"author": author_name
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
def call_backend():
|
|
||||||
return requests.post(backend_endpoint, json=answer_payload, timeout=10)
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
res = await loop.run_in_executor(None, call_backend)
|
|
||||||
if res.status_code in [200, 201]:
|
|
||||||
await message.reply("✅ **Answer posted successfully to the Q&A board!**")
|
|
||||||
else:
|
|
||||||
await message.reply(f"❌ **Failed to post answer to backend.** (Status: {res.status_code})")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error calling backend endpoint {backend_endpoint}: {e}")
|
|
||||||
await message.reply(f"❌ **Connection error to backend.** ({e})")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Direct query fallback to Go chatbot service
|
|
||||||
if not content.strip():
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.info(f"Querying Go chatbot service for Discord user {message.author.id}: '{content}'")
|
|
||||||
chatbot_service_url = "http://localhost:8081/api/chat"
|
|
||||||
try:
|
|
||||||
def call_chatbot():
|
|
||||||
return requests.post(chatbot_service_url, json={"message": content}, timeout=10)
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
res = await loop.run_in_executor(None, call_chatbot)
|
|
||||||
|
|
||||||
if res.status_code == 200:
|
|
||||||
ans_data = res.json()
|
|
||||||
bot_response = ans_data.get("answer", "I am having trouble processing that question.")
|
|
||||||
await message.reply(bot_response)
|
|
||||||
else:
|
|
||||||
logging.error(f"Go chatbot API returned status code {res.status_code}")
|
|
||||||
await message.reply("⚠️ The RIT Chatbot service is currently experiencing issues. Please try again later.")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to connect to Go chatbot service: {e}")
|
|
||||||
await message.reply("⚠️ I cannot connect to the RIT Chatbot database right now. Please make sure the service is online.")
|
|
||||||
|
|
||||||
async def broadcast_discord_question(question_id: int, title: str, body: str, author: str, user_ids: list):
|
|
||||||
formatted_msg = (
|
|
||||||
f"❓ **New Student Question!**\n\n"
|
|
||||||
f"👤 **Author:** {author}\n"
|
|
||||||
f"📌 **Topic:** {title}\n"
|
|
||||||
f"📝 **Details:** {body}\n\n"
|
|
||||||
f"💬 **Reply directly to this message to submit your answer.**"
|
|
||||||
)
|
|
||||||
for user_id_val in user_ids:
|
|
||||||
try:
|
|
||||||
user_id = int(user_id_val)
|
|
||||||
user = await discord_client.fetch_user(user_id)
|
|
||||||
if user:
|
|
||||||
msg = await user.send(formatted_msg)
|
|
||||||
save_mapping(user_id, msg.id, question_id)
|
|
||||||
logging.info(f"Sent Discord DM to helper {user_id}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to send Discord DM to helper {user_id_val}: {e}")
|
|
||||||
|
|
||||||
async def run_discord_bot():
|
|
||||||
global discord_loop
|
|
||||||
logging.info("Starting Discord bot...")
|
|
||||||
discord_loop = asyncio.get_running_loop()
|
|
||||||
try:
|
|
||||||
await discord_client.start(DISCORD_TOKEN)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Discord Bot failed to run: {e}")
|
|
||||||
|
|
||||||
# FastAPI Web Server Setup
|
# FastAPI Web Server Setup
|
||||||
app = FastAPI(title="RIT Telegram & Discord Intermediary Bot HTTP Server")
|
app = FastAPI(title="RIT Telegram Intermediary Bot HTTP Server")
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
@@ -920,7 +661,6 @@ class CollabApplicationPayload(BaseModel):
|
|||||||
author_name: str
|
author_name: str
|
||||||
contact_info: Optional[str] = None
|
contact_info: Optional[str] = None
|
||||||
telegram_chat_id: Optional[int] = None
|
telegram_chat_id: Optional[int] = None
|
||||||
discord_user_id: Optional[str] = None
|
|
||||||
applicant_name: str
|
applicant_name: str
|
||||||
applicant_dept: str
|
applicant_dept: str
|
||||||
applicant_year: str
|
applicant_year: str
|
||||||
@@ -932,7 +672,7 @@ def send_question(payload: QuestionPayload):
|
|||||||
current_config = load_config()
|
current_config = load_config()
|
||||||
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
|
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
|
||||||
|
|
||||||
# 1. Telegram Broadcast via Senior Helper Community Bot
|
# Telegram Broadcast via Senior Helper Community Bot
|
||||||
telegram_helpers = current_config.get("helper_chat_ids", [])
|
telegram_helpers = current_config.get("helper_chat_ids", [])
|
||||||
telegram_sent = 0
|
telegram_sent = 0
|
||||||
if telegram_helpers and comm_token:
|
if telegram_helpers and comm_token:
|
||||||
@@ -951,27 +691,9 @@ def send_question(payload: QuestionPayload):
|
|||||||
save_mapping(chat_id, message_id, payload.question_id)
|
save_mapping(chat_id, message_id, payload.question_id)
|
||||||
telegram_sent += 1
|
telegram_sent += 1
|
||||||
|
|
||||||
# 2. Discord Broadcast
|
|
||||||
discord_helpers = current_config.get("discord_helper_user_ids", [])
|
|
||||||
discord_sent = 0
|
|
||||||
if discord_helpers and DISCORD_TOKEN:
|
|
||||||
logging.info(f"Broadcasting question {payload.question_id} to {len(discord_helpers)} Discord helpers.")
|
|
||||||
if discord_loop:
|
|
||||||
try:
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
broadcast_discord_question(payload.question_id, payload.title, payload.body, payload.author, discord_helpers),
|
|
||||||
discord_loop
|
|
||||||
)
|
|
||||||
discord_sent = len(discord_helpers)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error scheduling Discord broadcast: {e}")
|
|
||||||
else:
|
|
||||||
logging.warning("Discord loop not running. Skipping Discord broadcast.")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"telegram_delivered_to": telegram_sent,
|
"telegram_delivered_to": telegram_sent
|
||||||
"discord_queued_for": discord_sent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.post("/send_collab_application")
|
@app.post("/send_collab_application")
|
||||||
@@ -980,9 +702,8 @@ def send_collab_application(payload: CollabApplicationPayload):
|
|||||||
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token")
|
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token")
|
||||||
|
|
||||||
telegram_sent = False
|
telegram_sent = False
|
||||||
discord_sent = False
|
|
||||||
|
|
||||||
# 1. Telegram Notification strictly to Person A (the project author)
|
# Telegram Notification strictly to Person A (the project author)
|
||||||
chat_id = payload.telegram_chat_id
|
chat_id = payload.telegram_chat_id
|
||||||
|
|
||||||
if chat_id:
|
if chat_id:
|
||||||
@@ -1009,13 +730,9 @@ def send_collab_application(payload: CollabApplicationPayload):
|
|||||||
else:
|
else:
|
||||||
logging.info(f"Skipping Telegram notification for collab application {payload.application_id}: No telegram_chat_id provided for author.")
|
logging.info(f"Skipping Telegram notification for collab application {payload.application_id}: No telegram_chat_id provided for author.")
|
||||||
|
|
||||||
# 2. Discord Broadcast - Disabled as requested
|
|
||||||
discord_sent = False
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"telegram_sent": telegram_sent,
|
"telegram_sent": telegram_sent
|
||||||
"discord_sent": discord_sent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def run_uvicorn():
|
async def run_uvicorn():
|
||||||
@@ -1024,20 +741,20 @@ async def run_uvicorn():
|
|||||||
await server.serve()
|
await server.serve()
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
current_config = load_config()
|
||||||
|
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
|
||||||
|
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token", BOT_TOKEN)
|
||||||
|
|
||||||
|
# If separate bot tokens are configured, run dedicated community polling thread
|
||||||
|
if comm_token and bot_token and comm_token != bot_token:
|
||||||
comm_thread = threading.Thread(target=community_bot_polling_thread, daemon=True)
|
comm_thread = threading.Thread(target=community_bot_polling_thread, daemon=True)
|
||||||
comm_thread.start()
|
comm_thread.start()
|
||||||
|
|
||||||
|
# Main unified thread handles Dev Collab, Chatbot, and unified Q&A
|
||||||
chat_thread = threading.Thread(target=telegram_polling_thread, daemon=True)
|
chat_thread = threading.Thread(target=telegram_polling_thread, daemon=True)
|
||||||
chat_thread.start()
|
chat_thread.start()
|
||||||
|
|
||||||
tasks = []
|
await run_uvicorn()
|
||||||
if DISCORD_TOKEN:
|
|
||||||
tasks.append(run_discord_bot())
|
|
||||||
else:
|
|
||||||
logging.warning("Discord Bot Token is empty. Skipping Discord bot startup.")
|
|
||||||
|
|
||||||
tasks.append(run_uvicorn())
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -50,8 +50,67 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
chunkSizeWarningLimit: 800,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id: string) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) {
|
||||||
|
return 'vendor-react';
|
||||||
|
}
|
||||||
|
if (id.includes('framer-motion') || id.includes('lucide-react')) {
|
||||||
|
return 'vendor-ui';
|
||||||
|
}
|
||||||
|
if (id.includes('@radix-ui')) {
|
||||||
|
return 'vendor-radix';
|
||||||
|
}
|
||||||
|
if (id.includes('@tanstack')) {
|
||||||
|
return 'vendor-tanstack';
|
||||||
|
}
|
||||||
|
if (id.includes('leaflet') || id.includes('react-leaflet')) {
|
||||||
|
return 'vendor-map';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
|
proxy: {
|
||||||
|
'/ims': {
|
||||||
|
target: 'https://ims.ritchennai.edu.in',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/ims/, ''),
|
||||||
|
secure: false,
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on('proxyRes', (proxyRes) => {
|
||||||
|
// Rewrite Location header to keep redirects through local / VPS proxy
|
||||||
|
const location = proxyRes.headers['location'];
|
||||||
|
if (location) {
|
||||||
|
let rewritten = location;
|
||||||
|
if (rewritten.includes('ims.ritchennai.edu.in')) {
|
||||||
|
rewritten = rewritten.replace(/https?:\/\/ims\.ritchennai\.edu\.in/gi, '/ims');
|
||||||
|
} else if (rewritten.startsWith('/') && !rewritten.startsWith('/ims')) {
|
||||||
|
rewritten = '/ims' + rewritten;
|
||||||
|
}
|
||||||
|
proxyRes.headers['location'] = rewritten;
|
||||||
|
}
|
||||||
|
// Strip domain from cookies so session cookies bind to current host (localhost or VPS domain)
|
||||||
|
const setCookie = proxyRes.headers['set-cookie'];
|
||||||
|
if (setCookie) {
|
||||||
|
proxyRes.headers['set-cookie'] = (Array.isArray(setCookie) ? setCookie : [setCookie]).map((cookie: string) =>
|
||||||
|
cookie
|
||||||
|
.replace(/domain=[^;]+/gi, '')
|
||||||
|
.replace(/path=[^;]+/gi, 'Path=/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: [
|
ignored: [
|
||||||
'**/backend/**',
|
'**/backend/**',
|
||||||
|
|||||||