Compare commits
14 Commits
56ce10fc55
...
64f0c5d754
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64f0c5d754 | ||
|
|
d9ed15b481 | ||
|
|
87865f810f | ||
|
|
906e9c757b | ||
|
|
23bf4fd86a | ||
|
|
95c97e81fa | ||
|
|
128e033a9a | ||
|
|
4e2edb7c52 | ||
|
|
bbb2cf7d30 | ||
|
|
b712b1e2f1 | ||
|
|
1641bf2110 | ||
|
|
2f69b0a29d | ||
|
|
5ea7de558b | ||
|
|
596504840e |
1606
attendance_report.html
Normal file
1606
attendance_report.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,11 +30,14 @@ public class CommunityAnswer {
|
|||||||
private String author;
|
private String author;
|
||||||
|
|
||||||
@Column(name = "upvotes")
|
@Column(name = "upvotes")
|
||||||
|
@Builder.Default
|
||||||
private Integer upvotes = 0;
|
private Integer upvotes = 0;
|
||||||
|
|
||||||
@Column(name = "is_accepted")
|
@Column(name = "is_accepted")
|
||||||
|
@Builder.Default
|
||||||
private Boolean isAccepted = false;
|
private Boolean isAccepted = false;
|
||||||
|
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
|
@Builder.Default
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +29,19 @@ public class CommunityQuestion {
|
|||||||
private String author;
|
private String author;
|
||||||
|
|
||||||
@Column(name = "upvotes")
|
@Column(name = "upvotes")
|
||||||
|
@Builder.Default
|
||||||
private Integer upvotes = 0;
|
private Integer upvotes = 0;
|
||||||
|
|
||||||
@Column(name = "tags", columnDefinition = "text[]")
|
@Column(name = "tags", columnDefinition = "text[]")
|
||||||
|
@Builder.Default
|
||||||
private List<String> tags = new ArrayList<>();
|
private List<String> tags = new ArrayList<>();
|
||||||
|
|
||||||
@Column(name = "is_answered")
|
@Column(name = "is_answered")
|
||||||
|
@Builder.Default
|
||||||
private Boolean isAnswered = false;
|
private Boolean isAnswered = false;
|
||||||
|
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
|
@Builder.Default
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ public class NotePyq {
|
|||||||
private String fileSize;
|
private String fileSize;
|
||||||
|
|
||||||
@Column(name = "downloads_count")
|
@Column(name = "downloads_count")
|
||||||
|
@Builder.Default
|
||||||
private Integer downloadsCount = 0;
|
private Integer downloadsCount = 0;
|
||||||
|
|
||||||
@Column(name = "uploaded_at")
|
@Column(name = "uploaded_at")
|
||||||
|
@Builder.Default
|
||||||
private LocalDateTime uploadedAt = LocalDateTime.now();
|
private LocalDateTime uploadedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,17 @@ public class BusLocationService {
|
|||||||
|
|
||||||
public List<BusLocation> getActiveLocations() {
|
public List<BusLocation> getActiveLocations() {
|
||||||
Instant threshold = Instant.now().minus(STALE_DURATION);
|
Instant threshold = Instant.now().minus(STALE_DURATION);
|
||||||
// Clean up stale entries on read
|
return locationCache.values().stream()
|
||||||
|
.filter(loc -> !loc.getLastUpdated().isBefore(threshold))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic background eviction of stale GPS coordinates every 5 minutes
|
||||||
|
*/
|
||||||
|
@org.springframework.scheduling.annotation.Scheduled(fixedRate = 300000)
|
||||||
|
public void evictStaleLocations() {
|
||||||
|
Instant threshold = Instant.now().minus(STALE_DURATION);
|
||||||
locationCache.entrySet().removeIf(entry -> entry.getValue().getLastUpdated().isBefore(threshold));
|
locationCache.entrySet().removeIf(entry -> entry.getValue().getLastUpdated().isBefore(threshold));
|
||||||
return List.copyOf(locationCache.values());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,24 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=
|
|||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
spring.datasource.password=${DB_PASSWORD:Amudiesh22@.}
|
spring.datasource.password=${DB_PASSWORD:Amudiesh22@.}
|
||||||
|
|
||||||
|
# ─── HIKARICP CONNECTION POOL OPTIMIZATIONS ───
|
||||||
|
spring.datasource.hikari.maximum-pool-size=30
|
||||||
|
spring.datasource.hikari.minimum-idle=10
|
||||||
|
spring.datasource.hikari.idle-timeout=300000
|
||||||
|
spring.datasource.hikari.max-lifetime=1200000
|
||||||
|
spring.datasource.hikari.connection-timeout=20000
|
||||||
|
|
||||||
# ─── JPA / HIBERNATE SETTINGS ───
|
# ─── JPA / HIBERNATE SETTINGS ───
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.jpa.show-sql=true
|
spring.jpa.show-sql=false
|
||||||
spring.jpa.properties.hibernate.format_sql=true
|
spring.jpa.properties.hibernate.format_sql=false
|
||||||
|
|
||||||
# ─── SERVER PORT ───
|
# ─── SERVER PORT & HTTP GZIP COMPRESSION ───
|
||||||
server.port=8085
|
server.port=8085
|
||||||
|
server.compression.enabled=true
|
||||||
|
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css
|
||||||
|
server.compression.min-response-size=1024
|
||||||
|
|
||||||
# ─── GOOGLE OAUTH2 CONFIGURATION ───
|
# ─── GOOGLE OAUTH2 CONFIGURATION ───
|
||||||
google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
|
google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
|
||||||
@@ -23,10 +33,8 @@ admin.super.password=${SUPER_ADMIN_PASSWORD:RIT@2026}
|
|||||||
admin.username=${ADMIN_USERNAME:ritadmin}
|
admin.username=${ADMIN_USERNAME:ritadmin}
|
||||||
admin.password=${ADMIN_PASSWORD:ritadmin2026}
|
admin.password=${ADMIN_PASSWORD:ritadmin2026}
|
||||||
|
|
||||||
|
|
||||||
# ─── IMS INTEGRATION CONFIGURATION ───
|
# ─── IMS INTEGRATION CONFIGURATION ───
|
||||||
# Options: "mock" (active now for testing & simulation) | "real" (for live college IMS)
|
# Options: "mock" (active now for testing & simulation) | "real" (for live college IMS)
|
||||||
ims.client.type=mock
|
ims.client.type=mock
|
||||||
ims.base-url=${IMS_BASE_URL:https://api.ims.ritchennai.edu.in}
|
ims.base-url=${IMS_BASE_URL:https://api.ims.ritchennai.edu.in}
|
||||||
ims.api-key=${IMS_API_KEY:rit_nexus_vps_secret_key_2026}
|
ims.api-key=${IMS_API_KEY:rit_nexus_vps_secret_key_2026}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.rit.portal;
|
||||||
|
|
||||||
|
import com.rit.portal.model.BusLocation;
|
||||||
|
import com.rit.portal.service.BusLocationService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class BusLocationServiceTest {
|
||||||
|
|
||||||
|
private BusLocationService busLocationService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
busLocationService = new BusLocationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateAndGetLocation() {
|
||||||
|
busLocationService.updateLocation("24A", 13.0827, 80.2707);
|
||||||
|
|
||||||
|
BusLocation loc = busLocationService.getLocation("24A");
|
||||||
|
assertNotNull(loc);
|
||||||
|
assertEquals("24A", loc.getRouteNumber());
|
||||||
|
assertEquals(13.0827, loc.getLatitude());
|
||||||
|
assertEquals(80.2707, loc.getLongitude());
|
||||||
|
assertFalse(loc.isStopped());
|
||||||
|
assertNotNull(loc.getLastUpdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStopLocation() {
|
||||||
|
busLocationService.updateLocation("15B", 12.9716, 77.5946);
|
||||||
|
busLocationService.stopLocation("15B");
|
||||||
|
|
||||||
|
BusLocation loc = busLocationService.getLocation("15B");
|
||||||
|
assertNotNull(loc);
|
||||||
|
assertTrue(loc.isStopped());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetNonExistentLocationReturnsNull() {
|
||||||
|
BusLocation loc = busLocationService.getLocation("NON_EXISTENT");
|
||||||
|
assertNull(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetActiveLocations() {
|
||||||
|
busLocationService.updateLocation("R1", 13.0, 80.0);
|
||||||
|
busLocationService.updateLocation("R2", 13.1, 80.1);
|
||||||
|
|
||||||
|
List<BusLocation> active = busLocationService.getActiveLocations();
|
||||||
|
assertEquals(2, active.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/test/java/com/rit/portal/EntityMappingTest.java
Normal file
60
backend/src/test/java/com/rit/portal/EntityMappingTest.java
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package com.rit.portal;
|
||||||
|
|
||||||
|
import com.rit.portal.entity.CommunityAnswer;
|
||||||
|
import com.rit.portal.entity.CommunityQuestion;
|
||||||
|
import com.rit.portal.entity.NotePyq;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class EntityMappingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNotePyqBuilderDefaults() {
|
||||||
|
NotePyq note = NotePyq.builder()
|
||||||
|
.title("Data Structures Notes")
|
||||||
|
.subject("Data Structures")
|
||||||
|
.department("CSE")
|
||||||
|
.semester(3)
|
||||||
|
.fileType("notes")
|
||||||
|
.downloadUrl("https://example.com/ds.pdf")
|
||||||
|
.fileSize("2.4 MB")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertNotNull(note);
|
||||||
|
assertEquals(0, note.getDownloadsCount());
|
||||||
|
assertNotNull(note.getUploadedAt());
|
||||||
|
assertTrue(note.getUploadedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommunityQuestionBuilderDefaults() {
|
||||||
|
CommunityQuestion question = CommunityQuestion.builder()
|
||||||
|
.title("How to prepare for CAT-1?")
|
||||||
|
.body("Looking for reference materials and past questions.")
|
||||||
|
.author("Student123")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertNotNull(question);
|
||||||
|
assertEquals(0, question.getUpvotes());
|
||||||
|
assertFalse(question.getIsAnswered());
|
||||||
|
assertNotNull(question.getTags());
|
||||||
|
assertNotNull(question.getAnswers());
|
||||||
|
assertNotNull(question.getCreatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommunityAnswerBuilderDefaults() {
|
||||||
|
CommunityAnswer answer = CommunityAnswer.builder()
|
||||||
|
.body("Focus on Unit 1 and Unit 2 standard questions.")
|
||||||
|
.author("Senior456")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertNotNull(answer);
|
||||||
|
assertEquals(0, answer.getUpvotes());
|
||||||
|
assertFalse(answer.getIsAccepted());
|
||||||
|
assertNotNull(answer.getCreatedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
1540
grade_report.html
Normal file
1540
grade_report.html
Normal file
File diff suppressed because it is too large
Load Diff
3543
real_timetable.html
Normal file
3543
real_timetable.html
Normal file
File diff suppressed because one or more lines are too long
18
scratch/inspect_table_cells.py
Normal file
18
scratch/inspect_table_cells.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('real_timetable.html', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find rows in the timetable table
|
||||||
|
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL)
|
||||||
|
print(f"Total tr rows: {len(rows)}")
|
||||||
|
|
||||||
|
for idx, r in enumerate(rows):
|
||||||
|
if 'tuesday' in r.lower() or 'tue' in r.lower():
|
||||||
|
print(f"\n--- ROW {idx} (TUESDAY) ---")
|
||||||
|
tds = re.findall(r'<td[^>]*>(.*?)</td>', r, re.DOTALL)
|
||||||
|
print(f"Total td cells in Tuesday row: {len(tds)}")
|
||||||
|
for col_i, td in enumerate(tds):
|
||||||
|
clean_td = ' '.join(re.sub(r'<[^>]+>', ' ', td).split())
|
||||||
|
forms_in_td = re.findall(r'<form[^>]*>(.*?)</form>', td, re.DOTALL)
|
||||||
|
print(f" Col {col_i}: forms_count={len(forms_in_td)} | text={clean_td[:80]}")
|
||||||
24
scratch/inspect_tt_periods.py
Normal file
24
scratch/inspect_tt_periods.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('real_timetable.html', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
forms = re.findall(r'<form[^>]*class="[^"]*period_form[^"]*"[^>]*>(.*?)</form>', content, re.DOTALL)
|
||||||
|
print(f"Total forms found: {len(forms)}")
|
||||||
|
|
||||||
|
by_day = {}
|
||||||
|
for form in forms:
|
||||||
|
day_m = re.search(r'name="day"\s+value="([^"]+)"', form)
|
||||||
|
per_m = re.search(r'name="period"\s+value="([^"]+)"', form)
|
||||||
|
day = day_m.group(1).lower() if day_m else 'unknown'
|
||||||
|
per = per_m.group(1) if per_m else '0'
|
||||||
|
by_day.setdefault(day, []).append((per, form))
|
||||||
|
|
||||||
|
for day in sorted(by_day.keys()):
|
||||||
|
print(f"\n=== DAY: {day.upper()} (Count: {len(by_day[day])}) ===")
|
||||||
|
for per, form in by_day[day]:
|
||||||
|
primaries = re.findall(r'class="[^"]*text-primary[^"]*"[^>]*>(.*?)</span>', form, re.DOTALL)
|
||||||
|
clean_p = [re.sub(r'<[^>]+>', '', p).strip() for p in primaries]
|
||||||
|
full_txt = re.sub(r'<[^>]+>', ' ', form).strip()
|
||||||
|
full_txt = ' '.join(full_txt.split())
|
||||||
|
print(f" Period {per}: primaries={clean_p} | full_txt={full_txt[:100]}")
|
||||||
13
scratch/inspect_tue_context.py
Normal file
13
scratch/inspect_tue_context.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('real_timetable.html', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
print("File length:", len(content))
|
||||||
|
# Search for Tuesday in content
|
||||||
|
tue_matches = [m.start() for m in re.finditer(r'tuesday', content, re.IGNORECASE)]
|
||||||
|
print("Tuesday matches at indices:", tue_matches)
|
||||||
|
|
||||||
|
for pos in tue_matches[:3]:
|
||||||
|
print("\n--- Context around Tuesday ---")
|
||||||
|
print(content[max(0, pos-200):min(len(content), pos+400)])
|
||||||
53
scratch/test_lab_assignment_marks.py
Normal file
53
scratch/test_lab_assignment_marks.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import http.cookiejar
|
||||||
|
import re
|
||||||
|
|
||||||
|
cj = http.cookiejar.CookieJar()
|
||||||
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||||
|
|
||||||
|
login_url = "https://ims.rajalakshmi.edu.in/ims/login"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: GET login page
|
||||||
|
req = urllib.request.Request(login_url, headers=headers)
|
||||||
|
with opener.open(req) as resp:
|
||||||
|
html = resp.read().decode('utf-8')
|
||||||
|
|
||||||
|
csrf_m = re.search(r'name="_token"\s+value="([^"]+)"', html)
|
||||||
|
csrf_token = csrf_m.group(1) if csrf_m else ''
|
||||||
|
print(f"CSRF Token: {csrf_token}")
|
||||||
|
|
||||||
|
# Step 2: POST login
|
||||||
|
post_data = urllib.parse.urlencode({
|
||||||
|
'_token': csrf_token,
|
||||||
|
'username': '2117240070293',
|
||||||
|
'password': '7010406809'
|
||||||
|
}).encode('utf-8')
|
||||||
|
|
||||||
|
req_post = urllib.request.Request(login_url, data=post_data, headers=headers)
|
||||||
|
with opener.open(req_post) as resp_post:
|
||||||
|
dash_html = resp_post.read().decode('utf-8')
|
||||||
|
|
||||||
|
print(f"Login success! Dash HTML length: {len(dash_html)}")
|
||||||
|
|
||||||
|
# Step 3: Find all <a> hrefs containing lab or assignment or mark or report
|
||||||
|
all_links = re.findall(r'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>', dash_html, re.DOTALL)
|
||||||
|
print(f"Total links found: {len(all_links)}")
|
||||||
|
|
||||||
|
for href, label in all_links:
|
||||||
|
clean_label = re.sub(r'<[^>]+>', '', label).strip()
|
||||||
|
clean_label_lower = clean_label.lower()
|
||||||
|
if 'lab' in clean_label_lower or 'assignment' in clean_label_lower or 'mark' in clean_label_lower or 'grade' in clean_label_lower or 'cat' in clean_label_lower:
|
||||||
|
print(f"Link: '{clean_label}' -> Href: '{href}'")
|
||||||
|
|
||||||
|
# Also search for all hrefs in the full HTML to be sure
|
||||||
|
all_hrefs = re.findall(r'href="([^"]+)"', dash_html)
|
||||||
|
print("\n--- ALL UNIQUE HREFS IN DASHBOARD ---")
|
||||||
|
for h in set(all_hrefs):
|
||||||
|
if 'admin' in h or 'student' in h or 'mark' in h or 'report' in h:
|
||||||
|
print(" ", h)
|
||||||
43
src/App.tsx
43
src/App.tsx
@@ -1,29 +1,35 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, lazy, Suspense } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AuthProvider } from '@/context/AuthContext';
|
import { AuthProvider } from '@/context/AuthContext';
|
||||||
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
|
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
|
||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Home from '@/pages/Home/Home';
|
import PageLoader from '@/components/PageLoader/PageLoader';
|
||||||
import Notes from '@/pages/Notes/Notes';
|
import ErrorBoundary from '@/components/ErrorBoundary/ErrorBoundary';
|
||||||
import Toolkit from '@/pages/Toolkit/Toolkit';
|
|
||||||
import AIAssistant from '@/pages/AIAssistant/AIAssistant';
|
// Code-split route components with dynamic imports for minimal initial bundle size
|
||||||
import Campus from '@/pages/Campus/Campus';
|
const Home = lazy(() => import('@/pages/Home/Home'));
|
||||||
import Events from '@/pages/Events/Events';
|
const Notes = lazy(() => import('@/pages/Notes/Notes'));
|
||||||
import Community from '@/pages/Community/Community';
|
const Toolkit = lazy(() => import('@/pages/Toolkit/Toolkit'));
|
||||||
import BusRoutes from '@/pages/BusRoutes/BusRoutes';
|
const AIAssistant = lazy(() => import('@/pages/AIAssistant/AIAssistant'));
|
||||||
import Faculty from '@/pages/Faculty/Faculty';
|
const Campus = lazy(() => import('@/pages/Campus/Campus'));
|
||||||
import DevCollab from '@/pages/DevCollab/DevCollab';
|
const Events = lazy(() => import('@/pages/Events/Events'));
|
||||||
import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard';
|
const Community = lazy(() => import('@/pages/Community/Community'));
|
||||||
import AdminDashboard from '@/pages/Admin/AdminDashboard';
|
const BusRoutes = lazy(() => import('@/pages/BusRoutes/BusRoutes'));
|
||||||
import Raise from '@/pages/Raise/Raise';
|
const Faculty = lazy(() => import('@/pages/Faculty/Faculty'));
|
||||||
import StudentDashboard from '@/pages/StudentDashboard/StudentDashboard';
|
const DevCollab = lazy(() => import('@/pages/DevCollab/DevCollab'));
|
||||||
|
const LeetcodeLeaderboard = lazy(() => import('@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard'));
|
||||||
|
const AdminDashboard = lazy(() => import('@/pages/Admin/AdminDashboard'));
|
||||||
|
const Raise = lazy(() => import('@/pages/Raise/Raise'));
|
||||||
|
const StudentDashboard = lazy(() => import('@/pages/StudentDashboard/StudentDashboard'));
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
gcTime: 10 * 60 * 1000,
|
gcTime: 10 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -40,10 +46,12 @@ function ScrollToTop() {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Standalone Admin Route */}
|
{/* Standalone Admin Route */}
|
||||||
<Route path="/admin" element={<AdminDashboard />} />
|
<Route path="/admin" element={<AdminDashboard />} />
|
||||||
@@ -62,8 +70,9 @@ function App() {
|
|||||||
<Route path="/faculty" element={<Faculty />} />
|
<Route path="/faculty" element={<Faculty />} />
|
||||||
<Route path="/events" element={<Events />} />
|
<Route path="/events" element={<Events />} />
|
||||||
|
|
||||||
{/* Student Dashboard — Timetable, Staff, Location (IMS Reg No & Password) */}
|
{/* Student Dashboard & Timetable — Real IMS Reg No & Password */}
|
||||||
<Route path="/dashboard" element={<StudentDashboard />} />
|
<Route path="/dashboard" element={<StudentDashboard />} />
|
||||||
|
<Route path="/timetable" element={<StudentDashboard />} />
|
||||||
|
|
||||||
{/* Protected Routes — Require Google Sign-In */}
|
{/* Protected Routes — Require Google Sign-In */}
|
||||||
<Route
|
<Route
|
||||||
@@ -102,9 +111,11 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,43 +224,27 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Quick Demo Fill Buttons */}
|
{/* Quick Demo Fill Buttons for Admins */}
|
||||||
<div className="pt-3 border-t border-white/10 space-y-1.5">
|
<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">
|
<span className="text-[10px] text-slate-400 font-semibold block text-center uppercase tracking-wider">
|
||||||
Quick Demo Credentials:
|
Official Admin Portals:
|
||||||
</span>
|
</span>
|
||||||
<div className="grid grid-cols-2 gap-1.5 text-[11px]">
|
<div className="grid grid-cols-2 gap-1.5 text-[11px]">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleQuickFill('2114251001', 'rit@2026')}
|
|
||||||
className="p-2 rounded-lg 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">🎓 CSE Student</span>
|
|
||||||
<span className="text-[9px] text-orange-400 font-mono">Fill</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleQuickFill('2114251002', 'rit@2026')}
|
|
||||||
className="p-2 rounded-lg 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">🎓 AI&DS Student</span>
|
|
||||||
<span className="text-[9px] text-orange-400 font-mono">Fill</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleQuickFill('Transport', 'RIT@2026')}
|
onClick={() => handleQuickFill('Transport', 'RIT@2026')}
|
||||||
className="p-2 rounded-lg bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
|
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="truncate">🚌 Transport Admin</span>
|
||||||
<span className="text-[9px] text-blue-400 font-mono">Fill</span>
|
<span className="text-[9px] text-blue-400 font-mono">Select</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleQuickFill('Admin', 'RIT@2026')}
|
onClick={() => handleQuickFill('Admin', 'RIT@2026')}
|
||||||
className="p-2 rounded-lg bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
|
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="truncate">⚡ Super Admin</span>
|
||||||
<span className="text-[9px] text-emerald-400 font-mono">Fill</span>
|
<span className="text-[9px] text-emerald-400 font-mono">Select</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
91
src/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
91
src/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||||
|
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('[ErrorBoundary caught error]:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleReset = () => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleGoHome = () => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[70vh] flex items-center justify-center p-6">
|
||||||
|
<div className="max-w-md w-full bg-card/80 backdrop-blur-xl border border-destructive/20 rounded-2xl p-8 shadow-2xl text-center space-y-6">
|
||||||
|
<div className="w-16 h-16 mx-auto rounded-2xl bg-destructive/10 border border-destructive/20 flex items-center justify-center text-destructive shadow-inner">
|
||||||
|
<AlertTriangle className="w-8 h-8 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl font-bold tracking-tight text-foreground">
|
||||||
|
Something went wrong
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
An unexpected error occurred while rendering this section. You can try refreshing the page or navigating back home.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.error && (
|
||||||
|
<div className="p-3 rounded-lg bg-muted/50 border border-border text-left overflow-x-auto text-xs font-mono text-muted-foreground max-h-24">
|
||||||
|
{this.state.error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={this.handleReset}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium transition-all shadow-lg shadow-orange-600/25 hover:shadow-orange-600/40 active:scale-95 cursor-pointer"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={this.handleGoHome}
|
||||||
|
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl bg-secondary hover:bg-secondary/80 text-foreground text-sm font-medium transition-all active:scale-95 border border-border cursor-pointer"
|
||||||
|
>
|
||||||
|
<Home className="w-4 h-4" />
|
||||||
|
Go to Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -36,7 +36,7 @@ interface NavItem {
|
|||||||
|
|
||||||
const PRIMARY_LINKS: NavItem[] = [
|
const PRIMARY_LINKS: NavItem[] = [
|
||||||
{ label: 'Home', path: '/', icon: HomeIcon },
|
{ label: 'Home', path: '/', icon: HomeIcon },
|
||||||
{ label: 'Time table', path: '/dashboard', icon: LayoutDashboard },
|
{ label: 'Student dashboard', path: '/dashboard', icon: LayoutDashboard },
|
||||||
{ label: 'Notes', path: '/notes', icon: BookOpen },
|
{ label: 'Notes', path: '/notes', icon: BookOpen },
|
||||||
{ label: 'Bus Routes', path: '/bus-routes', icon: Bus },
|
{ label: 'Bus Routes', path: '/bus-routes', icon: Bus },
|
||||||
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
|
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
|
||||||
@@ -318,7 +318,7 @@ export default function Navbar() {
|
|||||||
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"
|
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" />
|
<GraduationCap className="w-4 h-4 text-orange-400" />
|
||||||
My Time Table
|
Student Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|||||||
24
src/components/PageLoader/PageLoader.tsx
Normal file
24
src/components/PageLoader/PageLoader.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const PageLoader: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[60vh] flex flex-col items-center justify-center p-6 space-y-4">
|
||||||
|
<div className="relative flex items-center justify-center">
|
||||||
|
{/* Outer glowing ripple */}
|
||||||
|
<div className="absolute w-16 h-16 rounded-full bg-orange-500/20 animate-ping" />
|
||||||
|
{/* Spinning gradient ring */}
|
||||||
|
<div className="w-12 h-12 rounded-full border-3 border-transparent border-t-orange-500 border-r-orange-400 animate-spin" />
|
||||||
|
{/* Center dot */}
|
||||||
|
<div className="absolute w-4 h-4 rounded-full bg-gradient-to-tr from-orange-600 to-amber-400 shadow-lg shadow-orange-500/50" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center space-y-1">
|
||||||
|
<p className="text-sm font-semibold tracking-wide bg-gradient-to-r from-orange-600 via-amber-500 to-orange-400 bg-clip-text text-transparent animate-pulse">
|
||||||
|
Loading Freshers Hub
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Preparing your campus experience...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageLoader;
|
||||||
@@ -3832,9 +3832,5 @@ export const DEPARTMENTS = [
|
|||||||
'Computer Science & Engineering',
|
'Computer Science & Engineering',
|
||||||
'Electronics & Communication Engineering',
|
'Electronics & Communication Engineering',
|
||||||
'Electronics Engineering VLSI (Design and Technology)',
|
'Electronics Engineering VLSI (Design and Technology)',
|
||||||
'Humanities & Sciences',
|
|
||||||
'M.E. (VLSI Design)',
|
|
||||||
'M.Tech (Data Science)',
|
|
||||||
'Mathematics',
|
|
||||||
'Mechanical Engineering',
|
'Mechanical Engineering',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -200,7 +200,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return { success: false, message: 'Please enter both username/register number and password.' };
|
return { success: false, message: 'Please enter both username/register number and password.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. First, check if credentials match Admin / Transport
|
const isAdminUsername = ['admin', 'transport', 'ritadmin', 'superadmin'].includes(cleanedUser.toLowerCase());
|
||||||
|
|
||||||
|
// 1. First, check if credentials match Admin / Transport (only for admin accounts)
|
||||||
|
if (isAdminUsername) {
|
||||||
try {
|
try {
|
||||||
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -236,6 +239,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
} catch {
|
} catch {
|
||||||
// If admin endpoint fails or is offline, check fallback
|
// If admin endpoint fails or is offline, check fallback
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Direct local check for Admin credentials fallback
|
// Direct local check for Admin credentials fallback
|
||||||
if (
|
if (
|
||||||
@@ -295,9 +299,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
role: 'ROLE_STUDENT',
|
role: 'ROLE_STUDENT',
|
||||||
redirectTo: '/dashboard',
|
redirectTo: '/timetable',
|
||||||
message: 'Student login successful',
|
message: 'Student login successful',
|
||||||
};
|
};
|
||||||
|
} else if (!imsResult.success && imsResult.message) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: imsResult.message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback
|
// Fallback
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ import { getBackendUrl } from '@/lib/utils';
|
|||||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
|
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
|
||||||
|
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library', 'cat exam', 'semester', 'labs', 'exams'];
|
const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library', 'cat exam', 'semester', 'labs', 'exams'];
|
||||||
|
|
||||||
const AVATARS: Record<string, string> = {
|
const AVATARS: Record<string, string> = {
|
||||||
|
|||||||
@@ -45,11 +45,13 @@ function normalizeDepartmentName(input?: string | null): string | null {
|
|||||||
|
|
||||||
export default function Faculty() {
|
export default function Faculty() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [searchFaculty, setSearchFaculty] = useState('');
|
const [searchFaculty, setSearchFaculty] = useState(() => searchParams.get('search') || '');
|
||||||
const [sortBy, setSortBy] = useState('Name A-Z');
|
const [sortBy, setSortBy] = useState('Name A-Z');
|
||||||
|
|
||||||
// Initial department detection (from URL query string or logged-in IMS student session)
|
// Initial department detection (from URL query string or logged-in IMS student session)
|
||||||
const initialDepartment = useMemo(() => {
|
const initialDepartment = useMemo(() => {
|
||||||
|
if (searchParams.has('search')) return 'All Departments';
|
||||||
|
|
||||||
const queryDept = searchParams.get('dept');
|
const queryDept = searchParams.get('dept');
|
||||||
const matchedQuery = normalizeDepartmentName(queryDept);
|
const matchedQuery = normalizeDepartmentName(queryDept);
|
||||||
if (matchedQuery) return matchedQuery;
|
if (matchedQuery) return matchedQuery;
|
||||||
@@ -65,10 +67,16 @@ export default function Faculty() {
|
|||||||
|
|
||||||
const [selectedDept, setSelectedDept] = useState<string>(initialDepartment);
|
const [selectedDept, setSelectedDept] = useState<string>(initialDepartment);
|
||||||
|
|
||||||
// Update selectedDept if URL parameter changes
|
// Update selectedDept and searchFaculty if URL parameter changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const querySearch = searchParams.get('search');
|
||||||
|
if (querySearch !== null) {
|
||||||
|
setSearchFaculty(querySearch);
|
||||||
|
setSelectedDept('All Departments');
|
||||||
|
} else {
|
||||||
setSelectedDept(initialDepartment);
|
setSelectedDept(initialDepartment);
|
||||||
}, [initialDepartment]);
|
}
|
||||||
|
}, [searchParams, initialDepartment]);
|
||||||
|
|
||||||
const studentSession = useMemo(() => getStoredImsSession(), []);
|
const studentSession = useMemo(() => getStoredImsSession(), []);
|
||||||
const isStudentDeptAutoFiltered = selectedDept !== 'All Departments' && (
|
const isStudentDeptAutoFiltered = selectedDept !== 'All Departments' && (
|
||||||
@@ -286,7 +294,7 @@ export default function Faculty() {
|
|||||||
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
|
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
|
||||||
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
|
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
|
||||||
>
|
>
|
||||||
{DEPARTMENTS.filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => {
|
{Array.from(new Set(['All Departments', ...DEPARTMENTS, ...FACULTY_DATA.map(f => f.department)])).filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => {
|
||||||
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
|
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
|
||||||
return <option key={dept} value={dept}>{dept} ({count})</option>;
|
return <option key={dept} value={dept}>{dept} ({count})</option>;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useState, useEffect } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Trophy, Search, Plus, RefreshCw, ExternalLink, Code2,
|
Trophy, Search, Plus, RefreshCw, ExternalLink, Code2,
|
||||||
Sparkles, Flame, Award, CheckCircle2, Shield, User, Filter, X, Clock
|
Sparkles, Flame, Award, CheckCircle2, Shield, User, Filter, X, Clock, Lock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { getBackendUrl } from '@/lib/utils';
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
interface LeetcodeProfile {
|
interface LeetcodeProfile {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -25,6 +26,7 @@ const DEPARTMENTS = ['All', 'CSE', 'IT', 'AI&DS', 'ECE', 'EEE', 'MECH', 'CIVIL',
|
|||||||
const YEARS = ['All', '1st Year', '2nd Year', '3rd Year', '4th Year'];
|
const YEARS = ['All', '1st Year', '2nd Year', '3rd Year', '4th Year'];
|
||||||
|
|
||||||
export default function LeetcodeLeaderboard() {
|
export default function LeetcodeLeaderboard() {
|
||||||
|
const { isAuthenticated, openAuthModal } = useAuth();
|
||||||
const [profiles, setProfiles] = useState<LeetcodeProfile[]>([]);
|
const [profiles, setProfiles] = useState<LeetcodeProfile[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -42,8 +44,36 @@ export default function LeetcodeLeaderboard() {
|
|||||||
const [year, setYear] = useState('1st Year');
|
const [year, setYear] = useState('1st Year');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
fetchLeaderboard();
|
fetchLeaderboard();
|
||||||
}, []);
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen pt-24 pb-16 bg-[#FAF9FC] text-[#1A0B2E] px-4 flex flex-col items-center justify-center text-center">
|
||||||
|
<div className="w-full max-w-md bg-white border border-[#E9E5EE] rounded-3xl p-8 shadow-xl space-y-5">
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-orange-100 text-[#FF6B00] flex items-center justify-center mx-auto border border-orange-200 shadow-md">
|
||||||
|
<Lock className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-extrabold text-[#1A0B2E]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
Student Sign In Required
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-[#4A3E5E] mt-1.5 leading-relaxed">
|
||||||
|
RIT LeetCode Arena & Campus Rankings are accessible exclusively to verified RIT students. Please sign in with your student credentials to view the rankings and register your handle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={openAuthModal}
|
||||||
|
className="w-full py-3.5 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 cursor-pointer"
|
||||||
|
>
|
||||||
|
Sign In to LeetCode Arena
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const fetchLeaderboard = async () => {
|
const fetchLeaderboard = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -670,8 +670,24 @@ export default function Toolkit() {
|
|||||||
const [calculationType, setCalculationType] = useState<'gpa' | 'internal' | null>(null);
|
const [calculationType, setCalculationType] = useState<'gpa' | 'internal' | null>(null);
|
||||||
const [gpaStep, setGpaStep] = useState<1 | 2>(1);
|
const [gpaStep, setGpaStep] = useState<1 | 2>(1);
|
||||||
const [calculatedScore, setCalculatedScore] = useState<string | null>(null);
|
const [calculatedScore, setCalculatedScore] = useState<string | null>(null);
|
||||||
|
const resultCardRef = useRef<HTMLDivElement>(null);
|
||||||
const [imsData, setImsData] = useState<any>(null);
|
const [imsData, setImsData] = useState<any>(null);
|
||||||
const [isImsAutoFilled, setIsImsAutoFilled] = useState(false);
|
const [isImsAutoFilled, setIsImsAutoFilled] = useState(false);
|
||||||
|
const [cat1Input, setCat1Input] = useState('');
|
||||||
|
const [cat2Input, setCat2Input] = useState('');
|
||||||
|
const [cat3Input, setCat3Input] = useState('');
|
||||||
|
const [assignmentInput, setAssignmentInput] = useState('');
|
||||||
|
const [internalError, setInternalError] = useState<string | null>(null);
|
||||||
|
const [showWeightages, setShowWeightages] = useState<Record<string, boolean>>({
|
||||||
|
cat1: false,
|
||||||
|
cat2: false,
|
||||||
|
cat3: false,
|
||||||
|
assignment: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleWeightage = (key: string) => {
|
||||||
|
setShowWeightages((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const session = getStoredImsSession();
|
const session = getStoredImsSession();
|
||||||
@@ -699,6 +715,7 @@ export default function Toolkit() {
|
|||||||
grade: string;
|
grade: string;
|
||||||
cat1: string;
|
cat1: string;
|
||||||
cat2: string;
|
cat2: string;
|
||||||
|
cat3: string;
|
||||||
assignment: string;
|
assignment: string;
|
||||||
attendance: string;
|
attendance: string;
|
||||||
isElective?: boolean;
|
isElective?: boolean;
|
||||||
@@ -706,13 +723,7 @@ export default function Toolkit() {
|
|||||||
electiveCode?: string;
|
electiveCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [subjects, setSubjects] = useState<SubjectItem[]>([
|
const [subjects, setSubjects] = useState<SubjectItem[]>([]);
|
||||||
{ name: 'Subject 1', credits: 4, grade: 'O', cat1: '45', cat2: '48', assignment: '10', attendance: '95' },
|
|
||||||
{ name: 'Subject 2', credits: 3, grade: 'A+', cat1: '42', cat2: '44', assignment: '9', attendance: '92' },
|
|
||||||
{ name: 'Subject 3', credits: 3, grade: 'A', cat1: '38', cat2: '40', assignment: '8', attendance: '88' },
|
|
||||||
{ name: 'Subject 4', credits: 4, grade: 'O', cat1: '46', cat2: '47', assignment: '10', attendance: '96' },
|
|
||||||
{ name: 'Subject 5', credits: 2, grade: 'A+', cat1: '44', cat2: '45', assignment: '9', attendance: '94' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleProceedToStep2 = (type: 'gpa' | 'internal') => {
|
const handleProceedToStep2 = (type: 'gpa' | 'internal') => {
|
||||||
if (!gpaDepartment || !gpaSemester) return;
|
if (!gpaDepartment || !gpaSemester) return;
|
||||||
@@ -766,62 +777,47 @@ export default function Toolkit() {
|
|||||||
foundAnyImsMatch = true;
|
foundAnyImsMatch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gradeVal = matchedGrade?.grade || 'O';
|
const gradeVal = matchedGrade?.grade || '';
|
||||||
const cat1Val = matchedCat?.cat1 ? String(matchedCat.cat1) : '45';
|
const cat1Val = matchedCat?.cat1 ? String(matchedCat.cat1) : '';
|
||||||
const cat2Val = matchedCat?.cat2 ? String(matchedCat.cat2) : '45';
|
const cat2Val = matchedCat?.cat2 ? String(matchedCat.cat2) : '';
|
||||||
const assgnVal = matchedCat?.assignment ? String(matchedCat.assignment) : '9';
|
const cat3Val = matchedCat?.cat3 ? String(matchedCat.cat3) : '';
|
||||||
|
const assgnVal = matchedCat?.assignment ? String(matchedCat.assignment) : '';
|
||||||
if (isElective) {
|
|
||||||
const slot = s.electiveSlot || electiveCounter++;
|
|
||||||
const defaultElectiveObj = PROFESSIONAL_ELECTIVES_LIST[(slot - 1) % PROFESSIONAL_ELECTIVES_LIST.length] || PROFESSIONAL_ELECTIVES_LIST[0];
|
|
||||||
return {
|
|
||||||
name: `${defaultElectiveObj.code} - ${defaultElectiveObj.name}`,
|
|
||||||
credits: s.credits,
|
|
||||||
grade: gradeVal,
|
|
||||||
cat1: cat1Val,
|
|
||||||
cat2: cat2Val,
|
|
||||||
assignment: assgnVal,
|
|
||||||
attendance: '95',
|
|
||||||
isElective: true,
|
|
||||||
electiveSlot: slot,
|
|
||||||
electiveCode: defaultElectiveObj.code,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
name: s.name,
|
name: s.name,
|
||||||
credits: s.credits,
|
credits: s.credits,
|
||||||
grade: gradeVal,
|
grade: gradeVal,
|
||||||
cat1: cat1Val,
|
cat1: cat1Val,
|
||||||
cat2: cat2Val,
|
cat2: cat2Val,
|
||||||
|
cat3: cat3Val,
|
||||||
assignment: assgnVal,
|
assignment: assgnVal,
|
||||||
attendance: '95',
|
attendance: '95',
|
||||||
isElective: false,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setSubjects(loaded);
|
setSubjects(loaded);
|
||||||
|
|
||||||
if (type === 'gpa') {
|
if (type === 'gpa') {
|
||||||
setIsImsAutoFilled(foundAnyImsMatch || imsGrades.length > 0);
|
setIsImsAutoFilled(foundAnyImsMatch && imsGrades.length > 0);
|
||||||
|
if (foundAnyImsMatch && imsGrades.length > 0) {
|
||||||
let totalCredits = 0;
|
let totalCredits = 0;
|
||||||
let totalPoints = 0;
|
let totalPoints = 0;
|
||||||
const GRADE_POINTS: Record<string, number> = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'RA': 0 };
|
const GRADE_POINTS: Record<string, number> = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'UA': 0, 'RA': 0 };
|
||||||
loaded.forEach((sub: any) => {
|
loaded.forEach((sub: any) => {
|
||||||
const pts = GRADE_POINTS[sub.grade] ?? 10;
|
if (sub.grade) {
|
||||||
|
const pts = GRADE_POINTS[sub.grade] ?? 0;
|
||||||
totalCredits += sub.credits;
|
totalCredits += sub.credits;
|
||||||
totalPoints += pts * sub.credits;
|
totalPoints += pts * sub.credits;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (totalCredits > 0) {
|
if (totalCredits > 0) {
|
||||||
setCalculatedScore((totalPoints / totalCredits).toFixed(2));
|
setCalculatedScore((totalPoints / totalCredits).toFixed(2));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setCalculatedScore(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setSubjects([
|
setSubjects([]);
|
||||||
{ name: 'Subject 1', credits: 4, grade: 'O', cat1: '45', cat2: '48', assignment: '10', attendance: '95' },
|
setCalculatedScore(null);
|
||||||
{ name: 'Subject 2', credits: 3, grade: 'A+', cat1: '42', cat2: '44', assignment: '9', attendance: '92' },
|
|
||||||
{ name: 'Subject 3', credits: 3, grade: 'A', cat1: '38', cat2: '40', assignment: '8', attendance: '88' },
|
|
||||||
{ name: 'Subject 4', credits: 4, grade: 'O', cat1: '46', cat2: '47', assignment: '10', attendance: '96' },
|
|
||||||
{ name: 'Subject 5', credits: 2, grade: 'A+', cat1: '44', cat2: '45', assignment: '9', attendance: '94' },
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setGpaStep(2);
|
setGpaStep(2);
|
||||||
@@ -946,21 +942,105 @@ export default function Toolkit() {
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
{selectedToolkit === 'gpa' ? (
|
{selectedToolkit === 'gpa' ? (
|
||||||
<motion.div initial={{ opacity: 0, y: 15 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }} className="max-w-3xl mx-auto">
|
<motion.div initial={{ opacity: 0, y: 15 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }} className="max-w-3xl mx-auto">
|
||||||
{gpaStep === 1 ? (
|
{calculationType === null ? (
|
||||||
/* ── STEP 1: Department and Semester Selection ── */
|
/* ── INITIAL MODE SELECTION: Calculate Internal Mark vs Calculate GPA ── */
|
||||||
|
<div className="bg-white p-8 md:p-10 rounded-3xl border border-[#E5E7EB] shadow-xl relative overflow-hidden space-y-6">
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-[#FF6B00] via-[#8B5CF6] to-[#7C3AED]" />
|
||||||
|
|
||||||
|
<div className="text-center max-w-xl mx-auto space-y-2">
|
||||||
|
<h3 className="text-2xl md:text-3xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
GPA & Internal Calculator
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
|
Select what you would like to calculate to begin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
|
||||||
|
{/* Option 1: Internal Mark Calculator */}
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setCalculationType('internal');
|
||||||
|
setGpaStep(2);
|
||||||
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
|
}}
|
||||||
|
className="p-6 md:p-8 rounded-3xl bg-gradient-to-br from-[#F5F3FF] via-[#FAF5FF] to-white border-2 border-[#DDD6FE] hover:border-[#7C3AED] hover:shadow-lg transition-all cursor-pointer group flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="w-14 h-14 rounded-2xl bg-[#7C3AED] text-white flex items-center justify-center font-bold shadow-md group-hover:scale-110 transition-transform">
|
||||||
|
<BarChart3 className="w-7 h-7" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xl font-extrabold text-[#1E293B] group-hover:text-[#7C3AED] transition-colors mb-1.5" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
Calculate Internal Mark
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-[#64748B] leading-relaxed">
|
||||||
|
Calculate your converted internal mark out of 40 directly from CAT I, CAT II, CAT III, and Assignment marks. Direct calculation with no department or subject inputs needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex items-center gap-2 text-xs font-extrabold text-[#7C3AED]">
|
||||||
|
<span>Open Internal Calculator</span>
|
||||||
|
<ChevronRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Option 2: GPA Calculator */}
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setCalculationType('gpa');
|
||||||
|
setGpaStep(1);
|
||||||
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
|
}}
|
||||||
|
className="p-6 md:p-8 rounded-3xl bg-gradient-to-br from-[#FFF7ED] via-[#FFFBEB] to-white border-2 border-[#FED7AA] hover:border-[#FF6B00] hover:shadow-lg transition-all cursor-pointer group flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="w-14 h-14 rounded-2xl bg-[#FF6B00] text-white flex items-center justify-center font-bold shadow-md group-hover:scale-110 transition-transform">
|
||||||
|
<Calculator className="w-7 h-7" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xl font-extrabold text-[#1E293B] group-hover:text-[#FF6B00] transition-colors mb-1.5" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
|
Calculate GPA
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-[#64748B] leading-relaxed">
|
||||||
|
Select your department and semester to load curriculum subjects, enter your grades, and compute your Semester Grade Point Average (SGPA / 10.00).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex items-center gap-2 text-xs font-extrabold text-[#FF6B00]">
|
||||||
|
<span>Select Department & Semester</span>
|
||||||
|
<ChevronRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : calculationType === 'gpa' && gpaStep === 1 ? (
|
||||||
|
/* ── STEP 1: Department and Semester Selection for GPA ── */
|
||||||
<div className="bg-white p-8 md:p-10 rounded-3xl border border-[#E5E7EB] shadow-xl relative overflow-hidden">
|
<div className="bg-white p-8 md:p-10 rounded-3xl border border-[#E5E7EB] shadow-xl relative overflow-hidden">
|
||||||
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-[#FF6B00] via-[#EF4444] to-[#F97316]" />
|
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-[#FF6B00] via-[#EF4444] to-[#F97316]" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCalculationType(null);
|
||||||
|
setGpaStep(1);
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-1.5 text-xs font-bold text-[#FF6B00] hover:text-[#EA580C] transition-colors mb-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-3.5 h-3.5" /> Back to Calculation Options
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#FEF2F2] text-[#EF4444] flex items-center justify-center font-bold shadow-xs">
|
<div className="w-14 h-14 rounded-2xl bg-[#FEF2F2] text-[#EF4444] flex items-center justify-center font-bold shadow-xs">
|
||||||
<Calculator className="w-7 h-7" />
|
<Calculator className="w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
<h3 className="text-2xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
GPA & Internal Calculator
|
GPA Calculator
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<p className="text-sm text-[#64748B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Step 1 of 2: Select your department and semester to begin.
|
Select your department and semester to load curriculum subjects.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1002,26 +1082,11 @@ export default function Toolkit() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Two Action Buttons to proceed to Step 2 */}
|
<div className="pt-2">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleProceedToStep2('internal')}
|
|
||||||
disabled={!gpaDepartment || !gpaSemester}
|
|
||||||
className={`py-3.5 px-5 rounded-2xl text-sm font-extrabold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md ${
|
|
||||||
gpaDepartment && gpaSemester
|
|
||||||
? 'bg-gradient-to-r from-[#8B5CF6] to-[#7C3AED] hover:from-[#7C3AED] hover:to-[#6D28D9] text-white shadow-purple-500/20 hover:scale-[1.01]'
|
|
||||||
: 'bg-[#E2E8F0] text-[#94A3B8] cursor-not-allowed shadow-none'
|
|
||||||
}`}
|
|
||||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
|
||||||
>
|
|
||||||
<BarChart3 className="w-4 h-4" />
|
|
||||||
Calculate Internal
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleProceedToStep2('gpa')}
|
onClick={() => handleProceedToStep2('gpa')}
|
||||||
disabled={!gpaDepartment || !gpaSemester}
|
disabled={!gpaDepartment || !gpaSemester}
|
||||||
className={`py-3.5 px-5 rounded-2xl text-sm font-extrabold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md ${
|
className={`w-full py-4 px-5 rounded-2xl text-sm font-extrabold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md ${
|
||||||
gpaDepartment && gpaSemester
|
gpaDepartment && gpaSemester
|
||||||
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:from-[#EA580C] hover:to-[#DD6B20] text-white shadow-orange-500/20 hover:scale-[1.01]'
|
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:from-[#EA580C] hover:to-[#DD6B20] text-white shadow-orange-500/20 hover:scale-[1.01]'
|
||||||
: 'bg-[#E2E8F0] text-[#94A3B8] cursor-not-allowed shadow-none'
|
: 'bg-[#E2E8F0] text-[#94A3B8] cursor-not-allowed shadow-none'
|
||||||
@@ -1029,29 +1094,38 @@ export default function Toolkit() {
|
|||||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
<Calculator className="w-4 h-4" />
|
<Calculator className="w-4 h-4" />
|
||||||
Calculate GPA
|
Proceed to GPA Calculation
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* ── STEP 2: Subject Input and Marks Section ── */
|
/* ── STEP 2: Calculation Form (GPA or Direct Internal) ── */
|
||||||
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.3 }} className="space-y-6">
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.3 }} className="space-y-6">
|
||||||
<div className="bg-white p-6 md:p-8 rounded-3xl border border-[#E5E7EB] shadow-xl relative overflow-hidden">
|
<div className="bg-white p-6 md:p-8 rounded-3xl border border-[#E5E7EB] shadow-xl relative overflow-hidden">
|
||||||
<div className="flex items-center justify-between gap-4 mb-6 pb-4 border-b border-[#F1F5F9]">
|
<div className="flex items-center justify-between gap-4 mb-6 pb-4 border-b border-[#F1F5F9]">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setGpaStep(1)}
|
onClick={() => {
|
||||||
|
if (calculationType === 'internal') {
|
||||||
|
setCalculationType(null);
|
||||||
|
setGpaStep(1);
|
||||||
|
} else {
|
||||||
|
setGpaStep(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="inline-flex items-center gap-1.5 text-xs font-bold text-[#FF6B00] hover:text-[#EA580C] transition-colors mb-2 cursor-pointer"
|
className="inline-flex items-center gap-1.5 text-xs font-bold text-[#FF6B00] hover:text-[#EA580C] transition-colors mb-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-3.5 h-3.5" /> Back to Selection
|
<ArrowLeft className="w-3.5 h-3.5" /> {calculationType === 'internal' ? 'Back to Options' : 'Back to Department & Semester'}
|
||||||
</button>
|
</button>
|
||||||
<h3 className="text-xl md:text-2xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
<h3 className="text-xl md:text-2xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{calculationType === 'gpa' ? 'GPA Calculation' : 'Internal Marks Calculation'}
|
{calculationType === 'gpa' ? 'GPA Calculation' : 'Internal Mark Calculator'}
|
||||||
</h3>
|
</h3>
|
||||||
|
{calculationType === 'gpa' && (
|
||||||
<p className="text-xs font-semibold text-[#64748B] mt-0.5">
|
<p className="text-xs font-semibold text-[#64748B] mt-0.5">
|
||||||
{gpaDepartment} — <span className="text-[#FF6B00]">{gpaSemester}</span>
|
{gpaDepartment} — <span className="text-[#FF6B00]">{gpaSemester}</span>
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#FFF7ED] text-[#FF6B00] flex items-center justify-center font-bold shrink-0">
|
<div className="w-12 h-12 rounded-2xl bg-[#FFF7ED] text-[#FF6B00] flex items-center justify-center font-bold shrink-0">
|
||||||
{calculationType === 'gpa' ? <Calculator className="w-6 h-6" /> : <BarChart3 className="w-6 h-6" />}
|
{calculationType === 'gpa' ? <Calculator className="w-6 h-6" /> : <BarChart3 className="w-6 h-6" />}
|
||||||
@@ -1086,43 +1160,12 @@ export default function Toolkit() {
|
|||||||
<span>Grade Achieved</span>
|
<span>Grade Achieved</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(() => {
|
{subjects.filter(s => s.credits > 0).map((sub, idx) => (
|
||||||
const selectedElectiveCodes = new Set(subjects.filter((s) => s.isElective && s.electiveCode).map((s) => s.electiveCode));
|
|
||||||
|
|
||||||
return subjects.filter(s => s.credits > 0).map((sub, idx) => (
|
|
||||||
<div key={idx} className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 p-4 rounded-2xl bg-[#F8FAFC] border border-[#E2E8F0] hover:border-[#CBD5E1] transition-all">
|
<div key={idx} className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 p-4 rounded-2xl bg-[#F8FAFC] border border-[#E2E8F0] hover:border-[#CBD5E1] transition-all">
|
||||||
<div className="flex-1 min-w-0 pr-2">
|
<div className="flex-1 min-w-0 pr-2">
|
||||||
{sub.isElective ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-[11px] font-bold text-[#FF6B00] uppercase tracking-wider">
|
|
||||||
Professional Elective {sub.electiveSlot}
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={sub.electiveCode}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newCode = e.target.value;
|
|
||||||
const found = PROFESSIONAL_ELECTIVES_LIST.find((el) => el.code === newCode);
|
|
||||||
const updated = [...subjects];
|
|
||||||
updated[idx].electiveCode = newCode;
|
|
||||||
updated[idx].name = found ? `${found.code} - ${found.name}` : newCode;
|
|
||||||
setSubjects(updated);
|
|
||||||
setCalculatedScore(null);
|
|
||||||
}}
|
|
||||||
className="w-full px-3 py-2 rounded-xl bg-white border border-[#CBD5E1] focus:border-[#FF6B00] text-xs font-bold text-[#1E293B] outline-none cursor-pointer"
|
|
||||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
|
||||||
>
|
|
||||||
{PROFESSIONAL_ELECTIVES_LIST.filter((opt) => opt.code === sub.electiveCode || !selectedElectiveCodes.has(opt.code)).map((opt) => (
|
|
||||||
<option key={opt.code} value={opt.code}>
|
|
||||||
{opt.code} - {opt.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
<p className="text-sm font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{sub.name}
|
{sub.name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
<span className="inline-block mt-1 px-2.5 py-0.5 rounded-md bg-[#FFF7ED] text-[#FF6B00] text-[11px] font-bold border border-[#FFEDD5]">
|
<span className="inline-block mt-1 px-2.5 py-0.5 rounded-md bg-[#FFF7ED] text-[#FF6B00] text-[11px] font-bold border border-[#FFEDD5]">
|
||||||
{sub.credits} {sub.credits === 1 ? 'Credit' : 'Credits'}
|
{sub.credits} {sub.credits === 1 ? 'Credit' : 'Credits'}
|
||||||
</span>
|
</span>
|
||||||
@@ -1135,10 +1178,12 @@ export default function Toolkit() {
|
|||||||
updated[idx].grade = e.target.value;
|
updated[idx].grade = e.target.value;
|
||||||
setSubjects(updated);
|
setSubjects(updated);
|
||||||
setCalculatedScore(null);
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
}}
|
}}
|
||||||
className="w-full px-3.5 py-2.5 rounded-xl bg-white border border-[#CBD5E1] focus:border-[#FF6B00] focus:ring-2 focus:ring-[#FF6B00]/20 text-xs font-bold text-[#1E293B] outline-none cursor-pointer shadow-2xs"
|
className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${!sub.grade ? 'border-amber-300 bg-amber-50/20 text-[#64748B]' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#FF6B00]'} text-xs font-bold focus:ring-2 focus:ring-[#FF6B00]/20 outline-none cursor-pointer shadow-2xs`}
|
||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
>
|
>
|
||||||
|
<option value="" disabled>Select Grade</option>
|
||||||
<option value="O">O</option>
|
<option value="O">O</option>
|
||||||
<option value="A+">A+</option>
|
<option value="A+">A+</option>
|
||||||
<option value="A">A</option>
|
<option value="A">A</option>
|
||||||
@@ -1149,127 +1194,239 @@ export default function Toolkit() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
))}
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* ── Internal Marks Layout ── */
|
/* ── Direct Internal Marks Layout (Assigned 40-Mark Weightages & Per-Field Weightage Calculations) ── */
|
||||||
<div className="space-y-4 mb-6">
|
(() => {
|
||||||
<div className="flex items-center justify-between text-xs font-bold uppercase tracking-wider text-[#94A3B8]">
|
const n1 = cat1Input === '' ? NaN : Number(cat1Input);
|
||||||
<span>Subject Details</span>
|
const isCat1Err = cat1Input !== '' && (isNaN(n1) || n1 < 0 || n1 > 50);
|
||||||
<span>{subjects.length} Subjects</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{subjects.map((sub, idx) => (
|
const n2 = cat2Input === '' ? NaN : Number(cat2Input);
|
||||||
<div key={idx} className="p-4 rounded-2xl bg-[#F8FAFC] border border-[#E2E8F0] space-y-3">
|
const isCat2Err = cat2Input !== '' && (isNaN(n2) || n2 < 0 || n2 > 25);
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
|
const n3 = cat3Input === '' ? NaN : Number(cat3Input);
|
||||||
|
const isCat3Err = cat3Input !== '' && (isNaN(n3) || n3 < 0 || n3 > 50);
|
||||||
|
|
||||||
|
const nAss = assignmentInput === '' ? NaN : Number(assignmentInput);
|
||||||
|
const isAssErr = assignmentInput !== '' && (isNaN(nAss) || nAss < 0 || nAss > 50);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 mb-6">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{/* CAT I */}
|
||||||
|
<div className={`p-4 rounded-2xl ${isCat1Err ? 'bg-red-50/40 border border-red-300' : 'bg-[#F8FAFC] border border-[#E2E8F0] focus-within:border-[#8B5CF6]'} transition-all space-y-2.5`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold text-[#1E293B]">CAT I Mark</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${isCat1Err ? 'bg-red-100 text-red-700 border-red-200' : 'bg-[#F5F3FF] text-[#7C3AED] border-[#DDD6FE]'}`}>
|
||||||
|
Max 50
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-extrabold px-2 py-0.5 rounded-md border bg-purple-100/70 text-[#5B21B6] border-purple-200">
|
||||||
|
Assigned: 11.43 / 40
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="number"
|
||||||
value={sub.name}
|
value={cat1Input}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updated = [...subjects];
|
setCat1Input(e.target.value);
|
||||||
updated[idx].name = e.target.value;
|
|
||||||
setSubjects(updated);
|
|
||||||
setCalculatedScore(null);
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
|
setShowWeightages((prev) => ({ ...prev, cat1: false }));
|
||||||
}}
|
}}
|
||||||
placeholder={`Subject ${idx + 1} Name`}
|
placeholder="Enter CAT 1 mark (0 - 50)"
|
||||||
className="flex-1 px-3 py-2 rounded-xl bg-white border border-[#CBD5E1] text-xs font-bold text-[#1E293B] focus:border-[#FF6B00] outline-none"
|
className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isCat1Err ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`}
|
||||||
/>
|
/>
|
||||||
{subjects.length > 1 && (
|
{isCat1Err ? (
|
||||||
|
<p className="text-[11px] font-bold text-red-500 mt-1 flex items-center gap-1">
|
||||||
|
<AlertTriangle className="w-3.5 h-3.5" /> Enter a valid mark between 0 and 50
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between pt-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSubjects(subjects.filter((_, i) => i !== idx));
|
if (cat1Input !== '' && !isCat1Err) {
|
||||||
setCalculatedScore(null);
|
setShowWeightages((prev) => ({ ...prev, cat1: true }));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 rounded-xl text-[#EF4444] hover:bg-[#FEF2F2] transition-colors cursor-pointer"
|
className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
|
||||||
title="Remove subject"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Calculator className="w-3.5 h-3.5" /> Calculate Weightage
|
||||||
</button>
|
</button>
|
||||||
|
{showWeightages['cat1'] && !isNaN(n1) && n1 >= 0 && n1 <= 50 && (
|
||||||
|
<span className="text-[11px] font-extrabold text-[#6D28D9] bg-purple-50 px-2.5 py-1 rounded-lg border border-purple-200 animate-in fade-in duration-200">
|
||||||
|
Obtained: {((n1 / 50) * 11.43).toFixed(2)} / 11.43
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
{/* CAT II */}
|
||||||
<div>
|
<div className={`p-4 rounded-2xl ${isCat2Err ? 'bg-red-50/40 border border-red-300' : 'bg-[#F8FAFC] border border-[#E2E8F0] focus-within:border-[#8B5CF6]'} transition-all space-y-2.5`}>
|
||||||
<label className="block text-[10px] font-semibold text-[#64748B] mb-1">CAT 1 (/50)</label>
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold text-[#1E293B]">CAT II Mark</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${isCat2Err ? 'bg-red-100 text-red-700 border-red-200' : 'bg-[#F5F3FF] text-[#7C3AED] border-[#DDD6FE]'}`}>
|
||||||
|
Max 25
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-extrabold px-2 py-0.5 rounded-md border bg-purple-100/70 text-[#5B21B6] border-purple-200">
|
||||||
|
Assigned: 5.71 / 40
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
value={cat2Input}
|
||||||
max="50"
|
|
||||||
value={sub.cat1}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updated = [...subjects];
|
setCat2Input(e.target.value);
|
||||||
updated[idx].cat1 = e.target.value;
|
|
||||||
setSubjects(updated);
|
|
||||||
setCalculatedScore(null);
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
|
setShowWeightages((prev) => ({ ...prev, cat2: false }));
|
||||||
}}
|
}}
|
||||||
className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none"
|
placeholder="Enter CAT 2 mark (0 - 25)"
|
||||||
|
className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isCat2Err ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`}
|
||||||
/>
|
/>
|
||||||
</div>
|
{isCat2Err ? (
|
||||||
<div>
|
<p className="text-[11px] font-bold text-red-500 mt-1 flex items-center gap-1">
|
||||||
<label className="block text-[10px] font-semibold text-[#64748B] mb-1">CAT 2 (/50)</label>
|
<AlertTriangle className="w-3.5 h-3.5" /> Enter a valid mark between 0 and 25
|
||||||
<input
|
</p>
|
||||||
type="number"
|
) : (
|
||||||
min="0"
|
<div className="flex items-center justify-between pt-1">
|
||||||
max="50"
|
|
||||||
value={sub.cat2}
|
|
||||||
onChange={(e) => {
|
|
||||||
const updated = [...subjects];
|
|
||||||
updated[idx].cat2 = e.target.value;
|
|
||||||
setSubjects(updated);
|
|
||||||
setCalculatedScore(null);
|
|
||||||
}}
|
|
||||||
className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] font-semibold text-[#64748B] mb-1">Assignment (/10)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
value={sub.assignment}
|
|
||||||
onChange={(e) => {
|
|
||||||
const updated = [...subjects];
|
|
||||||
updated[idx].assignment = e.target.value;
|
|
||||||
setSubjects(updated);
|
|
||||||
setCalculatedScore(null);
|
|
||||||
}}
|
|
||||||
className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] font-semibold text-[#64748B] mb-1">Attendance %</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={sub.attendance}
|
|
||||||
onChange={(e) => {
|
|
||||||
const updated = [...subjects];
|
|
||||||
updated[idx].attendance = e.target.value;
|
|
||||||
setSubjects(updated);
|
|
||||||
setCalculatedScore(null);
|
|
||||||
}}
|
|
||||||
className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSubjects([
|
if (cat2Input !== '' && !isCat2Err) {
|
||||||
...subjects,
|
setShowWeightages((prev) => ({ ...prev, cat2: true }));
|
||||||
{ name: `Subject ${subjects.length + 1}`, credits: 3, grade: 'A', cat1: '40', cat2: '40', assignment: '8', attendance: '90' }
|
}
|
||||||
]);
|
|
||||||
setCalculatedScore(null);
|
|
||||||
}}
|
}}
|
||||||
className="w-full py-2.5 rounded-2xl border-2 border-dashed border-[#CBD5E1] hover:border-[#FF6B00] text-xs font-bold text-[#64748B] hover:text-[#FF6B00] transition-colors flex items-center justify-center gap-1.5 cursor-pointer"
|
className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" /> Add Subject
|
<Calculator className="w-3.5 h-3.5" /> Calculate Weightage
|
||||||
</button>
|
</button>
|
||||||
|
{showWeightages['cat2'] && !isNaN(n2) && n2 >= 0 && n2 <= 25 && (
|
||||||
|
<span className="text-[11px] font-extrabold text-[#6D28D9] bg-purple-50 px-2.5 py-1 rounded-lg border border-purple-200 animate-in fade-in duration-200">
|
||||||
|
Obtained: {((n2 / 25) * 5.71).toFixed(2)} / 5.71
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CAT III */}
|
||||||
|
<div className={`p-4 rounded-2xl ${isCat3Err ? 'bg-red-50/40 border border-red-300' : 'bg-[#F8FAFC] border border-[#E2E8F0] focus-within:border-[#8B5CF6]'} transition-all space-y-2.5`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold text-[#1E293B]">CAT III Mark</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${isCat3Err ? 'bg-red-100 text-red-700 border-red-200' : 'bg-[#F5F3FF] text-[#7C3AED] border-[#DDD6FE]'}`}>
|
||||||
|
Max 50
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-extrabold px-2 py-0.5 rounded-md border bg-purple-100/70 text-[#5B21B6] border-purple-200">
|
||||||
|
Assigned: 11.43 / 40
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={cat3Input}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCat3Input(e.target.value);
|
||||||
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
|
setShowWeightages((prev) => ({ ...prev, cat3: false }));
|
||||||
|
}}
|
||||||
|
placeholder="Enter CAT 3 mark (0 - 50)"
|
||||||
|
className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isCat3Err ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`}
|
||||||
|
/>
|
||||||
|
{isCat3Err ? (
|
||||||
|
<p className="text-[11px] font-bold text-red-500 mt-1 flex items-center gap-1">
|
||||||
|
<AlertTriangle className="w-3.5 h-3.5" /> Enter a valid mark between 0 and 50
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (cat3Input !== '' && !isCat3Err) {
|
||||||
|
setShowWeightages((prev) => ({ ...prev, cat3: true }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Calculator className="w-3.5 h-3.5" /> Calculate Weightage
|
||||||
|
</button>
|
||||||
|
{showWeightages['cat3'] && !isNaN(n3) && n3 >= 0 && n3 <= 50 && (
|
||||||
|
<span className="text-[11px] font-extrabold text-[#6D28D9] bg-purple-50 px-2.5 py-1 rounded-lg border border-purple-200 animate-in fade-in duration-200">
|
||||||
|
Obtained: {((n3 / 50) * 11.43).toFixed(2)} / 11.43
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assignment */}
|
||||||
|
<div className={`p-4 rounded-2xl ${isAssErr ? 'bg-red-50/40 border border-red-300' : 'bg-[#F8FAFC] border border-[#E2E8F0] focus-within:border-[#8B5CF6]'} transition-all space-y-2.5`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold text-[#1E293B]">Assignment Mark</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${isAssErr ? 'bg-red-100 text-red-700 border-red-200' : 'bg-[#F5F3FF] text-[#7C3AED] border-[#DDD6FE]'}`}>
|
||||||
|
Max 50
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-extrabold px-2 py-0.5 rounded-md border bg-purple-100/70 text-[#5B21B6] border-purple-200">
|
||||||
|
Assigned: 11.43 / 40
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={assignmentInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAssignmentInput(e.target.value);
|
||||||
|
setCalculatedScore(null);
|
||||||
|
setInternalError(null);
|
||||||
|
setShowWeightages((prev) => ({ ...prev, assignment: false }));
|
||||||
|
}}
|
||||||
|
placeholder="Enter Assignment mark (0 - 50)"
|
||||||
|
className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isAssErr ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`}
|
||||||
|
/>
|
||||||
|
{isAssErr ? (
|
||||||
|
<p className="text-[11px] font-bold text-red-500 mt-1 flex items-center gap-1">
|
||||||
|
<AlertTriangle className="w-3.5 h-3.5" /> Enter a valid mark between 0 and 50
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (assignmentInput !== '' && !isAssErr) {
|
||||||
|
setShowWeightages((prev) => ({ ...prev, assignment: true }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Calculator className="w-3.5 h-3.5" /> Calculate Weightage
|
||||||
|
</button>
|
||||||
|
{showWeightages['assignment'] && !isNaN(nAss) && nAss >= 0 && nAss <= 50 && (
|
||||||
|
<span className="text-[11px] font-extrabold text-[#6D28D9] bg-purple-50 px-2.5 py-1 rounded-lg border border-purple-200 animate-in fade-in duration-200">
|
||||||
|
Obtained: {((nAss / 50) * 11.43).toFixed(2)} / 11.43
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
)}
|
||||||
|
|
||||||
|
{internalError && (
|
||||||
|
<div className="mb-4 p-3.5 rounded-2xl bg-red-50 border border-red-200 text-xs font-bold text-red-600 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-4 h-4 shrink-0 text-red-500" />
|
||||||
|
<span>{internalError}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1277,26 +1434,52 @@ export default function Toolkit() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (calculationType === 'gpa') {
|
if (calculationType === 'gpa') {
|
||||||
|
const unselected = subjects.filter(s => s.credits > 0 && !s.grade);
|
||||||
|
if (unselected.length > 0) {
|
||||||
|
setInternalError('Please select a grade for all subjects before calculating GPA.');
|
||||||
|
setCalculatedScore(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInternalError(null);
|
||||||
const GRADE_MAP: Record<string, number> = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'UA': 0, 'RA': 0 };
|
const GRADE_MAP: Record<string, number> = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'UA': 0, 'RA': 0 };
|
||||||
let totalPts = 0;
|
let totalPts = 0;
|
||||||
let totalCreds = 0;
|
let totalCreds = 0;
|
||||||
subjects.forEach(s => {
|
subjects.forEach(s => {
|
||||||
|
if (s.credits > 0) {
|
||||||
const pts = GRADE_MAP[s.grade] ?? 0;
|
const pts = GRADE_MAP[s.grade] ?? 0;
|
||||||
const cred = Number(s.credits) || 0;
|
const cred = Number(s.credits) || 0;
|
||||||
totalPts += pts * cred;
|
totalPts += pts * cred;
|
||||||
totalCreds += cred;
|
totalCreds += cred;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setCalculatedScore(totalCreds > 0 ? (totalPts / totalCreds).toFixed(2) : '0.00');
|
setCalculatedScore(totalCreds > 0 ? (totalPts / totalCreds).toFixed(2) : '0.00');
|
||||||
|
setTimeout(() => {
|
||||||
|
resultCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
let total = 0;
|
const c1 = Number(cat1Input);
|
||||||
subjects.forEach(s => {
|
const c2 = Number(cat2Input);
|
||||||
const c1 = (Number(s.cat1) || 0) / 50 * 15;
|
const c3 = Number(cat3Input);
|
||||||
const c2 = (Number(s.cat2) || 0) / 50 * 15;
|
const ass = Number(assignmentInput);
|
||||||
const ass = (Number(s.assignment) || 0) / 10 * 5;
|
|
||||||
const att = (Number(s.attendance) || 0) >= 80 ? 5 : 3;
|
if (
|
||||||
total += (c1 + c2 + ass + att);
|
cat1Input === '' || isNaN(c1) || c1 < 0 || c1 > 50 ||
|
||||||
});
|
cat2Input === '' || isNaN(c2) || c2 < 0 || c2 > 25 ||
|
||||||
setCalculatedScore(subjects.length > 0 ? (total / subjects.length).toFixed(1) : '0.0');
|
cat3Input === '' || isNaN(c3) || c3 < 0 || c3 > 50 ||
|
||||||
|
assignmentInput === '' || isNaN(ass) || ass < 0 || ass > 50
|
||||||
|
) {
|
||||||
|
setInternalError('Please enter a valid mark for all fields within their maximum limits.');
|
||||||
|
setCalculatedScore(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInternalError(null);
|
||||||
|
const totalRaw = c1 + c2 + c3 + ass; // max 175
|
||||||
|
const internal40 = ((totalRaw / 175) * 40).toFixed(2);
|
||||||
|
setCalculatedScore(internal40);
|
||||||
|
setTimeout(() => {
|
||||||
|
resultCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`w-full py-4 rounded-2xl text-sm font-extrabold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md ${
|
className={`w-full py-4 rounded-2xl text-sm font-extrabold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md ${
|
||||||
@@ -1307,38 +1490,97 @@ export default function Toolkit() {
|
|||||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
{calculationType === 'gpa' ? 'Calculate Final GPA' : 'Calculate Total Internal Marks'}
|
{calculationType === 'gpa' ? 'Calculate Final GPA' : 'Calculate Total Internal Mark'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Calculated Result Card */}
|
{/* Calculated Result Card */}
|
||||||
{calculatedScore !== null && (
|
{calculatedScore !== null && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
ref={resultCardRef}
|
||||||
initial={{ opacity: 0, scale: 0.96, y: 10 }}
|
initial={{ opacity: 0, scale: 0.96, y: 10 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
className="mt-6 p-6 rounded-3xl bg-gradient-to-br from-[#FFF7ED] to-[#FFFBEB] border border-[#FED7AA] shadow-sm space-y-3"
|
className="mt-6 p-6 rounded-3xl bg-gradient-to-br from-[#FFF7ED] to-[#FFFBEB] border border-[#FED7AA] shadow-sm space-y-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-[#EA580C]">
|
<span className="text-xs font-bold uppercase tracking-wider text-[#EA580C]">
|
||||||
{calculationType === 'gpa' ? 'Calculated SGPA' : 'Average Internal Score'}
|
{calculationType === 'gpa' ? 'Calculated SGPA' : 'Total Internal Mark'}
|
||||||
</span>
|
</span>
|
||||||
|
{gpaSemester && (
|
||||||
<span className="px-3 py-1 rounded-full bg-[#FF6B00]/10 text-[#FF6B00] text-xs font-bold">
|
<span className="px-3 py-1 rounded-full bg-[#FF6B00]/10 text-[#FF6B00] text-xs font-bold">
|
||||||
{gpaSemester}
|
{gpaSemester}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-2xl bg-white border border-[#FFEDD5] flex items-baseline justify-between">
|
<div className="p-4 rounded-2xl bg-white border border-[#FFEDD5] flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-[#64748B] mb-1">{gpaDepartment}</p>
|
<p className="text-xs font-semibold text-[#64748B] mb-1">
|
||||||
|
{calculationType === 'gpa' ? gpaDepartment : 'Converted Internal Score'}
|
||||||
|
</p>
|
||||||
<span className="text-4xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
<span className="text-4xl font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
|
||||||
{calculatedScore} {calculationType === 'gpa' ? '/ 10.00' : '/ 40.0'}
|
{calculatedScore} {calculationType === 'gpa' ? '/ 10.00' : '/ 40.0'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="px-3 py-1.5 rounded-xl bg-[#ECFDF5] text-[#10B981] text-xs font-bold">
|
<div className="flex flex-col sm:items-end justify-center gap-1">
|
||||||
{calculationType === 'gpa'
|
{calculationType === 'internal' && (() => {
|
||||||
? (Number(calculatedScore) >= 9.0 ? 'First Class with Distinction (O)' : Number(calculatedScore) >= 8.5 ? 'First Class with Distinction' : Number(calculatedScore) >= 7.0 ? 'First Class' : Number(calculatedScore) >= 5.0 ? 'Second Class' : 'UA / Reappear')
|
const c1 = Math.min(50, Math.max(0, Number(cat1Input) || 0));
|
||||||
: (Number(calculatedScore) >= 35 ? 'Excellent' : 'Good')}
|
const c2 = Math.min(25, Math.max(0, Number(cat2Input) || 0));
|
||||||
</span>
|
const c3 = Math.min(50, Math.max(0, Number(cat3Input) || 0));
|
||||||
|
const ass = Math.min(50, Math.max(0, Number(assignmentInput) || 0));
|
||||||
|
const totalRaw = c1 + c2 + c3 + ass;
|
||||||
|
return (
|
||||||
|
<div className="sm:text-right">
|
||||||
|
<p className="text-xs text-[#64748B] font-semibold">Total Raw Marks Obtained</p>
|
||||||
|
<p className="text-xl font-extrabold text-[#7C3AED]">
|
||||||
|
{totalRaw} <span className="text-xs font-bold text-[#94A3B8]">/ 175 Marks</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{calculationType === 'internal' && (() => {
|
||||||
|
const c1 = Math.min(50, Math.max(0, Number(cat1Input) || 0));
|
||||||
|
const c2 = Math.min(25, Math.max(0, Number(cat2Input) || 0));
|
||||||
|
const c3 = Math.min(50, Math.max(0, Number(cat3Input) || 0));
|
||||||
|
const ass = Math.min(50, Math.max(0, Number(assignmentInput) || 0));
|
||||||
|
|
||||||
|
const w1 = ((c1 / 50) * 11.43).toFixed(2);
|
||||||
|
const w2 = ((c2 / 25) * 5.71).toFixed(2);
|
||||||
|
const w3 = ((c3 / 50) * 11.43).toFixed(2);
|
||||||
|
const wAss = ((ass / 50) * 11.43).toFixed(2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 rounded-2xl bg-white border border-purple-100 space-y-3">
|
||||||
|
<p className="text-xs font-bold text-[#5B21B6] uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<Zap className="w-3.5 h-3.5 text-[#7C3AED]" /> Assigned 40-Mark Weightage Breakdown
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5 text-center">
|
||||||
|
<div className="p-2.5 rounded-xl bg-purple-50/50 border border-purple-100">
|
||||||
|
<p className="text-[10px] font-bold text-[#64748B]">CAT I (11.43)</p>
|
||||||
|
<p className="text-xs font-black text-[#6D28D9] mt-0.5">{w1} / 11.43</p>
|
||||||
|
<p className="text-[9px] text-[#94A3B8] font-medium">{c1}/50</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-purple-50/50 border border-purple-100">
|
||||||
|
<p className="text-[10px] font-bold text-[#64748B]">CAT II (5.71)</p>
|
||||||
|
<p className="text-xs font-black text-[#6D28D9] mt-0.5">{w2} / 5.71</p>
|
||||||
|
<p className="text-[9px] text-[#94A3B8] font-medium">{c2}/25</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-purple-50/50 border border-purple-100">
|
||||||
|
<p className="text-[10px] font-bold text-[#64748B]">CAT III (11.43)</p>
|
||||||
|
<p className="text-xs font-black text-[#6D28D9] mt-0.5">{w3} / 11.43</p>
|
||||||
|
<p className="text-[9px] text-[#94A3B8] font-medium">{c3}/50</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-purple-50/50 border border-purple-100">
|
||||||
|
<p className="text-[10px] font-bold text-[#64748B]">Assignment (11.43)</p>
|
||||||
|
<p className="text-xs font-black text-[#6D28D9] mt-0.5">{wAss} / 11.43</p>
|
||||||
|
<p className="text-[9px] text-[#94A3B8] font-medium">{ass}/50</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"community_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
"community_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"telegram_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
"telegram_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"helper_chat_ids": [971749136, 5567776672, 1873240361],
|
"helper_chat_ids" : [ 1873240361, 5567776672, 8518850169, 7238144438 ],
|
||||||
"spring_backend_url" : "http://localhost:8085"
|
"spring_backend_url" : "http://localhost:8085"
|
||||||
}
|
}
|
||||||
@@ -50,8 +50,68 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
chunkSizeWarningLimit: 800,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id: string) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) {
|
||||||
|
return 'vendor-react';
|
||||||
|
}
|
||||||
|
if (id.includes('framer-motion') || id.includes('lucide-react')) {
|
||||||
|
return 'vendor-ui';
|
||||||
|
}
|
||||||
|
if (id.includes('@radix-ui')) {
|
||||||
|
return 'vendor-radix';
|
||||||
|
}
|
||||||
|
if (id.includes('@tanstack')) {
|
||||||
|
return 'vendor-tanstack';
|
||||||
|
}
|
||||||
|
if (id.includes('leaflet') || id.includes('react-leaflet')) {
|
||||||
|
return 'vendor-map';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
|
proxy: {
|
||||||
|
'/ims': {
|
||||||
|
target: 'https://ims.ritchennai.edu.in',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/ims/, ''),
|
||||||
|
secure: false,
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on('proxyRes', (proxyRes) => {
|
||||||
|
// Rewrite Location header to keep redirects through local proxy
|
||||||
|
const location = proxyRes.headers['location'];
|
||||||
|
if (location) {
|
||||||
|
let rewritten = location;
|
||||||
|
if (rewritten.includes('ims.ritchennai.edu.in')) {
|
||||||
|
rewritten = rewritten.replace(/https?:\/\/ims\.ritchennai\.edu\.in/gi, 'http://localhost:5173/ims');
|
||||||
|
} else if (rewritten.startsWith('/')) {
|
||||||
|
rewritten = '/ims' + rewritten;
|
||||||
|
}
|
||||||
|
proxyRes.headers['location'] = rewritten;
|
||||||
|
}
|
||||||
|
// Tweak cookies for localhost
|
||||||
|
const setCookie = proxyRes.headers['set-cookie'];
|
||||||
|
if (setCookie) {
|
||||||
|
proxyRes.headers['set-cookie'] = (Array.isArray(setCookie) ? setCookie : [setCookie]).map((cookie: string) =>
|
||||||
|
cookie
|
||||||
|
.replace(/Secure/gi, '')
|
||||||
|
.replace(/samesite=none/gi, 'SameSite=Lax')
|
||||||
|
.replace(/domain=[^;]+/gi, '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: [
|
ignored: [
|
||||||
'**/backend/**',
|
'**/backend/**',
|
||||||
|
|||||||
Reference in New Issue
Block a user