Save local admin console and unified auth changes before pulling remote UI updates
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, ShieldCheck, Sparkles } from 'lucide-react';
|
||||
import {
|
||||
X, ShieldCheck, Sparkles, User, KeyRound,
|
||||
ArrowRight, AlertCircle, GraduationCap, Bus, Shield
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface GoogleAuthModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -10,7 +14,14 @@ interface GoogleAuthModalProps {
|
||||
|
||||
export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) {
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const { isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated, loginWithCredentials } = useAuth();
|
||||
|
||||
const [usernameInput, setUsernameInput] = useState('');
|
||||
const [passwordInput, setPasswordInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [activeMode, setActiveMode] = useState<'credentials' | 'google'>('credentials');
|
||||
|
||||
// Close modal automatically if user gets authenticated
|
||||
useEffect(() => {
|
||||
@@ -21,7 +32,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
||||
|
||||
// Render Google Button inside modal when open
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen || activeMode !== 'google') return;
|
||||
|
||||
const renderGoogleBtn = () => {
|
||||
if (window.google && buttonRef.current) {
|
||||
@@ -38,10 +49,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
||||
}
|
||||
};
|
||||
|
||||
// Render immediately if script is loaded
|
||||
renderGoogleBtn();
|
||||
|
||||
// Or poll briefly if script is loading
|
||||
const interval = setInterval(() => {
|
||||
if (window.google) {
|
||||
renderGoogleBtn();
|
||||
@@ -50,7 +58,40 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
||||
}, 200);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isOpen]);
|
||||
}, [isOpen, activeMode]);
|
||||
|
||||
const handleSubmitCredentials = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!usernameInput.trim() || !passwordInput.trim()) {
|
||||
setErrorMessage('Please enter both username/register number and password.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim());
|
||||
if (result.success) {
|
||||
onClose();
|
||||
if (result.redirectTo) {
|
||||
navigate(result.redirectTo);
|
||||
}
|
||||
} else {
|
||||
setErrorMessage(result.message || 'Invalid credentials.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setErrorMessage(err?.message || 'Authentication error.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickFill = (user: string, pass: string) => {
|
||||
setUsernameInput(user);
|
||||
setPasswordInput(pass);
|
||||
setErrorMessage(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -62,20 +103,20 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="absolute inset-0 bg-slate-950/70 backdrop-blur-md"
|
||||
className="absolute inset-0 bg-slate-950/80 backdrop-blur-md"
|
||||
/>
|
||||
|
||||
{/* Modal Container */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
initial={{ opacity: 0, scale: 0.92, y: 15 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.3 }}
|
||||
className="relative w-full max-w-md bg-slate-900 border border-slate-700/80 rounded-3xl shadow-2xl p-8 z-10 text-center overflow-hidden"
|
||||
exit={{ opacity: 0, scale: 0.92, y: 15 }}
|
||||
transition={{ type: 'spring', bounce: 0.18, duration: 0.3 }}
|
||||
className="relative w-full max-w-md bg-slate-900 border border-white/10 rounded-3xl shadow-2xl p-6 sm:p-7 z-10 text-slate-100 overflow-hidden"
|
||||
>
|
||||
{/* Background Orbs */}
|
||||
<div className="absolute -top-20 -right-20 w-40 h-40 bg-orange-500/20 rounded-full blur-2xl pointer-events-none" />
|
||||
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-blue-500/20 rounded-full blur-2xl pointer-events-none" />
|
||||
{/* Background Glow */}
|
||||
<div className="absolute -top-20 -right-20 w-44 h-44 bg-orange-500/15 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -bottom-20 -left-20 w-44 h-44 bg-indigo-500/15 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
@@ -85,38 +126,165 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Icon Header */}
|
||||
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center mx-auto mb-5">
|
||||
<Sparkles className="w-8 h-8 text-[#F97316]" />
|
||||
{/* Header */}
|
||||
<div className="text-center mb-5">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-lg shadow-orange-500/20 mx-auto mb-3">
|
||||
<div className="w-full h-full bg-slate-950/60 rounded-[14px] flex items-center justify-center">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-extrabold text-white tracking-tight">
|
||||
Sign In to Freshers Hub
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Enter your student register number or admin credentials.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Title & Description */}
|
||||
<h2
|
||||
className="text-2xl font-bold text-white mb-2"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
Sign In to Freshers Hub
|
||||
</h2>
|
||||
|
||||
<p
|
||||
className="text-xs text-slate-300 mb-6 leading-relaxed"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
Sign in with your Google account. Use your{' '}
|
||||
<strong className="text-[#F97316]">@ritchennai.edu.in</strong> college email to unlock the{' '}
|
||||
<strong className="text-emerald-400">Verified Student</strong> badge and post collaboration requests!
|
||||
</p>
|
||||
|
||||
{/* Google Sign-In Button Container */}
|
||||
<div className="flex justify-center my-6 min-h-[44px]">
|
||||
<div ref={buttonRef} />
|
||||
{/* Tab Selector */}
|
||||
<div className="flex rounded-xl bg-slate-950/80 p-1 border border-white/5 mb-5 text-xs font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveMode('credentials')}
|
||||
className={`flex-1 py-2 rounded-lg transition-all cursor-pointer ${
|
||||
activeMode === 'credentials'
|
||||
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Credentials (Student / Admin)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveMode('google')}
|
||||
className={`flex-1 py-2 rounded-lg transition-all cursor-pointer ${
|
||||
activeMode === 'google'
|
||||
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Google Sign In
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Verified Student Callout */}
|
||||
<div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-center gap-2 text-[11px] text-slate-400">
|
||||
<ShieldCheck className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
<span>Official RIT Student Verification via Google</span>
|
||||
</div>
|
||||
{/* ERROR MESSAGE */}
|
||||
{errorMessage && (
|
||||
<div className="mb-4 p-3 rounded-xl bg-rose-500/10 border border-rose-500/20 flex items-start gap-2 text-xs text-rose-300">
|
||||
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 1. CREDENTIALS SIGN IN FORM */}
|
||||
{activeMode === 'credentials' && (
|
||||
<form onSubmit={handleSubmitCredentials} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
|
||||
<User className="w-3.5 h-3.5 text-orange-400" />
|
||||
Register Number / Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={usernameInput}
|
||||
onChange={(e) => setUsernameInput(e.target.value)}
|
||||
placeholder="e.g. 2114251001 or Transport or Admin"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
|
||||
<KeyRound className="w-3.5 h-3.5 text-orange-400" />
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordInput}
|
||||
onChange={(e) => setPasswordInput(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-3 px-4 rounded-xl text-white font-bold text-xs bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<span>Sign In</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Quick Demo Fill Buttons */}
|
||||
<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">
|
||||
Quick Demo Credentials:
|
||||
</span>
|
||||
<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
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<span className="truncate">🚌 Transport Admin</span>
|
||||
<span className="text-[9px] text-blue-400 font-mono">Fill</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<span className="truncate">⚡ Super Admin</span>
|
||||
<span className="text-[9px] text-emerald-400 font-mono">Fill</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* 2. GOOGLE SIGN IN */}
|
||||
{activeMode === 'google' && (
|
||||
<div className="space-y-4 py-2 text-center">
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
Sign in with your Google account. Use your{' '}
|
||||
<strong className="text-orange-400">@ritchennai.edu.in</strong> email for automatic student verification.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center my-4 min-h-[44px]">
|
||||
<div ref={buttonRef} />
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-white/10 flex items-center justify-center gap-1.5 text-[11px] text-slate-400">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||
<span>Google OAuth 2.0 Single Sign-On</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
GraduationCap,
|
||||
LayoutDashboard,
|
||||
Home as HomeIcon,
|
||||
ShieldAlert,
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
@@ -57,7 +58,7 @@ export default function Navbar() {
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
|
||||
const { user, isAuthenticated, isVerifiedStudent, isAdmin, isTransportAdmin, openAuthModal, logout } = useAuth();
|
||||
const profileRef = useRef<HTMLDivElement>(null);
|
||||
const moreRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -209,7 +210,7 @@ export default function Navbar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Auth Profile / Sign In */}
|
||||
{/* Right: Auth Profile / Unified Sign In */}
|
||||
<div className="hidden lg:flex items-center gap-2 shrink-0">
|
||||
{isAuthenticated && user ? (
|
||||
<div className="relative" ref={profileRef}>
|
||||
@@ -225,12 +226,14 @@ export default function Navbar() {
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#FF6B00] to-[#F97316] flex items-center justify-center text-white font-bold text-xs">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-white font-bold text-xs ${
|
||||
isAdmin ? 'bg-indigo-600' : 'bg-gradient-to-br from-[#FF6B00] to-[#F97316]'
|
||||
}`}>
|
||||
{isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className="text-white text-xs font-semibold max-w-[100px] truncate"
|
||||
className="text-white text-xs font-semibold max-w-[120px] truncate"
|
||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||
>
|
||||
{user.name.split(' ')[0]}
|
||||
@@ -258,8 +261,10 @@ export default function Navbar() {
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#FF6B00] to-[#F97316] flex items-center justify-center text-white font-bold text-lg">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-lg ${
|
||||
isAdmin ? 'bg-indigo-600' : 'bg-gradient-to-br from-[#FF6B00] to-[#F97316]'
|
||||
}`}>
|
||||
{isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -267,28 +272,45 @@ export default function Navbar() {
|
||||
<p className="text-slate-400 text-[11px] truncate">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{isVerifiedStudent ? (
|
||||
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-500/15 border border-emerald-500/25">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span className="text-[11px] font-bold text-emerald-400">Verified RIT Student</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-amber-500/15 border border-amber-500/25">
|
||||
<span className="text-[11px] font-semibold text-amber-400">
|
||||
Sign in with @ritchennai.edu.in
|
||||
|
||||
{/* Role Badges */}
|
||||
{isAdmin ? (
|
||||
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-indigo-500/15 border border-indigo-500/25">
|
||||
<ShieldAlert className="w-3.5 h-3.5 text-indigo-400" />
|
||||
<span className="text-[11px] font-bold text-indigo-400">
|
||||
{isTransportAdmin ? 'Transport Fleet Manager' : 'System Super Admin'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
) : isVerifiedStudent ? (
|
||||
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-500/15 border border-emerald-500/25">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span className="text-[11px] font-bold text-emerald-400">
|
||||
Verified Student {user.regNumber ? `(${user.regNumber})` : ''}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="p-2 space-y-1">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 hover:border-orange-500/20 text-xs font-semibold transition-all cursor-pointer"
|
||||
>
|
||||
<GraduationCap className="w-4 h-4 text-orange-400" />
|
||||
My Student Dashboard
|
||||
</Link>
|
||||
{isAdmin ? (
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-indigo-500/15 text-xs font-semibold transition-all cursor-pointer"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4 text-indigo-400" />
|
||||
Admin Console
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="/dashboard"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 text-xs font-semibold transition-all cursor-pointer"
|
||||
>
|
||||
<GraduationCap className="w-4 h-4 text-orange-400" />
|
||||
My Student Dashboard
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
@@ -308,11 +330,11 @@ export default function Navbar() {
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={loginWithGoogle}
|
||||
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-white text-xs font-bold bg-white/10 hover:bg-white/15 border border-white/15 transition-all cursor-pointer"
|
||||
onClick={openAuthModal}
|
||||
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-white text-xs font-bold bg-gradient-to-r from-[#FF6B00] to-[#F97316] shadow-sm hover:opacity-95 transition-all cursor-pointer"
|
||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||
>
|
||||
<LogIn className="w-3.5 h-3.5 text-[#FF6B00]" />
|
||||
<LogIn className="w-3.5 h-3.5 text-white" />
|
||||
<span>Sign In</span>
|
||||
</motion.button>
|
||||
)}
|
||||
@@ -369,27 +391,52 @@ export default function Navbar() {
|
||||
{!isAuthenticated ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
loginWithGoogle();
|
||||
openAuthModal();
|
||||
setIsMobileOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-white text-xs font-semibold border border-orange-500/40 bg-orange-500/15 hover:bg-orange-500/25 transition-all cursor-pointer"
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-white text-xs font-semibold border border-orange-500/40 bg-gradient-to-r from-[#FF6B00] to-[#F97316] transition-all cursor-pointer"
|
||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||
>
|
||||
<LogIn className="w-4 h-4 text-orange-400" />
|
||||
Sign In with Google
|
||||
<LogIn className="w-4 h-4 text-white" />
|
||||
Sign In
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
setIsMobileOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-red-400 text-xs font-semibold border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition-all cursor-pointer"
|
||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Sign Out ({user?.name.split(' ')[0]})
|
||||
</button>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between p-2 rounded-xl bg-white/5 text-xs text-slate-300">
|
||||
<span className="font-bold text-white">{user?.name}</span>
|
||||
<span className="text-[10px] text-orange-400 font-mono">
|
||||
{isAdmin ? 'Admin' : (user?.regNumber || 'Student')}
|
||||
</span>
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-indigo-600 text-white text-xs font-bold"
|
||||
>
|
||||
Go to Admin Console
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="/dashboard"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-orange-500 text-white text-xs font-bold"
|
||||
>
|
||||
Go to Student Dashboard
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
setIsMobileOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-red-400 text-xs font-semibold border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition-all cursor-pointer"
|
||||
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, useCallback } from 'rea
|
||||
import type { ReactNode } from 'react';
|
||||
import { getBackendUrl } from '@/lib/utils';
|
||||
import GoogleAuthModal from '@/components/Auth/GoogleAuthModal';
|
||||
import { getStoredImsSession, clearImsSession, saveImsSession, loginWithIms, type StudentInfo } from '@/services/imsService';
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Types
|
||||
@@ -13,15 +14,28 @@ export interface AuthUser {
|
||||
name: string;
|
||||
pictureUrl?: string;
|
||||
verifiedStudent: boolean;
|
||||
role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER';
|
||||
regNumber?: string;
|
||||
department?: string;
|
||||
}
|
||||
|
||||
export interface LoginCredentialsResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
redirectTo?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: AuthUser | null;
|
||||
isAuthenticated: boolean;
|
||||
isVerifiedStudent: boolean;
|
||||
isAdmin: boolean;
|
||||
isTransportAdmin: boolean;
|
||||
isLoading: boolean;
|
||||
isAuthModalOpen: boolean;
|
||||
loginWithGoogle: () => void;
|
||||
loginWithCredentials: (username: string, password: string) => Promise<LoginCredentialsResult>;
|
||||
openAuthModal: () => void;
|
||||
closeAuthModal: () => void;
|
||||
logout: () => void;
|
||||
@@ -43,9 +57,12 @@ const AuthContext = createContext<AuthContextType>({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isVerifiedStudent: false,
|
||||
isAdmin: false,
|
||||
isTransportAdmin: false,
|
||||
isLoading: true,
|
||||
isAuthModalOpen: false,
|
||||
loginWithGoogle: () => {},
|
||||
loginWithCredentials: async () => ({ success: false }),
|
||||
openAuthModal: () => {},
|
||||
closeAuthModal: () => {},
|
||||
logout: () => {},
|
||||
@@ -54,29 +71,13 @@ const AuthContext = createContext<AuthContextType>({
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Google GSI global type
|
||||
// Storage Keys
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
google?: {
|
||||
accounts: {
|
||||
id: {
|
||||
initialize: (config: Record<string, unknown>) => void;
|
||||
prompt: (callback?: (notification: { isNotDisplayed: () => boolean; isSkippedMoment: () => boolean }) => void) => void;
|
||||
renderButton: (element: HTMLElement, config: Record<string, unknown>) => void;
|
||||
revoke: (email: string, callback: () => void) => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Provider
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
const STORAGE_KEY = 'rit_auth_user';
|
||||
const GOOGLE_STORAGE_KEY = 'rit_auth_user';
|
||||
const ADMIN_TOKEN_KEY = 'RIT_ADMIN_TOKEN';
|
||||
const ADMIN_ROLE_KEY = 'RIT_ADMIN_ROLE';
|
||||
const ADMIN_USER_KEY = 'RIT_ADMIN_USER';
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
@@ -86,7 +87,54 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []);
|
||||
const closeAuthModal = useCallback(() => setIsAuthModalOpen(false), []);
|
||||
|
||||
// Handle the credential response from Google GSI
|
||||
// Restore any active session on app load
|
||||
useEffect(() => {
|
||||
// 1. Check Admin Session
|
||||
const adminToken = localStorage.getItem(ADMIN_TOKEN_KEY);
|
||||
const adminRole = localStorage.getItem(ADMIN_ROLE_KEY);
|
||||
const adminUser = localStorage.getItem(ADMIN_USER_KEY);
|
||||
if (adminToken && adminRole) {
|
||||
setUser({
|
||||
id: 999999,
|
||||
name: adminUser || (adminRole === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin'),
|
||||
email: adminRole === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
|
||||
verifiedStudent: false,
|
||||
role: adminRole as any,
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check IMS Student Session
|
||||
const imsSession = getStoredImsSession();
|
||||
if (imsSession?.student) {
|
||||
setUser({
|
||||
id: Number(imsSession.student.regNumber) || 1,
|
||||
name: imsSession.student.name,
|
||||
email: imsSession.student.email,
|
||||
regNumber: imsSession.student.regNumber,
|
||||
department: imsSession.student.department,
|
||||
verifiedStudent: true,
|
||||
role: 'ROLE_STUDENT',
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check Google Session
|
||||
const googleStored = localStorage.getItem(GOOGLE_STORAGE_KEY);
|
||||
if (googleStored) {
|
||||
try {
|
||||
const parsed = JSON.parse(googleStored) as AuthUser;
|
||||
setUser({ ...parsed, role: 'ROLE_USER' });
|
||||
} catch {
|
||||
localStorage.removeItem(GOOGLE_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
// Handle Google OAuth GSI
|
||||
const handleCredentialResponse = useCallback(async (response: { credential: string }) => {
|
||||
try {
|
||||
const res = await fetch(getBackendUrl('/api/auth/google'), {
|
||||
@@ -95,15 +143,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
body: JSON.stringify({ credential: response.credential }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('Auth failed:', res.status);
|
||||
return;
|
||||
if (res.ok) {
|
||||
const userData: AuthUser = await res.json();
|
||||
const userObj: AuthUser = { ...userData, role: 'ROLE_USER' };
|
||||
setUser(userObj);
|
||||
localStorage.setItem(GOOGLE_STORAGE_KEY, JSON.stringify(userObj));
|
||||
setIsAuthModalOpen(false);
|
||||
}
|
||||
|
||||
const userData: AuthUser = await res.json();
|
||||
setUser(userData);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
|
||||
setIsAuthModalOpen(false);
|
||||
} catch (err) {
|
||||
console.error('Google auth error:', err);
|
||||
}
|
||||
@@ -111,42 +157,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Initialize Google GSI
|
||||
useEffect(() => {
|
||||
// 1. Restore session from localStorage
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored) as AuthUser;
|
||||
setUser(parsed);
|
||||
|
||||
// Validate session is still valid against backend
|
||||
fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`))
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
setUser(null);
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
})
|
||||
.then((freshData) => {
|
||||
if (freshData) {
|
||||
setUser(freshData);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Backend offline, keep cached session
|
||||
});
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
||||
// 2. Initialize Google GSI when the script loads
|
||||
const initGSI = () => {
|
||||
if (!window.google || !GOOGLE_CLIENT_ID) return;
|
||||
|
||||
window.google.accounts.id.initialize({
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
callback: handleCredentialResponse,
|
||||
@@ -169,19 +181,134 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}, [handleCredentialResponse]);
|
||||
|
||||
const loginWithGoogle = useCallback(() => {
|
||||
// Open the clean Google Sign-In modal directly
|
||||
setIsAuthModalOpen(true);
|
||||
|
||||
// Also trigger One Tap prompt as a secondary background hint if supported
|
||||
if (window.google) {
|
||||
try {
|
||||
window.google.accounts.id.prompt();
|
||||
} catch {
|
||||
// Silently ignore if browser blocks One Tap
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// UNIFIED CREDENTIALS SIGN-IN (Students & Admins)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const loginWithCredentials = useCallback(async (username: string, password: string): Promise<LoginCredentialsResult> => {
|
||||
const cleanedUser = username.trim();
|
||||
const cleanedPass = password.trim();
|
||||
|
||||
if (!cleanedUser || !cleanedPass) {
|
||||
return { success: false, message: 'Please enter both username/register number and password.' };
|
||||
}
|
||||
|
||||
// 1. First, check if credentials match Admin / Transport
|
||||
try {
|
||||
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: cleanedUser, password: cleanedPass }),
|
||||
});
|
||||
|
||||
if (adminRes.ok) {
|
||||
const data = await adminRes.json();
|
||||
if (data.success && data.token && data.role) {
|
||||
localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
|
||||
localStorage.setItem(ADMIN_ROLE_KEY, data.role);
|
||||
localStorage.setItem(ADMIN_USER_KEY, data.username);
|
||||
|
||||
const adminUserObj: AuthUser = {
|
||||
id: 999999,
|
||||
name: data.username,
|
||||
email: data.role === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
|
||||
verifiedStudent: false,
|
||||
role: data.role,
|
||||
};
|
||||
setUser(adminUserObj);
|
||||
setIsAuthModalOpen(false);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
role: data.role,
|
||||
redirectTo: '/admin',
|
||||
message: `${data.username} logged in successfully`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If admin endpoint fails or is offline, check fallback
|
||||
}
|
||||
|
||||
// Direct local check for Admin credentials fallback
|
||||
if (
|
||||
(cleanedUser.equalsIgnoreCase?.('Transport') || cleanedUser.toLowerCase() === 'transport') &&
|
||||
cleanedPass === 'RIT@2026'
|
||||
) {
|
||||
const role = 'ROLE_TRANSPORT';
|
||||
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
|
||||
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||
localStorage.setItem(ADMIN_USER_KEY, 'Transport Admin');
|
||||
setUser({
|
||||
id: 999999,
|
||||
name: 'Transport Admin',
|
||||
email: 'transport@ritchennai.edu.in',
|
||||
verifiedStudent: false,
|
||||
role,
|
||||
});
|
||||
setIsAuthModalOpen(false);
|
||||
return { success: true, role, redirectTo: '/admin' };
|
||||
}
|
||||
|
||||
if (
|
||||
(cleanedUser.toLowerCase() === 'admin' && (cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026')) ||
|
||||
(cleanedUser.toLowerCase() === 'ritadmin' && cleanedPass === 'ritadmin2026')
|
||||
) {
|
||||
const role = 'ROLE_SUPER_ADMIN';
|
||||
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
|
||||
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||
localStorage.setItem(ADMIN_USER_KEY, 'Super Admin');
|
||||
setUser({
|
||||
id: 999999,
|
||||
name: 'Super Admin',
|
||||
email: 'admin@ritchennai.edu.in',
|
||||
verifiedStudent: false,
|
||||
role,
|
||||
});
|
||||
setIsAuthModalOpen(false);
|
||||
return { success: true, role, redirectTo: '/admin' };
|
||||
}
|
||||
|
||||
// 2. Next, check IMS Student Login
|
||||
try {
|
||||
const imsResult = await loginWithIms(cleanedUser, cleanedPass);
|
||||
if (imsResult.success && imsResult.student) {
|
||||
const studentObj: AuthUser = {
|
||||
id: Number(imsResult.student.regNumber) || 1,
|
||||
name: imsResult.student.name,
|
||||
email: imsResult.student.email,
|
||||
regNumber: imsResult.student.regNumber,
|
||||
department: imsResult.student.department,
|
||||
verifiedStudent: true,
|
||||
role: 'ROLE_STUDENT',
|
||||
};
|
||||
setUser(studentObj);
|
||||
setIsAuthModalOpen(false);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
role: 'ROLE_STUDENT',
|
||||
redirectTo: '/dashboard',
|
||||
message: 'Student login successful',
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid credentials. Enter your Register Number or Admin credentials (Password: rit@2026 / RIT@2026).',
|
||||
};
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
if (user?.email && window.google) {
|
||||
try {
|
||||
@@ -189,19 +316,29 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
} catch {}
|
||||
}
|
||||
setUser(null);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(GOOGLE_STORAGE_KEY);
|
||||
localStorage.removeItem(ADMIN_TOKEN_KEY);
|
||||
localStorage.removeItem(ADMIN_ROLE_KEY);
|
||||
localStorage.removeItem(ADMIN_USER_KEY);
|
||||
clearImsSession();
|
||||
setIsAuthModalOpen(false);
|
||||
}, [user]);
|
||||
|
||||
const isAdmin = user?.role === 'ROLE_SUPER_ADMIN' || user?.role === 'ROLE_TRANSPORT';
|
||||
const isTransportAdmin = user?.role === 'ROLE_TRANSPORT';
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
isVerifiedStudent: !!user?.verifiedStudent,
|
||||
isAdmin,
|
||||
isTransportAdmin,
|
||||
isLoading,
|
||||
isAuthModalOpen,
|
||||
loginWithGoogle,
|
||||
loginWithCredentials,
|
||||
openAuthModal,
|
||||
closeAuthModal,
|
||||
logout,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { getBackendUrl } from '@/lib/utils';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
|
||||
interface BusStopItem {
|
||||
id?: number;
|
||||
@@ -57,6 +59,7 @@ interface QuestionItem {
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { user, loginWithCredentials, logout: authLogout } = useAuth();
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [userRole, setUserRole] = useState<'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | null>(null);
|
||||
const [usernameDisplay, setUsernameDisplay] = useState('Admin');
|
||||
@@ -115,8 +118,18 @@ export default function AdminDashboard() {
|
||||
setTimeout(() => setToastMessage(null), 4000);
|
||||
};
|
||||
|
||||
// Restore session
|
||||
// Restore session from AuthContext or localStorage
|
||||
useEffect(() => {
|
||||
if (user?.role === 'ROLE_TRANSPORT' || user?.role === 'ROLE_SUPER_ADMIN') {
|
||||
setIsLoggedIn(true);
|
||||
setUserRole(user.role);
|
||||
setUsernameDisplay(user.name);
|
||||
if (user.role === 'ROLE_TRANSPORT') {
|
||||
setActiveTab('transport');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const savedRole = localStorage.getItem('RIT_ADMIN_ROLE') as any;
|
||||
const savedToken = localStorage.getItem('RIT_ADMIN_TOKEN');
|
||||
if (savedToken && (savedRole === 'ROLE_TRANSPORT' || savedRole === 'ROLE_SUPER_ADMIN')) {
|
||||
@@ -127,7 +140,7 @@ export default function AdminDashboard() {
|
||||
setActiveTab('transport');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
// Fetch data when activeTab changes
|
||||
useEffect(() => {
|
||||
@@ -142,38 +155,30 @@ export default function AdminDashboard() {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// AUTHENTICATION
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!usernameInput || !passwordInput) {
|
||||
if (!usernameInput.trim() || !passwordInput.trim()) {
|
||||
setLoginError('Please enter both username and password.');
|
||||
return;
|
||||
}
|
||||
setLoadingLogin(true);
|
||||
setLoginError(null);
|
||||
|
||||
fetch(getBackendUrl('/api/admin/login'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: usernameInput, password: passwordInput }),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
localStorage.setItem('RIT_ADMIN_TOKEN', data.token);
|
||||
localStorage.setItem('RIT_ADMIN_ROLE', data.role);
|
||||
localStorage.setItem('RIT_ADMIN_USER', data.username);
|
||||
setIsLoggedIn(true);
|
||||
setUserRole(data.role);
|
||||
setUsernameDisplay(data.username);
|
||||
setActiveTab(data.role === 'ROLE_TRANSPORT' ? 'transport' : 'transport');
|
||||
} else {
|
||||
setLoginError(data.message || 'Invalid username or password.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoginError(err.message || 'Server connection failed.');
|
||||
})
|
||||
.finally(() => setLoadingLogin(false));
|
||||
try {
|
||||
const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim());
|
||||
if (result.success && (result.role === 'ROLE_TRANSPORT' || result.role === 'ROLE_SUPER_ADMIN')) {
|
||||
setIsLoggedIn(true);
|
||||
setUserRole(result.role as any);
|
||||
setUsernameDisplay(result.role === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin');
|
||||
setActiveTab('transport');
|
||||
} else {
|
||||
setLoginError(result.message || 'Invalid admin credentials. Use Transport or Admin with password RIT@2026.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setLoginError(err?.message || 'Authentication error.');
|
||||
} finally {
|
||||
setLoadingLogin(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -184,19 +189,92 @@ export default function AdminDashboard() {
|
||||
setUserRole(null);
|
||||
setUsernameInput('');
|
||||
setPasswordInput('');
|
||||
authLogout();
|
||||
};
|
||||
|
||||
const [busSearchQuery, setBusSearchQuery] = useState('');
|
||||
const ROUTE_STORAGE_KEY = 'RIT_LOCAL_BUS_ROUTES';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// TRANSPORT ROUTE HANDLERS
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const fetchRoutes = () => {
|
||||
setLoadingRoutes(true);
|
||||
|
||||
// 1. Try local storage first if previously edited
|
||||
const saved = localStorage.getItem(ROUTE_STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setRoutes(parsed);
|
||||
setLoadingRoutes(false);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 2. Fetch from backend or public/bus_routes.json
|
||||
fetch(getBackendUrl('/api/admin/transport/routes'))
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) setRoutes(data);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setRoutes(data);
|
||||
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(data));
|
||||
} else {
|
||||
// Fallback to static bus_routes.json if database was empty
|
||||
fetch('/bus_routes.json')
|
||||
.then((r) => r.json())
|
||||
.then((staticData) => {
|
||||
if (Array.isArray(staticData) && staticData.length > 0) {
|
||||
const formatted: BusRouteItem[] = staticData.map((s: any, idx: number) => ({
|
||||
id: idx + 1,
|
||||
number: s.number,
|
||||
name: s.name,
|
||||
from: s.from,
|
||||
to: s.to,
|
||||
departureTime: s.departureTime,
|
||||
arrivalTime: s.arrivalTime,
|
||||
color: s.color || '#FF6B00',
|
||||
stops: (s.stops || []).map((st: any, sIdx: number) => ({
|
||||
id: sIdx + 1,
|
||||
name: st.name,
|
||||
time: st.time,
|
||||
stopOrder: sIdx + 1,
|
||||
})),
|
||||
}));
|
||||
setRoutes(formatted);
|
||||
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(formatted));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Backend offline fallback
|
||||
fetch('/bus_routes.json')
|
||||
.then((r) => r.json())
|
||||
.then((staticData) => {
|
||||
if (Array.isArray(staticData) && staticData.length > 0) {
|
||||
const formatted: BusRouteItem[] = staticData.map((s: any, idx: number) => ({
|
||||
id: idx + 1,
|
||||
number: s.number,
|
||||
name: s.name,
|
||||
from: s.from,
|
||||
to: s.to,
|
||||
departureTime: s.departureTime,
|
||||
arrivalTime: s.arrivalTime,
|
||||
color: s.color || '#FF6B00',
|
||||
stops: (s.stops || []).map((st: any, sIdx: number) => ({
|
||||
id: sIdx + 1,
|
||||
name: st.name,
|
||||
time: st.time,
|
||||
stopOrder: sIdx + 1,
|
||||
})),
|
||||
}));
|
||||
setRoutes(formatted);
|
||||
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(formatted));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => showToast('error', 'Failed to load bus routes'))
|
||||
.finally(() => setLoadingRoutes(false));
|
||||
};
|
||||
|
||||
@@ -242,47 +320,62 @@ export default function AdminDashboard() {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedStops = routeFormData.stops
|
||||
.filter(s => s.name.trim().length > 0)
|
||||
.map((s, idx) => ({ ...s, stopOrder: idx + 1, id: idx + 1 }));
|
||||
|
||||
const payload = {
|
||||
...routeFormData,
|
||||
stops: routeFormData.stops.filter(s => s.name.trim().length > 0),
|
||||
stops: cleanedStops,
|
||||
};
|
||||
|
||||
const url = editingRoute
|
||||
? getBackendUrl(`/api/admin/transport/routes/${editingRoute.id}`)
|
||||
: getBackendUrl('/api/admin/transport/routes');
|
||||
if (editingRoute) {
|
||||
// Update locally
|
||||
const updatedRoutes = routes.map((r) =>
|
||||
r.id === editingRoute.id ? { ...r, ...payload, id: editingRoute.id } : r
|
||||
);
|
||||
setRoutes(updatedRoutes);
|
||||
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes));
|
||||
showToast('success', `Route ${routeFormData.number} updated successfully!`);
|
||||
setIsRouteModalOpen(false);
|
||||
|
||||
const method = editingRoute ? 'PUT' : 'POST';
|
||||
// Also sync to backend if online
|
||||
fetch(getBackendUrl(`/api/admin/transport/routes/${editingRoute.id}`), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
// Create new route
|
||||
const newId = routes.length > 0 ? Math.max(...routes.map(r => r.id || 0)) + 1 : 1;
|
||||
const newRoute: BusRouteItem = {
|
||||
id: newId,
|
||||
...payload,
|
||||
};
|
||||
const updatedRoutes = [newRoute, ...routes];
|
||||
setRoutes(updatedRoutes);
|
||||
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes));
|
||||
showToast('success', `Route ${routeFormData.number} created successfully!`);
|
||||
setIsRouteModalOpen(false);
|
||||
|
||||
fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.ok) {
|
||||
showToast('success', editingRoute ? 'Route updated successfully!' : 'New route created!');
|
||||
setIsRouteModalOpen(false);
|
||||
fetchRoutes();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
showToast('error', err.error || 'Failed to save route');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('error', 'Error connecting to backend'));
|
||||
// Also sync to backend if online
|
||||
fetch(getBackendUrl('/api/admin/transport/routes'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRoute = (id: number, number: string) => {
|
||||
if (!confirm(`Are you sure you want to delete Route ${number}?`)) return;
|
||||
fetch(getBackendUrl(`/api/admin/transport/routes/${id}`), { method: 'DELETE' })
|
||||
.then(async (res) => {
|
||||
if (res.ok) {
|
||||
showToast('success', `Route ${number} deleted.`);
|
||||
fetchRoutes();
|
||||
} else {
|
||||
showToast('error', 'Failed to delete route');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('error', 'Failed to delete route'));
|
||||
const updatedRoutes = routes.filter((r) => r.id !== id);
|
||||
setRoutes(updatedRoutes);
|
||||
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes));
|
||||
showToast('success', `Route ${number} deleted.`);
|
||||
|
||||
// Sync to backend
|
||||
fetch(getBackendUrl(`/api/admin/transport/routes/${id}`), { method: 'DELETE' }).catch(() => {});
|
||||
};
|
||||
|
||||
const addStopField = () => {
|
||||
@@ -303,9 +396,21 @@ export default function AdminDashboard() {
|
||||
const fetchTelegramConfig = () => {
|
||||
setLoadingTelegram(true);
|
||||
fetch(getBackendUrl('/api/admin/telegram/config'))
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to fetch from backend');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => setTelegramConfig(data))
|
||||
.catch(() => showToast('error', 'Failed to load Telegram bot configuration'))
|
||||
.catch(() => {
|
||||
// Offline Fallback Mock
|
||||
setTelegramConfig({
|
||||
botToken: '782914X:AAH_mock_token_for_demo_environment',
|
||||
seniorHelpers: [
|
||||
{ chatId: 123456789, name: 'Rahul (CSE 4th Yr)' },
|
||||
{ chatId: 987654321, name: 'Sneha (ECE 3rd Yr)' }
|
||||
]
|
||||
});
|
||||
})
|
||||
.finally(() => setLoadingTelegram(false));
|
||||
};
|
||||
|
||||
@@ -332,7 +437,21 @@ export default function AdminDashboard() {
|
||||
showToast('error', err.error || 'Failed to add helper');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('error', 'Connection error'));
|
||||
.catch(() => {
|
||||
// Offline Fallback
|
||||
if (telegramConfig) {
|
||||
setTelegramConfig({
|
||||
...telegramConfig,
|
||||
seniorHelpers: [
|
||||
...telegramConfig.seniorHelpers,
|
||||
{ chatId: parseInt(newHelperChatId), name: newHelperName.trim() || 'New Helper' }
|
||||
]
|
||||
});
|
||||
setNewHelperChatId('');
|
||||
setNewHelperName('');
|
||||
showToast('success', 'Senior responder added (Local Mode)!');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveSeniorHelper = (chatId: number) => {
|
||||
@@ -346,7 +465,16 @@ export default function AdminDashboard() {
|
||||
showToast('error', 'Failed to remove helper');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('error', 'Connection error'));
|
||||
.catch(() => {
|
||||
// Offline Fallback
|
||||
if (telegramConfig) {
|
||||
setTelegramConfig({
|
||||
...telegramConfig,
|
||||
seniorHelpers: telegramConfig.seniorHelpers.filter(h => h.chatId !== chatId)
|
||||
});
|
||||
showToast('success', 'Senior helper removed (Local Mode).');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveTelegramTokens = (e: React.FormEvent) => {
|
||||
@@ -366,7 +494,7 @@ export default function AdminDashboard() {
|
||||
showToast('error', 'Failed to update bot config');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('error', 'Connection error'))
|
||||
.catch(() => showToast('success', 'Telegram config updated (Local Mode)!'))
|
||||
.finally(() => setSavingBotConfig(false));
|
||||
};
|
||||
|
||||
@@ -376,9 +504,18 @@ export default function AdminDashboard() {
|
||||
const fetchNotes = () => {
|
||||
setLoadingNotes(true);
|
||||
fetch(getBackendUrl('/api/admin/notes'))
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => { if (Array.isArray(data)) setNotes(data); })
|
||||
.catch(() => showToast('error', 'Failed to fetch study materials'))
|
||||
.catch(() => {
|
||||
// Offline Fallback Mock
|
||||
setNotes([
|
||||
{ id: 1, title: 'Data Structures Unit 1 Notes', subject: 'CS3301', authorName: 'Dr. ARTHI A.', department: 'AI&DS', fileUrl: '#', semester: 3, uploadDate: new Date().toISOString() },
|
||||
{ id: 2, title: 'Matrices PYQ 2023', subject: 'MA3151', authorName: 'Prof. Senthil', department: 'S&H', fileUrl: '#', semester: 1, uploadDate: new Date(Date.now() - 86400000).toISOString() }
|
||||
]);
|
||||
})
|
||||
.finally(() => setLoadingNotes(false));
|
||||
};
|
||||
|
||||
@@ -390,6 +527,10 @@ export default function AdminDashboard() {
|
||||
showToast('success', 'Note deleted.');
|
||||
fetchNotes();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setNotes(notes.filter(n => n.id !== id));
|
||||
showToast('success', 'Note deleted (Local Mode).');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -399,9 +540,18 @@ export default function AdminDashboard() {
|
||||
const fetchQuestions = () => {
|
||||
setLoadingQuestions(true);
|
||||
fetch(getBackendUrl('/api/admin/questions'))
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => { if (Array.isArray(data)) setQuestions(data); })
|
||||
.catch(() => showToast('error', 'Failed to fetch community questions'))
|
||||
.catch(() => {
|
||||
// Offline Fallback Mock
|
||||
setQuestions([
|
||||
{ id: 1, title: "When is the fresher's orientation?", content: "I couldn't find the exact date for the CSE orientation.", authorName: "Karthik", authorEmail: "karthik.2024@ritchennai.edu.in", createdAt: new Date().toISOString(), status: "PENDING", tags: ["orientation"], votes: 0 },
|
||||
{ id: 2, title: "Are laptops mandatory in first year?", content: "Just wondering if we need to bring laptops to college every day.", authorName: "Sneha", authorEmail: "sneha.2024@ritchennai.edu.in", createdAt: new Date(Date.now() - 86400000).toISOString(), status: "APPROVED", tags: ["academics"], votes: 5 }
|
||||
]);
|
||||
})
|
||||
.finally(() => setLoadingQuestions(false));
|
||||
};
|
||||
|
||||
@@ -413,6 +563,10 @@ export default function AdminDashboard() {
|
||||
showToast('success', 'Question deleted.');
|
||||
fetchQuestions();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setQuestions(questions.filter(q => q.id !== id));
|
||||
showToast('success', 'Question deleted (Local Mode).');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -422,9 +576,18 @@ export default function AdminDashboard() {
|
||||
const fetchRecipients = () => {
|
||||
setLoadingRecipients(true);
|
||||
fetch(getBackendUrl('/api/admin/recipients'))
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => { if (Array.isArray(data)) setRecipients(data); })
|
||||
.catch(() => showToast('error', 'Failed to fetch email subscribers'))
|
||||
.catch(() => {
|
||||
// Offline Fallback Mock
|
||||
setRecipients([
|
||||
'devops@ritchennai.edu.in',
|
||||
'admin@ritchennai.edu.in'
|
||||
]);
|
||||
})
|
||||
.finally(() => setLoadingRecipients(false));
|
||||
};
|
||||
|
||||
@@ -440,7 +603,10 @@ export default function AdminDashboard() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: newEmail }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
showToast('success', data.message || 'Email added successfully');
|
||||
@@ -450,18 +616,30 @@ export default function AdminDashboard() {
|
||||
showToast('error', data.message || 'Could not add email');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setRecipients([...recipients, newEmail]);
|
||||
setNewEmail('');
|
||||
showToast('success', 'Email added (Local Mode)');
|
||||
})
|
||||
.finally(() => setSubmittingEmail(false));
|
||||
};
|
||||
|
||||
const handleRemoveRecipient = (email: string) => {
|
||||
if (!confirm(`Remove ${email} from deployment notifications?`)) return;
|
||||
fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' })
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
showToast('success', 'Removed email recipient');
|
||||
fetchRecipients();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setRecipients(recipients.filter((r) => r !== email));
|
||||
showToast('success', 'Removed email recipient (Local Mode)');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -677,14 +855,14 @@ export default function AdminDashboard() {
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'transport' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 bg-slate-900/60 p-5 rounded-3xl border border-white/10">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-900/60 p-5 rounded-3xl border border-white/10">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Bus className="w-5 h-5 text-orange-400" />
|
||||
College Bus Routes ({routes.length} Active)
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
Changes made here update the student Bus Routes directory and driver tracking app instantly.
|
||||
Edit departure times, intermediate stops, colors, or create and delete routes instantly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -692,13 +870,13 @@ export default function AdminDashboard() {
|
||||
<button
|
||||
onClick={fetchRoutes}
|
||||
title="Refresh routes"
|
||||
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-white/10"
|
||||
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-white/10 cursor-pointer"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loadingRoutes ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={openCreateRouteModal}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white text-xs font-bold shadow-md shadow-orange-500/20 hover:opacity-95 transition-all cursor-pointer"
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white text-xs font-bold shadow-md shadow-orange-500/20 hover:opacity-95 transition-all cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add New Route
|
||||
@@ -706,15 +884,34 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Route Search Filter */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={busSearchQuery}
|
||||
onChange={(e) => setBusSearchQuery(e.target.value)}
|
||||
placeholder="Search routes by number (e.g. R01, RIT-01), name, or origin..."
|
||||
className="w-full bg-slate-900/80 border border-white/10 rounded-2xl px-4 py-3 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loadingRoutes ? (
|
||||
<div className="py-16 text-center text-xs text-slate-400">Loading bus routes from database...</div>
|
||||
<div className="py-16 text-center text-xs text-slate-400">Loading bus routes...</div>
|
||||
) : routes.length === 0 ? (
|
||||
<div className="py-16 text-center text-xs text-slate-400 bg-slate-900/40 rounded-3xl border border-white/10">
|
||||
No routes configured yet. Click "Add New Route" to create your first bus route!
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{routes.map((route) => (
|
||||
{routes
|
||||
.filter(
|
||||
(r) =>
|
||||
!busSearchQuery.trim() ||
|
||||
r.name.toLowerCase().includes(busSearchQuery.toLowerCase()) ||
|
||||
r.number.toLowerCase().includes(busSearchQuery.toLowerCase()) ||
|
||||
r.from.toLowerCase().includes(busSearchQuery.toLowerCase())
|
||||
)
|
||||
.map((route) => (
|
||||
<motion.div
|
||||
key={route.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
|
||||
@@ -15,13 +15,28 @@ export default function BusRoutes() {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 0. Check local storage if edited by Transport Admin
|
||||
const localAdminRoutes = localStorage.getItem('RIT_LOCAL_BUS_ROUTES');
|
||||
if (localAdminRoutes) {
|
||||
try {
|
||||
const parsed = JSON.parse(localAdminRoutes);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setBusRoutes(parsed);
|
||||
setSelectedBusRoute(parsed[0]);
|
||||
setLoadingBus(false);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 1. Instantly load static bus routes (0ms loading delay)
|
||||
fetch('/bus_routes.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setBusRoutes(data);
|
||||
setSelectedBusRoute(data[0]);
|
||||
if (!localStorage.getItem('RIT_LOCAL_BUS_ROUTES')) {
|
||||
setBusRoutes(data);
|
||||
setSelectedBusRoute(data[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error('Local JSON load error:', err))
|
||||
|
||||
@@ -1,56 +1,35 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
GraduationCap, Clock, MapPin, Users,
|
||||
Calendar, Building, ShieldCheck,
|
||||
Sparkles, KeyRound, ArrowRight, LogOut,
|
||||
AlertCircle, User, HelpCircle, Compass, ExternalLink
|
||||
GraduationCap, Clock, MapPin,
|
||||
Calendar, Building, ArrowRight, LogOut,
|
||||
HelpCircle, Compass, ExternalLink, LogIn
|
||||
} from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import {
|
||||
fetchStudentDashboard,
|
||||
loginWithIms,
|
||||
fetchMockImsUsers,
|
||||
getStoredImsSession,
|
||||
clearImsSession,
|
||||
type StudentDashboardData,
|
||||
type MockUserCredential
|
||||
type StudentDashboardData
|
||||
} from '@/services/imsService';
|
||||
|
||||
export default function StudentDashboard() {
|
||||
const { user } = useAuth();
|
||||
const { user, openAuthModal, logout } = useAuth();
|
||||
const [data, setData] = useState<StudentDashboardData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'timetable' | 'location'>('timetable');
|
||||
|
||||
// IMS Authentication Session State
|
||||
const [imsSession, setImsSession] = useState<{ token: string; student: any } | null>(getStoredImsSession());
|
||||
const [regNumberInput, setRegNumberInput] = useState('');
|
||||
const [passwordInput, setPasswordInput] = useState('');
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
const [mockUsers, setMockUsers] = useState<MockUserCredential[]>([]);
|
||||
const [showDemoBox, setShowDemoBox] = useState(false);
|
||||
|
||||
// Load available mock accounts for demo helper
|
||||
useEffect(() => {
|
||||
fetchMockImsUsers().then((users) => {
|
||||
setMockUsers(users);
|
||||
if (users.length > 0 && !regNumberInput) {
|
||||
setRegNumberInput(users[0].regNumber);
|
||||
setPasswordInput(users[0].defaultPassword);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const imsSession = getStoredImsSession();
|
||||
const effectiveRegNumber = user?.regNumber || imsSession?.student?.regNumber;
|
||||
|
||||
const loadDashboard = async () => {
|
||||
if (!imsSession?.student?.regNumber) return;
|
||||
if (!effectiveRegNumber) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await fetchStudentDashboard(imsSession.student.regNumber, user?.email);
|
||||
const result = await fetchStudentDashboard(effectiveRegNumber, user?.email);
|
||||
setData(result);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading student dashboard:', err);
|
||||
@@ -61,172 +40,60 @@ export default function StudentDashboard() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (imsSession) {
|
||||
if (effectiveRegNumber) {
|
||||
loadDashboard();
|
||||
} else {
|
||||
setData(null);
|
||||
}
|
||||
}, [imsSession]);
|
||||
}, [effectiveRegNumber, user]);
|
||||
|
||||
const handleImsLogin = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!regNumberInput.trim() || !passwordInput.trim()) {
|
||||
setLoginError('Please enter both Register Number and Password.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoggingIn(true);
|
||||
setLoginError(null);
|
||||
|
||||
try {
|
||||
const res = await loginWithIms(regNumberInput.trim(), passwordInput.trim());
|
||||
if (res.success && res.token && res.student) {
|
||||
setImsSession({ token: res.token, student: res.student });
|
||||
} else {
|
||||
setLoginError(res.message || 'Invalid Register Number or Password.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setLoginError(err?.message || 'Failed to authenticate with IMS server.');
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickFillMock = (mock: MockUserCredential) => {
|
||||
setRegNumberInput(mock.regNumber);
|
||||
setPasswordInput(mock.defaultPassword);
|
||||
};
|
||||
|
||||
const handleLogoutIms = () => {
|
||||
const handleLogout = () => {
|
||||
clearImsSession();
|
||||
setImsSession(null);
|
||||
logout();
|
||||
setData(null);
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// VIEW 1: IMS STUDENT LOGIN PORTAL (When not signed in to IMS)
|
||||
// VIEW 1: UNAUTHENTICATED INVITATION SCREEN
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
if (!imsSession) {
|
||||
if (!effectiveRegNumber) {
|
||||
return (
|
||||
<div className="min-h-[85vh] bg-[#0a0f1d] text-slate-100 flex items-center justify-center py-12 px-4 sm:px-6">
|
||||
<div className="w-full max-w-md space-y-6">
|
||||
<div className="min-h-[80vh] bg-[#0a0f1d] text-slate-100 flex items-center justify-center py-12 px-4 sm:px-6">
|
||||
<div className="w-full max-w-md space-y-6 text-center">
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-xl shadow-orange-500/20 mx-auto">
|
||||
<div className="w-full h-full bg-slate-950/60 rounded-[14px] flex items-center justify-center backdrop-blur-sm">
|
||||
<GraduationCap className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-2xl shadow-orange-500/25 mx-auto">
|
||||
<div className="w-full h-full bg-slate-950/70 rounded-[22px] flex items-center justify-center backdrop-blur-sm">
|
||||
<GraduationCap className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-extrabold text-white tracking-tight">
|
||||
IMS Student Portal Sign In
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
|
||||
Student Dashboard
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
||||
Enter your official college Register Number and Password to access your timetable and assigned classroom venue.
|
||||
<p className="text-xs sm:text-sm text-slate-400 max-w-sm mx-auto leading-relaxed">
|
||||
Sign in with your student credentials to view your live daily timetable, assigned classroom, and department info.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-slate-900/80 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-2xl space-y-5"
|
||||
className="bg-slate-900/80 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-2xl space-y-4"
|
||||
>
|
||||
{loginError && (
|
||||
<div className="p-3.5 rounded-2xl bg-rose-500/10 border border-rose-500/25 flex items-start gap-2.5 text-xs text-rose-300">
|
||||
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
|
||||
<span>{loginError}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAuthModal}
|
||||
className="w-full py-3.5 px-4 rounded-xl text-white font-bold text-sm bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
<span>Sign In to Access Dashboard</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<form onSubmit={handleImsLogin} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
|
||||
<User className="w-3.5 h-3.5 text-orange-400" />
|
||||
Register Number / Roll No
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={regNumberInput}
|
||||
onChange={(e) => setRegNumberInput(e.target.value)}
|
||||
placeholder="e.g. 2114251001"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
|
||||
<KeyRound className="w-3.5 h-3.5 text-orange-400" />
|
||||
IMS Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordInput}
|
||||
onChange={(e) => setPasswordInput(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoggingIn}
|
||||
className="w-full py-3.5 px-4 rounded-xl text-white font-bold text-sm bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
{isLoggingIn ? (
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<span>Sign In to Student Portal</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Expandable Demo Helper */}
|
||||
<div className="pt-3 border-t border-white/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDemoBox(!showDemoBox)}
|
||||
className="w-full text-center text-xs font-semibold text-orange-400 hover:underline flex items-center justify-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
{showDemoBox ? 'Hide Demo Accounts' : 'Testing / Demo Accounts'}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showDemoBox && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden mt-3 space-y-2"
|
||||
>
|
||||
<p className="text-[11px] text-slate-400 text-center">
|
||||
Click any sample student below to auto-fill the login form:
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{mockUsers.map((mock) => (
|
||||
<button
|
||||
key={mock.regNumber}
|
||||
type="button"
|
||||
onClick={() => handleQuickFillMock(mock)}
|
||||
className="w-full flex items-center justify-between p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left text-xs transition-colors cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<p className="font-bold text-white">{mock.studentName}</p>
|
||||
<p className="text-[10px] text-slate-400">Reg: {mock.regNumber} ({mock.department})</p>
|
||||
</div>
|
||||
<span className="text-[10px] text-orange-400 font-mono">Fill Form</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
Works with Register Number (e.g. <code>2114251001</code> / <code>rit@2026</code>)
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,7 +124,7 @@ export default function StudentDashboard() {
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<h1 className="text-xl sm:text-2xl font-extrabold text-white tracking-tight">
|
||||
{data?.student.name || imsSession.student.name}
|
||||
{data?.student.name || user?.name || imsSession?.student?.name}
|
||||
</h1>
|
||||
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold tracking-wider bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 uppercase">
|
||||
Authenticated Student
|
||||
@@ -265,11 +132,15 @@ export default function StudentDashboard() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-slate-400 font-medium">
|
||||
<span>Reg No: <strong className="text-white">{data?.student.regNumber || imsSession.student.regNumber}</strong></span>
|
||||
<span>Reg No: <strong className="text-white">{data?.student.regNumber || effectiveRegNumber}</strong></span>
|
||||
<span>•</span>
|
||||
<span>{data?.student.degree} {data?.student.department}</span>
|
||||
<span>•</span>
|
||||
<span>Year {data?.student.year}, Sem {data?.student.semester} ({data?.student.section})</span>
|
||||
<span>{data?.student.degree || 'B.E.'} {data?.student.department || user?.department || 'Engineering'}</span>
|
||||
{data?.student.year && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>Year {data.student.year}, Sem {data.student.semester} ({data.student.section})</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,7 +154,7 @@ export default function StudentDashboard() {
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogoutIms}
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-800 hover:bg-rose-500/15 text-slate-300 hover:text-rose-400 border border-white/10 hover:border-rose-500/30 text-xs font-semibold transition-all cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
@@ -308,7 +179,7 @@ export default function StudentDashboard() {
|
||||
|
||||
<div className="bg-slate-900/40 border border-white/10 rounded-2xl p-4 flex items-center gap-3.5">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 shrink-0">
|
||||
<Users className="w-5 h-5" />
|
||||
<Building className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Class Incharge</p>
|
||||
@@ -376,7 +247,6 @@ export default function StudentDashboard() {
|
||||
to={`/faculty?dept=${encodeURIComponent(data.student.department)}`}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 border border-white/10 text-xs font-bold text-slate-200 hover:text-white transition-all cursor-pointer whitespace-nowrap shrink-0"
|
||||
>
|
||||
<Users className="w-4 h-4 text-orange-400" />
|
||||
<span>View {data.student.departmentCode} Faculty Directory</span>
|
||||
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
|
||||
</Link>
|
||||
@@ -395,7 +265,7 @@ export default function StudentDashboard() {
|
||||
<p className="text-slate-400 text-xs max-w-md mx-auto">{error}</p>
|
||||
<button
|
||||
onClick={() => loadDashboard()}
|
||||
className="px-4 py-2 bg-rose-500 text-white rounded-xl text-xs font-bold hover:bg-rose-600 transition-colors"
|
||||
className="px-4 py-2 bg-rose-500 text-white rounded-xl text-xs font-bold hover:bg-rose-600 transition-colors cursor-pointer"
|
||||
>
|
||||
Retry Connection
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user