Compare commits

...

114 Commits

Author SHA1 Message Date
Shanmuga Krishnan S M
f368c83246 fix(skillrack): update PROFILE_URL to manageprofile.xhtml (fixing 404 error) and simplify auth failure check
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-28 09:48:50 +05:30
Shanmuga Krishnan S M
db5ac8e2ea fix(skillrack): update login to j_security_check endpoint, exact ion-icon medal and points regex extraction, and rename input label to SkillRack Login ID
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-28 09:44:56 +05:30
Shanmuga Krishnan S M
a19c36579e feat(skillrack): add SkillRack leaderboard with server-side login, AES-encrypted credential storage, Jsoup HTML scraping, daily auto-sync, and full frontend tab with registration modal
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-28 09:35:42 +05:30
Shanmuga Krishnan S M
6be263771d feat(admin): add dedicated role-based logins and scoped permissions for Community Q&A, Clubs & Centers, and GPA Curriculum 2026-08-24 15:22:35 +05:30
Shanmuga Krishnan S M
9275722e16 feat(admin): make telegram bot config and senior helper id management fully functional with live test ping and verification 2026-08-24 14:04:11 +05:30
Shanmuga Krishnan S M
6740400788 Merge remote-tracking branch 'github/main' 2026-08-24 12:22:38 +05:30
Devesh S
6d386273fb Merge branch 'main' of https://github.com/Amudieshwar-AG/Freshers-Hub 2026-08-24 11:47:28 +05:30
Devesh S
56c3849357 student leaderboard - certifications 2026-08-24 11:46:58 +05:30
Shanmuga Krishnan S M
f58c98cd46 fix(ims): add cache: no-store and timestamp cache-busters to all live IMS requests
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:56:08 +05:30
Shanmuga Krishnan S M
8c7a3d7f1a fix(ims): restore 100% real-time direct IMS scraping and fix Vite proxy Location header rewrite for VPS
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:49:57 +05:30
Shanmuga Krishnan S M
f8047b6cd3 fix(gpa): normalize department code to AIDS in fetchImsSemesterResults for reliable CGPA, GPA curve & grade distribution rendering
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:42:52 +05:30
Shanmuga Krishnan S M
72c213f2b6 fix(auth): add resilient fallback for missing CSRF token and proxy unavailability on VPS
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:38:53 +05:30
Shanmuga Krishnan S M
e6ea5cb302 fix(ims): resolve department code mapping and add curriculum fallbacks for attendance, CAT marks, assignment, lab & semester grades
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 08:59:51 +05:30
Shanmuga Krishnan S M
64f0c5d754 Merge https://github.com/Amudieshwar-AG/Freshers-Hub
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-08-24 08:12:53 +05:30
Amudieshwar A G
d9ed15b481 Redesign Admin UI theme, add Merge Routes & Bus Status Manager features 2026-08-21 12:10:26 +05:30
Shanmuga Krishnan S M
87865f810f latest update 2026-08-21 10:39:40 +05:30
Anbu-dev-creator
906e9c757b New 2026-08-21 09:26:47 +05:30
Shanmuga Krishnan S M
23bf4fd86a feat(dashboard): complete professional UI overhaul for student dashboard with executive analytics & matrix agenda 2026-08-20 11:55:15 +05:30
Shanmuga Krishnan S M
95c97e81fa perf: complete industry-standard optimization for frontend & backend 2026-08-20 10:18:55 +05:30
Shanmuga Krishnan S M
128e033a9a fix(backend): add @Builder.Default to entity fields and add unit test suite 2026-08-20 10:14:29 +05:30
Shanmuga Krishnan S M
4e2edb7c52 feat: implement reference agenda weekly timetable grid with category pills, live indicator, and star favorites 2026-08-20 08:40:14 +05:30
Shanmuga Krishnan S M
bbb2cf7d30 feat: add LAB and Assignment marks fetching with lazy loading and system protection 2026-08-19 14:58:59 +05:30
Shanmuga Krishnan S M
b712b1e2f1 feat: match reference timetable grid matrix UI, support period cell arrays, preserve empty cells with dash without fake Free Period fallback 2026-08-19 14:20:44 +05:30
Shanmuga Krishnan S M
1641bf2110 fix: guarantee 7 full period slots per day for all working days (Monday-Friday) with free period padding 2026-08-19 14:11:53 +05:30
Shanmuga Krishnan S M
2f69b0a29d fix: universal Anna University 2021 Regulation credit engine (IC=1, VA=1, Lab=1, Mandatory=0) 2026-08-19 14:10:01 +05:30
Shanmuga Krishnan S M
5ea7de558b fix: completely remove Saturday from timetable model, parser, fallback, and UI 2026-08-19 14:07:48 +05:30
Shanmuga Krishnan S M
596504840e feat: Anna University CGPA formula, credit fixes, academic overview dashboard, lab/theory badges 2026-08-19 14:06:20 +05:30
Shanmuga Krishnan S M
56ce10fc55 Integrate real RIT-IMS-API: live student profile, attendance breakdown, CAT marks, auto-calculated CGPA and GPA sync
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 10:31:54 +05:30
Shanmuga Krishnan S M
0c86c087e4 Integrate new Telegram bot with DevCollab workflow for notifications, actions, and project creation
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 09:28:44 +05:30
Shanmuga Krishnan S M
fe7ec3d61e Configure new Telegram bot token across frontend Admin dashboard and Python bot service
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 09:25:26 +05:30
Shanmuga Krishnan S M
34ea034899 Merge remote-tracking branch github/main into main and resolve Events conflict
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 09:15:07 +05:30
Shanmuga Krishnan S M
a99c54c705 Complete audit: fix strict TypeScript errors, Window types, and clean production build 2026-08-19 09:13:43 +05:30
Shanmuga Krishnan S M
d028e86bdd Restore fetchQuestions and handleDeleteQuestion handlers in AdminDashboard.tsx 2026-08-19 09:08:20 +05:30
Shanmuga Krishnan S M
b5b152f43c Fix import path for getBackendUrl in Faculty.tsx 2026-08-19 09:07:25 +05:30
Shanmuga Krishnan S M
82c6444c28 Wire live Spring Boot API fetch to Faculty page with local fallback support 2026-08-19 09:06:18 +05:30
Shanmuga Krishnan S M
f937efce5c Remove notes and deployment alert recipients from Admin console and improve Telegram Senior Responders persistence 2026-08-19 09:03:15 +05:30
Shanmuga Krishnan S M
e15c95fafd Fix AdminDashboard state declarations and handle offline fetch gracefully in Notes.tsx 2026-08-19 08:58:12 +05:30
Shanmuga Krishnan S M
9a0c601955 Add Admin management for Centers of Excellence, Faculty Directory, and GPA Calculator course & credit assignments 2026-08-19 08:55:41 +05:30
Amudieshwar
774ae7e7ae fix: persist like counts across refreshes and remove social links card from non-RAISE centers 2026-08-19 08:52:20 +05:30
Shanmuga Krishnan S M
9a58b2a53e Make Telegram Q&A, Notes, Community Questions and Email Alerts fully functional with persistent localStorage state 2026-08-19 08:49:15 +05:30
Shanmuga Krishnan S M
a4ad5e5815 Merge remote UI updates 2026-08-19 08:46:57 +05:30
Amudieshwar
7cd28fa974 fix: restrict RAISE Incubator portal link to RAISE entity and render official website link for other centers 2026-08-19 08:37:48 +05:30
Shanmuga Krishnan S M
bba33ef05b Save local admin console and unified auth changes before pulling remote UI updates 2026-08-19 08:34:31 +05:30
Amudieshwar
5f3b3f5973 feat: persist club and center likes in postgresql database 2026-08-19 08:31:12 +05:30
Amudieshwar A G
f8ca5a3368 refactor: rename Dashboard nav label to Time table and remove faculty directory shortcut 2026-08-19 08:24:03 +05:30
Anbu-dev-creator
2d91f6f5c4 new commit 2026-08-18 15:32:10 +05:30
Anbu-dev-creator
154f19d546 Changes in the UI 2026-08-18 15:10:23 +05:30
Amudieshwar A G
02d8dea9a9 feat: reorder toolkit & navbar, update RAISE incubator & portal modules, fix faculty department filtering 2026-08-18 12:27:27 +05:30
Anbu-dev-creator
d3bd00e506 Added cgpa 2026-08-18 09:43:36 +05:30
Devesh S
e83b934e9c Campus Map 2026-08-18 09:06:46 +05:30
Shanmuga Krishnan S M
1eebd10f3a added fake end points to simulate real arhitecture 2026-08-17 14:36:32 +05:30
Shanmuga Krishnan S M
d827f4aab9 Update campus map layout, refine navbar, remove discord integration
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-17 09:02:33 +05:30
Anbu-dev-creator
29377ce56e New commit 2026-08-17 08:28:01 +05:30
Anbu-dev-creator
97a59ee715 Some changes in toolkit section 2026-08-16 21:53:41 +05:30
Amudieshwar
3afb40d451 feat: integrate 15 official RIT Future Tech Centers of Excellence with logos, faculty leads, and AI Matcher 2026-08-16 17:28:23 +05:30
Amudieshwar-AG
bb71b725d9 Update Faculty page, FacultyCard component, types, and constants 2026-08-14 18:43:41 +05:30
Amudieshwar A G
fcaf16405d feat: upgrade UI design system with Orange, White & Dark Purple theme 2026-08-14 13:46:41 +05:30
Shanmuga Krishnan S M
3f277f53f4 revert: restore UI to commit ec06c59985
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-14 09:56:50 +05:30
Shanmuga Krishnan S M
4e0300836e fix header alignment and restore navigation dropdown button
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-14 09:49:35 +05:30
Shanmuga Krishnan S M
122e839d36 ui enhancement,topdown menu
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-14 09:43:11 +05:30
Shanmuga Krishnan S M
68e8f6e002 video playback enhanced
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 23:23:48 +05:30
Shanmuga Krishnan S M
a6efffdefd ui changes
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 23:07:13 +05:30
Shanmuga Krishnan S M
052fd76a99 fix(ui): remove top header login button, fix mobile viewport cropping, update footer CTA to slate gray 2026-08-13 14:52:44 +05:30
Shanmuga Krishnan S M
bede7ca962 style: complete orange theme removal across footer, events, and dev collab; optimize mobile layout
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 14:22:21 +05:30
Shanmuga Krishnan S M
dad13527c6 style: unify module theme colors to enterprise violet and remove AI buzzwords
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 14:15:35 +05:30
Shanmuga Krishnan S M
ff606eb255 feat(dashboard): transform home dashboard into full viewport video overlay with transparent quick launch modules bar 2026-08-13 14:12:47 +05:30
Shanmuga Krishnan S M
2615dabd02 feat(ui): transform portal interface to enterprise dark purple sidebar navigation architecture with video playback dashboard
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 13:50:18 +05:30
Shanmuga Krishnan S M
ec06c59985 feat(raise): add RaiseIncubator page assets and component files
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 13:45:03 +05:30
Shanmuga Krishnan S M
c7fba0331d Merge branch 'main' of https://github.com/Amudieshwar-AG/Freshers-Hub
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 13:38:09 +05:30
RIT Services
6543a72206 feat: Add Pitch your Idea - RAISE navigation button to Navbar and Hero section
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 06:46:14 +00:00
RIT Services
4588eef714 feat: Integrate RAISE Incubator website as a module under /raise
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 06:41:46 +00:00
Amudieshwar
64c5180549 Update club contact email handling and clear blank emails 2026-08-13 12:05:21 +05:30
Shanmuga Krishnan S M
241cb5c714 fix(notes): fix unmatched JSX closing tag in Notes.tsx
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 11:44:45 +05:30
Shanmuga Krishnan S M
47f4269bf6 merge: pull latest main from github repository
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-13 11:39:31 +05:30
Amudieshwar
b953624446 Update club details from presentation, remove member count, reset likes to 0, and add Telugu club logo 2026-08-12 14:32:33 +05:30
Shanmuga Krishnan S M
bce9b544f3 revert: undo UI changes from commit 17c981e 2026-08-12 13:36:52 +05:30
Shanmuga Krishnan S M
d087827283 fix(notes): fix missing closing div tag in Notes.tsx 2026-08-12 13:27:56 +05:30
Shanmuga Krishnan S M
0a9ff3964b merge: sync remote github/main with local main 2026-08-12 12:22:16 +05:30
Shanmuga Krishnan S M
c486267c3f chore(cleanup): remove legacy send_git_email.py and post-receive.sh (replaced by Frissco Apex)
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-10 14:27:49 +05:30
Shanmuga Krishnan S M
501b928e05 test(deploy): verify full Frissco Apex webhook deployment and email reporting
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-10 13:55:36 +05:30
Shanmuga Krishnan S M
da0fae2acd feat(watchdog): add service health monitoring script with email alerts on DOWN/UP events
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-10 11:23:13 +05:30
Devesh S
38a64783d3 separate Toolkit into standalone page and route 2026-08-07 14:47:19 +05:30
Devesh S
17c981eec0 adjust ui and bus route 2026-08-07 14:47:19 +05:30
Shanmuga Krishnan S M
5d02cfc7ee fix(bus-map): completely remove route polyline lines from map, focus strictly on clean map & live GPS bus markers 2026-08-07 14:47:19 +05:30
Shanmuga Krishnan S M
0a94f2fc65 fix(email): fix UnicodeEncodeError in print output, format App Password without spaces, add UTF-8 sys.stdout reconfigure 2026-08-07 14:47:19 +05:30
Shanmuga Krishnan S M
0e22ad7b66 docs: add email automation setup guide 2026-08-07 14:47:18 +05:30
Shanmuga Krishnan S M
31e562ab89 fix(bus-map): remove polyline route line overlay 2026-08-07 14:47:18 +05:30
Shanmuga Krishnan S M
31a1b66b74 ci: update recipient email to dorutoslayer@gmail.com 2026-08-07 14:47:18 +05:30
Shanmuga Krishnan S M
e1d7dee45f test(ci): verify automated email push report notification system 2026-08-07 14:47:18 +05:30
Shanmuga Krishnan S M
5c8754a155 ci: update git notification scripts with email credentials 2026-08-07 14:47:18 +05:30
Shanmuga Krishnan S M
2d344b84c7 fix(admin): remove @CrossOrigin that conflicts with WebConfig allowCredentials=true
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:45:32 +05:30
Shanmuga Krishnan S M
9a88479532 fix(admin): simplify AdminController - hardcoded creds, no Value injection, Map body
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:41:19 +05:30
Shanmuga Krishnan S M
4361ca819c fix(admin): refactor AdminController with explicit LoginDTO/RecipientDTO to prevent Jackson 500 error
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:36:48 +05:30
Shanmuga Krishnan S M
a5a97b6e1a fix(admin): wrap login endpoint in try-catch block to prevent 500 internal server error
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:34:19 +05:30
Shanmuga Krishnan S M
2f978ab244 fix(admin): make login endpoint return HTTP 200 with explicit error message for debugging
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:28:59 +05:30
Shanmuga Krishnan S M
9357c18d69 fix(admin): trim login inputs and explicitly match ritadmin / ritadmin2026! credentials
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:22:17 +05:30
Shanmuga Krishnan S M
aa18713be7 fix(router): match both /admin and /admin/* routes in App.tsx
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:18:34 +05:30
Shanmuga Krishnan S M
4d593b7fe7 fix(pwa): enable skipWaiting and cleanupOutdatedCaches to force browser to load new routes on push
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:15:27 +05:30
Shanmuga Krishnan S M
8cc1580dfa fix(admin): make /admin a top-level route and add Maven compile to deploy.sh
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:08:34 +05:30
Shanmuga Krishnan S M
f714519373 feat(admin): add Admin Dashboard GUI portal at /admin for managing email subscribers
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 14:03:00 +05:30
Shanmuga Krishnan S M
59c2b53ed1 feat(email): support dynamic multi-recipient list from recipients.txt file
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 13:57:00 +05:30
Shanmuga Krishnan S M
332352d789 feat(email): enhance email report with live online status, microservices health checks, and site link
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 13:35:52 +05:30
Shanmuga Krishnan S M
0c380f2caf test(email): trigger live webhook deployment and email automation report
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 13:33:15 +05:30
Shanmuga Krishnan S M
52a6f2719e docs(email): update deploy.sh to run asynchronously in background to prevent webhook 5s timeout
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 12:20:52 +05:30
Shanmuga Krishnan S M
49232fdad1 fix(bus-map): completely remove route polyline lines from map, focus strictly on clean map & live GPS bus markers
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 12:06:11 +05:30
Shanmuga Krishnan S M
85092a2655 fix(email): fix UnicodeEncodeError in print output, format App Password without spaces, add UTF-8 sys.stdout reconfigure
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 12:00:33 +05:30
Shanmuga Krishnan S M
e6520363d0 docs: add email automation setup guide
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 11:58:22 +05:30
Shanmuga Krishnan S M
cd30366555 fix(bus-map): remove polyline route line overlay
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 11:38:26 +05:30
Shanmuga Krishnan S M
1970d0863b ci: update recipient email to dorutoslayer@gmail.com
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 11:27:44 +05:30
Shanmuga Krishnan S M
bea89ce766 test(ci): verify automated email push report notification system
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 11:25:50 +05:30
Amudieshwar A G
75002ffbb2 Merge pull request #7 from Amudieshwar-AG/feature/notes-pyqs
feat(notes): integrate official RIT Moodle portal hub for notes and PYQs
2026-08-07 11:24:17 +05:30
Shanmuga Krishnan S M
cc0dda9749 ci: update git notification scripts with email credentials
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-07 11:24:07 +05:30
Amudieshwar A G
e1f3fe2ec2 feat(notes): integrate official RIT Moodle portal hub for notes and PYQs 2026-08-07 11:20:37 +05:30
Amudieshwar A G
70694da2e2 feat(notes): integrate official RIT Moodle portal hub for notes and PYQs 2026-08-07 11:20:01 +05:30
117 changed files with 30409 additions and 3025 deletions

120
EMAIL_AUTOMATION_GUIDE.md Normal file
View File

@@ -0,0 +1,120 @@
# 📧 Git Push & Deployment Email Notification Automation Guide
This guide explains how the automated email reporting system for **Freshers-Hub** works, including configuration, scripts, and integration steps for VPS & Gitea CI/CD pipelines.
---
## 🛠️ Overview of Included Scripts
Two core scripts were added to the repository under the [`scripts/`](file:///f:/fresher/Freshers-Hub/scripts) directory:
1. **[`scripts/send_git_email.py`](file:///f:/fresher/Freshers-Hub/scripts/send_git_email.py)**: Python script using `smtplib` to send rich HTML formatted deployment reports to `dorutoslayer@gmail.com` via Gmail SMTP.
2. **[`scripts/post-receive.sh`](file:///f:/fresher/Freshers-Hub/scripts/post-receive.sh)**: Shell script template for Git post-receive hook execution.
---
## 🎯 Verification Status
- ✅ Multi-threaded webhook receiver active on port 9000
- ✅ Automatic HTML email reports configured for `dorutoslayer@gmail.com`
---
## ⚙️ Configuration Details
| Parameter | Value |
| :--- | :--- |
| **Sender Email** | `krishnaowoxd@gmail.com` |
| **App Password** | `qkwylwtcsnlrpree` (no spaces) |
| **Recipient Email** | `dorutoslayer@gmail.com` |
| **SMTP Host / Port** | `smtp.gmail.com:587` (TLS) |
---
## 🚀 How to Enable Automated Email Reports
### Method A: Add to VPS Webhook / Deployment Script (Recommended)
If your VPS uses the Webhook endpoint (`http://129.121.126.66:9000/hooks/deploy`), update `/var/www/freshers-hub/deploy.sh` on your VPS to execute asynchronously in the background. This ensures the Webhook returns an instant `200 OK` response to Gitea in 0.1 seconds, preventing timeout errors!
```bash
#!/bin/bash
# /var/www/freshers-hub/deploy.sh
(
cd /var/www/freshers-hub
# 1. Pull latest code from main
git pull origin main
# 2. Rebuild Spring Boot Java Backend & Vite Frontend
cd backend && ./mvnw clean package -DskipTests && cd ..
npm run build
# 3. Restart Microservices
systemctl restart springboot chatbot telegram-bot nginx
# 3. 📧 Send Email Report Notification (sent AFTER build & restart finish!)
python3 /var/www/freshers-hub/scripts/send_git_email.py \
"$(git rev-parse --abbrev-ref HEAD)" \
"$(git log -1 --format='%an <%ae>')" \
"$(git log -1 --format='%h')" \
"$(git log -1 --format='%cd' --date=local)" \
"$(git log -1 --format='%B')" \
"$(git diff-tree --no-commit-id --name-status -r HEAD | head -n 15)" \
"SUCCESS (LIVE ONLINE)"
) >> /var/log/deploy.log 2>&1 &
echo '{"status": "ok", "message": "Deployment triggered in background"}'
```
---
### Method B: Configure Gitea Git Hook
If using Gitea custom hooks:
1. Go to your Gitea repository page: `https://git.rit-services.in/ShanmugaKrishnanSM/RIT-nexus`.
2. Navigate to **Settings** -> **Git Hooks**.
3. Edit **`post-receive`** (or `custom_hooks/post-receive`).
4. Paste the following hook script:
```bash
#!/bin/bash
/usr/bin/python3 /var/www/freshers-hub/scripts/send_git_email.py \
"main" \
"$(git log -1 --format='%an <%ae>')" \
"$(git log -1 --format='%h')" \
"$(git log -1 --format='%cd' --date=local)" \
"$(git log -1 --format='%B')" \
"$(git diff-tree --no-commit-id --name-status -r HEAD | head -n 15)"
```
5. Save changes.
---
## 🧪 Manual Verification Command
To verify that email delivery works from your VPS terminal:
```bash
python3 /var/www/freshers-hub/scripts/send_git_email.py \
"main" \
"Test Author <krishnaowoxd@gmail.com>" \
"cd30366" \
"Fri Aug 7 11:55:00 2026" \
"Testing email notification system" \
"M src/components/BusRouteMap/BusRouteMap.tsx"
```
---
## 📊 Sample HTML Email Report Format
When triggered, the recipient receives a formatted HTML email containing:
- **Branch**: `main`
- **Author**: Commit Author Name & Email
- **Commit Hash**: Short 7-character hash
- **Timestamp**: Date & Time of push
- **Commit Message**: Full commit message text
- **Changed Files**: List of top modified files (`M`, `A`, `D`)

View 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
View File

@@ -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)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## 🏗️ System Architecture & Service Stack
## 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
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"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```
┌───────────────────────────────┐
│ Nginx Web Server │
│ (Ports 80 / 443) │
└───────────────┬───────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ React Frontend │ │Spring Boot Backend│ │Go Chatbot Service │
│ (Vite App) │ │ (Port 8085) │ │ (Port 8081) │
└───────────────────┘ └─────────┬─────────┘ └───────────────────┘
│
▼
┌───────────────────┐ ┌───────────────────┐
│ 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

File diff suppressed because it is too large Load Diff

View File

@@ -69,6 +69,13 @@
<artifactId>spring-boot-starter-validation</artifactId>
</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 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -0,0 +1,708 @@
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.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
@RestController
@RequestMapping("/api/admin")
@CrossOrigin(originPatterns = "*")
public class AdminController {
@Autowired
private BusRouteRepository busRouteRepository;
@Autowired
private BusStopRepository busStopRepository;
@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")
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, Object> payload) {
Map<String, Object> response = new HashMap<>();
try {
String inputUser = "";
String inputPass = "";
if (payload != null) {
Object u = payload.get("username");
Object p = payload.get("password");
if (u != null) inputUser = u.toString().trim();
if (p != null) inputPass = p.toString().trim();
}
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("role", "ROLE_TRANSPORT");
response.put("username", "Transport Admin");
response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026");
response.put("message", "Transport fleet admin login successful");
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) {
response.put("success", false);
response.put("message", "Login error: " + e.getMessage());
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")
public ResponseEntity<List<String>> getRecipients() {
Path path = getRecipientsFilePath();
return ResponseEntity.ok(getRecipientsList(path));
}
@PostMapping("/recipients")
public ResponseEntity<Map<String, Object>> addRecipient(@RequestBody Map<String, Object> payload) {
Map<String, Object> response = new HashMap<>();
Object emailObj = (payload != null) ? payload.get("email") : null;
String email = (emailObj != null) ? emailObj.toString().trim().toLowerCase() : null;
if (email == null || !email.contains("@")) {
response.put("success", false);
response.put("message", "Please enter a valid email address");
return ResponseEntity.ok(response);
}
Path path = getRecipientsFilePath();
List<String> existing = getRecipientsList(path);
if (existing.contains(email)) {
response.put("success", false);
response.put("message", "Email is already subscribed");
return ResponseEntity.ok(response);
}
existing.add(email);
saveRecipientsList(path, existing);
response.put("success", true);
response.put("message", "Added recipient: " + email);
response.put("recipients", existing);
return ResponseEntity.ok(response);
}
@DeleteMapping("/recipients")
public ResponseEntity<Map<String, Object>> removeRecipient(@RequestParam String email) {
Map<String, Object> response = new HashMap<>();
if (email == null) {
response.put("success", false);
response.put("message", "Email parameter is required");
return ResponseEntity.ok(response);
}
String target = email.trim().toLowerCase();
Path path = getRecipientsFilePath();
List<String> existing = getRecipientsList(path);
boolean removed = existing.removeIf(e -> e.equalsIgnoreCase(target));
if (removed) {
saveRecipientsList(path, existing);
response.put("success", true);
response.put("message", "Removed: " + target);
response.put("recipients", existing);
} else {
response.put("success", false);
response.put("message", "Email not found");
}
return ResponseEntity.ok(response);
}
private List<String> getRecipientsList(Path path) {
List<String> recipients = new ArrayList<>();
if (Files.exists(path)) {
try {
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
String trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
recipients.add(trimmed);
}
}
} catch (IOException ignored) {}
}
return recipients;
}
private void saveRecipientsList(Path path, List<String> recipients) {
try {
if (path.getParent() != null && !Files.exists(path.getParent())) {
Files.createDirectories(path.getParent());
}
Files.write(path, recipients, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

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

View File

@@ -101,7 +101,7 @@ public class CollabController {
request.setApplicationsCount(request.getApplicationsCount() + 1);
collabRequestRepository.save(request);
// Notify Telegram/Discord intermediary bot asynchronously
// Notify Telegram intermediary bot asynchronously
new Thread(() -> {
try {
Map<String, Object> payload = new HashMap<>();
@@ -112,7 +112,6 @@ public class CollabController {
payload.put("author_name", request.getAuthorName());
payload.put("contact_info", request.getContactInfo());
payload.put("telegram_chat_id", request.getTelegramChatId());
payload.put("discord_user_id", request.getDiscordUserId());
payload.put("applicant_name", savedApp.getApplicantName());
payload.put("applicant_email", savedApp.getApplicantEmail());
payload.put("applicant_dept", savedApp.getApplicantDept());
@@ -172,11 +171,6 @@ public class CollabController {
return collabRequestRepository.findByTelegramChatIdAndStatusOrderByCreatedAtDesc(chatId, "OPEN");
}
@GetMapping("/active/discord/{userId}")
public List<CollabRequest> getActiveRequestsByDiscord(@PathVariable String userId) {
return collabRequestRepository.findByDiscordUserIdAndStatusOrderByCreatedAtDesc(userId, "OPEN");
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrCancelCollabRequest(@PathVariable Integer id) {
return collabRequestRepository.findById(id).map(req -> {

View File

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

View File

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

View File

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

View 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();
}

View File

@@ -46,9 +46,6 @@ public class CollabRequest {
@Column(name = "telegram_chat_id")
private Long telegramChatId;
@Column(name = "discord_user_id")
private String discordUserId;
@Column(name = "status")
@Builder.Default
private String status = "OPEN";

View File

@@ -30,11 +30,14 @@ public class CommunityAnswer {
private String author;
@Column(name = "upvotes")
@Builder.Default
private Integer upvotes = 0;
@Column(name = "is_accepted")
@Builder.Default
private Boolean isAccepted = false;
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
}

View File

@@ -29,15 +29,19 @@ public class CommunityQuestion {
private String author;
@Column(name = "upvotes")
@Builder.Default
private Integer upvotes = 0;
@Column(name = "tags", columnDefinition = "text[]")
@Builder.Default
private List<String> tags = new ArrayList<>();
@Column(name = "is_answered")
@Builder.Default
private Boolean isAnswered = false;
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.EAGER)

View File

@@ -39,8 +39,10 @@ public class NotePyq {
private String fileSize;
@Column(name = "downloads_count")
@Builder.Default
private Integer downloadsCount = 0;
@Column(name = "uploaded_at")
@Builder.Default
private LocalDateTime uploadedAt = LocalDateTime.now();
}

View File

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

View File

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

View File

@@ -10,5 +10,4 @@ public interface CollabRequestRepository extends JpaRepository<CollabRequest, In
List<CollabRequest> findAllByOrderByCreatedAtDesc();
List<CollabRequest> findByAuthorEmailOrderByCreatedAtDesc(String authorEmail);
List<CollabRequest> findByTelegramChatIdAndStatusOrderByCreatedAtDesc(Long telegramChatId, String status);
List<CollabRequest> findByDiscordUserIdAndStatusOrderByCreatedAtDesc(String discordUserId, String status);
}

View File

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

View File

@@ -45,8 +45,17 @@ public class BusLocationService {
public List<BusLocation> getActiveLocations() {
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));
return List.copyOf(locationCache.values());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,14 +3,38 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=
spring.datasource.username=postgres
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 ───
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false
# ─── SERVER PORT ───
# ─── SERVER PORT & HTTP GZIP COMPRESSION ───
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.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
# ─── 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.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}

View File

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

View 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

File diff suppressed because it is too large Load Diff

67
package-lock.json generated
View File

@@ -1614,21 +1614,32 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
"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,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1776,9 +1787,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3602,9 +3610,6 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3619,9 +3624,6 @@
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3636,9 +3638,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3653,9 +3652,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3670,9 +3666,6 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3687,9 +3680,6 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3704,9 +3694,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3721,9 +3708,6 @@
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3738,9 +3722,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3755,9 +3736,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3772,9 +3750,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3789,9 +3764,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3806,9 +3778,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [

Binary file not shown.

BIN
public/RAISE.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
public/Raiselogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/clubs/center_ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
public/clubs/center_iot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
public/clubs/telugu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 8.8 MiB

BIN
public/raise/RAISE.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
public/raise/Raiselogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

3543
real_timetable.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -135,3 +135,13 @@ CREATE TABLE IF NOT EXISTS confessions (
reactions_count INT DEFAULT 0,
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)
);

View 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]}")

View 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]}")

View 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)])

View 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)

View File

@@ -1,25 +0,0 @@
#!/bin/bash
# Configuration (or set in environment variables on VPS)
export SENDER_EMAIL="your-gmail-id@gmail.com"
export SENDER_PASSWORD="your-16-digit-app-password"
export RECIPIENT_EMAIL="your-recipient-email@gmail.com"
while read oldrev newrev refname
do
BRANCH=$(echo $refname | cut -d/ -f3)
COMMIT_HASH=$(git log -1 --format="%h" $newrev)
AUTHOR=$(git log -1 --format="%an <%ae>" $newrev)
MESSAGE=$(git log -1 --format="%B" $newrev)
TIMESTAMP=$(git log -1 --format="%cd" --date=local $newrev)
CHANGED_FILES=$(git diff-tree --no-commit-id --name-status -r $newrev | head -n 15)
python3 /var/www/freshers-hub/scripts/send_git_email.py \
"$BRANCH" \
"$AUTHOR" \
"$COMMIT_HASH" \
"$TIMESTAMP" \
"$MESSAGE" \
"$CHANGED_FILES"
done

1
scripts/recipients.txt Normal file
View File

@@ -0,0 +1 @@
dorutoslayer@gmail.com

View File

@@ -1,83 +0,0 @@
import sys
import smtplib
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Load configuration from environment or fallback defaults
SMTP_SERVER = os.environ.get("SMTP_SERVER", "smtp.gmail.com")
SMTP_PORT = int(os.environ.get("SMTP_PORT", 587))
SENDER_EMAIL = os.environ.get("SENDER_EMAIL", "YOUR_EMAIL@gmail.com")
SENDER_PASSWORD = os.environ.get("SENDER_PASSWORD", "YOUR_GMAIL_APP_PASSWORD")
RECIPIENT_EMAIL = os.environ.get("RECIPIENT_EMAIL", "YOUR_EMAIL@gmail.com")
if len(sys.argv) < 7:
print("Usage: python send_git_email.py <branch> <author> <commit_hash> <timestamp> <message> <changed_files>")
sys.exit(1)
branch = sys.argv[1]
author = sys.argv[2]
commit_hash = sys.argv[3]
timestamp = sys.argv[4]
message = sys.argv[5]
changed_files = sys.argv[6]
subject = f"[Freshers-Hub VPS] Code Push Report - Commit {commit_hash} ({branch})"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f6f8; color: #333; padding: 20px; }}
.card {{ background: #ffffff; border-radius: 10px; padding: 28px; max-width: 650px; margin: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.08); border: 1px solid #e2e8f0; }}
.header {{ color: #2563eb; font-size: 22px; font-weight: 700; border-bottom: 2px solid #3b82f6; padding-bottom: 12px; margin-bottom: 20px; }}
.field {{ margin-bottom: 12px; font-size: 14px; }}
.label {{ font-weight: 600; color: #475569; display: inline-block; width: 120px; }}
.value {{ color: #0f172a; font-weight: 500; }}
.badge {{ background-color: #e0e7ff; color: #3730a3; padding: 3px 8px; border-radius: 4px; font-family: monospace; font-size: 13px; }}
.message-box {{ background: #f8fafc; border-left: 4px solid #3b82f6; padding: 12px; border-radius: 4px; font-style: italic; margin-top: 6px; font-size: 14px; color: #334155; }}
.code-box {{ background: #0f172a; color: #38bdf8; padding: 14px; border-radius: 6px; font-family: Consolas, Monaco, monospace; white-space: pre-wrap; word-break: break-all; margin-top: 6px; font-size: 13px; line-height: 1.5; }}
.footer {{ font-size: 12px; color: #94a3b8; margin-top: 28px; text-align: center; border-top: 1px solid #f1f5f9; padding-top: 14px; }}
</style>
</head>
<body>
<div class="card">
<div class="header">🚀 Freshers-Hub Deployment & Push Report</div>
<div class="field"><span class="label">Branch:</span> <span class="badge">{branch}</span></div>
<div class="field"><span class="label">Author:</span> <span class="value">{author}</span></div>
<div class="field"><span class="label">Commit Hash:</span> <span class="badge">{commit_hash}</span></div>
<div class="field"><span class="label">Time:</span> <span class="value">{timestamp}</span></div>
<div class="field" style="margin-top: 18px;">
<span class="label" style="width: 100%;">Commit Message:</span>
<div class="message-box">{message}</div>
</div>
<div class="field" style="margin-top: 18px;">
<span class="label" style="width: 100%;">Changed Files (Top Modified):</span>
<div class="code-box">{changed_files}</div>
</div>
<div class="footer">Automated push report generated by RIT Freshers-Hub VPS Git Hook</div>
</div>
</body>
</html>
"""
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = SENDER_EMAIL
msg["To"] = RECIPIENT_EMAIL
msg.attach(MIMEText(html_content, "html"))
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.sendmail(SENDER_EMAIL, RECIPIENT_EMAIL, msg.as_string())
print("✅ Email report sent successfully.")
except Exception as e:
print(f"❌ Failed to send email report: {e}")

249
scripts/service_watchdog.py Normal file
View File

@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""
Service Health Watchdog — Monitors systemd services and sends email alerts
when any service goes DOWN or comes back UP.
Runs via systemd timer every 60 seconds. Only sends email on STATUS CHANGE.
State is tracked in /tmp/service_watchdog_state.json
"""
import subprocess
import json
import os
import sys
import smtplib
from datetime import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8')
# ─── CONFIGURATION ───
SERVICES = [
{"name": "springboot", "label": "Spring Boot API", "port": "8085"},
{"name": "chatbot", "label": "Go AI Chatbot Engine", "port": "8081"},
{"name": "telegram-bot", "label": "Telegram Bot Service", "port": "8082"},
{"name": "nginx", "label": "Nginx Web Server", "port": "80/443"},
{"name": "webhook", "label": "Webhook Receiver", "port": "9000"},
{"name": "postgresql", "label": "PostgreSQL Database", "port": "5432"},
]
STATE_FILE = "/tmp/service_watchdog_state.json"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Email config (reuses same Gmail credentials as deploy emails)
SMTP_SERVER = os.environ.get("SMTP_SERVER", "smtp.gmail.com")
SMTP_PORT = int(os.environ.get("SMTP_PORT", 587))
SENDER_EMAIL = os.environ.get("SENDER_EMAIL", "krishnaowoxd@gmail.com")
SENDER_PASSWORD = os.environ.get("SENDER_PASSWORD", "qkwylwtcsnlrpree").replace(" ", "")
# Load recipients from shared recipients.txt
recipients_file = os.path.join(SCRIPT_DIR, "recipients.txt")
recipients = []
if os.path.exists(recipients_file):
with open(recipients_file, "r", encoding="utf-8") as f:
recipients = [line.strip() for line in f if line.strip() and not line.startswith("#")]
if not recipients:
recipients = [os.environ.get("RECIPIENT_EMAIL", "dorutoslayer@gmail.com")]
def is_service_active(service_name):
"""Check if a systemd service is currently active (running)."""
try:
result = subprocess.run(
["systemctl", "is-active", service_name],
capture_output=True, text=True, timeout=5
)
return result.stdout.strip() == "active"
except Exception:
return False
def load_previous_state():
"""Load the last known state of all services."""
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, "r") as f:
return json.load(f)
except Exception:
pass
return {}
def save_current_state(state):
"""Save current state to disk."""
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
def send_alert_email(changes, current_state):
"""Send an HTML email alert for service status changes."""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Determine if any service went DOWN
has_down = any(c["status"] == "DOWN" for c in changes)
has_up = any(c["status"] == "UP" for c in changes)
if has_down and has_up:
subject = f"[RIT VPS] ⚠️ Service Status Change — {now}"
banner_bg = "#fef3c7"
banner_border = "#f59e0b"
banner_text = "#92400e"
banner_icon = "⚠️"
banner_msg = "Some services changed status"
elif has_down:
subject = f"[RIT VPS] 🔴 SERVICE DOWN ALERT — {now}"
banner_bg = "#fef2f2"
banner_border = "#ef4444"
banner_text = "#b91c1c"
banner_icon = "🚨"
banner_msg = "One or more services went DOWN"
else:
subject = f"[RIT VPS] 🟢 SERVICE RECOVERED — {now}"
banner_bg = "#ecfdf5"
banner_border = "#10b981"
banner_text = "#047857"
banner_icon = "✅"
banner_msg = "Services have recovered and are back online"
# Build change rows
change_rows = ""
for c in changes:
if c["status"] == "DOWN":
icon = "🔴"
status_label = "WENT DOWN"
row_bg = "#fef2f2"
row_color = "#b91c1c"
else:
icon = "🟢"
status_label = "RECOVERED"
row_bg = "#ecfdf5"
row_color = "#047857"
change_rows += f"""
<tr style="background: {row_bg};">
<td style="padding: 10px 14px; font-weight: 600; color: #1e293b;">{icon} {c['label']}</td>
<td style="padding: 10px 14px; font-family: monospace; color: #64748b;">{c['name']}.service</td>
<td style="padding: 10px 14px; font-family: monospace; color: #64748b;">:{c['port']}</td>
<td style="padding: 10px 14px; font-weight: 700; color: {row_color};">{status_label}</td>
</tr>"""
# Build full status rows
status_rows = ""
for svc in SERVICES:
is_up = current_state.get(svc["name"], False)
s_icon = "🟢" if is_up else "🔴"
s_label = "Running" if is_up else "Down"
s_color = "#047857" if is_up else "#b91c1c"
status_rows += f"""
<tr>
<td style="padding: 8px 14px; color: #1e293b;">{s_icon} {svc['label']}</td>
<td style="padding: 8px 14px; font-family: monospace; color: #64748b;">:{svc['port']}</td>
<td style="padding: 8px 14px; font-weight: 600; color: {s_color};">{s_label}</td>
</tr>"""
html = f"""
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; background: #f4f6f8; padding: 20px;">
<div style="background: #fff; border-radius: 12px; padding: 28px; max-width: 650px; margin: auto; box-shadow: 0 4px 15px rgba(0,0,0,0.08); border: 1px solid #e2e8f0;">
<div style="font-size: 22px; font-weight: 700; color: #1e293b; border-bottom: 2px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 20px;">
🛡️ RIT VPS Service Health Monitor
</div>
<div style="background: {banner_bg}; border: 1px solid {banner_border}; color: {banner_text}; padding: 14px; border-radius: 8px; font-weight: 700; font-size: 15px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px;">
<span>{banner_icon}</span>
<span>{banner_msg}</span>
</div>
<div style="font-size: 13px; color: #64748b; margin-bottom: 16px;">
Detected at: <strong style="color: #1e293b;">{now}</strong>
</div>
<h3 style="font-size: 15px; color: #1e293b; margin: 20px 0 10px;">Status Changes</h3>
<table style="width: 100%; border-collapse: collapse; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; font-size: 13px;">
<thead>
<tr style="background: #f1f5f9;">
<th style="padding: 10px 14px; text-align: left; color: #475569;">Service</th>
<th style="padding: 10px 14px; text-align: left; color: #475569;">Unit</th>
<th style="padding: 10px 14px; text-align: left; color: #475569;">Port</th>
<th style="padding: 10px 14px; text-align: left; color: #475569;">Event</th>
</tr>
</thead>
<tbody>{change_rows}</tbody>
</table>
<h3 style="font-size: 15px; color: #1e293b; margin: 24px 0 10px;">Full System Status</h3>
<table style="width: 100%; border-collapse: collapse; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; font-size: 13px;">
<thead>
<tr style="background: #f1f5f9;">
<th style="padding: 8px 14px; text-align: left; color: #475569;">Service</th>
<th style="padding: 8px 14px; text-align: left; color: #475569;">Port</th>
<th style="padding: 8px 14px; text-align: left; color: #475569;">Status</th>
</tr>
</thead>
<tbody>{status_rows}</tbody>
</table>
<div style="text-align: center; margin-top: 20px;">
<a href="https://rit-services.in" style="display: inline-block; background: #f97316; color: #fff; padding: 10px 20px; border-radius: 6px; text-decoration: none; font-weight: 700; font-size: 13px;" target="_blank">🌐 Check Live Site</a>
</div>
<div style="font-size: 12px; color: #94a3b8; margin-top: 28px; text-align: center; border-top: 1px solid #f1f5f9; padding-top: 14px;">
Automated health alert from RIT VPS Watchdog • Checks every 60 seconds
</div>
</div>
</body>
</html>
"""
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = SENDER_EMAIL
msg["To"] = ", ".join(recipients)
msg.attach(MIMEText(html, "html"))
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.sendmail(SENDER_EMAIL, recipients, msg.as_string())
print(f"✅ Alert email sent: {subject}")
except Exception as e:
print(f"❌ Failed to send alert: {e}")
def main():
previous_state = load_previous_state()
current_state = {}
changes = []
for svc in SERVICES:
is_up = is_service_active(svc["name"])
current_state[svc["name"]] = is_up
# Compare with previous state (skip first run — no alert on initial scan)
if svc["name"] in previous_state:
was_up = previous_state[svc["name"]]
if was_up and not is_up:
changes.append({**svc, "status": "DOWN"})
print(f"🔴 {svc['label']} ({svc['name']}) went DOWN!")
elif not was_up and is_up:
changes.append({**svc, "status": "UP"})
print(f"🟢 {svc['label']} ({svc['name']}) recovered!")
# Save current state for next run
save_current_state(current_state)
# Send email only if there are status changes
if changes:
send_alert_email(changes, current_state)
else:
print(f"✓ All services stable — no changes detected ({datetime.now().strftime('%H:%M:%S')})")
if __name__ == "__main__":
main()

View File

@@ -1,25 +1,35 @@
import { useEffect } from 'react';
import { useEffect, lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@/context/AuthContext';
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
import MainLayout from '@/layouts/MainLayout';
import Home from '@/pages/Home/Home';
import Notes from '@/pages/Notes/Notes';
import AIAssistant from '@/pages/AIAssistant/AIAssistant';
import Campus from '@/pages/Campus/Campus';
import Events from '@/pages/Events/Events';
import Community from '@/pages/Community/Community';
import BusRoutes from '@/pages/BusRoutes/BusRoutes';
import Faculty from '@/pages/Faculty/Faculty';
import DevCollab from '@/pages/DevCollab/DevCollab';
import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard';
import PageLoader from '@/components/PageLoader/PageLoader';
import ErrorBoundary from '@/components/ErrorBoundary/ErrorBoundary';
// Code-split route components with dynamic imports for minimal initial bundle size
const Home = lazy(() => import('@/pages/Home/Home'));
const Notes = lazy(() => import('@/pages/Notes/Notes'));
const Toolkit = lazy(() => import('@/pages/Toolkit/Toolkit'));
const AIAssistant = lazy(() => import('@/pages/AIAssistant/AIAssistant'));
const Campus = lazy(() => import('@/pages/Campus/Campus'));
const Events = lazy(() => import('@/pages/Events/Events'));
const Community = lazy(() => import('@/pages/Community/Community'));
const BusRoutes = lazy(() => import('@/pages/BusRoutes/BusRoutes'));
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({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
},
},
});
@@ -36,60 +46,87 @@ function ScrollToTop() {
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<ScrollToTop />
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/notes" element={<Notes />} />
<Route path="/ai-assistant" element={<AIAssistant />} />
<Route path="/campus" element={<Campus />} />
<Route path="/bus-routes" element={<BusRoutes />} />
<Route path="/faculty" element={<Faculty />} />
<Route path="/events" element={<Events />} />
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<ScrollToTop />
<Suspense fallback={<PageLoader />}>
<Routes>
{/* Standalone Admin Route */}
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/*" element={<AdminDashboard />} />
{/* Protected Routes — Require Google Sign-In */}
<Route
path="/community"
element={
<ProtectedRoute
title="RIT Q&A Community"
description="Connect with fellow RIT students and ask senior helpers any question about campus life and academics."
>
<Community />
</ProtectedRoute>
}
/>
<Route
path="/collab"
element={
<ProtectedRoute
title="Developer Collab Hub"
description="Post open-source projects, search for co-developers, and connect with fellow programmers."
requireVerifiedStudent={true}
>
<DevCollab />
</ProtectedRoute>
}
/>
<Route
path="/leetcode"
element={
<ProtectedRoute
title="RIT LeetCode Leaderboard"
description="Track campus competitive programming rankings and see top RIT coders."
>
<LeetcodeLeaderboard />
</ProtectedRoute>
}
/>
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
{/* Standalone RAISE Route */}
<Route path="/raise" element={<Raise />} />
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/notes" element={<Notes />} />
<Route path="/toolkit" element={<Toolkit />} />
<Route path="/ai-assistant" element={<AIAssistant />} />
<Route path="/campus" element={<Campus />} />
<Route path="/bus-routes" element={<BusRoutes />} />
<Route path="/faculty" element={<Faculty />} />
<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 */}
<Route
path="/community"
element={
<ProtectedRoute
title="RIT Q&A Community"
description="Connect with fellow RIT students and ask senior helpers any question about campus life and academics."
>
<Community />
</ProtectedRoute>
}
/>
<Route
path="/collab"
element={
<ProtectedRoute
title="Developer Collab Hub"
description="Post open-source projects, search for co-developers, and connect with fellow programmers."
requireVerifiedStudent={true}
>
<DevCollab />
</ProtectedRoute>
}
/>
<Route
path="/leetcode"
element={
<ProtectedRoute
title="RIT Student Leaderboard"
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 />
</ProtectedRoute>
}
/>
</Route>
</Routes>
</Suspense>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
</ErrorBoundary>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
src/assets/raise/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -58,8 +58,7 @@ export function StaggerContainer({
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: '-50px' }}
animate="visible"
variants={{
hidden: {},
visible: {

View File

@@ -1,7 +1,11 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
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 { useNavigate } from 'react-router-dom';
interface GoogleAuthModalProps {
isOpen: boolean;
@@ -10,7 +14,14 @@ interface GoogleAuthModalProps {
export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) {
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
useEffect(() => {
@@ -21,7 +32,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
// Render Google Button inside modal when open
useEffect(() => {
if (!isOpen) return;
if (!isOpen || activeMode !== 'google') return;
const renderGoogleBtn = () => {
if (window.google && buttonRef.current) {
@@ -38,10 +49,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
}
};
// Render immediately if script is loaded
renderGoogleBtn();
// Or poll briefly if script is loading
const interval = setInterval(() => {
if (window.google) {
renderGoogleBtn();
@@ -50,7 +58,40 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
}, 200);
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 (
<AnimatePresence>
@@ -62,20 +103,20 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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 */}
<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 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', bounce: 0.2, 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"
exit={{ opacity: 0, scale: 0.92, y: 15 }}
transition={{ type: 'spring', bounce: 0.18, duration: 0.3 }}
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 */}
<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 -bottom-20 -left-20 w-40 h-40 bg-blue-500/20 rounded-full blur-2xl pointer-events-none" />
{/* Background Glow */}
<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-44 h-44 bg-indigo-500/15 rounded-full blur-3xl pointer-events-none" />
{/* Close Button */}
<button
@@ -85,38 +126,149 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
<X className="w-5 h-5" />
</button>
{/* Icon 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">
<Sparkles className="w-8 h-8 text-[#F97316]" />
{/* Header */}
<div className="text-center mb-5">
<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>
<h2 className="text-xl font-extrabold text-white tracking-tight">
Sign In to Freshers Hub
</h2>
<p className="text-xs text-slate-400 mt-1">
Enter your student register number or admin credentials.
</p>
</div>
{/* Title & Description */}
<h2
className="text-2xl font-bold text-white mb-2"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
Sign In to Freshers Hub
</h2>
<p
className="text-xs text-slate-300 mb-6 leading-relaxed"
style={{ fontFamily: 'Inter, sans-serif' }}
>
Sign in with your Google account. Use your{' '}
<strong className="text-[#F97316]">@ritchennai.edu.in</strong> college email to unlock the{' '}
<strong className="text-emerald-400">Verified Student</strong> badge and post collaboration requests!
</p>
{/* Google Sign-In Button Container */}
<div className="flex justify-center my-6 min-h-[44px]">
<div ref={buttonRef} />
{/* Tab Selector */}
<div className="flex rounded-xl bg-slate-950/80 p-1 border border-white/5 mb-5 text-xs font-semibold">
<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>
{/* Verified Student Callout */}
<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-4 h-4 text-emerald-400 shrink-0" />
<span>Official RIT Student Verification via Google</span>
</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{' '}
<strong className="text-orange-400">@ritchennai.edu.in</strong> email for automatic student verification.
</p>
<div className="flex justify-center my-4 min-h-[44px]">
<div ref={buttonRef} />
</div>
<div className="pt-3 border-t border-white/10 flex items-center justify-center gap-1.5 text-[11px] text-slate-400">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
<span>Google OAuth 2.0 Single Sign-On</span>
</div>
</div>
)}
</motion.div>
</div>
)}

View File

@@ -53,8 +53,8 @@ export default function ProtectedRoute({
</div>
<h1
className="text-2xl sm:text-3xl font-bold text-[#1E293B] mb-3"
style={{ fontFamily: 'Playfair Display, serif' }}
className="text-2xl sm:text-3xl font-extrabold text-[#1E293B] mb-3"
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
{title}
</h1>

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline, useMap } from 'react-leaflet';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import type { BusRoute } from '@/types';
@@ -17,35 +17,6 @@ L.Icon.Default.mergeOptions({
shadowUrl: markerShadow,
});
// Domain Boundary validation (Chennai and surrounding route regions)
const MIN_LAT = 12.65;
const MAX_LAT = 13.35;
const MIN_LNG = 79.25;
const MAX_LNG = 80.45;
const isValidCoordinate = (lat?: number, lng?: number): boolean => {
if (lat === undefined || lng === undefined) return false;
return lat >= MIN_LAT && lat <= MAX_LAT && lng >= MIN_LNG && lng <= MAX_LNG;
};
const isRealStopCoordinate = (stop: { name: string; lat?: number; lng?: number }): boolean => {
if (stop.lat === undefined || stop.lng === undefined) return false;
if (!isValidCoordinate(stop.lat, stop.lng)) return false;
const isRITCoord = Math.abs(stop.lat - 13.0118) < 0.001 && Math.abs(stop.lng - 80.0214) < 0.001;
const isRITName =
stop.name.toLowerCase().includes('rit') ||
stop.name.toLowerCase().includes('campus') ||
stop.name.toLowerCase().includes('college') ||
stop.name.toLowerCase().includes('rajalakshmi');
if (isRITCoord && !isRITName) {
return false;
}
return true;
};
const liveBusIcon = L.divIcon({
html: `<div class="relative flex items-center justify-center w-10 h-10 rounded-full bg-emerald-500 border-2 border-white shadow-xl text-white text-base font-bold cursor-pointer"><span class="absolute inset-0 rounded-full bg-emerald-500/50 animate-ping"></span>🚌</div>`,
className: '',
@@ -60,23 +31,23 @@ const stoppedBusIcon = L.divIcon({
iconAnchor: [20, 20],
});
// Helper component to auto-pan and fit the map bounds to the active route
function MapUpdater({ bounds }: { bounds: L.LatLngBoundsExpression | null }) {
// Helper component to pan map to active bus marker when selected
function MapPanFocus({ center }: { center: [number, number] | null }) {
const map = useMap();
useEffect(() => {
if (bounds && (bounds as any).length > 0) {
map.fitBounds(bounds, { padding: [60, 60], maxZoom: 14, animate: true });
if (center) {
map.setView(center, 13, { animate: true });
}
}, [bounds, map]);
}, [center, map]);
return null;
}
interface BusRouteMapProps {
selectedRoute: BusRoute | null;
allRoutes: BusRoute[];
allRoutes?: BusRoute[];
}
const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default
const DEFAULT_CENTER: [number, number] = [13.0118, 80.0214]; // RIT Campus default
export default function BusRouteMap({ selectedRoute }: BusRouteMapProps) {
const [allLiveLocations, setAllLiveLocations] = useState<
@@ -129,37 +100,26 @@ export default function BusRouteMap({ selectedRoute }: BusRouteMapProps) {
return () => clearInterval(interval);
}, []);
// Collect coordinates for polyline path
const pathCoordinates = useMemo(() => {
if (!selectedRoute) return [];
// Determine center focus point if a bus is live or selected
const activeFocusCenter = useMemo<[number, number] | null>(() => {
if (!selectedRoute) return null;
if (selectedRoute.polyline && selectedRoute.polyline.length > 0) {
return selectedRoute.polyline;
const normalizeRouteNum = (str: string) => (str || '').replace(/^[Rr]0*/, '').trim().toUpperCase();
const matchLocKey = Object.keys(allLiveLocations).find(
(k) => normalizeRouteNum(k) === normalizeRouteNum(selectedRoute.number)
);
if (matchLocKey && allLiveLocations[matchLocKey]) {
const loc = allLiveLocations[matchLocKey];
return [loc.latitude, loc.longitude];
}
const coords: [number, number][] = [];
if (isValidCoordinate(selectedRoute.from_lat, selectedRoute.from_lng)) {
coords.push([selectedRoute.from_lat!, selectedRoute.from_lng!]);
if (selectedRoute.from_lat && selectedRoute.from_lng) {
return [selectedRoute.from_lat, selectedRoute.from_lng];
}
selectedRoute.stops.forEach((stop) => {
if (isRealStopCoordinate(stop)) {
coords.push([stop.lat!, stop.lng!]);
}
});
if (isValidCoordinate(selectedRoute.to_lat, selectedRoute.to_lng)) {
coords.push([selectedRoute.to_lat!, selectedRoute.to_lng!]);
}
return coords;
}, [selectedRoute]);
const mapBounds = useMemo(() => {
if (pathCoordinates.length === 0) return null;
return pathCoordinates as L.LatLngBoundsExpression;
}, [pathCoordinates]);
return DEFAULT_CENTER;
}, [selectedRoute, allLiveLocations]);
return (
<div className="relative h-full w-full bg-slate-950 overflow-hidden">
@@ -178,21 +138,7 @@ export default function BusRouteMap({ selectedRoute }: BusRouteMapProps) {
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* Polyline Route Path */}
{pathCoordinates.length > 0 && selectedRoute && (
<Polyline
positions={pathCoordinates}
pathOptions={{
color: selectedRoute.color || '#F97316',
weight: 5,
opacity: 0.8,
lineCap: 'round',
lineJoin: 'round',
}}
/>
)}
{/* Live GPS Bus Markers */}
{/* Live GPS Bus Markers ONLY - No polyline routes */}
{Object.entries(allLiveLocations).map(([rNum, loc]) => {
const normalizeRouteNum = (str: string) => (str || '').replace(/^[Rr]0*/, '').trim().toUpperCase();
if (selectedRoute && normalizeRouteNum(selectedRoute.number) !== normalizeRouteNum(rNum)) return null;
@@ -228,7 +174,7 @@ export default function BusRouteMap({ selectedRoute }: BusRouteMapProps) {
);
})}
<MapUpdater bounds={mapBounds} />
<MapPanFocus center={activeFocusCenter} />
</MapContainer>
{/* Floating Info Card on Selected Route */}

View 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;

View File

@@ -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' },
];
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 (
<>
<motion.div
whileHover={{ y: -6, boxShadow: '0 20px 40px -10px rgba(0,0,0,0.1)' }}
whileHover={{ y: -4 }}
transition={{ duration: 0.3 }}
className="bg-white border border-[#E8ECF4] rounded-[20px] p-5 flex flex-col gap-4 relative"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
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"
>
{/* Avatar + Name */}
<div className="flex items-center gap-4">
<div className="relative">
<div
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}
</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>
<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}
</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}
</p>
</div>
@@ -68,27 +113,27 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
{/* Details */}
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-xs text-[#475569]">
<Building2 className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
<div className="flex items-center gap-2 text-xs text-[#4A3E5E]">
<Building2 className="w-3.5 h-3.5 text-[#9E91B6] shrink-0" />
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
</div>
</div>
{/* 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
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"
style={{ fontFamily: 'Poppins, sans-serif' }}
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: 'Plus Jakarta Sans, sans-serif' }}
aria-label={`Email ${faculty.name}`}
>
<Mail className="w-3.5 h-3.5" />
<Mail className="w-3.5 h-3.5 text-[#FF6B00]" />
Email
</button>
<button
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]"
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
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: '#FF6B00', fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
View Profile
<ChevronRight className="w-3.5 h-3.5" />
@@ -135,11 +180,11 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
>
<div
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}
</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}
</h2>
<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">
<User className="w-5 h-5 text-[#FF7A00]" />
</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 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">
<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>
</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">
<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>
@@ -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>
</div>
{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-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.joinedInstitution}</p>
</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>
@@ -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">
<BookOpen className="w-5 h-5 text-indigo-500" />
</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 className="flex flex-col gap-4">
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.subjectsHandling || 'Not specified'}</p>
</div>
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.researchAreas || 'Not specified'}</p>
{renderPointList(faculty.subjectsHandling)}
</div>
{faculty.researchAreas && (
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.researchAreas}</p>
</div>
)}
{faculty.studentsGuided && (
<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>
@@ -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">
<GraduationCap className="w-5 h-5 text-pink-500" />
</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 className="flex flex-col gap-6">
{faculty.achievements && faculty.achievements.length > 0 && (
@@ -295,30 +342,36 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
)}
{/* Section 4: Office Info */}
<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="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" />
</div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Office Information</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 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" />
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.officeHours || 'Not specified'}</p>
{(faculty.office || faculty.officeHours) && (
<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="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" />
</div>
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Office Information</h3>
</div>
<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" />
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office || 'Not specified'}</p>
</div>
<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">
<Clock className="w-6 h-6 text-emerald-500 shrink-0 mt-0.5" />
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.officeHours}</p>
</div>
</div>
)}
{faculty.office && (
<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" />
<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-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office}</p>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</motion.div>
@@ -349,7 +402,7 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
>
<div className="flex justify-between items-center">
<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>
</div>
<button onClick={() => setShowEmailOptions(false)} className="p-2 rounded-full hover:bg-gray-100 transition-colors">

View File

@@ -13,14 +13,13 @@ export default function FeatureCard({ feature }: FeatureCardProps) {
return (
<motion.div
whileHover={{ y: -6, boxShadow: '0 20px 50px -12px rgba(0,0,0,0.15)' }}
whileHover={{ y: -4 }}
transition={{ duration: 0.25 }}
className="bg-white border border-[#E5E7EB] rounded-2xl p-6 flex flex-col gap-4 group cursor-pointer"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
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"
>
{/* Icon */}
<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 }}
>
{IconComponent && <IconComponent className="w-6 h-6" />}
@@ -29,12 +28,12 @@ export default function FeatureCard({ feature }: FeatureCardProps) {
{/* Content */}
<div className="flex-1">
<h3
className="text-base font-semibold text-[#1E293B] mb-1.5"
style={{ fontFamily: 'Poppins, sans-serif' }}
className="text-base font-bold text-[#1A0B2E] mb-1.5"
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
{feature.title}
</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}
</p>
</div>
@@ -42,9 +41,9 @@ export default function FeatureCard({ feature }: FeatureCardProps) {
{/* Arrow */}
<Link
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={{
fontFamily: 'Poppins, sans-serif',
fontFamily: 'Plus Jakarta Sans, sans-serif',
color: feature.color,
}}
aria-label={`Explore ${feature.title}`}

View File

@@ -1,123 +1,239 @@
import { Link } from 'react-router-dom';
import { GraduationCap, MapPin, Phone, Mail, ArrowRight } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
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 = [
{ label: 'Home', path: '/' },
{ label: 'Notes & PYQs', path: '/notes' },
{ label: 'RIT Chatbot', path: '/ai-assistant' },
{ label: '3D Campus Map', path: '/campus' },
{ label: 'Bus Routes', path: '/bus-routes' },
{ label: 'Faculty Directory', path: '/faculty' },
{ label: 'Events', path: '/events' },
{ label: 'Community', path: '/community' },
{ label: 'Dev Hub', path: '/collab' },
const exploreLinks = [
{ label: 'Notes & PYQs', path: '/notes', icon: BookOpen },
{ label: 'Student Toolkit', path: '/toolkit', icon: Wrench },
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
{ label: 'Student Leaderboard', path: '/student-leaderboard', icon: Trophy },
];
const campusLinks = [
{ 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() {
const location = useLocation();
const isHome = location.pathname === '/';
return (
<footer className="bg-[#1E293B] text-white">
{/* CTA Banner */}
<div style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }} className="py-12">
<div className="container-custom flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h3 className="text-2xl font-bold text-white" style={{ fontFamily: 'Playfair Display, serif' }}>
Ready to explore RIT?
</h3>
<p className="text-orange-100 mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>
Everything a fresher needs — all in one place.
</p>
</div>
<Link
to="/ai-assistant"
className="flex items-center gap-2 bg-white text-[#F97316] px-6 py-3 rounded-xl font-semibold transition-all hover:shadow-lg hover:-translate-y-0.5"
style={{ fontFamily: 'Poppins, sans-serif' }}
<div className="w-full relative z-20">
{/* ─── 1. CTA Banner (Floating Overlay Box - Only on Home Page) ────────── */}
{isHome && (
<div className="container-custom relative z-20 -mb-20 lg:-mb-24 px-4">
<motion.div
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%)',
}}
>
Ask Chatbot
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
{/* 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" />
{/* Main Footer */}
<div className="container-custom py-14">
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
{/* Brand */}
<div>
<div className="flex items-center gap-2.5 mb-4">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
>
<GraduationCap className="w-5 h-5 text-white" />
</div>
<div>
<div className="font-bold text-white text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Freshers Hub</div>
<div className="text-[10px] text-slate-400">Rajalakshmi Institute of Technology</div>
</div>
</div>
<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>
</div>
{/* Quick Links */}
<div>
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
Quick Navigation
</h4>
<div className="grid grid-cols-2 gap-2">
{quickLinks.map((link) => (
<Link
key={link.path}
to={link.path}
className="text-slate-400 hover:text-[#F97316] text-sm transition-colors flex items-center gap-1.5 group py-1"
<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?
</h3>
<p
className="text-orange-100 text-sm lg:text-base mt-2 max-w-xl font-medium"
style={{ fontFamily: 'Inter, sans-serif' }}
>
<span className="w-1 h-1 rounded-full bg-[#F97316] opacity-0 group-hover:opacity-100 transition-opacity" />
{link.label}
</Link>
))}
Everything a fresher needs — semester notes, campus map, chatbot, bus routes, and developer tools all in one place.
</p>
</div>
<Link to="/ai-assistant" className="shrink-0">
<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' }}
>
<span>Ask Chatbot</span>
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
</motion.button>
</Link>
</div>
</motion.div>
</div>
)}
{/* ─── 2. Main Footer Section (Deep Royal Dark Purple) ──────────── */}
<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="container-custom">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-12 gap-12 xl:gap-14 items-start">
{/* Column 1: Brand Info (xl:col-span-4) */}
<div className="xl:col-span-4 flex flex-col gap-5">
<Link to="/" className="flex items-center gap-3.5 group w-fit">
<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 className="flex flex-col">
<span className="font-extrabold text-white text-xl tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
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>
</Link>
<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>
<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>
{/* Column 2: Academics & Tools (xl:col-span-2) */}
<div className="xl:col-span-2">
<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" />
Academics
</h4>
<ul className="space-y-4 lg:space-y-4.5">
{exploreLinks.map((link) => {
const Icon = link.icon;
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>
);
})}
</ul>
</div>
{/* Column 3: Campus Life (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" />
Campus Life
</h4>
<ul className="space-y-4 lg:space-y-4.5">
{campusLinks.map((link) => {
const Icon = link.icon;
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>
);
})}
</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 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">
<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>
</ul>
</div>
</div>
</div>
{/* Contact */}
<div>
<h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
Campus Location & Contact
</h4>
<ul className="space-y-3">
<li className="flex items-start gap-3 text-slate-400 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>
<MapPin className="w-4 h-4 mt-0.5 shrink-0 text-[#F97316]" />
<span>Rajalakshmi Institute of Technology,<br />Kuthambakkam, Chennai - 600 124</span>
</li>
<li className="flex items-center gap-3 text-slate-400 text-sm">
<Phone className="w-4 h-4 shrink-0 text-[#F97316]" />
<a href="tel:+914423422890" className="hover:text-[#F97316] transition-colors">+91 44 2342 2890</a>
</li>
<li className="flex items-center gap-3 text-slate-400 text-sm">
<Mail className="w-4 h-4 shrink-0 text-[#F97316]" />
<a href="mailto:info@rit.ac.in" className="hover:text-[#F97316] transition-colors">info@rit.ac.in</a>
</li>
</ul>
{/* Bottom Copyright Bar */}
<div className="border-t border-[#2A104E] bg-[#090314] py-9 lg:py-10 px-4 mt-20">
<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>
{/* Bottom Bar */}
<div className="border-t border-slate-700">
<div className="container-custom py-5 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-slate-500 text-xs" style={{ fontFamily: 'Inter, sans-serif' }}>
© 2025 RIT Freshers Hub. All rights reserved.
</p>
<div className="flex items-center gap-4 text-xs text-slate-500">
<a href="#" className="hover:text-[#F97316] transition-colors">Privacy Policy</a>
<span>·</span>
<a href="#" className="hover:text-[#F97316] transition-colors">Terms of Use</a>
<span>·</span>
<a href="#" className="hover:text-[#F97316] transition-colors">Accessibility</a>
</div>
</div>
</div>
</footer>
</footer>
</div>
);
}

View File

@@ -1,156 +1,254 @@
import { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X, LogIn, LogOut, ShieldCheck, ChevronDown } from 'lucide-react';
import { NAV_LINKS } from '@/constants';
import {
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';
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() {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileOpen, setIsMobileOpen] = useState(false);
const [isProfileOpen, setIsProfileOpen] = useState(false);
const [isMoreOpen, setIsMoreOpen] = useState(false);
const location = useLocation();
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
const { user, isAuthenticated, isVerifiedStudent, isAdmin, isTransportAdmin, openAuthModal, logout } = useAuth();
const profileRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => setIsScrolled(window.scrollY > 20);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const moreRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setIsMobileOpen(false);
setIsMoreOpen(false);
}, [location.pathname]);
// Close profile dropdown when clicking outside
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (profileRef.current && !profileRef.current.contains(e.target as Node)) {
setIsProfileOpen(false);
}
if (moreRef.current && !moreRef.current.contains(e.target as Node)) {
setIsMoreOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
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;
}
if (linkPath === '/') return location.pathname === '/';
if (linkPath === '/notes') return location.pathname === '/notes' && !location.search.includes('portal=');
if (linkPath.includes('?')) {
return location.pathname + location.search === linkPath;
}
return true;
return location.pathname.startsWith(linkPath);
};
const isMoreActive = MORE_LINKS.some((link) => checkIsActive(link.path));
return (
<>
<motion.nav
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, 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)]"
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">
{/* Logo - RIT Event Hub Style */}
<a href="/" className="flex items-center gap-2 group">
{/* Logo Image */}
{/* 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
initial={{ y: -60, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.45, ease: 'easeOut' }}
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"
>
{/* Left: Logo & College Brand */}
<Link to="/" className="flex items-center gap-2.5 group shrink-0">
<motion.img
whileHover={{ scale: 1.05 }}
whileHover={{ scale: 1.06 }}
src="/logo.png"
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">
<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
</span>
<div className="h-7 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' }}>
<span>RAJALAKSHMI</span>
<span>INSTITUTE OF</span>
<span>TECHNOLOGY</span>
<div className="h-4 lg:h-5 w-[1px] bg-white/20 mx-2" />
<div
className="hidden xl:flex flex-col text-[7.5px] lg:text-[8px] font-bold text-slate-300 leading-[1.15] tracking-wider uppercase"
style={{ fontFamily: 'Inter, sans-serif' }}
>
<span>Rajalakshmi</span>
<span>Institute of</span>
<span>Technology</span>
</div>
</div>
</a>
</Link>
{/* Desktop Nav */}
<div className="hidden md:flex items-center gap-1">
{NAV_LINKS.map((link) => {
{/* Center: Desktop Navigation Links */}
<div className="hidden lg:flex items-center gap-1 xl:gap-1.5 min-w-0">
{PRIMARY_LINKS.map((link) => {
const isActive = checkIsActive(link.path);
return (
<Link
key={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"
style={{
fontFamily: 'Poppins, sans-serif',
color: isActive ? '#FFFFFF' : '#CBD5E1',
}}
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 ${
isActive ? 'text-white' : 'text-slate-300 hover:text-white hover:bg-white/10'
}`}
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
{isActive && (
<motion.div
layoutId="nav-active"
className="absolute inset-0 rounded-xl"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
transition={{ type: 'spring', bounce: 0.2, duration: 0.4 }}
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">{link.label}</span>
</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>
{/* Right Side: Auth + Mobile Toggle */}
<div className="flex items-center gap-2">
{/* Auth Section */}
{/* Right: Auth Profile / Unified Sign In */}
<div className="hidden lg:flex items-center gap-2 shrink-0">
{isAuthenticated && user ? (
<div className="relative" ref={profileRef}>
<button
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 ? (
<img
src={user.pictureUrl}
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"
/>
) : (
<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">
{user.name.charAt(0).toUpperCase()}
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-white font-bold text-xs ${
isAdmin ? 'bg-indigo-600' : 'bg-gradient-to-br from-[#FF6B00] to-[#F97316]'
}`}>
{isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="hidden lg:flex flex-col items-start">
<span className="text-white text-[11px] font-semibold leading-tight max-w-[100px] truncate">
{user.name.split(' ')[0]}
</span>
{isVerifiedStudent && (
<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" />
<span
className="text-white text-xs font-semibold max-w-[120px] truncate"
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
{user.name.split(' ')[0]}
</span>
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
</button>
{/* Profile Dropdown */}
@@ -161,9 +259,9 @@ export default function Navbar() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.95 }}
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">
{user.pictureUrl ? (
<img
@@ -173,8 +271,10 @@ export default function Navbar() {
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">
{user.name.charAt(0).toUpperCase()}
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-lg ${
isAdmin ? 'bg-indigo-600' : 'bg-gradient-to-br from-[#FF6B00] to-[#F97316]'
}`}>
{isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="flex-1 min-w-0">
@@ -182,28 +282,53 @@ export default function Navbar() {
<p className="text-slate-400 text-[11px] truncate">{user.email}</p>
</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 RIT Student</span>
</div>
) : (
<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">
<span className="text-[11px] font-semibold text-amber-400">
Sign in with @ritchennai.edu.in for verified badge
{/* Role Badges */}
{isAdmin ? (
<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">
<ShieldAlert className="w-3.5 h-3.5 text-indigo-400" />
<span className="text-[11px] font-bold text-indigo-400">
{isTransportAdmin ? 'Transport Fleet Manager' : 'System Super Admin'}
</span>
</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>
) : null}
</div>
<div className="p-2">
<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
onClick={() => {
logout();
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
</button>
</div>
@@ -215,91 +340,147 @@ export default function Navbar() {
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={loginWithGoogle}
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"
style={{ fontFamily: 'Poppins, sans-serif' }}
onClick={openAuthModal}
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: 'Plus Jakarta Sans, sans-serif' }}
>
<LogIn className="w-3.5 h-3.5" />
Sign In
<LogIn className="w-3.5 h-3.5 text-white" />
<span>Sign In</span>
</motion.button>
)}
</div>
{/* Mobile Toggle */}
{/* Mobile Hamburger Toggle */}
<div className="lg:hidden flex items-center">
<motion.button
whileTap={{ scale: 0.9 }}
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"
>
{isMobileOpen ? <X className="w-5 h-5 stroke-[2.5]" /> : <Menu className="w-5 h-5 stroke-[2.5]" />}
</motion.button>
</div>
</div>
{/* Mobile Menu */}
<AnimatePresence>
{isMobileOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25 }}
className="overflow-hidden md:hidden max-h-[80vh] overflow-y-auto"
{/* Mobile Menu Dropdown */}
<AnimatePresence>
{isMobileOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25 }}
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="p-4 space-y-3">
<div className="grid grid-cols-2 gap-2">
{ALL_MOBILE_LINKS.map((link) => {
const isActive = checkIsActive(link.path);
const isRaise = link.path === '/raise';
const IconComp = link.icon;
return (
<Link
key={link.path}
to={link.path}
onClick={() => setIsMobileOpen(false)}
className={`px-3 py-2.5 rounded-xl text-xs font-semibold flex items-center gap-2 transition-all ${
isRaise
? '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'
: isActive
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-sm'
: 'bg-white/5 text-slate-200 hover:bg-white/10 border border-white/5'
}`}
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
{IconComp && <IconComp className="w-3.5 h-3.5 opacity-80" />}
<span className="truncate">{link.label}</span>
</Link>
);
})}
</div>
{/* Mobile Auth Button */}
<div className="pt-2 border-t border-white/10">
{!isAuthenticated ? (
<button
onClick={() => {
openAuthModal();
setIsMobileOpen(false);
}}
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: 'Plus Jakarta Sans, sans-serif' }}
>
<LogIn className="w-4 h-4 text-white" />
Sign In
</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
onClick={() => {
logout();
setIsMobileOpen(false);
}}
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: 'Plus Jakarta Sans, sans-serif' }}
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* 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' }}
>
<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">
{NAV_LINKS.map((link) => {
const isActive = checkIsActive(link.path);
return (
<Link
key={link.path}
to={link.path}
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]"
style={{
fontFamily: 'Poppins, sans-serif',
color: isActive ? '#FFFFFF' : '#CBD5E1',
backgroundColor: isActive ? '#F97316' : 'rgba(255, 255, 255, 0.05)',
border: isActive ? '1px solid #FB923C' : '1px solid rgba(255, 255, 255, 0.08)',
}}
>
{link.label}
</Link>
);
})}
<Rocket className="w-4 h-4 text-white animate-pulse" />
<span>RAISE Incubator</span>
</Link>
</motion.div>
</motion.nav>
</header>
{/* Mobile Auth Button */}
{!isAuthenticated ? (
<button
onClick={() => {
loginWithGoogle();
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"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<LogIn className="w-4 h-4" />
Sign In with Google
</button>
) : (
<button
onClick={() => {
logout();
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"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<LogOut className="w-4 h-4" />
Sign Out ({user?.name.split(' ')[0]})
</button>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.nav>
{/* Spacer */}
{/* Spacer to prevent content overlap */}
<div className="h-20" />
</>
);

View 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;

View File

@@ -28,12 +28,12 @@ export default function SectionTitle({
className={`inline-flex items-center gap-2 mb-3 ${isCenter ? 'mx-auto' : ''}`}
>
<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={{
fontFamily: 'Poppins, sans-serif',
fontFamily: 'Plus Jakarta Sans, sans-serif',
backgroundColor: '#FFF7ED',
color: '#F97316',
border: '1px solid #FED7AA',
color: '#FF6B00',
border: '1px solid #FFEDD5',
}}
>
{tag}
@@ -46,15 +46,15 @@ export default function SectionTitle({
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="text-3xl md:text-4xl font-bold"
style={{ fontFamily: 'Playfair Display, serif', color: '#1E293B' }}
className="text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight"
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif', color: '#1A0B2E' }}
>
{title}{' '}
{highlight && (
<span
className="relative inline-block"
style={{
background: 'linear-gradient(135deg, #F97316, #FB923C)',
background: 'linear-gradient(135deg, #FF6B00, #F97316)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
@@ -71,7 +71,7 @@ export default function SectionTitle({
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
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' }}
>
{subtitle}

View 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 }
]
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, useCallback } from 'rea
import type { ReactNode } from 'react';
import { getBackendUrl } from '@/lib/utils';
import GoogleAuthModal from '@/components/Auth/GoogleAuthModal';
import { getStoredImsSession, clearImsSession, saveImsSession, loginWithIms, type StudentInfo } from '@/services/imsService';
// ──────────────────────────────────────────────────
// Types
@@ -13,15 +14,31 @@ export interface AuthUser {
name: string;
pictureUrl?: string;
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 {
user: AuthUser | null;
isAuthenticated: boolean;
isVerifiedStudent: boolean;
isAdmin: boolean;
isTransportAdmin: boolean;
isCommunityAdmin: boolean;
isClubsAdmin: boolean;
isCurriculumAdmin: boolean;
isLoading: boolean;
isAuthModalOpen: boolean;
loginWithGoogle: () => void;
loginWithCredentials: (username: string, password: string) => Promise<LoginCredentialsResult>;
openAuthModal: () => void;
closeAuthModal: () => void;
logout: () => void;
@@ -43,9 +60,15 @@ const AuthContext = createContext<AuthContextType>({
user: null,
isAuthenticated: false,
isVerifiedStudent: false,
isAdmin: false,
isTransportAdmin: false,
isCommunityAdmin: false,
isClubsAdmin: false,
isCurriculumAdmin: false,
isLoading: true,
isAuthModalOpen: false,
loginWithGoogle: () => {},
loginWithCredentials: async () => ({ success: false }),
openAuthModal: () => {},
closeAuthModal: () => {},
logout: () => {},
@@ -54,29 +77,13 @@ const AuthContext = createContext<AuthContextType>({
export const useAuth = () => useContext(AuthContext);
// ──────────────────────────────────────────────────
// Google GSI global type
// Storage Keys
// ──────────────────────────────────────────────────
declare global {
interface Window {
google?: {
accounts: {
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';
const GOOGLE_STORAGE_KEY = 'rit_auth_user';
const ADMIN_TOKEN_KEY = 'RIT_ADMIN_TOKEN';
const ADMIN_ROLE_KEY = 'RIT_ADMIN_ROLE';
const ADMIN_USER_KEY = 'RIT_ADMIN_USER';
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
@@ -86,7 +93,54 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []);
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 }) => {
try {
const res = await fetch(getBackendUrl('/api/auth/google'), {
@@ -95,15 +149,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
body: JSON.stringify({ credential: response.credential }),
});
if (!res.ok) {
console.error('Auth failed:', res.status);
return;
if (res.ok) {
const userData: AuthUser = await res.json();
const userObj: AuthUser = { ...userData, role: 'ROLE_USER' };
setUser(userObj);
localStorage.setItem(GOOGLE_STORAGE_KEY, JSON.stringify(userObj));
setIsAuthModalOpen(false);
}
const userData: AuthUser = await res.json();
setUser(userData);
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
setIsAuthModalOpen(false);
} catch (err) {
console.error('Google auth error:', err);
}
@@ -111,42 +163,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Initialize Google GSI
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 = () => {
if (!window.google || !GOOGLE_CLIENT_ID) return;
window.google.accounts.id.initialize({
client_id: GOOGLE_CLIENT_ID,
callback: handleCredentialResponse,
@@ -169,17 +187,192 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [handleCredentialResponse]);
const loginWithGoogle = useCallback(() => {
// Open the clean Google Sign-In modal directly
setIsAuthModalOpen(true);
// Also trigger One Tap prompt as a secondary background hint if supported
if (window.google) {
try {
window.google.accounts.id.prompt();
} catch {}
}
}, []);
// ─────────────────────────────────────────────────────────────
// 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 {
// Silently ignore if browser blocks One Tap
// 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.',
};
}
}, []);
const logout = useCallback(() => {
@@ -189,19 +382,36 @@ export function AuthProvider({ children }: { children: ReactNode }) {
} catch {}
}
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);
}, [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 (
<AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
isVerifiedStudent: !!user?.verifiedStudent,
isAdmin,
isTransportAdmin,
isCommunityAdmin,
isClubsAdmin,
isCurriculumAdmin,
isLoading,
isAuthModalOpen,
loginWithGoogle,
loginWithCredentials,
openAuthModal,
closeAuthModal,
logout,

View File

@@ -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&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";
@theme {
--color-primary: #F97316;
--color-primary-light: #FB923C;
--color-primary: #FF6B00;
--color-primary-light: #F97316;
--color-primary-dark: #EA580C;
--color-primary-50: #FFF7ED;
--color-primary-100: #FFEDD5;
--color-primary-200: #FED7AA;
--color-secondary: #FB923C;
--color-background: #FAFAFA;
--color-secondary: #F97316;
--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-border: #E5E7EB;
--color-heading: #1E293B;
--color-body: #475569;
--color-muted: #94A3B8;
--color-border: #E9E5EE;
--color-heading: #1A0B2E;
--color-body: #4A3E5E;
--color-muted: #9E91B6;
--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-button: 'Poppins', system-ui, sans-serif;
--font-button: 'Plus Jakarta Sans', 'Poppins', system-ui, sans-serif;
--radius-sm: 8px;
--radius-md: 12px;
@@ -30,14 +36,17 @@
--radius-3xl: 32px;
--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-medium: 0 4px 25px -5px rgba(0,0,0,0.1), 0 10px 30px -5px rgba(0,0,0,0.07);
--shadow-strong: 0 8px 40px -8px rgba(0,0,0,0.15), 0 20px 45px -8px rgba(0,0,0,0.1);
--shadow-orange: 0 8px 30px -4px rgba(249,115,22,0.3);
--shadow-soft: 0 4px 20px -4px rgba(19, 9, 36, 0.05), 0 1px 3px rgba(19, 9, 36, 0.02);
--shadow-medium: 0 8px 30px -6px rgba(19, 9, 36, 0.08), 0 2px 6px rgba(19, 9, 36, 0.03);
--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(255, 107, 0, 0.35);
--color-brand-orange: #FF6B00;
--color-brand-purple: #130924;
--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 {
scroll-behavior: smooth;
font-size: 16px;
@@ -45,8 +54,8 @@ html {
body {
font-family: var(--font-body);
background-color: #FAFAFA;
color: #475569;
background-color: #FAF9FC;
color: #4A3E5E;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -57,10 +66,10 @@ body {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
background: #FAF9FC;
}
::-webkit-scrollbar-thumb {
background: #F97316;
background: #FF6B00;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
@@ -69,20 +78,23 @@ body {
/* Selection */
::selection {
background-color: rgba(249, 115, 22, 0.2);
color: #1E293B;
background-color: rgba(255, 107, 0, 0.2);
color: #1A0B2E;
}
/* Typography base */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
color: #1E293B;
line-height: 1.2;
@layer base {
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
color: #1A0B2E;
font-weight: 700;
line-height: 1.25;
}
}
/* Focus ring */
:focus-visible {
outline: 2px solid #F97316;
outline: 2px solid #FF6B00;
outline-offset: 2px;
}
@@ -105,12 +117,12 @@ h1, h2, h3, h4, h5, h6 {
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
50% { transform: translateY(-8px); }
}
@keyframes pulse-orange {
0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(249, 115, 22, 0); }
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 0, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(255, 107, 0, 0); }
}
@keyframes shimmer {
@@ -118,12 +130,6 @@ h1, h2, h3, h4, h5, h6 {
100% { background-position: 200% 0; }
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animate-float {
animation: float 4s ease-in-out infinite;
}
@@ -144,36 +150,37 @@ h1, h2, h3, h4, h5, h6 {
/* Glass effect */
.glass {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.6);
}
.glass-dark {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: rgba(19, 9, 36, 0.85);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Card base */
.card-base {
background: #FFFFFF;
border-radius: 20px;
border: 1px solid #E5E7EB;
box-shadow: 0 2px 15px -3px rgba(0,0,0,0.07), 0 10px 20px -2px rgba(0,0,0,0.04);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border-radius: 16px;
border: 1px solid #E9E5EE;
box-shadow: 0 4px 20px -4px rgba(19, 9, 36, 0.04), 0 1px 3px rgba(19, 9, 36, 0.02);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.card-base:hover {
transform: translateY(-4px);
box-shadow: 0 8px 40px -8px rgba(0,0,0,0.15), 0 20px 45px -8px rgba(0,0,0,0.1);
transform: translateY(-3px);
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 {
background: linear-gradient(135deg, #F97316, #FB923C, #FBBF24);
background: linear-gradient(135deg, #FF6B00 0%, #F97316 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -182,16 +189,17 @@ h1, h2, h3, h4, h5, h6 {
/* Orange button */
.btn-primary {
font-family: var(--font-button);
background: linear-gradient(135deg, #F97316, #FB923C);
background: linear-gradient(135deg, #FF6B00 0%, #F97316 100%);
color: white;
border: none;
border-radius: 12px;
padding: 12px 28px;
font-weight: 600;
font-size: 15px;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.01em;
cursor: pointer;
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;
align-items: center;
gap: 8px;
@@ -200,8 +208,8 @@ h1, h2, h3, h4, h5, h6 {
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(249, 115, 22, 0.4);
background: linear-gradient(135deg, #EA580C, #F97316);
box-shadow: 0 8px 24px rgba(255, 107, 0, 0.38);
background: linear-gradient(135deg, #EA580C 0%, #FF6B00 100%);
}
.btn-primary:active {
@@ -212,12 +220,12 @@ h1, h2, h3, h4, h5, h6 {
.btn-ghost {
font-family: var(--font-button);
background: transparent;
color: #1E293B;
border: 2px solid #E5E7EB;
color: #1A0B2E;
border: 1.5px solid #E9E5EE;
border-radius: 12px;
padding: 10px 26px;
font-weight: 600;
font-size: 15px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
@@ -227,8 +235,9 @@ h1, h2, h3, h4, h5, h6 {
}
.btn-ghost:hover {
border-color: #F97316;
color: #F97316;
border-color: #FF6B00;
color: #FF6B00;
background-color: rgba(255, 107, 0, 0.04);
transform: translateY(-2px);
}
@@ -258,23 +267,13 @@ h1, h2, h3, h4, h5, h6 {
/* Orange dot pattern background */
.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;
}
/* 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-user {
background: linear-gradient(135deg, #F97316, #FB923C);
background: linear-gradient(135deg, #FF6B00, #F97316);
color: white;
border-radius: 18px 18px 4px 18px;
padding: 12px 16px;
@@ -284,25 +283,26 @@ h1, h2, h3, h4, h5, h6 {
}
.chat-bubble-ai {
background: #F8FAFC;
color: #1E293B;
background: #FFFFFF;
color: #1A0B2E;
border-radius: 18px 18px 18px 4px;
padding: 12px 16px;
font-size: 14px;
max-width: 80%;
align-self: flex-start;
border: 1px solid #E5E7EB;
border: 1px solid #E9E5EE;
}
/* Verified card style input focus state */
.verified-focus-input {
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
background-color: #FFFFFF;
border: 1.5px solid #E5E7EB !important;
border: 1.5px solid #E9E5EE !important;
}
.verified-focus-input:focus {
outline: none !important;
border-color: #F97316 !important;
box-shadow: none !important;
border-color: #FF6B00 !important;
box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.15) !important;
}

View File

@@ -109,17 +109,17 @@ export default function AIAssistant() {
};
return (
<div className="min-h-screen flex flex-col bg-[#FAFAFA]">
<div className="min-h-screen flex flex-col bg-[#FAF9FC]">
{/* 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="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" />
</div>
<div>
<h1 className="font-bold text-[#1E293B] text-base" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Assistant</h1>
<div className="flex items-center gap-1.5 text-xs text-[#64748B]">
<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-[#9E91B6]">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
Campus Knowledge Engine Online
</div>
@@ -139,19 +139,6 @@ export default function AIAssistant() {
<Send className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Telegram Bot</span>
</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
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
@@ -187,8 +174,8 @@ export default function AIAssistant() {
{/* Chat Container */}
<div
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"
style={{ minHeight: '420px', maxHeight: '65vh', boxShadow: '0 8px 30px -6px rgba(0,0,0,0.04)' }}
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 4px 20px_-4px_rgba(19,9,36,0.04)' }}
>
{messages.map((msg) => (
<motion.div
@@ -199,17 +186,17 @@ export default function AIAssistant() {
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
>
{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" />
</div>
)}
<div
className="max-w-[85%] sm:max-w-[78%] px-4 py-3 text-sm leading-relaxed"
style={{
background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #EA580C)' : '#F8FAFC',
color: msg.role === 'user' ? 'white' : '#1E293B',
background: msg.role === 'user' ? 'linear-gradient(135deg, #FF6B00, #F97316)' : '#FFFFFF',
color: msg.role === 'user' ? 'white' : '#1A0B2E',
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',
}}
dangerouslySetInnerHTML={{ __html: formatContent(msg.content) }}

File diff suppressed because it is too large Load Diff

View File

@@ -15,13 +15,28 @@ export default function BusRoutes() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
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)
fetch('/bus_routes.json')
.then((res) => res.json())
.then((data) => {
if (Array.isArray(data) && data.length > 0) {
setBusRoutes(data);
setSelectedBusRoute(data[0]);
if (!localStorage.getItem('RIT_LOCAL_BUS_ROUTES')) {
setBusRoutes(data);
setSelectedBusRoute(data[0]);
}
}
})
.catch((err) => console.error('Local JSON load error:', err))
@@ -50,18 +65,18 @@ export default function BusRoutes() {
);
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 */}
<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>
<div className="flex items-center gap-2 text-xs text-slate-400 mb-1">
<Link to="/" className="hover:text-orange-400">Home</Link>
<div className="flex items-center gap-2 text-xs text-purple-300/80 mb-1">
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
<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 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
</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">
@@ -74,25 +89,25 @@ export default function BusRoutes() {
{/* Controls: Search + Toggle Drawer Button */}
<div className="flex items-center gap-2">
<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
type="text"
placeholder="Search route or bus no..."
value={busSearch}
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' }}
/>
</div>
<button
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
? 'bg-orange-500 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-gradient-to-r from-[#FF6B00] to-[#F97316] text-white border-orange-400 shadow-lg shadow-orange-500/20'
: '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" />}
<span className="hidden sm:inline">

View File

@@ -32,38 +32,37 @@ const BLUEPRINT_LEGEND = [
];
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: 'Playground', x: 40.0, y: 11.0, 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: 'Steve Jobs Block', x: 16.0, y: 21.0, 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: 'MEC Lab', x: 14.0, y: 39.5, 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: '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.0, y: 23.0, category: 'Amenity', desc: 'Iconic Landmark Monument', area: 'Central Park' },
{ name: 'Campus Centre', x: 67.0, y: 23.0, category: 'Admin / Facility', desc: 'Student Activity & Administrative Center', area: 'North East' },
{ name: 'Hut', x: 70.0, y: 37.0, 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: '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: '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: 'C Block', x: 87.0, y: 38.0, category: 'C Block', desc: 'Labs, Classrooms & Staff Rooms', area: 'East Wing' },
{ name: 'Alan Turing Block', x: 77.0, y: 53.0, category: 'Lab / Academic', desc: 'Computer Science & AI Labs', area: 'East Wing' },
{ name: 'Arcade', x: 35.0, y: 57.0, category: 'Amenity', desc: 'Student Hub & Stores', area: 'Central South' },
{ name: 'Green Building', x: 23.0, y: 71.0, category: 'Admin / Facility', desc: 'Eco-Friendly Academic Complex & Library', area: 'South West' },
{ name: 'Security Booth', x: 49.0, y: 77.0, category: 'Amenity', desc: 'Main Gate Security Checkpoint', area: 'South Entrance' },
{ name: 'ATM', x: 49.0, y: 86.0, category: 'Amenity', desc: '24/7 Campus Banking ATM', area: 'South Entrance' },
{ name: 'Bike Parking', x: 75.0, y: 67.0, category: 'Parking Area', desc: 'Two-Wheeler Vehicle Parking', area: 'South East' },
{ name: 'Generator Room', x: 76.0, y: 81.0, category: 'Admin / Facility', desc: 'Campus Power & Utility Station', area: 'South East' },
{ name: 'Side Gate', x: 23.0, y: 93.0, category: 'Amenity', desc: 'Secondary Campus Gate', area: 'South West' },
{ name: 'Main Gate', x: 59.0, y: 93.0, category: 'Amenity', desc: 'Primary Campus Entrance', area: 'South Entrance' },
{ name: 'Girls Hostel', x: 29.4, y: 7.3, category: 'Hostel / Mess', desc: 'Residential Quarter for Women', area: 'Top Left' },
{ name: 'Playground', x: 48.5, y: 8.8, category: 'Amenity', desc: 'Main Sports & Athletic Field', area: 'Top Center' },
{ name: 'Canteen', x: 68.3, y: 8.3, category: 'Amenity', desc: 'Food Court & Refreshments', area: 'Top Right' },
{ 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: 25.7, y: 25.9, category: 'Hostel / Mess', desc: 'Dedicated Dining Hall for Women', 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: 21.3, y: 43.5, category: 'Lab / Academic', desc: 'Engineering Practices Laboratory', area: 'West Wing' },
{ name: 'Astronaut Statue', x: 51.4, y: 16.1, 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: 'Nikola Tesla Block', x: 43.3, y: 34.7, category: 'B Block', desc: 'Main Electrical & Engineering Research Block', area: 'Central Block' },
{ name: 'Hut', x: 66.8, y: 31.3, category: 'Amenity', desc: 'Student Discussion & Rest Area', area: 'Central East' },
{ name: 'Homi Jehangir Bhabha Block', x: 79.3, y: 22.9, category: 'A Block', desc: 'Science & Physics Research 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: 'Alan Turing Block', x: 66.8, y: 50.8, category: 'Lab / Academic', desc: 'Computer Science & AI Labs', area: 'East Wing' },
{ name: 'Arcade', x: 36.7, y: 53.2, category: 'Amenity', desc: 'Student Hub & Stores', area: 'Central South' },
{ name: 'Green Building', x: 27.2, y: 63.5, category: 'Admin / Facility', desc: 'Eco-Friendly Academic Complex & Library', area: 'South West' },
{ name: 'Security Booth', x: 44.8, y: 65.9, category: 'Amenity', desc: 'Main Gate Security Checkpoint', area: 'South Entrance' },
{ name: 'ATM', x: 44.3, y: 73.0, category: 'Amenity', desc: '24/7 Campus Banking ATM', area: 'South Entrance' },
{ name: 'Bike Parking', x: 65.3, y: 61.5, category: 'Parking Area', desc: 'Two-Wheeler Vehicle Parking', area: 'South East' },
{ name: 'Generator Room', x: 63.1, y: 71.8, category: 'Admin / Facility', desc: 'Campus Power & Utility Station', area: 'South East' },
{ name: 'Side Gate', x: 22.8, y: 75.0, category: 'Amenity', desc: 'Secondary Campus Gate', area: 'South West' },
{ name: 'Main Gate', x: 49.2, y: 77.9, category: 'Amenity', desc: 'Primary Campus Entrance', area: 'South Entrance' },
{ name: 'Bus Stop', x: 49.6, y: 92.8, category: 'Amenity', desc: 'Public & College Bus Boarding Point', area: 'Highway' },
];
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'],
library: ['Green Building'],
labs: ['MEC Lab', 'EPL Lab', 'Steve Jobs Block', 'Alan Turing Block', 'Nikola Tesla Block', 'C Block'],
hostel: ['Girls Hostel', 'Boys Mess'],
canteen: ['Canteen', 'Girls Mess', 'Boys Mess', 'Staff Mess', 'Arcade'],
hostel: ['Girls Hostel', 'Girls Mess'],
canteen: ['Canteen', 'Girls Mess', 'Arcade'],
auditorium: ['Steve Jobs Block', 'Campus Centre'],
sports: ['Playground'],
};
@@ -148,27 +147,27 @@ export default function Campus() {
{/* Header */}
<div className="bg-white border-b border-[#E5E7EB] py-10">
<div className="container-custom">
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
<Link to="/" className="hover:text-[#F97316]">Home</Link>
<div className="flex items-center gap-2 text-xs text-[#9E91B6] mb-3">
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
<ChevronRight className="w-3 h-3" />
<span className="text-[#F97316]">Campus Map</span>
<span className="text-[#FF6B00] font-semibold">Campus Map</span>
</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{' '}
<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
</span>
</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.
</p>
</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 }}>
{/* 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 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">
@@ -178,10 +177,10 @@ export default function Campus() {
<CheckCircle2 className="w-3 h-3" /> Interactive Location Spotter
</span>
</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
</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" />
Kuthambakkam, Chennai • Interactive Map Spotting Enabled
</p>
@@ -189,23 +188,23 @@ export default function Campus() {
{/* Toolbar Controls */}
<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">
<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">
<div className="flex items-center bg-[#1E0C36] rounded-xl p-1 border border-[#3A1968]">
<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" />
</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">
{Math.round(zoomLevel * 100)}%
</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" />
</button>
</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" />
</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
</button>
@@ -269,9 +268,9 @@ export default function Campus() {
</div>
{/* 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">
<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 && (
<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;
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'}`}>
<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" />}
</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>
);
})}
@@ -293,16 +292,16 @@ export default function Campus() {
</div>
{/* 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">
<Layers className="w-5 h-5 text-[#F97316]" />
<h3 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>Blueprint Map Legend</h3>
<Layers className="w-5 h-5 text-[#FF6B00]" />
<h3 className="text-xl font-bold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Blueprint Map Legend</h3>
</div>
<div className="flex flex-wrap gap-3">
{BLUEPRINT_LEGEND.map((item) => {
const isActive = activeLegend === item.id;
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="text-xs font-semibold" style={{ fontFamily: 'Inter, sans-serif' }}>{item.label}</span>
</button>
@@ -312,28 +311,28 @@ export default function Campus() {
</div>
{/* 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>
<div className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-[#F97316]" />
<h3 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>Campus Blocks & Facilities Index</h3>
<Building2 className="w-5 h-5 text-[#FF6B00]" />
<h3 className="text-xl font-bold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Campus Blocks & Facilities Index</h3>
</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>
<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 className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredFacilities.map((facility) => {
const isSelected = selectedFacility?.name === facility.name;
const isHighlighted = highlightedSpotNames.has(facility.name);
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">
<span className={`text-sm font-bold ${isSelected ? 'text-white' : 'text-[#1E293B]'}`} style={{ fontFamily: 'Poppins, 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-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-[#E9E5EE] text-[#4A3E5E]'}`}>{facility.area}</span>
</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>
);
})}

View File

@@ -9,6 +9,8 @@ import { getBackendUrl } from '@/lib/utils';
import { StaggerContainer, StaggerItem } 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 AVATARS: Record<string, string> = {
@@ -261,24 +263,24 @@ export default function Community() {
};
return (
<div className="min-h-screen" style={{ backgroundColor: '#FAFBFD' }}>
<div className="min-h-screen bg-[#FAF9FC]">
{/* 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">
<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
</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.
</p>
</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="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)' }}>
<h3 className="text-base font-semibold text-[#1E293B] mb-3 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Ask a Question</h3>
<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-bold text-[#1A0B2E] mb-3 tracking-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>Ask a Question</h3>
{showSuccessBanner && (
<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'
}`}
style={{ fontFamily: 'Playfair Display, serif' }}>
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
{q.title}
</h3>

View File

@@ -24,7 +24,6 @@ export interface CollabRequestItem {
acceptedCount?: number;
contactInfo?: string;
telegramChatId?: number;
discordUserId?: string;
status: string;
applicationsCount: number;
createdAt: string;
@@ -432,23 +431,23 @@ export default function DevCollab() {
{/* Header */}
<div className="bg-white border-b border-[#E5E7EB] py-10">
<div className="container-custom">
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
<Link to="/" className="hover:text-[#F97316]">Home</Link>
<div className="flex items-center gap-2 text-xs text-[#9E91B6] mb-3">
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
<ChevronRight className="w-3 h-3" />
<span className="text-[#F97316]">Developer Collab Hub</span>
<span className="text-[#FF6B00] font-semibold">Developer Collab Hub</span>
</div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-[#F97316] 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" /> RIT Developers Environment
</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{' '}
<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' }}>
Collaborators
</span>
</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!
</p>
</div>
@@ -457,8 +456,8 @@ export default function DevCollab() {
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
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-orange-500/25 shrink-0 cursor-pointer"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
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, #FF6B00, #F97316)', fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
{isVerifiedStudent && <ShieldCheck className="w-4 h-4" />}
<Plus className="w-4.5 h-4.5" />
@@ -467,15 +466,15 @@ export default function DevCollab() {
</div>
{/* 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
onClick={() => setActiveTab('explore')}
className={`px-5 py-3 text-xs font-bold transition-all border-b-2 flex items-center gap-2 cursor-pointer ${
activeTab === 'explore'
? 'border-[#F97316] text-[#F97316]'
: 'border-transparent text-slate-500 hover:text-slate-800'
? 'border-[#FF6B00] text-[#FF6B00]'
: '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" />
Explore Projects ({requests.length})

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,156 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect } from 'react';
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 { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
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() {
const [searchFaculty, setSearchFaculty] = useState('');
const [selectedDept, setSelectedDept] = useState('All Departments');
const [searchParams] = useSearchParams();
const [searchFaculty, setSearchFaculty] = useState(() => searchParams.get('search') || '');
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 filtered = FACULTY_DATA.filter((f) => {
const filtered = facultyList.filter((f) => {
const searchLower = searchFaculty.trim().toLowerCase();
const matchSearch =
f.name.toLowerCase().includes(searchLower) ||
f.department.toLowerCase().includes(searchLower) ||
f.designation.toLowerCase().includes(searchLower) ||
(f.specialization && f.specialization.toLowerCase().includes(searchLower));
(f.name && f.name.toLowerCase().includes(searchLower)) ||
(f.department && f.department.toLowerCase().includes(searchLower)) ||
(f.designation && f.designation.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;
return matchSearch && matchDept;
});
return filtered.sort((a, b) => {
if (sortBy === 'Name A-Z') return a.name.localeCompare(b.name);
if (sortBy === 'Department') return a.department.localeCompare(b.department);
if (sortBy === 'Designation') return a.designation.localeCompare(b.designation);
if (sortBy === 'Name A-Z') return (a.name || '').localeCompare(b.name || '');
if (sortBy === 'Department') return (a.department || '').localeCompare(b.department || '');
if (sortBy === 'Designation') return (a.designation || '').localeCompare(b.designation || '');
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 (
<div className="min-h-screen relative overflow-hidden bg-[#F8FAFC]">
@@ -44,58 +164,67 @@ export default function Faculty() {
<div className="relative z-10">
{/* 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="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
<Link to="/" className="hover:text-[#F97316]">Home</Link>
<div className="flex items-center gap-2 text-xs text-[#9E91B6] mb-3">
<Link to="/" className="hover:text-[#FF6B00]">Home</Link>
<ChevronRight className="w-3 h-3" />
<span className="text-[#F97316]">Faculty Directory</span>
<span className="text-[#FF6B00] font-semibold">Faculty Directory</span>
</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{' '}
<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
</span>
</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.
</p>
</div>
</div>
<div className="container-custom py-8 space-y-8">
{/* Under Development Banner */}
<motion.div
initial={{ opacity: 0, y: -10 }}
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"
>
<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">
<AlertTriangle className="w-5 h-5 text-amber-600" />
</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-medium text-amber-900" style={{ fontFamily: 'Inter, sans-serif' }}>
Currently displaying sample mock faculty profiles. Official RIT faculty directory integration is in progress.
<div className="container-custom pt-8 pb-20 md:pb-28 space-y-6">
{/* Active Student Department Filter Badge */}
{isStudentDeptAutoFiltered && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
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="w-9 h-9 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-[#FF6B00]" />
</div>
<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-orange-950">
Showing faculty for your department: <strong>{selectedDept}</strong>
</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">
Mock Data Active
</span>
</motion.div>
<button
onClick={() => setSelectedDept('All Departments')}
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>
)}
{/* Faculty Hero Section */}
<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="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" />
</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
</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.
</p>
</div>
@@ -112,16 +241,31 @@ export default function Faculty() {
{ abrv: 'CIVIL', full: 'Civil Engineering', color: 'emerald' },
{ abrv: 'AI & DS', full: 'Artificial Intelligence & Data Science', color: 'pink' },
{ 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 isSelected = selectedDept === d.full;
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">
<span className={`text-xs font-bold text-${d.color}-700`} style={{ fontFamily: 'Poppins, sans-serif' }}>{d.abrv}</span>
<BookOpen className={`w-3.5 h-3.5 text-${d.color}-400`} />
<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 ${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>
<span className={`text-2xl font-bold ${isSelected ? 'text-white' : 'text-[#1E293B]'}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{count}</span>
</button>
);
})}
</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]" />
<input
type="text"
placeholder="Search by faculty name, department, designation or specialization..."
placeholder="Search by faculty name, department, designation or interest..."
value={searchFaculty}
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"
@@ -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]"
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;
return <option key={dept} value={dept}>{dept} ({count})</option>;
})}
@@ -172,15 +316,38 @@ export default function Faculty() {
</div>
</div>
{/* Grid */}
{/* Grouped Grid */}
{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">
{filteredAndSortedFaculty.map((faculty) => (
<StaggerItem key={faculty.id}>
<FacultyCard faculty={faculty} />
</StaggerItem>
))}
</StaggerContainer>
<div className="space-y-12 pb-20">
{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}>
<FacultyCard faculty={faculty} />
</StaggerItem>
))}
</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">
<Users className="w-12 h-12 text-[#CBD5E1] mb-4" />
@@ -194,7 +361,7 @@ export default function Faculty() {
<motion.div
initial={{ opacity: 0, y: 20 }}
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-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>

View File

@@ -2,14 +2,12 @@ import { motion, useScroll, useTransform } from 'framer-motion';
import { Link } from 'react-router-dom';
import {
ArrowRight, BookOpen, Bot, GraduationCap,
CheckCircle, Zap, Shield, Trophy, Award, Cpu, Users, MapPin, Sparkles
CheckCircle, Zap, Shield, Trophy, Award, Cpu, Users, Sparkles
} from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle';
import FeatureCard from '@/components/FeatureCard/FeatureCard';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { FEATURES, CAMPUS_LOCATIONS } from '@/constants';
import * as LucideIcons from 'lucide-react';
import { FEATURES } from '@/constants';
import LazyVideoHero from '@/components/LazyVideoHero/LazyVideoHero';
@@ -33,7 +31,7 @@ export default function Home() {
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: 'Playfair Display, serif', textShadow: '0 2px 14px rgba(0,0,0,0.95)' }}
style={{ color: '#FFFFFF', fontFamily: 'Plus Jakarta Sans, sans-serif', textShadow: '0 2px 14px rgba(0,0,0,0.95)' }}
>
Welcome to
</motion.h1>
@@ -42,8 +40,8 @@ export default function Home() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2 }}
className="text-5xl md:text-7xl font-black leading-tight mb-6"
style={{ color: '#FFFFFF', fontFamily: 'Playfair Display, serif', textShadow: '0 4px 24px rgba(0,0,0,0.95)' }}
className="text-5xl md:text-7xl font-extrabold tracking-tight leading-tight mb-6"
style={{ color: '#FFFFFF', fontFamily: 'Plus Jakarta Sans, sans-serif', textShadow: '0 4px 24px rgba(0,0,0,0.95)' }}
>
RIT Freshers Hub
</motion.h2>
@@ -67,12 +65,12 @@ export default function Home() {
>
<a href="#built-for-freshers">
<motion.button
whileHover={{ scale: 1.04, boxShadow: '0 10px 30px -5px rgba(249,115,22,0.4)' }}
whileHover={{ scale: 1.04, boxShadow: '0 10px 30px -5px rgba(255,107,0,0.4)' }}
whileTap={{ scale: 0.96 }}
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-semibold text-sm cursor-pointer shadow-md"
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-bold text-sm cursor-pointer shadow-md"
style={{
fontFamily: 'Poppins, sans-serif',
background: 'linear-gradient(135deg, #F97316, #FB923C)',
fontFamily: 'Plus Jakarta Sans, sans-serif',
background: 'linear-gradient(135deg, #FF6B00, #F97316)',
}}
>
<BookOpen className="w-4 h-4" />
@@ -83,12 +81,12 @@ export default function Home() {
<Link to="/ai-assistant">
<motion.button
whileHover={{ scale: 1.04, borderColor: '#F97316', backgroundColor: 'rgba(255,255,255,0.2)' }}
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-semibold text-sm border-2 border-white/40 text-white bg-black/30 backdrop-blur-md transition-all cursor-pointer shadow-2xs"
style={{ fontFamily: 'Poppins, sans-serif' }}
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-[#F97316]" />
<Bot className="w-4 h-4 text-[#FF6B00]" />
Ask Assistant
</motion.button>
</Link>
@@ -104,28 +102,28 @@ export default function Home() {
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
className="rounded-3xl border border-[#E5E7EB] p-6 md:p-8 bg-white shadow-[0_20px_50px_-15px_rgba(0,0,0,0.06)]"
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: '18', label: 'CLUBS', icon: Award },
{ 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-[#F97316]" />
<stat.icon className="w-5 h-5 text-[#FF6B00]" />
</div>
<div
className="text-2xl md:text-3xl font-black text-[#1E293B] mb-0.5"
style={{ fontFamily: 'Poppins, sans-serif' }}
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-[#94A3B8] uppercase"
className="text-[11px] font-bold tracking-wider text-[#9E91B6] uppercase"
style={{ fontFamily: 'Inter, sans-serif' }}
>
{stat.label}
@@ -156,80 +154,6 @@ export default function Home() {
</StaggerContainer>
</div>
</section>
{/* ─── Campus Quick Nav ─────────────────────────────────────────────────── */}
<section className="section-padding relative z-20 bg-white border-y border-[#E5E7EB]">
<div className="container-custom">
<SectionTitle
tag="Navigate Campus"
title="Explore"
highlight="RIT Campus"
subtitle="Find your way around campus — departments, labs, library, and key locations."
/>
<AnimatedContainer>
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden shadow-sm">
{/* Map Teaser Header */}
<div
className="h-52 flex items-center justify-center relative overflow-hidden"
style={{ background: 'linear-gradient(135deg, #1E293B, #0F172A)' }}
>
<div
className="absolute inset-0 opacity-15"
style={{
backgroundImage: 'linear-gradient(rgba(249,115,22,0.4) 1px, transparent 1px), linear-gradient(90deg, rgba(249,115,22,0.4) 1px, transparent 1px)',
backgroundSize: '28px 28px',
}}
/>
<div className="relative z-10 text-center px-4">
<div className="w-12 h-12 rounded-2xl bg-[#F97316]/20 border border-[#F97316]/30 flex items-center justify-center mx-auto mb-3">
<MapPin className="w-6 h-6 text-[#F97316]" />
</div>
<h3 className="text-white text-lg font-bold mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
Interactive Campus Map
</h3>
<p className="text-slate-400 text-xs mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>
Locate all major campus blocks, labs, library & amenities.
</p>
<Link to="/campus">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-5 py-2 rounded-xl text-white text-xs font-semibold shadow-sm cursor-pointer"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
>
Open Full Map
</motion.button>
</Link>
</div>
</div>
{/* Quick Nav Grid */}
<div className="p-5 grid grid-cols-3 sm:grid-cols-7 gap-3 bg-[#FAFAFA]">
{CAMPUS_LOCATIONS.map((loc) => {
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
return (
<Link to="/campus" key={loc.id}>
<motion.div
whileHover={{ y: -3, backgroundColor: '#FFF7ED' }}
className="flex flex-col items-center gap-2 p-3 rounded-xl cursor-pointer transition-all border border-transparent hover:border-[#FED7AA] bg-white shadow-2xs"
>
<div className="w-9 h-9 rounded-xl bg-[#FFF7ED] flex items-center justify-center">
{Icon && <Icon className="w-4.5 h-4.5 text-[#F97316]" />}
</div>
<span className="text-[11px] text-[#475569] text-center font-medium leading-tight" style={{ fontFamily: 'Inter, sans-serif' }}>
{loc.name}
</span>
</motion.div>
</Link>
);
})}
</div>
</div>
</AnimatedContainer>
</div>
</section>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
import { useState, useRef, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
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 } 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';
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 { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import { TOOLKIT_ITEMS, DEPARTMENTS } from '@/constants';
@@ -307,7 +311,7 @@ const CS_TOOLS_CATEGORIES = [
bg: '#FEFCE8',
tools: [
{ 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: '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/' }
@@ -1327,40 +1331,55 @@ export default function Notes() {
const [selectedCategory, setSelectedCategory] = 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 [activePortalTab, setActivePortalTab] = useState<'moodle' | 'gatepass' | 'proficiency'>('moodle');
const [notes, setNotes] = useState<any[]>([]);
useEffect(() => {
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 {
const response = await fetch(getBackendUrl('/api/notes'));
if (response.ok) {
const data = await response.json();
const mappedData = data.map((n: any) => ({
id: String(n.id),
title: n.title,
subject: n.subject,
department: n.department,
semester: n.semester,
type: n.fileType,
downloads: n.downloadsCount || 0,
fileSize: n.fileSize,
uploadedAt: n.uploadedAt ? n.uploadedAt.split('T')[0] : '',
downloadUrl: n.downloadUrl
}));
setNotes(mappedData);
} else {
setNotes(NOTES_DATA);
if (Array.isArray(data) && data.length > 0) {
const mappedData = data.map((n: any) => ({
id: String(n.id),
title: n.title,
subject: n.subject,
department: n.department,
semester: n.semester,
type: n.fileType || 'notes',
downloads: n.downloadsCount || 0,
fileSize: n.fileSize || '2.4 MB',
uploadedAt: n.uploadedAt ? n.uploadedAt.split('T')[0] : '',
downloadUrl: n.downloadUrl || '#'
}));
setNotes(mappedData);
return;
}
}
} catch (err) {
console.error('Error fetching notes:', err);
setNotes(NOTES_DATA);
setNotes(localFallback);
} catch {
setNotes(localFallback);
}
};
fetchNotes();
}, []);
useEffect(() => {
const portalParam = searchParams.get('portal');
if (portalParam === 'gatepass' || portalParam === 'proficiency' || portalParam === 'moodle') {
setActivePortalTab(portalParam as any);
}
if (searchParams.get('section') === 'toolkit') {
setTimeout(() => {
toolkitRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -1405,6 +1424,42 @@ export default function Notes() {
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 (
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
{/* Header */}
@@ -1415,44 +1470,21 @@ export default function Notes() {
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>
<span>Home</span>
<ChevronRight className="w-3 h-3" />
<span className="text-[#F97316]">Notes & PYQs</span>
<span className="text-[#F97316]">{headerBreadcrumb}</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
Notes &{' '}
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
PYQs
</span>
<h1 className="text-3xl md:text-4xl font-extrabold text-[#1E293B] mb-2" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
{headerTitle}
</h1>
<p className="text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
Access semester-wise notes, previous year question papers, and study materials.
{headerSubtitle}
</p>
</motion.div>
</div>
</div>
)}
<div className="container-custom py-10">
{/* Under Development Banner */}
<motion.div
initial={{ opacity: 0, y: -10 }}
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 mb-8"
>
<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">
<AlertTriangle className="w-5 h-5 text-amber-600" />
</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-medium text-amber-900" style={{ fontFamily: 'Inter, sans-serif' }}>
Currently displaying sample mock notes & syllabus data. Official semester notes repository integration is in progress.
</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">
Mock Data Active
</span>
</motion.div>
<div className="container-custom pt-10 pb-32 lg:pb-44">
{selectedToolkit ? (
(() => {
@@ -1504,7 +1536,7 @@ export default function Notes() {
: 'Back to Toolkit'}
</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.name}{' '}
@@ -1566,7 +1598,7 @@ export default function Notes() {
</div>
<div>
<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}
</h3>
<span
@@ -1693,7 +1725,7 @@ export default function Notes() {
</div>
<div>
<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}
</h3>
<span
@@ -1936,220 +1968,303 @@ export default function Notes() {
})()
) : (
<div>
{/* Search + Filters */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 mb-8"
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
>
{/* Search */}
<div className="relative mb-4">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-[#94A3B8]" />
<input
type="text"
placeholder="Search notes, subjects..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-11 pr-4 py-3 rounded-xl border border-[#E5E7EB] text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none focus:border-[#F97316] transition-colors"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
</div>
<div className="mb-12">
<div className="flex flex-wrap gap-3">
{/* Type Filter */}
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-[#94A3B8]" />
{['all', 'notes', 'pyq', 'syllabus'].map((type) => (
<button
key={type}
onClick={() => setSelectedType(type)}
className="px-3 py-1.5 rounded-lg text-xs font-medium capitalize transition-all"
style={{
fontFamily: 'Poppins, sans-serif',
backgroundColor: selectedType === type ? '#F97316' : '#F8FAFC',
color: selectedType === type ? 'white' : '#475569',
border: `1px solid ${selectedType === type ? '#F97316' : '#E5E7EB'}`,
}}
>
{type === 'all' ? 'All Types' : type.toUpperCase()}
</button>
))}
</div>
{/* Semester Filter */}
<div className="flex gap-2 flex-wrap items-center">
{SEMESTERS.slice(0, 3).map((sem) => (
<button
key={sem}
onClick={() => { setSelectedSem(sem); setSelectedSubject('All Subjects'); }}
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all cursor-pointer"
style={{
fontFamily: 'Poppins, sans-serif',
backgroundColor: selectedSem === sem ? '#1E293B' : '#F8FAFC',
color: selectedSem === sem ? 'white' : '#475569',
border: `1px solid ${selectedSem === sem ? '#1E293B' : '#E5E7EB'}`,
}}
>
{sem}
</button>
))}
</div>
{/* 1. MOODLE LMS CARD */}
{activePortalTab === 'moodle' && (
<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" />
{/* Subject Dropdown */}
{selectedSem !== 'All' && (
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-[#64748B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Subject:</span>
<select
value={selectedSubject}
onChange={(e) => setSelectedSubject(e.target.value)}
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-[#F8FAFC] border border-[#E5E7EB] text-[#475569] focus:outline-none focus:border-[#F97316] transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<option value="All Subjects">All Subjects</option>
{(SUBJECTS_BY_SEM[parseInt(selectedSem)] || []).map((sub) => (
<option key={sub} value={sub}>
{sub}
</option>
))}
</select>
</div>
)}
</div>
</motion.div>
<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">
<BookOpen className="w-4 h-4 text-[#F97316]" />
<span>Official RIT Learning Management System</span>
</div>
{/* Results */}
<StaggerContainer key={`${filtered.length}-${selectedSubject}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-12">
{filtered.map((note) => {
const typeConfig = TYPE_COLORS[note.type] || { bg: '#F1F5F9', text: '#475569', icon: BookOpen };
const TypeIcon = typeConfig.icon;
return (
<StaggerItem key={note.id}>
<motion.div
whileHover={{ y: -4 }}
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex flex-col justify-between gap-4 h-full"
style={{
borderLeft: `4px solid ${typeConfig.text}`,
boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)'
}}
>
<div className="flex gap-4">
{/* Visual PDF Preview Icon */}
<div className="w-14 h-20 border border-slate-200 rounded-xl p-2 flex flex-col justify-between bg-[#F8FAFC] shrink-0 shadow-sm">
<div className="bg-[#EF4444] text-[8px] font-extrabold text-white px-1 py-0.5 rounded w-fit uppercase tracking-wider" style={{ fontFamily: 'Poppins, sans-serif' }}>
PDF
</div>
<div className="flex flex-col gap-1.5">
<div className="w-full h-1 bg-slate-200 rounded" />
<div className="w-5/6 h-1 bg-slate-200 rounded" />
<div className="w-2/3 h-1 bg-slate-200 rounded" />
</div>
</div>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#1E293B] mb-4 leading-tight" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
Access Academic Notes & Study Materials on{' '}
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
Moodle
</span>
</h2>
{/* Text Details */}
<div className="flex flex-col justify-between flex-1 min-w-0">
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="w-6 h-6 rounded-lg flex items-center justify-center shrink-0" style={{ backgroundColor: typeConfig.bg }}>
<TypeIcon className="w-3.5 h-3.5" style={{ color: typeConfig.text }} />
</div>
<span
className="px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
style={{ backgroundColor: typeConfig.bg, color: typeConfig.text, fontFamily: 'Poppins, sans-serif' }}
>
{note.type}
</span>
</div>
<h3 className="text-sm font-bold text-[#1E293B] line-clamp-2 leading-tight mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
{note.title}
</h3>
<p className="text-[11px] text-[#64748B] font-medium truncate" style={{ fontFamily: 'Inter, sans-serif' }}>
{note.subject} • Sem {note.semester}
</p>
</div>
</div>
</div>
<p className="text-base text-[#64748B] mb-8 leading-relaxed max-w-2xl mx-auto" style={{ fontFamily: 'Inter, sans-serif' }}>
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>
{/* Metadata Line */}
<div className="flex items-center justify-between border-t border-[#F1F5F9] pt-3 text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-800">{note.downloads} downloads · {note.fileSize}</span>
<span className="px-1 py-0.2 bg-[#F1F5F9] rounded text-[9px] font-bold text-slate-600">PDF</span>
</div>
<span>{note.uploadedAt ? `Updated ${note.uploadedAt}` : 'Recently'}</span>
</div>
{/* Download Button */}
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
onClick={() => handleDownload(note.id, note.downloadUrl)}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-semibold text-white cursor-pointer"
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-10">
<a
href={MOODLE_URL}
target="_blank"
rel="noopener noreferrer"
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)',
}}
>
<Download className="w-4 h-4" />
Download
</motion.button>
</motion.div>
</StaggerItem>
);
})}
</StaggerContainer>
<ExternalLink className="w-5 h-5" />
Launch Official RIT Moodle Portal
</a>
{/* Toolkit Section */}
<div ref={toolkitRef} />
<SectionTitle tag="Useful Downloads" title="Student" highlight="Toolkit" align="left" />
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-20">
{TOOLKIT_ITEMS.map((item) => {
const iconConfig = TOOLKIT_ICONS[item.title] || { icon: Code, bg: '#FFF7ED', color: '#F97316' };
const IconComponent = iconConfig.icon;
return (
<StaggerItem key={item.id} className="flex">
<motion.div
onClick={() => {
if (item.title === 'CS tools') {
setSelectedToolkit('cs');
} else if (item.title === 'Business tools') {
setSelectedToolkit('business');
} else if (item.title === 'Electrical and Electronic tools') {
setSelectedToolkit('electrical');
} else if (item.title === 'Cybersecurity tools') {
setSelectedToolkit('cybersecurity');
} else if (item.title === 'AI tools') {
setSelectedToolkit('ai');
} else if (item.title === 'Biotech tools') {
setSelectedToolkit('biotech');
} else if (item.url && item.url !== '#') {
window.open(item.url, '_blank');
}
}}
whileHover={{ y: -4, boxShadow: '0 12px 30px -10px rgba(249,115,22,0.15)' }}
className="flex items-start gap-5 p-7 bg-white hover:bg-[#FFF7ED] rounded-[22px] border border-[#E5E7EB] hover:border-[#FED7AA] cursor-pointer group w-full transition-all shadow-xs hover:shadow-md"
>
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center shrink-0 font-bold text-sm transition-transform group-hover:scale-105"
style={{ backgroundColor: iconConfig.bg, color: iconConfig.color }}
<button
onClick={() => {
navigator.clipboard.writeText(MOODLE_URL);
alert('Moodle 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' }}
>
<IconComponent className="w-7 h-7" />
</div>
<div className="flex-1 min-w-0">
<div className="text-base lg:text-lg font-bold text-[#1E293B] group-hover:text-[#F97316] transition-colors mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
{item.title}
<Globe className="w-4 h-4 text-[#64748B]" />
Copy Moodle 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-[#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">
<ShieldCheck className="w-4 h-4" />
</div>
<div className="text-sm text-[#64748B] leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
{item.description}
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
100% Direct & Secure
</div>
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
Authenticates directly on official college server <code className="bg-slate-200/80 px-1 py-0.5 rounded text-[10px]">182.74.17.142</code>.
</div>
</div>
<ChevronRight className="w-5 h-5 text-[#94A3B8] group-hover:text-[#F97316] group-hover:translate-x-1.5 transition-all shrink-0 mt-1" />
</motion.div>
</StaggerItem>
);
})}
</StaggerContainer>
<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-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-600">
<FileText className="w-4 h-4" />
</div>
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
Official Semester Notes
</div>
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
Complete 1st to 8th Semester course notes, PYQs, and assignments.
</div>
</div>
<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-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-600">
<Lock className="w-4 h-4" />
</div>
<div className="text-xs font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
Student Login
</div>
<div className="text-[11px] text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
Log in using your RIT Register Number and official Moodle password.
</div>
</div>
</div>
</div>
</motion.div>
)}
{/* 2. STUDENT GATE PASS CARD */}
{activePortalTab === 'gatepass' && (
<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>
</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>

40
src/pages/Raise/Raise.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { Hero } from './components/Hero';
import { AboutSection } from './components/AboutSection';
import { ProcessSection } from './components/ProcessSection';
import { StatsSection } from './components/StatsSection';
import { PortfolioShowcase } from './components/PortfolioShowcase';
import { PitchBooking } from './components/PitchBooking';
import { FooterContact } from './components/FooterContact';
export default function Raise() {
const handleScrollToSection = (sectionId: string) => {
if (sectionId === 'hero') {
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
}
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<div className="raise-website relative min-h-screen bg-[#fafafa] text-neutral-900 selection:bg-neutral-950 selection:text-white">
<div id="hero">
<Hero onScrollToSection={handleScrollToSection} />
</div>
<AboutSection />
<ProcessSection />
<StatsSection />
<PortfolioShowcase />
<PitchBooking />
<FooterContact onScrollToSection={handleScrollToSection} />
</div>
);
}

View File

@@ -0,0 +1,143 @@
import React from 'react';
import { motion } from 'framer-motion';
import { BookOpen, Target, Sparkles, Milestone, Award, Lightbulb, Cpu, ShieldCheck } from 'lucide-react';
export const AboutSection: React.FC = () => {
const offerings = [
{ icon: <Milestone className="w-5 h-5 text-neutral-800" />, name: "Guidance & Training" },
{ icon: <Cpu className="w-5 h-5 text-neutral-800" />, name: "Infrastructure support" },
{ icon: <Target className="w-5 h-5 text-neutral-800" />, name: "1-on-1 Mentorship" },
{ icon: <Award className="w-5 h-5 text-neutral-800" />, name: "Seed Funding Support" },
{ icon: <Sparkles className="w-5 h-5 text-neutral-800" />, name: "IPR & Patent Filing" },
{ icon: <ShieldCheck className="w-5 h-5 text-neutral-800" />, name: "Networking Events" }
];
const curriculumPoints = [
{ title: "Academic Ideation", desc: "Formulate ideas with solid validation and engineering frameworks built into your college curriculum." },
{ title: "Business Model Design", desc: "Map your target market, construct unit economics, and define your revenue capture strategy." },
{ title: "IPR & Pitch Readiness", desc: "Gain legal structures, learn patent filing mechanics, and polish your investor-facing deck." }
];
return (
<section
id="about"
className="relative w-full py-24 md:py-32 px-6 md:px-12 bg-white overflow-hidden"
>
<div className="absolute top-0 right-1/3 w-96 h-96 bg-brand-orange/5 rounded-full blur-[120px] pointer-events-none" />
<div className="max-w-7xl mx-auto space-y-24 md:space-y-32">
{/* Core Vision & Mission */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="lg:col-span-5 space-y-6"
>
<span className="font-sans text-xs font-bold uppercase tracking-widest text-[#e91e63] bg-[#e91e63]/10 px-3.5 py-1.5 rounded-full">
Who We Are
</span>
<h2 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-900 tracking-tight leading-none">
Empowering Student Founders
</h2>
<p className="font-sans text-sm md:text-base text-neutral-500 leading-relaxed">
RAISE Incubator is a dedicated Section 8 (not-for-profit) company situated directly within the Rajalakshmi Institute of Technology campus. We transform engineering ideas into market-viable products.
</p>
{/* Offerings Icons List */}
<div className="pt-6">
<h4 className="font-heading font-bold text-xs uppercase tracking-wider text-neutral-400 mb-4">
What We Provide
</h4>
<div className="grid grid-cols-2 gap-4">
{offerings.map((item, i) => (
<div key={i} className="flex items-center gap-2.5">
<div className="p-2 bg-neutral-50 border border-neutral-200/50 rounded-lg">
{item.icon}
</div>
<span className="font-sans text-xs font-bold text-neutral-700">{item.name}</span>
</div>
))}
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6"
>
{/* Vision Card */}
<div className="bg-neutral-50 rounded-3xl p-6 md:p-8 border border-neutral-200/60 shadow-premium">
<div className="w-12 h-12 rounded-xl bg-neutral-900 text-white flex items-center justify-center mb-6">
<Target className="w-6 h-6" />
</div>
<h3 className="font-heading text-xl font-bold text-neutral-900 mb-3">Our Vision</h3>
<p className="font-sans text-xs md:text-sm text-neutral-500 leading-relaxed">
To create a vibrant innovation and entrepreneurship ecosystem that transforms ideas into impactful startups, fosters industry-relevant technologies, and nurtures socially responsible innovators.
</p>
</div>
{/* Mission Card */}
<div className="bg-neutral-50 rounded-3xl p-6 md:p-8 border border-neutral-200/60 shadow-premium">
<div className="w-12 h-12 rounded-xl bg-neutral-900 text-white flex items-center justify-center mb-6">
<Lightbulb className="w-6 h-6" />
</div>
<h3 className="font-heading text-xl font-bold text-neutral-900 mb-3">Our Mission</h3>
<ul className="font-sans text-xs md:text-sm text-neutral-500 space-y-3 list-disc pl-4 leading-relaxed">
<li>Promote entrepreneurial thinking among students & faculty through structured ideation.</li>
<li>Facilitate industry partnerships, IP/Patent creation, and critical seed funding.</li>
<li>Align initiatives with national missions for long-term societal and economic impact.</li>
</ul>
</div>
</motion.div>
</div>
{/* Unique Differentiator - Academic Course */}
<div className="relative rounded-3xl bg-neutral-950 text-white p-8 md:p-16 border border-neutral-800 shadow-2xl overflow-hidden">
{/* Subtle decoration */}
<div className="absolute right-0 bottom-0 w-80 h-80 bg-brand-pink/10 rounded-full blur-[100px] pointer-events-none" />
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center relative z-10">
<div className="lg:col-span-5 space-y-6">
<span className="font-sans text-xs font-bold uppercase tracking-widest text-yellow-400 bg-yellow-400/10 px-3.5 py-1.5 rounded-full border border-yellow-400/20">
Unique Differentiator
</span>
<h2 className="font-heading text-3xl md:text-4xl font-extrabold tracking-tight leading-none">
Not Just a Club. <br />An Academic Course.
</h2>
<p className="font-sans text-xs md:text-sm text-neutral-400 leading-relaxed">
RAISE is different from regular university incubators. We integrate entrepreneurship as a structured academic course for RIT students.
</p>
<div className="flex items-center gap-3">
<div className="p-3 bg-neutral-800 rounded-xl">
<BookOpen className="w-6 h-6 text-yellow-400" />
</div>
<div>
<span className="font-heading font-bold text-sm block">Structured Syllabus</span>
<span className="font-sans text-xs text-neutral-400">Earn course credits while launching a company.</span>
</div>
</div>
</div>
{/* Differentiator Milestones */}
<div className="lg:col-span-7 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{curriculumPoints.map((point, index) => (
<div key={index} className="bg-neutral-900 border border-neutral-800 rounded-2xl p-5 space-y-3">
<span className="font-heading font-extrabold text-sm text-neutral-600">0{index + 1}</span>
<h4 className="font-heading font-bold text-sm text-white">{point.title}</h4>
<p className="font-sans text-xs text-neutral-400 leading-relaxed">{point.desc}</p>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,174 @@
import React from 'react';
import { MapPin, Phone, Mail, Landmark, ExternalLink, User } from 'lucide-react';
import raiseLogo from '@/assets/raise/Raiselogo.webp';
interface FooterProps {
onScrollToSection: (sectionId: string) => void;
}
export const FooterContact: React.FC<FooterProps> = ({ onScrollToSection }) => {
return (
<footer
id="contact"
className="relative w-full bg-white border-t border-neutral-200/60 pt-20 pb-10 px-6 md:px-12 grid-bg"
>
<div className="max-w-7xl mx-auto space-y-16">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8">
{/* Brand Column */}
<div className="lg:col-span-4 space-y-6">
<div className="flex items-center gap-3">
<img src={raiseLogo} alt="RAISE Incubator Logo" className="h-10 w-auto object-contain rounded-lg" />
<div>
<span className="font-heading font-extrabold text-lg tracking-tight text-neutral-900 block leading-none">RAISE</span>
<span className="font-sans font-semibold text-[9px] text-neutral-400 uppercase tracking-wider block mt-1">
Incubation Cell
</span>
</div>
</div>
<p className="font-sans text-xs md:text-sm text-neutral-400 leading-relaxed max-w-sm">
Rajalakshmi Accelerator & Incubator for Startup Enterprises Association (RAISE) is a Sec 8 Not-For-Profit company built to elevate student builders at Rajalakshmi Institute of Technology.
</p>
<div className="flex items-center gap-2 pt-2 text-xs font-semibold text-neutral-500">
<Landmark className="w-4 h-4 text-neutral-400" />
<span>Affiliated with Rajalakshmi Institute of Technology</span>
</div>
</div>
{/* Quick Links Column */}
<div className="lg:col-span-3 space-y-6">
<h3 className="font-heading font-bold text-xs uppercase tracking-wider text-neutral-400">
Quick Navigation
</h3>
<div className="flex flex-col gap-3">
<button
onClick={() => onScrollToSection('about')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors text-left"
>
About Incubator
</button>
<button
onClick={() => onScrollToSection('process')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors text-left"
>
Incubation Pathway
</button>
<button
onClick={() => onScrollToSection('stats')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors text-left"
>
Our Metrics
</button>
<button
onClick={() => onScrollToSection('portfolio')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors text-left"
>
Startups Directory
</button>
<button
onClick={() => onScrollToSection('pitch')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors text-left"
>
Pitch Session Request
</button>
</div>
</div>
{/* Contact Details Column */}
<div className="lg:col-span-5 space-y-6">
<h3 className="font-heading font-bold text-xs uppercase tracking-wider text-neutral-400">
Contact & Location
</h3>
<div className="space-y-6">
{/* Incubation Manager Contact Card */}
<div className="bg-neutral-50/50 border border-neutral-200/60 rounded-2xl p-5 space-y-4 shadow-sm">
<div className="flex gap-3 items-center">
<div className="p-2 bg-neutral-100 border border-neutral-200/50 rounded-xl text-neutral-700">
<User className="w-4 h-4" />
</div>
<div>
<h4 className="font-heading font-extrabold text-[10px] text-neutral-400 uppercase tracking-widest leading-none">
Incubation Manager
</h4>
<p className="font-heading font-bold text-sm text-neutral-900 mt-1.5">
Mr. B. Aravind
</p>
</div>
</div>
<div className="space-y-3 pl-1">
{/* Email */}
<div className="flex items-center gap-2.5 text-xs">
<Mail className="w-4 h-4 text-neutral-400" />
<a
href="mailto:aravind.b@ritchennai.edu.in"
className="font-sans font-semibold text-neutral-500 hover:text-neutral-900 hover:underline transition-all"
>
aravind.b@ritchennai.edu.in
</a>
</div>
{/* Phone */}
<div className="flex items-center gap-2.5 text-xs">
<Phone className="w-4 h-4 text-neutral-400" />
<a
href="tel:+918838976942"
className="font-sans font-semibold text-neutral-500 hover:text-neutral-900 hover:underline transition-all"
>
+91 88389 76942
</a>
</div>
{/* Office */}
<div className="flex items-start gap-2.5 text-xs leading-normal">
<Landmark className="w-4 h-4 text-neutral-400 mt-0.5 flex-shrink-0" />
<span className="font-sans text-neutral-500">
<strong>Office:</strong> C Block 3rd floor, C306,<br />Panel 4 Inside Placement Cell
</span>
</div>
</div>
</div>
{/* Address */}
<div className="flex gap-3.5 items-start pl-1 pt-2">
<div className="p-2 bg-neutral-50 border border-neutral-200/50 rounded-lg text-neutral-600 mt-1">
<MapPin className="w-4 h-4" />
</div>
<div>
<h4 className="font-heading font-bold text-xs text-neutral-800 uppercase tracking-wide">
Campus Lab Address
</h4>
<p className="font-sans text-xs md:text-sm text-neutral-400 leading-relaxed max-w-sm mt-1">
C Block 3rd floor, C306,<br />
Panel 4 Inside Placement Cell,<br />
RIT Campus, Chennai – 600124.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Footer Bottom Bar */}
<div className="border-t border-neutral-200/60 pt-8 flex flex-col md:flex-row justify-between items-center gap-4 text-xs font-semibold text-neutral-400">
<div>
&copy; {new Date().getFullYear()} RAISE Incubation Cell. All rights reserved.
</div>
<div className="flex gap-6">
<a
href="https://www.ritchennai.org"
target="_blank"
rel="noreferrer"
className="hover:text-neutral-900 flex items-center gap-1 transition-colors"
>
ritchennai.org <ExternalLink className="w-3.5 h-3.5" />
</a>
</div>
</div>
</div>
</footer>
);
};

View File

@@ -0,0 +1,253 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { ArrowDown, Award } from 'lucide-react';
import raiseLogo from '@/assets/raise/Raiselogo.webp';
import collegeLogo from '@/assets/raise/college-logo.webp';
interface HeroProps {
onScrollToSection: (sectionId: string) => void;
}
export const Hero: React.FC<HeroProps> = ({ onScrollToSection }) => {
const letters = ['R', 'A', 'I', 'S', 'E'];
// Track drag/hover status for each letter
const [hoveredStates, setHoveredStates] = useState<boolean[]>([false, false, false, false, false]);
const [isIntro, setIsIntro] = useState(true);
const [entryCompleted, setEntryCompleted] = useState(false);
const [hoverActive, setHoverActive] = useState(false);
useEffect(() => {
// Start entrance animation almost immediately after mount
const entryTimer = setTimeout(() => {
setEntryCompleted(true);
}, 10);
// Clear entrance delays for responsive hover events
const hoverTimer = setTimeout(() => {
setHoverActive(true);
}, 300);
// End intro state and introduce surrounding content
const introTimer = setTimeout(() => {
setIsIntro(false);
}, 400);
return () => {
clearTimeout(entryTimer);
clearTimeout(hoverTimer);
clearTimeout(introTimer);
};
}, []);
const handleMouseEnter = (index: number) => {
if (!hoverActive) return; // disable hover actions during entry
setHoveredStates(prev => {
const next = [...prev];
next[index] = true;
return next;
});
};
const handleMouseLeave = (index: number) => {
setHoveredStates(prev => {
const next = [...prev];
next[index] = false;
return next;
});
};
return (
<header className="relative min-h-screen w-full flex flex-col justify-between items-center px-6 md:px-12 py-6 overflow-hidden bg-white grid-bg">
{/* Background Gradients for Premium Touch */}
<div className="absolute top-0 left-1/4 w-[500px] h-[500px] bg-gradient-to-tr from-brand-orange/5 to-brand-pink/5 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute bottom-0 right-1/4 w-[600px] h-[600px] bg-gradient-to-br from-brand-purple/5 to-brand-pink/5 rounded-full blur-[150px] pointer-events-none" />
{/* Glassmorphism Header */}
<motion.nav
animate={{ opacity: isIntro ? 0 : 1, y: isIntro ? -20 : 0 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
className="w-full max-w-7xl mx-auto flex items-center justify-between px-3.5 sm:px-6 py-3 sm:py-4 rounded-2xl glassmorphism shadow-premium z-50"
style={{ pointerEvents: isIntro ? 'none' : 'auto' }}
>
<div className="flex items-center gap-3 cursor-pointer" onClick={() => onScrollToSection('hero')}>
<div className="relative">
<img src={raiseLogo} alt="RAISE Incubator Logo" className="h-9 sm:h-11 w-auto object-contain rounded-lg" />
<div className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-emerald-500 border-2 border-white animate-pulse" />
</div>
</div>
{/* Nav links */}
<div className="hidden md:flex items-center gap-8">
<button
onClick={() => onScrollToSection('about')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors"
>
About
</button>
<button
onClick={() => onScrollToSection('process')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors"
>
Incubation Pathway
</button>
<button
onClick={() => onScrollToSection('stats')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors"
>
Our Impact
</button>
<button
onClick={() => onScrollToSection('portfolio')}
className="font-sans text-sm font-semibold text-neutral-600 hover:text-neutral-900 transition-colors"
>
Startups
</button>
</div>
{/* Call to Action Button & RIT Logo */}
<div className="flex items-center gap-2 sm:gap-4">
<button
onClick={() => onScrollToSection('pitch')}
className="relative px-3.5 sm:px-5 py-2 sm:py-2.5 rounded-xl bg-neutral-950 hover:bg-neutral-850 text-white font-sans text-xs sm:text-sm font-bold shadow-lg shadow-neutral-950/10 transition-all duration-300 hover:scale-[1.02] active:scale-[0.98] whitespace-nowrap"
>
Pitch Your Idea
</button>
<div className="hidden sm:block h-8 w-px bg-neutral-200/80" />
<a
href="https://ritchennai.org"
target="_blank"
rel="noopener noreferrer"
className="transition-opacity hover:opacity-80 cursor-pointer flex items-center"
>
<img
src={collegeLogo}
alt="RIT College Logo"
className="h-8 sm:h-10 w-auto object-contain rounded-lg"
onError={(e) => {
// Hide broken image frame if RIT logo is missing or placeholder is empty
(e.target as HTMLElement).style.display = 'none';
}}
/>
</a>
</div>
</motion.nav>
{/* Hero Central Content */}
<motion.div
layout
className="flex-1 w-full max-w-5xl flex flex-col justify-center items-center text-center mt-12 md:mt-16 z-10"
>
{/* RIT Institution Badge */}
{!isIntro && (
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-neutral-100/80 border border-neutral-200/50 mb-8"
>
<span className="w-2 h-2 rounded-full bg-[#e91e63] animate-ping" />
<span className="font-sans text-xs font-bold uppercase tracking-widest text-neutral-600">
Rajalakshmi Institute of Technology
</span>
</motion.div>
)}
{/* Giant RAISE Text with stroke outline, slanted format, and staggered load */}
<motion.div
layout
className="flex select-none justify-center items-center gap-4 md:gap-8 mb-6 cursor-ew-resize"
>
{letters.map((letter, idx) => (
<span
key={letter + idx}
onMouseEnter={() => handleMouseEnter(idx)}
onMouseLeave={() => handleMouseLeave(idx)}
onTouchStart={() => handleMouseEnter(idx)}
onTouchEnd={() => handleMouseLeave(idx)}
className="text-[18vw] md:text-[14vw] leading-none select-none inline-block"
style={{
fontFamily: '"Archivo Black", sans-serif',
WebkitTextStroke: '4.5px #000000',
WebkitTextFillColor: hoveredStates[idx] ? '#000000' : 'transparent',
color: hoveredStates[idx] ? '#000000' : 'transparent',
display: 'inline-block',
opacity: entryCompleted ? 1 : 0,
transform: !entryCompleted
? 'scale(0.3) skewX(0deg)'
: hoveredStates[idx]
? 'scale(1.08) translateY(-4px) skewX(-12deg)'
: 'scale(1) skewX(-12deg)',
transition: 'all 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
transitionDelay: hoverActive ? '0ms' : `${idx * 40}ms`,
textShadow: hoveredStates[idx] ? '0 10px 30px rgba(0,0,0,0.05)' : 'none'
}}
>
{letter}
</span>
))}
</motion.div>
{/* Subtitle, Description, and Action Pills */}
{!isIntro && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.15, ease: "easeOut" }}
className="flex flex-col items-center"
>
<h2 className="font-heading text-lg md:text-xl lg:text-2xl font-bold text-neutral-800 max-w-4xl tracking-normal md:tracking-wide mb-6 uppercase">
Rajalakshmi Accelerator & Incubator for Startup Enterprises Association
</h2>
<p className="font-sans text-sm md:text-base text-neutral-500 max-w-2xl leading-relaxed mb-12">
A section 8 (not-for-profit) company inside the RIT campus. We empower student founders by providing continuous mentorship, infrastructure, and structured startup paths from ideation to seed funding.
</p>
<motion.div
whileHover={{ scale: 1.03 }}
className="inline-flex items-center gap-3 px-6 py-3.5 rounded-2xl bg-gradient-to-r from-neutral-900 to-neutral-800 text-white font-sans font-bold shadow-xl shadow-neutral-950/20 mb-8 border border-neutral-700/50"
>
<Award className="w-5 h-5 text-yellow-400" />
<span className="text-sm tracking-wide">
APPROVED UNIQUE IDEAS SECURE FUNDS UP TO <span className="text-yellow-400 underline decoration-2 underline-offset-4">₹20,000</span>
</span>
</motion.div>
</motion.div>
)}
</motion.div>
{/* Footer / Scroll Indicator */}
<motion.div
animate={{ opacity: isIntro ? 0 : 1, y: isIntro ? 20 : 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="w-full flex justify-between items-center max-w-7xl mx-auto z-10 py-4 border-t border-neutral-100"
style={{ pointerEvents: isIntro ? 'none' : 'auto' }}
>
<div className="flex gap-4">
<div className="flex items-center gap-2 text-xs font-semibold text-neutral-400">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
Active Batch 2026
</div>
</div>
<motion.button
onClick={() => onScrollToSection('about')}
animate={{ y: [0, 8, 0] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="flex flex-col items-center gap-1 font-sans font-semibold text-xs text-neutral-400 hover:text-neutral-900 transition-colors uppercase tracking-wider cursor-pointer"
>
Scroll to explore
<ArrowDown className="w-4 h-4" />
</motion.button>
<div className="text-xs font-semibold text-neutral-400">
RIT Innovation Cell
</div>
</motion.div>
</header>
);
};

View File

@@ -0,0 +1,376 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Calendar, User, Mail, GraduationCap, FileText, CheckCircle, Clock } from 'lucide-react';
export const PitchBooking: React.FC = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
department: '',
projectTitle: '',
uniqueness: '',
slotDate: '',
slotTime: ''
});
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
if (name === 'slotDate' && value) {
const date = new Date(value);
const day = date.getUTCDay();
if (day === 6 || day === 0) {
setErrors(prev => ({
...prev,
slotDate: 'Sessions cannot be scheduled on weekends (Saturday/Sunday)'
}));
setFormData(prev => ({ ...prev, slotDate: '' }));
return;
}
}
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => {
const next = { ...prev };
delete next[name];
return next;
});
}
};
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) newErrors.name = 'Full name is required';
if (!formData.email.trim()) {
newErrors.email = 'RIT student email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.department) newErrors.department = 'Department is required';
if (!formData.projectTitle.trim()) newErrors.projectTitle = 'Project title is required';
if (!formData.uniqueness.trim()) {
newErrors.uniqueness = 'Please explain what makes your project unique';
} else if (formData.uniqueness.trim().length < 20) {
newErrors.uniqueness = 'Please provide a more detailed description (min 20 characters)';
}
if (!formData.slotDate) {
newErrors.slotDate = 'Please select a date';
} else {
const date = new Date(formData.slotDate);
const day = date.getUTCDay();
if (day === 6 || day === 0) {
newErrors.slotDate = 'Sessions cannot be scheduled on weekends (Saturday/Sunday)';
}
}
if (!formData.slotTime) newErrors.slotTime = 'Please select a time slot';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
setIsSubmitting(true);
// Simulate API submit delay
setTimeout(() => {
setIsSubmitting(false);
setIsSubmitted(true);
}, 1500);
}
};
const timeSlots = [
"09:00 AM - 11:00 AM",
"11:00 AM - 01:00 PM",
"01:00 PM - 03:00 PM"
];
return (
<section
id="pitch"
className="relative w-full py-24 md:py-32 px-6 md:px-12 bg-neutral-50/50 border-t border-neutral-100 overflow-hidden flex flex-col items-center"
>
<div className="absolute bottom-0 left-1/4 w-[600px] h-[600px] bg-gradient-to-br from-brand-pink/5 to-brand-purple/5 rounded-full blur-[150px] pointer-events-none" />
{/* Header */}
<div className="max-w-4xl text-center mb-16 z-10">
<span className="font-sans text-xs font-bold uppercase tracking-widest text-[#e91e63] bg-[#e91e63]/10 px-3.5 py-1.5 rounded-full">
Work With Us
</span>
<h2 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-900 mt-4 mb-6 tracking-tight">
Pitch Your Startup Idea
</h2>
<p className="font-sans text-sm md:text-base text-neutral-500 max-w-2xl mx-auto leading-relaxed">
Ready to present? Secure a time slot with our RAISE Incubation Manager, <span className="font-semibold text-neutral-800">Mr. B. Aravind</span>. Once approved, the project will be fast-tracked to college administration for seed grant checks up to <span className="font-bold text-neutral-800">₹20,000</span>.
</p>
</div>
<div className="w-full max-w-3xl z-10">
<AnimatePresence mode="wait">
{!isSubmitted ? (
<motion.form
key="booking-form"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
onSubmit={handleSubmit}
className="bg-white rounded-3xl p-6 md:p-10 border border-neutral-200/60 shadow-premium space-y-6 md:space-y-8"
>
{/* Form Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Founder Name */}
<div className="flex flex-col gap-2">
<label htmlFor="name" className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider flex items-center gap-1.5">
<User className="w-4 h-4 text-neutral-400" /> Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Enter full name"
className={`px-4 py-3 rounded-xl border font-sans text-sm outline-none transition-all ${
errors.name ? 'border-red-400 focus:border-red-500 bg-red-50/10' : 'border-neutral-200 focus:border-neutral-950'
}`}
/>
{errors.name && <span className="font-sans text-xs text-red-500 font-semibold">{errors.name}</span>}
</div>
{/* Email Address */}
<div className="flex flex-col gap-2">
<label htmlFor="email" className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider flex items-center gap-1.5">
<Mail className="w-4 h-4 text-neutral-400" /> RIT Student Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="studentname@ritchennai.edu.in"
className={`px-4 py-3 rounded-xl border font-sans text-sm outline-none transition-all ${
errors.email ? 'border-red-400 focus:border-red-500 bg-red-50/10' : 'border-neutral-200 focus:border-neutral-950'
}`}
/>
{errors.email && <span className="font-sans text-xs text-red-500 font-semibold">{errors.email}</span>}
</div>
{/* Department */}
<div className="flex flex-col gap-2">
<label htmlFor="department" className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider flex items-center gap-1.5">
<GraduationCap className="w-4 h-4 text-neutral-400" /> Department
</label>
<select
id="department"
name="department"
value={formData.department}
onChange={handleInputChange}
className={`px-4 py-3 rounded-xl border font-sans text-sm outline-none bg-white transition-all ${
errors.department ? 'border-red-400 focus:border-red-500 bg-red-50/10' : 'border-neutral-200 focus:border-neutral-950'
}`}
>
<option value="">Select your department</option>
<option value="Artificial Intelligence and Data Science">Artificial Intelligence and Data Science</option>
<option value="Computer Science and Engineering">Computer Science and Engineering</option>
<option value="Computer Science and Engineering (AI & ML)">Computer Science and Engineering (AI & ML)</option>
<option value="Computer Science and Business Systems">Computer Science and Business Systems</option>
<option value="Computer and Communication Engineering">Computer and Communication Engineering</option>
<option value="Electrical and Communication Engineering">Electrical and Communication Engineering</option>
<option value="EE(VLSI)">EE(VLSI)</option>
<option value="Mechanical Engineering">Mechanical Engineering</option>
<option value="Biotechnology">Biotechnology</option>
<option value="Other / Interdisciplinary">Other / Interdisciplinary</option>
</select>
{errors.department && <span className="font-sans text-xs text-red-500 font-semibold">{errors.department}</span>}
</div>
{/* Project Title */}
<div className="flex flex-col gap-2">
<label htmlFor="projectTitle" className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider flex items-center gap-1.5">
<FileText className="w-4 h-4 text-neutral-400" /> Project / Startup Title
</label>
<input
type="text"
id="projectTitle"
name="projectTitle"
value={formData.projectTitle}
onChange={handleInputChange}
placeholder="Enter project name"
className={`px-4 py-3 rounded-xl border font-sans text-sm outline-none transition-all ${
errors.projectTitle ? 'border-red-400 focus:border-red-500 bg-red-50/10' : 'border-neutral-200 focus:border-neutral-950'
}`}
/>
{errors.projectTitle && <span className="font-sans text-xs text-red-500 font-semibold">{errors.projectTitle}</span>}
</div>
</div>
{/* Uniqueness description */}
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<label htmlFor="uniqueness" className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider">
Uniqueness & Problem Solved
</label>
<span className="font-sans text-[10px] md:text-xs font-semibold text-neutral-400">
Required for Incubation Manager approval
</span>
</div>
<textarea
id="uniqueness"
name="uniqueness"
rows={4}
value={formData.uniqueness}
onChange={handleInputChange}
placeholder="Explain why your project idea is unique, what problem it solves, and how it is technically viable."
className={`px-4 py-3 rounded-xl border font-sans text-sm outline-none resize-y transition-all ${
errors.uniqueness ? 'border-red-400 focus:border-red-500 bg-red-50/10' : 'border-neutral-200 focus:border-neutral-950'
}`}
/>
{errors.uniqueness && <span className="font-sans text-xs text-red-500 font-semibold">{errors.uniqueness}</span>}
</div>
{/* Slot Scheduling Title */}
<div className="border-t border-neutral-100 pt-6">
<h3 className="font-heading font-bold text-base text-neutral-900 mb-4 flex items-center gap-2">
<Calendar className="w-5 h-5 text-neutral-600" /> Select Pitch Session Slot
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Date Input */}
<div className="flex flex-col gap-2">
<label htmlFor="slotDate" className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider flex justify-between">
<span>Presentation Date</span>
<span className="text-neutral-400 font-semibold text-[10px] normal-case">Mon - Fri only</span>
</label>
<input
type="date"
id="slotDate"
name="slotDate"
value={formData.slotDate}
onChange={handleInputChange}
min={new Date().toISOString().split('T')[0]} // limit to future dates
className={`px-4 py-3 rounded-xl border font-sans text-sm outline-none bg-white transition-all ${
errors.slotDate ? 'border-red-400 focus:border-red-500 bg-red-50/10' : 'border-neutral-200 focus:border-neutral-950'
}`}
/>
{errors.slotDate && <span className="font-sans text-xs text-red-500 font-semibold">{errors.slotDate}</span>}
</div>
{/* Time slot buttons */}
<div className="flex flex-col gap-2">
<label className="font-sans text-xs font-bold text-neutral-700 uppercase tracking-wider flex justify-between">
<span>Time Slot</span>
<span className="text-neutral-400 font-semibold text-[10px] normal-case">9:00 AM - 3:00 PM</span>
</label>
<div className="grid grid-cols-1 gap-2">
{timeSlots.map((slot) => (
<button
type="button"
key={slot}
onClick={() => handleInputChange({ target: { name: 'slotTime', value: slot } } as any)}
className={`px-4 py-2.5 rounded-xl border font-sans text-xs font-semibold text-left transition-all ${
formData.slotTime === slot
? 'bg-neutral-950 border-neutral-950 text-white shadow-md'
: 'bg-neutral-50 hover:bg-neutral-100 border-neutral-200 text-neutral-600 hover:text-neutral-900'
}`}
>
<div className="flex items-center gap-2">
<Clock className="w-3.5 h-3.5" />
{slot}
</div>
</button>
))}
</div>
{errors.slotTime && <span className="font-sans text-xs text-red-500 font-semibold">{errors.slotTime}</span>}
</div>
</div>
</div>
{/* Submit Action */}
<div className="pt-2">
<button
type="submit"
disabled={isSubmitting}
className="w-full relative py-4 rounded-xl bg-neutral-950 hover:bg-neutral-850 text-white font-sans text-sm font-bold shadow-xl shadow-neutral-950/15 transition-all duration-300 hover:scale-[1.01] active:scale-[0.99] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isSubmitting ? (
<>
<div className="w-5 h-5 rounded-full border-2 border-white/20 border-t-white animate-spin" />
Scheduling Session...
</>
) : (
"Submit Pitch Request"
)}
</button>
</div>
</motion.form>
) : (
<motion.div
key="booking-success"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="bg-white rounded-3xl p-8 md:p-12 border border-neutral-200/60 shadow-premium text-center flex flex-col items-center"
>
<div className="w-16 h-16 rounded-full bg-emerald-100 flex items-center justify-center mb-6">
<CheckCircle className="w-10 h-10 text-emerald-600" />
</div>
<h3 className="font-heading text-2xl md:text-3xl font-extrabold text-neutral-900 mb-2">
Showcase Session Scheduled!
</h3>
<p className="font-sans text-sm md:text-base text-neutral-500 max-w-md mb-8">
Your pitch request has been successfully queued for review by the Incubation Manager. We have sent a confirmation email containing review guidelines to your mailbox.
</p>
{/* Summary Receipt */}
<div className="w-full bg-neutral-50 rounded-2xl p-6 border border-neutral-200/40 text-left space-y-4 max-w-md mb-8">
<div className="flex justify-between items-center text-xs border-b border-neutral-200/50 pb-2">
<span className="font-bold text-neutral-400 uppercase">Project</span>
<span className="font-bold text-neutral-800">{formData.projectTitle}</span>
</div>
<div className="flex justify-between items-center text-xs border-b border-neutral-200/50 pb-2">
<span className="font-bold text-neutral-400 uppercase">Founder</span>
<span className="font-semibold text-neutral-700">{formData.name}</span>
</div>
<div className="flex justify-between items-center text-xs border-b border-neutral-200/50 pb-2">
<span className="font-bold text-neutral-400 uppercase">Scheduled Date</span>
<span className="font-semibold text-neutral-700">{formData.slotDate}</span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="font-bold text-neutral-400 uppercase">Time Slot</span>
<span className="font-semibold text-neutral-700">{formData.slotTime}</span>
</div>
</div>
<button
onClick={() => {
setFormData({
name: '',
email: '',
department: '',
projectTitle: '',
uniqueness: '',
slotDate: '',
slotTime: ''
});
setIsSubmitted(false);
}}
className="px-6 py-3 rounded-xl border border-neutral-200 hover:border-neutral-400 font-sans text-xs font-semibold text-neutral-600 hover:text-neutral-900 transition-colors"
>
Schedule Another Pitch
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</section>
);
};

View File

@@ -0,0 +1,323 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Sparkles, Layers, Box, TrendingUp, HelpCircle } from 'lucide-react';
interface Startup {
name: string;
stage: 'commercialization' | 'mvp' | 'prototype' | 'idea';
domain: string;
description: string;
remarks: string;
color: string;
}
export const PortfolioShowcase: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('all');
const startups: Startup[] = [
// Commercialisation
{
name: "Thrust.AI",
stage: "commercialization",
domain: "AI-based Systems",
description: "Advanced artificial intelligence solutions for automated decision-making pipelines.",
remarks: "Early users / revenue stage",
color: "bg-emerald-500"
},
{
name: "RIT Billbot",
stage: "commercialization",
domain: "Campus Solutions",
description: "Smart billing and administration utilities designed to optimize institution expenses.",
remarks: "Early users / revenue stage",
color: "bg-emerald-500"
},
{
name: "Machty",
stage: "commercialization",
domain: "Software Platform",
description: "Developer-focused orchestration suite for managing high-performance service endpoints.",
remarks: "Early users / revenue stage",
color: "bg-emerald-500"
},
{
name: "Num Thozhan Pvt Ltd",
stage: "commercialization",
domain: "Social Impact / SaaS",
description: "A collaborative social platform linking local volunteers to regional community development goals.",
remarks: "Early users / revenue stage",
color: "bg-emerald-500"
},
// MVP
{
name: "Atomik Minds",
stage: "mvp",
domain: "AI-based Systems",
description: "Cognitive educational platforms providing personalised student learning paths.",
remarks: "Testing with users",
color: "bg-blue-500"
},
{
name: "Innsole",
stage: "mvp",
domain: "Hardware & Health",
description: "Smart footwear soles embedded with sensor arrays to monitor athletic gait and posture.",
remarks: "Testing with users",
color: "bg-blue-500"
},
// Prototype
{
name: "HydrofarmIQ",
stage: "prototype",
domain: "Agri-Tech / IoT",
description: "Automated hydroponics watering and nutritional delivery platform built with smart controllers.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "Cropguard",
stage: "prototype",
domain: "Agri-Tech / AI",
description: "Computer vision app scanning crop leaves to diagnose diseases and recommend treatments.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "RIT Printbot",
stage: "prototype",
domain: "Hardware & Robotics",
description: "High-resolution, cost-effective 3D printing robot tailored for mechanical labs.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "Ecotrack",
stage: "prototype",
domain: "Social Impact / Ecology",
description: "A carbon emission tracker helping organizations monitor and offset corporate footprints.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "BeautifyNet",
stage: "prototype",
domain: "Software Platform",
description: "A virtual makeup trial and booking assistant running on real-time neural models.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "RIT Transport App",
stage: "prototype",
domain: "Campus Solutions",
description: "Live shuttle and transport fleet tracker providing precise arrival estimates for RIT riders.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "SafeHer Travels",
stage: "prototype",
domain: "Social Impact / Security",
description: "An emergency response and location verification application dedicated to women travelers.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "Elroi Automations Pvt Ltd",
stage: "prototype",
domain: "Hardware & Automation",
description: "Smart switchboard relays enabling legacy appliance management via internet platforms.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "Mishara",
stage: "prototype",
domain: "Software Platform",
description: "A secure digital credentials safe for students, using robust cryptographic proofs.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
{
name: "AgriNed Technologies",
stage: "prototype",
domain: "Agri-Tech / Hardware",
description: "A deployable field weather station analyzing soil moisture, humidity, and atmospheric data.",
remarks: "Hardware/software prototype development",
color: "bg-amber-500"
},
// Idea
{
name: "HirEd",
stage: "idea",
domain: "Software Platform / HR-tech",
description: "A recruitment network matching student projects to corporate internships based on skill graphs.",
remarks: "Problem validation in progress",
color: "bg-purple-500"
},
{
name: "Authenticate",
stage: "idea",
domain: "Cybersecurity / Blockchain",
description: "Decentralized document validation system built to eliminate certificate counterfeiting.",
remarks: "Problem validation in progress",
color: "bg-purple-500"
},
{
name: "Ivanus",
stage: "idea",
domain: "AI-based Systems",
description: "Custom Large Language Model micro-agents trained to automate specialized backend tasks.",
remarks: "Problem validation in progress",
color: "bg-purple-500"
},
{
name: "Customizer Consent",
stage: "idea",
domain: "Software Platform / Privacy",
description: "A centralized user data privacy panel that handles cookie compliance and user consent.",
remarks: "Problem validation in progress",
color: "bg-purple-500"
},
{
name: "Soundscape",
stage: "idea",
domain: "Social Impact / Audio",
description: "Spatial audio generation software built to assist visually impaired individuals navigate campus.",
remarks: "Problem validation in progress",
color: "bg-purple-500"
},
{
name: "Yoovan AI",
stage: "idea",
domain: "AI-based Systems / HR",
description: "Personalized resume preparation and AI-assisted interview screening tool.",
remarks: "Problem validation in progress",
color: "bg-purple-500"
}
];
const filteredStartups = activeTab === 'all'
? startups
: startups.filter(s => s.stage === activeTab);
const getStageIcon = (stage: string) => {
switch(stage) {
case 'commercialization': return <TrendingUp className="w-4 h-4 text-emerald-600" />;
case 'mvp': return <Box className="w-4 h-4 text-blue-600" />;
case 'prototype': return <Layers className="w-4 h-4 text-amber-600" />;
case 'idea': return <Sparkles className="w-4 h-4 text-purple-600" />;
default: return <HelpCircle className="w-4 h-4" />;
}
};
const getStageTitle = (stage: string) => {
if (stage === 'mvp') return 'MVP Stage';
return stage.charAt(0).toUpperCase() + stage.slice(1) + ' Stage';
};
return (
<section
id="portfolio"
className="relative w-full py-24 md:py-32 px-6 md:px-12 bg-white flex flex-col items-center overflow-hidden"
>
<div className="absolute top-0 right-1/4 w-[500px] h-[500px] bg-gradient-to-tr from-brand-orange/5 to-brand-pink/5 rounded-full blur-[120px] pointer-events-none" />
{/* Header */}
<div className="max-w-4xl text-center mb-16 z-10">
<span className="font-sans text-xs font-bold uppercase tracking-widest text-neutral-500 border border-neutral-200 px-3.5 py-1.5 rounded-full bg-neutral-50">
Startup Showcase
</span>
<h2 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-900 mt-4 mb-6 tracking-tight">
Portfolio Directory
</h2>
<p className="font-sans text-sm md:text-base text-neutral-500 max-w-2xl mx-auto">
Explore the 22 student-led ventures currently incubated at RAISE. See their development maturity, from problem-validation ideas to active commercialization.
</p>
</div>
{/* Filter Tabs */}
<div className="w-full max-w-4xl flex flex-wrap justify-center gap-2 md:gap-3 mb-16 z-10">
{['all', 'commercialization', 'mvp', 'prototype', 'idea'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-5 py-2.5 rounded-xl font-sans text-xs md:text-sm font-semibold tracking-wide capitalize transition-all duration-300 ${
activeTab === tab
? 'bg-neutral-950 text-white shadow-lg shadow-neutral-950/10 scale-[1.02]'
: 'bg-neutral-100 hover:bg-neutral-200/60 text-neutral-600 hover:text-neutral-900'
}`}
>
{tab === 'all' ? 'All Projects' : tab === 'mvp' ? 'MVP' : tab}
</button>
))}
</div>
{/* Projects Grid */}
<motion.div
layout
className="w-full max-w-6xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 z-10"
>
<AnimatePresence mode="popLayout">
{filteredStartups.map((startup) => (
<motion.div
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
key={startup.name}
className="bg-neutral-50/40 rounded-2xl p-6 md:p-8 border border-neutral-200/60 shadow-premium shadow-premium-hover flex flex-col justify-between"
>
<div>
{/* Stage Indicator Pill */}
<div className="flex justify-between items-center mb-6">
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] md:text-xs font-bold uppercase tracking-wider ${
startup.stage === 'commercialization' ? 'bg-emerald-100 text-emerald-800' :
startup.stage === 'mvp' ? 'bg-blue-100 text-blue-800' :
startup.stage === 'prototype' ? 'bg-amber-100 text-amber-800' :
'bg-purple-100 text-purple-800'
}`}>
{getStageIcon(startup.stage)}
{getStageTitle(startup.stage)}
</span>
{/* Status Indicator */}
<span className="w-2.5 h-2.5 rounded-full bg-neutral-200 relative">
<span className={`absolute inset-0 rounded-full ${startup.color} animate-ping opacity-75`} />
<span className={`absolute inset-0 rounded-full ${startup.color}`} />
</span>
</div>
<h3 className="font-heading text-xl font-bold text-neutral-900 mb-1">
{startup.name}
</h3>
<span className="font-sans text-[11px] md:text-xs font-semibold text-neutral-400 uppercase tracking-widest block mb-4">
{startup.domain}
</span>
<p className="font-sans text-xs md:text-sm text-neutral-500 leading-relaxed mb-6">
{startup.description}
</p>
</div>
{/* Remarks Box */}
<div className="border-t border-neutral-200/50 pt-4 mt-auto">
<span className="font-sans text-[10px] font-bold text-neutral-400 uppercase tracking-wider block mb-1">
Status Remarks
</span>
<span className="font-sans text-xs font-semibold text-neutral-800">
{startup.remarks}
</span>
</div>
</motion.div>
))}
</AnimatePresence>
</motion.div>
</section>
);
};

Some files were not shown because too many files have changed in this diff Show More