feat: Anna University CGPA formula, credit fixes, academic overview dashboard, lab/theory badges
This commit is contained in:
@@ -62,8 +62,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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' && (
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,39 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
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